Роберт Лав - Разработка ядра Linux
Для каждого процессора существует свой поток. Каждый поток имеет имя в виде ksoftirqd/n, где n — номер процессора. Так в двухпроцессорной системе будут запущены два потока с именами ksoftiqd/0 и ksoftirqd/1. To, что на каждом процессоре выполняется свой поток, гарантирует, что если в системе есть свободный процессор, то он всегда будет в состоянии выполнять отложенные прерывания. После того как потоки запущены, они выполняют замкнутый цикл, похожий на следующий.
for (;;) {
set_task_state(current, TASK_INTERRUPTIBLE);
add_wait_queue(&cwq->more_work, &wait);
if (list_empty(&cwq->worklist))
schedule();
else
set_task_state(current, TASK_RUNNING);
remove_wait_queue(&cwq->more_work, &wait);
if (!list_empty(&cwq->worklist))
run_workqueue(cwq);
}
Если есть отложенные прерывания, ожидающие на обработку (что определяет вызов функции softirq_pending()), то поток ядра ksoftirqd вызывает функцию do_softirq(), которая эти прерывания обрабатывает. Заметим, что это делается периодически, чтобы обработать также вновь активизированные отложенные прерывания. После каждой итерации при необходимости вызывается функция schedule(), чтобы дать возможность выполняться более важным процессам. После того как вся обработка выполнена, поток ядра устанавливает свое состояние в значение TASK_INTERRUPTIBLE и активизирует планировщик для выбора нового готового к выполнению процесса.
Поток обработки отложенных прерываний вновь возвращается в состояние готовности к выполнению, когда функция do_softirq() определяет, что отложенное прерывание реактивизировало себя.
Старый механизм BH
Хотя старый интерфейс BH, к счастью, уже отсутствует в ядрах серии 2.6, тем не менее им пользовались очень долгое время — с первых версий ядра. Учитывая, что этому интерфейсу удалось продержаться очень долго, он, конечно, представляет собой историческую ценность и заслуживает большего, чем просто беглого рассмотрения. Этот раздел никаким образом не касается ядер серии 2.6, но значение истории переоценить трудно.
Интерфейс BH очень древний, и это заметно. Каждый обработчик BH должен быть определен статически, и количество этих обработчиков ограничено максимальным значением 32. Так как все обработчики BH должны быть определены на этапе компиляции, загружаемые модули ядра не могли напрямую использовать интерфейс BH. Тем не менее можно было встраивать функции в уже существующие обработчики BH. Со временем необходимость статического объявления и максимальное количество обработчиков нижних половин, равное 32, стали надоедать.
Все обработчики BH выполнялись строго последовательно — никакие два обработчика BH, даже разных типов, не могли выполняться параллельно. Это позволяло обеспечить простую синхронизацию, но все же для получения высокой производительности при многопроцессорной обработке это было не очень хорошо. Драйверы, которые использовали интерфейс BH, очень плохо масштабировались на несколько процессоров. Например, страдала сетевая подсистема.
В остальном, за исключением указанных ограничений, механизм BH был похож на механизм тасклетов. На самом деле, в ядрах серии 2.4 механизм BH был реализован на основе тасклетов. Максимальное количество обработчиков нижних половин, равное 32, обеспечивалось значениями констант, определенных в заголовочном файле <linux/interrupt.h>. Для того чтобы отметить обработчик BH как ожидающий на выполнение, необходимо было вызвать функцию mark_bh() с передачей номера обработчика BH в качестве параметра. В ядрах серии 2.4 при этом планировался на выполнение тасклет BH, который выполнялся с помощью обработчика bh_action(). До серии ядер 2.4 механизм BH существовал самостоятельно, как сейчас механизм отложенных прерываний.
В связи с недостатками этого типа обработчиков нижних половин, разработчики ядра предложили механизм очередей заданий (task queue), чтобы заменить механизм нижних половин. Очереди заданий так и не смогли справиться с этой задачей, хотя и завоевали расположение большого количества пользователей. При разработке серии ядер 2.3 были предложены механизмы отложенных прерываний (softirq) и механизм тасклетов (tasklet), для того чтобы положить конец механизму BH. Механизм BH при этом был реализован на основе механизма тасклетов. К сожалению, достаточно сложно переносить обработчики нижних половин с использования интерфейса BH на использование механизм тасклетов или отложенных прерываний, в связи с тем что у новых интерфейсов нет свойства строгой последовательности выполнения[40].
Однако при разработке ядер серии 2.5 необходимую конвертацию все же сделали, когда таймеры ядра и подсистему SCSI (единственные оставшиеся системы, которые использовали механизм BH) наконец-то перевели на использование отложенных прерываний. И в завершение, разработчики ядра совсем убрали интерфейс BH. Скатертью дорога тебе, интерфейс BH!
Очереди отложенных действий
Очереди отложенных действий (work queue) — это еще один способ реализации отложенных операций, который отличается от рассмотренных ранее. Очереди действий позволяют откладывать некоторые операции для последующего выполнения потоком пространства ядра — отложенные действия всегда выполняются в контексте процесса. Поэтому код, выполнение которого отложено с помощью постановки в очередь отложенных действий, получает все преимущества, которыми обладает код, выполняющийся в контексте процесса. Наиболее важное свойство — это то, что выполнение очередей действий управляется планировщиком процессов и, соответственно, выполняющийся код может переходить в состояние ожидания (sleep).
Обычно принять решение о том, что необходимо использовать: очереди отложенных действий или отложенные прерывания/тасклеты, достаточно просто. Если отложенным действиям необходимо переходить в состояние ожидания, то следует использовать очереди действий. Если же отложенные операции не могут переходить в состояние ожидания, то воспользуйтесь тасклетами или отложенными прерываниями. Обычно альтернатива использованию очередей отложенных действий — это создание новых потоков пространства ядра. Поскольку при введении новых потоков пространства ядра разработчики ядра обычно хмурят брови (а у некоторых народов это означает смертельную обиду), настоятельно рекомендуется использовать очереди отложенных действий. Их действительно очень просто использовать.
Если для обработки нижних половин необходимо использовать нечто, что планируется на выполнение планировщиком процессов, то воспользуйтесь очередями отложенных действий. Это единственный механизм обработки нижних половин, который всегда выполняется в контексте процесса, и, соответственно, единственный механизм, с помощью которого обработчики нижних половин могут переходить в состояние ожидания. Это означает, что они полезны в ситуациях, когда необходимо выделять много памяти, захватывать семафор или выполнять блочные операции ввода-вывода. Если для выполнения отложенных операций нет необходимости использовать поток ядра, то стоит подумать об использовании тасклетов.
Реализация очередей отложенных действий
В своей наиболее общей форме подсистема очередей отложенных действий — это интерфейс для создания потоков пространства ядра, которые выполняют некоторые действия, где-то поставленные в очередь. Эти потоки ядра называются рабочими потоками (worker threads). Очереди действий позволяют драйверам создавать специальные рабочие потоки ядра для того, чтобы выполнять отложенные действия. Кроме того, подсистема очередей действий содержит рабочие потоки ядра, которые работают по умолчанию. Поэтому в своей общей форме очереди отложенных действий — это простой интерфейс пользователя для откладывания работы, которая будет выполнена потоком ядра.
Рабочие потоки, которые выполняются по умолчанию, называются events/n, где n — номер процессора. Для каждого процессора выполняется один такой поток. Например, в однопроцессорной системе выполняется один поток events/0. Б двухпроцессорной системе добавляется еще один поток— events/1. Рабочие потоки, которые выполняются по умолчанию, обрабатывают отложенные действия, которые приходят из разных мест. Многие драйверы, которые работают в режиме ядра, откладывают обработку своих нижних половин с помощью потоков, работающих по умолчанию. Если для драйвера или подсистемы нет строгой необходимости в создании своего собственного потока ядра, то использование потоков, работающих по умолчанию, более предпочтительно.
Тем не менее ничто не запрещает коду ядра создавать собственные потоки. Это может понадобиться, если в рабочем потоке выполняется большое количество вычислительных операций. Операции, критичные к процессорным ресурсам или к высокой производительности, могут получить преимущества от использования отдельного выделенного потока. Это также уменьшает нагрузку на потоки, работающие по умолчанию, и предотвращает нехватку ресурсов для остальных отложенных действий.