Семафоры и мьютексы

Предыдущая  Содержание  Следующая V*D*V

Давайте теперь посмотрим, каким образом мы можем добавить блокировку в scull. Нашей целью является сделать в scull наши операции со структурами данных атомарными, а это означает, что вся операция выполняется сразу, до того, как затребована другими потоками исполнения. В нашем примере с утечкой памяти нам необходимо обеспечить, чтобы если один поток считает, что должен быть выделен отдельный кусок памяти, он имел бы возможность выполнить это выделение перед тем, как любой другой поток сможет выполнить эту же проверку. Для этого мы должны создать критические секции: код, который в любой данный момент времени может быть выполнен только одним потоком.

 

Не все критические секции похожи, так что для разных потребностей ядро предоставляет различные примитивы. В этом случае каждое обращение к структуре данных scull происходит в контексте процесса, как результат прямого запроса от пользователя; нет запросов, которые будут сделаны обработчиками прерываний или другими асинхронными контекстами. Нет никаких особых требований к латентности (времени отклика); прикладные программисты понимают, что запросы ввода/вывода обычно не удовлетворяются немедленно. Кроме того, scull не удерживает какие-то другие важные системные ресурсы во время доступа к его собственным структурам данных. Это всё означает то, что если драйвер scull засыпает в ожидании своей очереди на доступ к структурам данных, никого это не тревожит.

 

"Идти спать" в этом контексте является чётко определенным термином. Когда процесс в Linux достигает точки, где он не может выполнять любые дальнейшие действия, он переходит в спящий режим (или "блокируется"), уступая процессор кому-то другому, пока когда-то в будущем он не сможет снова начать работать. Процессы часто засыпают ожидая завершения ввода/вывода. По мере углубления в ядро мы будем сталкиваться с разными ситуациями, в которых мы не можем спать. Однако, метод write в scull - не одна из этих ситуаций. Так что мы можем использовать механизм блокировки, который может послать этот процесс в спячку в ожидании доступа к критической секции.

 

Важно, что мы будем выполнять операцию (распределение памяти с kmalloc), которая может заснуть, так что в любом случае  возможны бездействия. Чтобы наши критические секции работали должным образом, мы должны использовать блокирующий примитив, который работает, когда поток, который владеет блокировкой, спит. Где возможно засыпание, могут быть использованы не все механизмы блокировки (далее в этой главе мы увидим такие, которые не делают это). Однако, для наших нынешних потребностей механизм, который подходит лучше всего, это семафор.

 

Семафоры - хорошо понимаемая концепция в компьютерной науке. По своей сути, семафор это одно целое значение в сочетании с парой функций, которые обычно называются P и V. Процесс, желающий войти в критическую секцию, вызовет P на соответствующем семафоре; если в семафоре значение больше нуля, это значение уменьшается на единицу и этот процесс продолжается. Если, наоборот, в семафоре значение равно 0 (или меньше), процесс должен ждать, пока кто-нибудь другой освободит семафор. Разблокирование семафора осуществляется вызовом V; эта функция увеличивает значение семафора и, если необходимо, будит ожидающие процессы.

 

Когда семафоры используются для взаимного исключения, предохраняя множество процессов от одновременного выполнения в критической секции, их значение будет проинициализировано в 1. Такой семафор в любой данный момент времени может удерживаться только одним процессом или потоком. Семафор, используемый в этом режиме, иногда называют мьютекс (флаг), что, конечно же, расшифровывается как "взаимное исключение" (mutex, mutual exclusion). Почти все семафоры, найденные в ядре Linux, используются для взаимного исключения.

Реализация семафоров в Linux

Ядро Linux обеспечивает реализацию семафоров, которая соответствует вышеприведённой семантике, хотя терминология немного отличается. Для использования семафоров код ядра должен подключить <asm/semaphore.h>. Соответствующим типом является struct semaphore; фактические семафоров могут быть объявлены и проинициализированы несколькими способами. Одним является прямое создание семафора и инициализация его затем с помощью sema_init:

 

void sema_init(struct semaphore *sem, int val);

 

где val является начальным значением для присвоения семафору.

 

Обычно, однако, семафоры используются в режиме мьютекса. Чтобы сделать использование этого общего случая немного легче, ядро обеспечивает набор вспомогательных функций и макросов. Таким образом, мьютекс может быть объявлен и проинициализирован одним из следующих действий:

 

DECLARE_MUTEX(name);

DECLARE_MUTEX_LOCKED(name);

 

Здесь результатом является переменная семафора (названная name), которая инициализируется в 1DECLARE_MUTEX) или в 0 (в DECLARE_MUTEX_LOCKED). В последнем случае мьютекс имеет начальное заблокированное состояние; он должен быть явным образом разблокирован до того, как какому-либо потоку будет разрешён доступ.

 

