Спин-блокировки

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

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

 

Спин-блокировки - простая концепция. Спин-блокировка является взаимным исключением устройства, которое может иметь только два значения: "заблокировано" и "разблокировано". Это обычно реализуется как один бит в целом числе. Код, желающий забрать какую-либо определённую блокировку, проверяет соответствующий бит. Если блокировка доступна, "блокирующий" бит устанавливается и этот код продолжает работу в критической секции. Вместо этого, если блокировка уже захвачена кем-то другим, код переходит в короткий цикл, где постоянно проверяет блокировку, пока она не станет доступна. Этот цикл является "спин" частью спин-блокировки.

 

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

 

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

Знакомство с API спин-блокировки

Для использования примитивов спин-блокировки необходимо подключить файл <linux/spinlock.h>. Фактическая блокировка имеет тип spinlock_t. Как и любые другие структуры данных, спин-блокировка должна быть проинициализирована. Эта инициализация может быть сделано во время компиляции следующим образом:

 

spinlock_t my_lock = SPIN_LOCK_UNLOCKED;

 

или во время работы:

 

void spin_lock_init(spinlock_t *lock);

 

Перед входом в критическую секцию ваш код должен получить необходимую блокировку:

 

void spin_lock(spinlock_t *lock);

 

Заметим, что все ожидания в спин-блокировках непрерываемы по своей природе. После вызова spin_lock вы будете крутиться в цикле, пока блокировка не станет доступной.

 

Для освобождения полученной блокировки передайте её в:

 

void spin_unlock(spinlock_t *lock);

 

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

Спин-блокировки и контекст атомарности

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

 

Большинство читателей согласятся, что такого сценария лучше избегать. Таким образом, основное правило, которое применяется к спин-блокировкам - любой код, удерживающий спин-блокировку, должен быть атомарным. Он не может спать; фактически, он не может отдать процессор по какой-то причине, кроме обслуживания прерываний (а иногда даже и для этого).

 

Случай вытеснения в ядре обрабатывается самим кодом спин-блокировки. Каждый раз, когда код ядра удерживает спин-блокировку, на соответствующем процессоре вытеснение запрещается. Даже однопроцессорные системы должны запрещать вытеснение, чтобы таким образом избежать состояния гонок. Вот почему требуется правильное блокирование, даже если вы никогда не ожидаете запуска вашего кода на многопроцессорной машины.

 

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

 

Вот ещё один сценарий: ваш драйвер исполняется и только что забрал блокировку, которая контролирует доступ к его устройству. Пока удерживается блокировка, устройство вызывает прерывание, которая запускает ваш обработчик прерываний. Обработчик прерываний перед обращением к устройству также должен получить блокировку. Получение спин-блокировки в обработчике прерывания является законной вещью; это является одной из причин того, что операции спин-блокировки не засыпают. Но что произойдёт, если подпрограмма прерывания выполняется тем же процессором, что и код, который до этого забрал спин-блокировку? Пока обработчик прерываний вращается в цикле, непрерываемый код не будет иметь возможность запуститься для снятия блокировки. Этот процессор зациклится навсегда.

 

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

 

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

Функции спин-блокировки

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

 

Фактически, есть четыре функции (* На самом деле это макросы, определённые в <linux/spinlock.h>, а не функции. Вот почему параметр flags в spin_lock_irqsave() не является указателем, как это можно было ожидать), которые могут блокировать спин-блокировку:

 

void spin_lock(spinlock_t *lock);

void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);

void spin_lock_irq(spinlock_t *lock);

void spin_lock_bh(spinlock_t *lock);

 

Мы уже видели, как работает spin_lock. spin_lock_irqsave запрещает прерывания (только на местном процессоре) до снятия блокировки; предыдущее состояние прерывания запоминается во флагах. Если вы абсолютно уверены, что никто другой не запрещал прерывания на вашем процессоре (или, другими словами, вы уверены, что должны разрешить прерывания, когда освободите вашу спин-блокировку), вы можете использовать взамен spin_lock_irq и сохранять флаги. Наконец, spin_lock_bh перед получением блокировки запрещает программные прерывания, но оставляет включёнными аппаратные прерывания.

 

