Второе издание
Скачать 3.09 Mb.
|
Создание и инициализация семафоров Реализация семафоров зависит от аппаратной платформы и определена в файле static DECLARE_SEMAPHORE_GENERIC(name, count); где name — имя переменной семефора, a count — счетчик семафора. Более короткая запись для создания взаимоисключающей блокировки (mutex), которая используют- ся наиболее часто, имеет следующий вид. static DECLARE_MUTEX(name); где name — это снова имя переменной типа семафор. Чаще всего семафоры создают- ся динамически, как часть больших структур данных. В таком случае для инициализа- ции семафора, который создается динамически и на который есть только непрямая ссылка через указатель, необходимо использовать функцию sema_init(sem, count); где sem— это указатель, a count — счетчик использования семафора. Аналогично для инициализации динамически создаваемой взаимоисключающей блокировки можно использовать функцию init_MUTEX(sem); Неизвестно, почему слово "mutex" в имени функции init_MUTEX() выделено большими буквами и почему слово "init" идет перед ним, в то время как имя функ- ции s e m a _ i n i t () таких особенностей не имеет. Тем не менее ясно, что это выгля- дит не логично, и я приношу свои извинения за это несоответствие. Надеюсь, что после прочтения главы 7 ни у кого уже не будет вызывать удивление то, какие имена придумывают символам ядра. Использование семафоров Функция d o w n _ i n t e r r u p t i b l e () выполняет попытку захватить данный семафор. Если эта попытка неудачна, то задание переводится в состояние ожидания с флагом TASK_INTERRUPTIBLE. Из материала главы 3 следует вспомнить, что такое состояние процесса означает, что задание может быть возвращено к выполнению с помощью сигнала и что такая возможность обычно очень ценная. Если сигнал приходит в тот момент, когда задание ожидает на освобождение семафора, то задание возвращает- ся к выполнению, а функция d o w n _ i n t e r r u p t i b l e () возвращает значение -EINTR. Альтернативой рассмотренной функции выступает функция down (), которая пере- водит задание в состояние ожидания с флагом TASK_UNINTERRUPTIBLE. В большин- стве случаев это нежелательно, так как процесс, который ожидает на освобождение семафора, не будет отвечать на сигналы. Поэтому функция d o w n _ i n t e r r u p t i b l e () используется значительно более широко, чем функция down (). Да, имена этих функ- ций, конечно, далеки от идеала. Средства синхронизации в ядре 193 Функция down_trylock () используется для неблокирующего захвата указанного семафора. Если семафор уже захвачен, то функция немедленно возвращает ненуле- вое значение. В случае успеха по захвату блокировки возвращается нулевое значение и захватывается блокировка. Для освобождения захваченного семафора необходимо вызвать функцию up ( ) . Рассмотрим следующий пример. /* объявление и описание семафора с именем mr_sem и первоначальным значением счетчика, равным 1 */ static DECLARE_MUTEX(mr_sem); if (down_interruptible(&mr_sem)) /* получен сигнал и семафор не захвачен */ /* критический участок ... */ /* освободить семафор */ up(&mr_sem); Полный список функций работы с семафорами приведен в табл. 9.5. Таблица 9.5. Список функций работы с семафорами sema_init(struct semaphore *, int) init_MUTEX(struct semaphore *) init_MUTEX_LOCKED (struct semaphore *) down_interruptible(struct semaphore *) down(struct semaphore *) down_trylock(struct semaphore *) up(struct semaphore *) Инициализация динамически созданного сема- фора и установка для него указанного значения счетчика использования Инициализация динамически созданного сема- фора и установка его счетчика использования в значение 1 Инициализация динамически созданного семафо- ра и установка его счетчика использования в зна- чение 0 (т.е. семафор изначально заблокирован) Выполнить попытку захватить семафор и пере- йти в прерываемое состояние ожидания, если семафор находится в состоянии конфликта при захвате (contended) Выполнить попытку захватить семафор и пере- йти в непрерываемое состояние ожидания, если семафор находится в состоянии конфликта при захвате (contended) Выполнить попытку захватить семафор и не- медленно возвратить ненулевое значение, если семафор находится в состоянии конфликта при захвате (contended) Освободить указанный семафор и возвратить к выполнению ожидающее задание, если такое есть 194 Глава 9 Функция Описание Семафоры чтения-записи Семафоры, так же как и спин-блокировки, могут быть типа чтения-записи. Ситуации, в которых предпочтительнее использовать семафоры чтения-записи та- кие же как и в случае использования спин-блокировок чтения-записи. Семафоры чтения-записи представляются с помощью структуры s t r u c t rw_ semaphore, которая определена в файле где name — это имя нового семафора. Семафоры чтения-записи, которые создаются динамически, могут быть инициа- лизированы с помощью следующей функции. init_rwsem(struct rw_semaphore *sem) Все семафоры чтения-записи являются взаимоисключающими (mutex), т.е. их счетчик использования равен единице. Любое количество потоков чтения может одновременно удерживать блокировку чтения, если при этом нет ни одного потока записи. И наоборот, только один поток записи может удерживать блокировку, захва- ченную на запись, если нет ни одного потока чтения. Все семафоры чтения-записи используют непрерываемое состояние ожидания, поэтому существует только одна версия функции down (). Рассмотрим следующий пример. static DECLARE_RWSEM(mr_rwsem); /* попытка захватить семафор для чтения */ down_read(&mr_rwsem); /* критический участок (только чтение) .. */ /* освобождаем семафор */ up_read(&rar_rwsem); /* ... */ /* попытка захватить семафор на запись */ down_write(&mr_rwsem); /* освобождаем семафор */ /* критический участок (чтение и запись) ... */ up write(&mr rwsem); Для семафоров есть реализации функций down_read_trylock () и down_write_ t r y l o c k ( ) . Каждая из них принимает один параметр — указатель на семафор чте- ния-записи. Обе функции возвращают ненулевое значение, если блокировка захва- чена успешно, и нуль, если блокировка находится в состоянии конфликта. Следует быть внимательными — поведение этих функций противоположно поведению анало- гичных функций для обычных семафоров, причем без всякой на то причины! Семафоры чтения-записи имеют уникальную функцию, аналога которой нет для спин-блокировок чтения-записи. Это функция d o w n g r a d e w r i t e r (), которая авто- Средства синхронизации в ядре 195 матичсски превращает блокировку, захваченную на запись, в блокировку, захвачен- ную на чтение. Семафоры чтения-записи, так же как и спин-блокировки аналогичного типа, должны использоваться, только если есть четкое разделение между участками кода, которые осуществляют чтение, и участками кода, которые осуществляют запись. Использование механизмов блокировок чтения-записи приводит к дополнительным затратам, поэтому их стоит использовать, только если код можно четко разделить на участки чтения и записи. Сравнение спин-блокировок и семафоров Понимание того, когда использовать спин-блокировки, а когда семафоры являет- ся важным для написания оптимального кода. Однако во многих случаях выбирать очень просто. В контексте прерывания могут использоваться только спин-блокиров- ки, и только семафор может удерживаться процессом, который находится в состо- янии ожидания. В табл. 9.6 показан обзор требований того, какой тип блокировок использовать. Таблица 9.6. Что следует использовать: семафоры или спин-блокировки Требование Рекомендуемый тип блокировки Блокировка с малыми накладными затрата- Спин-блокировки более предпочтительны ми (low overhead) Малое время удержания блокировки Спин-блокировки более предпочтительны Длительное время удержания блокировки Семафоры более предпочтительны Необходимо использовать блокировку в Необходима спин-блокировка контексте прерывания Необходимо переходить в состояние ожи- Необходимо использовать семафоры дания (steep) при захваченной блокировке Условные переменные Условные переменные (conditional variable, completion variable) — простое сред- ство синхронизации между двумя заданиями, которые работают в режиме ядра, ког- да необходимо, чтобы одно задание послало сигнал другому о том, что произошло некоторое событие. При этом одно задание ожидает на условной переменной, пока другое задание не выполнит некоторую работу. Когда другое задание завершит вы- полнение своей работы, оно использует условную переменную для того, чтобы воз- вратить к выполнению все ожидающие на ней задания. Если это кажется похожим на работу семафора, то именно так оно и есть, идея та же. В действительности, услов- ные переменные просто обеспечивают простое решение проблемы, для которой в других ситуациях используются семафоры. Например, в системном вызове vfork() условная переменная используется для возврата к выполнению родительского про- цесса при завершении порожденного. Условные переменные представляются с помощью структуры struct completion, которая определена в файле 196 Глава 9 Требование Рекомендуемый тип блокировки Блокировка с малыми накладными затрата- Спин-блокировки более предпочтительны ми (low overhead) Малое время удержания блокировки Спин-блокировки более предпочтительны Длительное время удержания блокировки Семафоры более предпочтительны Необходимо использовать блокировку в Необходима спин-блокировка контексте прерывания Необходимо переходить в состояние ожи- Необходимо использовать семафоры дания (steep) при захваченной блокировке Статически условная переменная может быть создана с помощью макроса DECLARE_COMPLETI0N(mr_comp); Динамически созданная условная переменная может быть инициализирована с помощью функции init_completion (). Задание, которое должно ожидать на условной переменной, вызывает функцию wait_for_completion () . После того как наступило ожидаемое событие, вызов функции complete () посылает сигнал заданию, которое ожидает на условной пере- менной, и это задание возвращается к выполнению. В табл. 9.7 приведены методы работы с условными переменными. Таблица. 9.7. Методы работы с условными переменными Для примеров использования условных переменных смотрите файлы k e r n e l / sched.с и k e r n e l / f o r k . с . Наиболее часто используются условные переменные, которые создаются динамически, как часть структур данных. Код ядра, который ожидает па инициализацию структуры данных, вызывает функцию w a i t _ f o r _ completion(). Когда инициализация закончена, ожидающие задания возвращаются к выполнению с помощью вызова функции complete(). BLK: Большая блокировка ядра Добро пожаловать к "рыжему пасынку" ядра. Большая блокировка ядра (Big Kernel Lock, BKL) — это глобальная спин-блокировка, которая была создана специ- ально для того, чтобы облегчить переход от первоначальной реализации SMP n опе- рационной системе Linux к мелкоструктурным блокировкам. Блокировка BKL имеет следующие интересные свойства. • Во время удержания BKL можно переходить в состояние ожидания. Блокировка автоматически освобождается, когда задание переходит в состояние ожидания, и снова захватывается, когда задание планируется на выполнение. Конечно, это не означает, что безопасно переходить в состояние ожидания при удержа- нии BKL, просто это можно делать и это не приведет к взаимоблокировке. • Блокировка BKL рекурсивна. Один процесс может захватывать эту блокировку несколько раз подряд, и это не приведет к самоблокировке, как в случае обыч- ных спин-блокировок. • Блокировка BKL может использоваться только в контексте процесса. • Блокировка BKL — это от лукавого. Средства синхронизации в ядре 197 Метод Описание init_completion(struct completion *) wait_for_completion(struct completion *) complete(struct completion *) Инициализация динамически созданной услов- ной переменной в заданной области памяти Ожидание сигнала на указанной условной переменной Отправка сигнала всем ожидающим заданиям и возвращение их к выполнению Рассмотренные свойства дали возможность упростить переход от ядер серии 2.0 к серии 2.2. Когда в ядро 2-0 была введена поддержка SMP, только одно задание могло выполняться в режиме ядра в любой момент времени (конечно, сейчас ядро распа- раллелено очень хорошо— пройден огромный путь). Целью создания ядра серии 2.2 было обеспечение возможности параллельного выполнения кода ядра на нескольких процессорах. Блокировка BKL была введена для того, чтобы упростить переход к мелкоструктурным блокировкам. В те времена она оказала большую помощь, а сегод- ня она приводит к ухудшению масштабируемости 5 Использовать блокировку BKL не рекомендуется. На самом деле, новый код ни- когда не должен использовать BKL. Однако эта блокировка все еще достаточно ин- тенсивно используется в некоторых частях ядра. Поэтому важно понимать особен- ности большой блокировки ядра и интерфейса к ней. Блокировка BKL ведет себя, как обычная спин-блокировка, за исключением тех особенностей, которые были рас- смотрены выше. Функция lock_kernel () позволяет захватить блокировку, а функ- ция unlock_kernel () — освободить блокировку. Каждый поток выполнения может рекурсивно захватывать эту блокировку, но после этого необходимо столько же раз вызвать функцию unlock_kernel (). При последнем вызове функции освобождения блокировки блокировка будет освобождена. Функция k e r n e l l o c k e d () возвращает ненулевое значение, если блокировка в данный момент захвачена, в противном слу- чае возвращается нуль. Эти интерфейсы определены в файле Рассмотрим простой пример использования этой блокировки. lock_kernel(); /* * Критический раздел, который синхронизирован со всеми пользователями * блокировки BKL... * Заметим, что здесь можно безопасно переходить в состояние ожидания * и блокировка будет прозрачным образом освобождаться. * После перепланирования блокировка будет прозрачным образом снова * захватываться. * Это гарантирует, что не возникнет состояния взаимоблокировки, * но все-таки лучше не переходить в состояние ожидания, * если необходимо гарантировать защиту данных! */ unlock_kernel(); Когда эта блокировка захвачена, происходит запрещение преемптивности. Для ядер, скомпилированных под однопроцессорную машину, код BKL на самом деле не выполняет никаких блокировок. В табл. 9.8 приведен полный список функций рабо- ты с BKL. 5 Хотя, может быть, она и не такая страшная, какой ее иногда пытаются представить, вес же неко- торые люди считают ее "воплощением дьявола" в ядре. 198 Глава 9 Таблица 9.8. Функции работы с большой блокировкой ядра Функция Описание lock_kernel() Захватить блокировку BKL unlock_kernel() Освободить блокировку BKL kernel_locked() Возвратить ненулевое значение, если блокировка захвачена, и нуль- в противном случае Одна из самых главных проблем, связанных с большой блокировкой ядра, — как определить, что защищается с помощью данной блокировки. Часто блокировка BKL ассоциируется с кодом (например, она "синхронизирует вызовы функции foo () "), а не с данными ("защита структуры foo "). Это приводит к тому, что заменить BKL обычными сиин-блокировками бывает сложно, потому что нелегко определить, что же все-таки необходимо блокировать. На самом деле, подобная замена еще более сложна, так как необходимо учитывать все взаимоотношения между всеми участками кода, которые используют эту блокировку. Секвентные блокировки Секвентная блокировка (seq lock) — это новый тип блокировки, который появил- ся в ядрах серии 2.6. Эти блокировки предоставляют очень простой механизм чте- ния и записи совместно используемых данных. Работа таких блокировок основана на счетчике последовательности событий. Перед записью рассматриваемых данных захватывается спин-блокировка, и значение счетчика увеличивается на единицу. После записи данных значение счетчика снова увеличивается на единицу, и спин- блокировка освобождается, давая возможность записи другим потокам. Перед чте- нием и после чтения данных проверяется значение счетчика. Если два полученных значения одинаковы, то во время чтения данных новый акт записи не начинался, Если к тому же оба эти значения четные, то к моменту начала чтения акт записи был закончен (при захвате блокировки на запись значение счетчика становится не- четным, а перед освобождением — снова четным, так как изначальное значение счет- чика равно нулю). Определение секвентной блокировки можно записать следующим образом. seqlock_t mr_seq_lock = SEQLOCK_UNLOCKED; Участок кода, который осуществляет запись, может выглядеть следующим образом. write_seqlock(&mr_seq_lock); /* блокировка захвачена на запись ... */ write_sequnlock(&mr_seq_lock); Это выглядит, как работа с обычной спин-блокировкой. Необычность появляется в коде чтения, который несколько отличается от ранее рассмотренных. unsigned long seq; do { seq = read_seqbegin(&mr_seq_lock); /* здесь нужно читать данные ... */ } while (read_seqretry(&mr_seq_lock, seq)); Средства синхронизации в ядре 199 Секвентные блокировки полезны для обеспечения очень быстрого доступа к данным в случае, когда применяется много потоков чтения и мало потоков записи. Кроме того, при использовании этого типа блокировок потоки записи получают более высокий приоритет перед потоками чтения. Блокировка записи всегда будет успешно захвачена, если нет других потоков записи. Потоки чтения никак не влия- ют на захват блокировки записи, в противоположность тому, что имеет место для спин-блокировок и семафоров чтения-записи. Более того, потоки, которые ожидают на запись, будут вызывать постоянные повторения цикла чтения (как в показанном примере) до тех пор, пока не останется ни одного потока, удерживающего блокиров- ку записи во время чтения данных. |