Если мьютекс должен быть проинициализирован во время исполнения (как в случае, если он создаётся динамически, например), используйте одно из следующих действий:

 

void init_MUTEX(struct semaphore *sem);

void init_MUTEX_LOCKED(struct semaphore *sem);

 

В мире Linux P функция названа down или некоторой вариацией этого имени. Здесь, "вниз" (down) указывает на тот факт, что функция уменьшает значение семафора и возможность вызывающего заснуть после вызова на какое-то время для ожидания, пока семафор станет доступным и предоставит доступ к защищённым ресурсам. Есть три версии down:

 

void down(struct semaphore *sem);

int down_interruptible(struct semaphore *sem);

int down_trylock(struct semaphore *sem);

 

down уменьшает значение семафора и ждёт столько, сколько необходимо. down_interruptible делает то же самое, но эта операция прерываемая. Прерываемая версия - это почти всегда та, которую вы будете хотеть; она позволяет процессу пространства пользователя, который ожидает на семафоре, быть прерванным пользователем. Как правило, вы не захотите использовать непрерываемые операции и сделаете это, если действительно нет альтернативы. Непрерываемые операции являются хорошим способом создания неубиваемых процессов (опасное "D state" ("состояние D") показываемое ps) и раздражения ваших пользователей. Однако, использование down_interruptible требует некоторой дополнительной заботы, если операция прервана, функция возвращает ненулевое значение и вызывающий не удерживает семафор. Правильное использование down_interruptible всегда требует проверки возвращаемого значения и соответствующего реагирования.

 

Последний вариант (down_trylock) никогда не засыпает; если семафор не доступен во время вызова, down_trylock немедленно возвращается с ненулевым значением.

 

После успешного вызова потоком одной из версий down, он считается "удерживающим" семафор (или "получает" или "приобретает" семафор). Теперь этот поток имеет право на доступ к критической секции, защищаемой семафором. После того, как операции, требующие взаимного исключения, закончены, семафор должен быть возвращён. Эквивалентом V в Linux является Up:

 

void up(struct semaphore *sem);

 

Как только up была вызвана, вызывающий больше не удерживает семафор.

 

Как и следовало ожидать, требуется, чтобы любой поток, который получает семафор, освободил его одним (и только одним) вызовом up. Часто при обработке ошибок необходимо особое внимание; если во время удержания семафора произошла ошибка,  перед возвратом статуса ошибки вызывающему этот семафор должен быть освобождён. Допустить ошибку с неосвобождением семафора легко; результат (процессы висят в кажущихся несвязанными местах) может быть трудно воспроизводимым и отслеживаемым.

Использование семафоров в scull

Семафорный механизм даёт scull инструмент, который можно использовать, чтобы избежать состояний гонок во время доступа к структуре данных scull_dev. Но на надо использовать этот инструмент правильно. Ключами к правильному использованию блокирующих примитивов являются точные указания, какие ресурсы должны быть защищены, и гарантия, что любой доступ к этим ресурсам использует правильную блокировку. В нашем примере драйвера всё представляющее интерес содержится в структуре scull_dev, так что она является логичной сферой применения для нашего режима блокировки.

 

Давайте взглянем ещё раз на эту структуру

 

struct scull_dev {

    struct scull_qset *data; /* Указатель, установленный на первый квант */

    int quantum;             /* размер текущего кванта */

    int qset;                /* размер текущего массива */

    unsigned long size;      /* количество данных, хранимых здесь */

    unsigned int access_key; /* используется sculluid и scullpriv */

    struct semaphore sem;    /* семафор взаимного исключения */

    struct cdev cdev;        /* структура символьного устройства */

};

 

В нижней части структуры имеется член, названный sem, который, конечно же, наш семафор. Мы решили использовать отдельный семафор для каждого виртуального устройства scull. Также было бы правильно использовать единственный глобальный семафор. Однако, различные устройства scull не имеют общих ресурсов и нет оснований делать так, чтобы один процесс ожидал, пока другой процесс работает с другим устройством scull. Использование отдельного семафора для каждого устройства позволяет операциям на разных устройствах выполняться параллельно и, следовательно, повышает производительность.

 

Семафоры должны быть проинициализированы перед использованием. scull выполняет эту инициализацию при загрузке в данном цикле:

 

for (i = 0; i < scull_nr_devs; i++) {

    scull_devices[i].quantum = scull_quantum;

    scull_devices[i].qset = scull_qset;

    init_MUTEX(&scull_devices[i].sem);

    scull_setup_cdev(&scull_devices[i], i);

}

 