Если у вас есть спин-блокировка, которая может быть получена кодом, который работает в контексте (аппаратного или программного) прерывания, необходимо использовать одну из форм spin_lock, которая запрещает прерывания. Если поступить иначе, рано или поздно в системе случится взаимоблокировка. Если вы не обращаетесь к вашей блокировке в обработчике аппаратного прерывания, а делаете это с помощью программных прерываний (например, в коде, который запускается микрозадачами, тема рассматривается в Главе 7), для безопасного избегания взаимоблокировок вы можете  использовать spin_lock_bh, разрешая в то же время обслуживание аппаратных прерываний.

 

Есть также четыре способа освободить блокировку; он должен соответствовать функции, которую вы использовали для получения блокировки:

 

void spin_unlock(spinlock_t *lock);

void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags);

void spin_unlock_irq(spinlock_t *lock);

void spin_unlock_bh(spinlock_t *lock);

 

Каждый вариант spin_unlock отменяет работу, выполненную соответствующей функцией spin_lock. Аргумент flags, передаваемый в spin_unlock_irqrestore, должен быть той же переменной, переданной в spin_lock_irqsave. Вы должны также вызвать spin_lock_irqsave и spin_unlock_irqrestore в той же самой функции; в противном случае, на некоторых архитектурах ваш код может сломаться.

 

Существует также набор неблокирующих операций спин-блокировки:

 

int spin_trylock(spinlock_t *lock);

int spin_trylock_bh(spinlock_t *lock);

 

Эти функции возвращают ненулевое значение при успехе (блокировка была получена), иначе - 0. Не существует версии "попробовать" ("try"), которая запрещает прерывания.

Чтение/Запись спин-блокировок

Ядро предоставляет формы для чтения/записи спин-блокировок, которые полностью аналогичны чтению/записи семафоров, рассмотренному ранее в этой главе. Эти блокировки разрешают в критической секции одновременно любое количество читателей, но писатели должны иметь эксклюзивный доступ. Блокировки чтения/записи имеют тип rwlock_t, определённый в <linux/spinlock.h>. Они могут быть объявлены и проинициализированы двумя способами:

 

rwlock_t my_rwlock = RW_LOCK_UNLOCKED; /* Статический способ */

 

rwlock_t my_rwlock;

rwlock_init(&my_rwlock); /* Динамический способ */

 

Список доступных функций должен выглядеть похожим на уже знакомые функции. Для читателей доступны следующие функции:

 

void read_lock(rwlock_t *lock);

void read_lock_irqsave(rwlock_t *lock, unsigned long flags);

void read_lock_irq(rwlock_t *lock);

void read_lock_bh(rwlock_t *lock);

void read_unlock(rwlock_t *lock);

void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags);

void read_unlock_irq(rwlock_t *lock);

void read_unlock_bh(rwlock_t *lock);

 

Интересно, что нет read_trylock.

 

Функции для записи аналогичны:

 

void write_lock(rwlock_t *lock);

void write_lock_irqsave(rwlock_t *lock, unsigned long flags);

void write_lock_irq(rwlock_t *lock);

void write_lock_bh(rwlock_t *lock);

int write_trylock(rwlock_t *lock);

void write_unlock(rwlock_t *lock);

void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags);

void write_unlock_irq(rwlock_t *lock);

void write_unlock_bh(rwlock_t *lock);

 

Блокировки чтения/записи могут оставить читателей голодными, так же как и rwsem-ы. Такое поведение является редкой проблемой; однако, если есть достаточно спорящих за блокировку, чтобы вызвать зависание, производительность в любом случае плохая.

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