Роберт Лав - Разработка ядра Linux
Таблица 7.4. Список функций управления обработкой нижних половин
Функция Описание void local_bh_disable() Запретить обработку всех отложенных прерываний (softirq) и тасклетов (tasklet) на локальном процессоре void local_bh_enable() Разрешить обработку всех отложенных прерываний (softirq) и тасклетов (tasklet) на локальном процессореВызовы этих функций могут быть вложенными — при этом только последний вызов функции local_bh_enable() разрешает обработку нижних половин. Например, при первом вызове функции local_bh_disable() запрещается выполнение отложенных прерываний на текущем процессоре. Если функция local_bh_disable() вызывается еще три раза, то выполнение отложенных прерываний будет запрещено. Их выполнение не будет разрешено до тех пор, пока функция local_bh_enable() не будет вызвана четыре раза.
Такая функциональность реализована с помощью счетчика preempt_count, который поддерживается для каждого задания (интересно, что этот же счетчик используется и для вытеснения процессов в режиме ядра)[42]. Когда значение этого счетчика достигает нуля, то можно начать обработку нижних половин. Так как при вызове функции local_bh_enable() обработка нижних половин запрещена, то эта функция также проверяет наличие ожидающих на обработку нижних половин и выполняет их.
Для каждой поддерживаемой аппаратной платформы имеются спои функции, которые обычно реализуются через сложные макросы, описанные в файле <asm/softirq.h>. Для любопытных ниже приведены соответствующие реализации на языке программирования С.
/*
* запрещение обработки нижних половин путем увеличения значения
счетчика preempt_count
*/
void local_bh_disable(void) {
struct thread_info *t = current_thread_info();
t->preempt_count += SOFTIRQ_OFFSET;
}
/*
* уменьшение значения счетчика preempt_count "автоматически" разрешает
* обработку нижних половин, если значение счетчика равно нулю
*
* опционально запускает все обработчики нижних половин,
* которые ожидают на обработку
*/
void local_bh_enable(void) {
struct thread_info *t = current_thread_info();
t->preempt_count -= SOFTIRQ_OFFSET;
/*
* равно ли значение переменной preempt_count нулю и ожидают ли
* на обработку какие-либо обработчики нижних половин?
* если да, то запустить их
*/
if (unlikely(!t->preempt_count &&
softirq_pending(smp_processor_id())))
do_softirq();
}
Эти функции не запрещают выполнения очередей действий. Так как очереди действий выполняются в контексте процесса, нет никаких проблем с асинхронным выполнением и нет необходимости запрещать их. Поскольку отложенные прерывания и тасклеты могут "возникать" асинхронно (например, при возвращении из обработчика аппаратного прерывания), то ядру может потребоваться запрещать их. В случае использования очередей отложенных действий защита совместно используемых данных осуществляется так же, как и при работе в контексте процесса. Детали рассмотрены в главах 8 и 9.
Внизу обработки нижних половин
В этой главе были рассмотрены три механизма, которые используются для реализации отложенных действий в ядре Linux, — отложенные прерывания (softirq), тасклеты (tasklet) и очереди отложенных действий (work queue). Было показано, как эти механизмы работают и как они реализованы. Также обсуждались основные моменты, связанные с использованием этих механизмов в собственном программном коде, и было показано, какие у них неподходящие названия. Для того чтобы восстановить историческую справедливость, мы также рассмотрели те механизмы обработки нижних половин, которые существовали в предыдущих версиях ядра Linux: механизмы BH и task queue.
Очень часто в главе поднимались вопросы, связанные с синхронизацией и параллельным выполнением, потому что эти моменты имеют прямое отношение к обработке нижних половин. В эту главу специально был включен раздел, который касается запрещения обработки нижних половин для защиты от конкурентного доступа, Теперь настало время углубиться в эти моменты с головой. В следующей главе будут рассмотрены особенности синхронизации и параллельного выполнения кода в ядре: основные понятия и соответствующие проблемы. Далее будут рассмотрены интерфейсы, которые позволяют осуществлять синхронизацию в ядре и решать указанные проблемы. Вооруженные следующими двумя главами, вы сможете покорить мир.
Глава 8
Введение в синхронизацию выполнения кода ядра
В приложениях, рассчитанных на работу с совместно используемой памятью (shared memory), необходимо позаботиться о том, чтобы совместно используемые ресурсы были защищены от конкурентного доступа. Ядро — не исключение. Совместно используемые ресурсы требуют защиты от конкурентного доступа в связи с тем, что несколько потоков выполнения[43] могут одновременно манипулировать одними и теми же данными: эти потоки могут переписывать изменения, сделанные другими потоками, а также обращаться к данным, которые находятся в несогласованном (противоречивом, неконсистентном) состоянии. Конкурентный доступ к совместно используемым данным — это хороший способ получить нестабильность системы, причины которой, как показывает опыт, впоследствии очень сложно обнаружить и исправить. В связи с этим важно при разработке сразу сделать все правильно.
Осуществить необходимую защиту совместно используемых ресурсов может оказаться трудной задачей. Много лет назад, когда операционная система Linux не поддерживала симметричную многопроцессорную обработку, предотвратить конкурентный доступ к данным было просто. Так как поддерживался только один процессор, то единственная возможность конкурентного доступа к данным возникала при получении прерывания или когда выполнение кода ядра явно перепланировалось, давая возможность выполняться другому заданию. Да, раньше жить было проще.
Эти дни закончились. Поддержка симметричной многопроцессорности была введена в ядрах серии 2.0, и с тех пор эта поддержка постоянно совершенствуется. Поддержка мультипроцессорности предполагает, что код ядра может одновременно выполняться на двух или более процессорах. Следовательно, без специальной защиты части кода ядра, которые выполняются на двух разных процессорах, принципиально могут обратиться к совместно используемым данным в один и тот же момент времени. Начиная с серии ядер 2.6 ядро операционной системы Linux является преемптивным (вытесняемым). Это подразумевает, что (при отсутствии необходимой защиты) планировщик может вытеснить код ядра в любой момент времени и запустить на выполнение другое задание. Сегодня есть много сценариев, благодаря которым может возникнуть конкурентный доступ к данным в ядре, и все эти варианты требуют защиты данных.
В этой главе рассматриваются проблемы, связанные с параллельным выполнением кода и синхронизацией выполнения кода в ядре операционной системы. В следующей главе детально рассмотрены механизмы и интерфейсы, которые предоставляет ядро операционной системы Linux для решения проблем синхронизации и предотвращения состояния конкуренции за ресурс (race condition, состояние "гонок").
Критические участки и состояние конкуренции за ресурсы
Ветки кода, которые получают доступ к совместно используемыми данным и манипулируют ими, называются критическими участками (critical region). Обычно небезопасно нескольким потокам выполнения одновременно обращаться к одному и тому же ресурсу. Для предотвращения конкурентного доступа во время выполнения критических участков программист, т.е. Вы, должен гарантировать, что код выполняется атомарно — без перерывов, так если бы весь критический участок был одной неделимой машинной инструкцией. Если два потока выполнения одновременно находятся в критическом участке, то это — ошибка в программе. Если такое вдруг случается, то такая ситуация называется состоянием, конкуренции за ресурс (состояние "гонок", race condition). Название связано с тем, что потоки как бы соревнуются друг с другом за доступ к ресурсу. Следует обратить внимание на то, насколько редко такая ситуация может возникать, — поэтому обнаружение состояний конкуренции за ресурсы при отладке программ часто очень сложная задача, потому что подобную ситуацию очень трудно воспроизвести. Обеспечение гарантии того, что конкуренции не будет и, следовательно, что состояний конкуренции за ресурсы возникнуть не может, называется синхронизацией.