Обратите внимание, что семафор должен быть проинициализирован до того, как устройство scull становится доступным остальной части системы. Таким образом, init_MUTEX вызывается перед scull_setup_cdev. Выполнение этих операций в обратном порядке создаст состояние гонок, где семафор может быть доступен, когда ещё не подготовлен.

 

Далее, мы должны просмотреть код и убедиться, что нет обращений к структуре данных scull_dev, производящихся без удерживания семафора. Так, например, scull_write начинается таким кодом:

 

if (down_interruptible(&dev->sem))

    return -ERESTARTSYS;

 

Обратите внимание на проверку возвращаемого значения down_interruptible; если она возвращает ненулевое значение, операция была прервана. Обычной вещью в этой ситуации является возвращение -ERESTARTSYS. Увидев этот код возврата, высшие слои ядра либо заново сделают вызов или вернут ошибку пользователю. Если вы возвращаете -ERESTARTSYS, вы должны сначала отменить любые видимые пользователю изменения, которые могли быть сделаны, так чтобы при повторном системном вызове всё сработало правильно. Если вы не можете отменить что-то таким образом, вы должна вернуть взамен -EINTR.

 

scull_write должна освободить семафор независимо от того, было ли возможно успешно выполнять свои другие задачи. Если всё пойдёт хорошо, выполнение функции заканчивается в конце несколькими строчками:

 

out:

    up(&dev->sem);

    return retval;

 

Этот код освобождает семафор и возвращает вызывающему какой-то статус. В scull_write есть несколько мест, где дела могут пойти не так; сюда относятся ошибки при выделении памяти или ошибки при попытке скопировать данные из пространства пользователя. В этих случаях для обеспечения выполнения корректной очистки код выполняет goto out.

Чтение/Запись семафоров

Семафоры выполняют взаимное исключение для всех вызывающих, независимо от того, что каждый поток может захотеть сделать. Однако, многие задачи распадаются на два отдельных типа работы: задачи, которым надо только прочитать защищаемые структуры данных и те, которые должны сделать изменения. Часто можно позволить несколько одновременных читателей, пока никто пытается сделать каких-то изменений. Это может значительно оптимизировать производительность; только читающие задачи могут выполнить свою работу параллельно, не дожидаясь, пока другой читатель покинет критическую секцию.

 

Для этой ситуации ядро Linux предоставляет специальный тип семафора, названный rwsem (или "семафор чтения/записи"). Семафоры rwsem используются в драйверах относительно редко, но они иногда полезны.

 

Код, использующий rwsem-ы, должен подключить <linux/rwsem.h>. Соответствующим типом данных для семафоров чтения/записи является структура rw_semaphore; rwsem должен быть явно проинициализирован во время работы с помощью:

 

void init_rwsem(struct rw_semaphore *sem);

 

Вновь проинициализированный rwsem доступен для последующих задач (читателя или писателя), использующих его. Интерфейс для кода, нуждающегося в доступе только для чтения:

 

void down_read(struct rw_semaphore *sem);

int down_read_trylock(struct rw_semaphore *sem);

void up_read(struct rw_semaphore *sem);

 

Вызов down_read обеспечивает доступ к защищённым ресурсам только для чтения, возможный одновременно для разных читателей. Обратите внимание, что down_read может поместить вызывающий процесс в непрерываемый сон. down_read_trylock не будет ждать, если  доступ на чтение невозможен; она возвращает ненулевое значение, если доступ был предоставлен, 0 в противном случае. Обратите внимание, что соглашение для down_read_trylock отличается от большинства функций ядра, где успех обозначается возвращаемым значением 0. rwsem, полученный от down_read, должен в конечном итоге быть освобождён с помощью up_read.

 

Интерфейс для записи аналогичен:

 

void down_write(struct rw_semaphore *sem);

int down_write_trylock(struct rw_semaphore *sem);

void up_write(struct rw_semaphore *sem);

void downgrade_write(struct rw_semaphore *sem);

 

down_write, down_write_trylock и up_write все ведут себя так же, как и их коллеги читатели, за исключением, конечно, что они обеспечивают доступ для записи. Если у вас возникла ситуация, когда для быстрого изменения необходима блокировка записи с последующим длительным периодом доступа только на чтение, вы можете использовать downgrade_write, чтобы разрешить работу другим читателям, как только вы завершили внесение изменений.

 

rwsem разрешает либо одного писателя, либо неограниченное число читателей для удержания семафора. Писатели получают приоритет; как только писатель пытается войти в критическую секцию, читатели не будут допускаться, пока все писатели не завершат свою работу. Эта реализация может привести к голоду читателя, когда читатели долгое время не имеют доступа, если у вас есть большое число писателей, борющихся за семафор. По этой причине rwsem-ы лучше всего использовать, когда доступ для записи требуется редко, и писатель удерживает доступ короткий период времени.

Предыдущая  Содержание  Следующая