06 сем Лекция 7 - chrislvt/OS GitHub Wiki
Мы продолжаем рассматривать отложенные действия. Мы с вами сказали, что аппаратные прерывания, то есть в первую очередь прерывания от внешних устройств в современных системах рассматриваются как медленные и делятся на две части историческое название top half и bottom half. Под верхней половиной понимается обработчик аппаратного прерывания, который устанавливается в системе с помощью функции request_irq. То что делаем в лабах.
Как правило такой обработчик прерывания должен выполнять минимальный объем действий и завершаться как можно быстрее. Обычно такой обработчик прерывания сохраняет даные, поступившие от внешнего устройства, в буффере ядра. Но для того чтобы обработать прерывания полностью, обработчик аппаратного прерывания должен инициализировать путем постановки в очередь на выполнение отложенное действие. Обработчик прерывания должен поставить тасклет в очередь на выполнение, это делается с помощью фунции tasklet_scheduler.
В современных системах существует три механизма выполнения отложенных действий: softirq, tasket, queue work (очередь работ).
Продолжим рассматривать softirq.
В системе существует возможность определения 32 softirq, но в настоящее время определено 10. В системе существует перечень softirq, мы с вами оформили это виде таблицы. Фактически название softirq это его индекс в векторе softirq_vec. Цифры которые мы видим от 0 до 9 это приоритет. Чем меньше цифра, тем выше приоритет. 0 приоритет имеет HI_SOFTIRQ. TASKLET_SOFTIRQ имеет приоритет 6.
Рассмотрим функции для работы softirq.
Tasklet является одним из типов softirq. Отличия softirq и tasklet. softirq инициализируется статически при компиляции ядра. Для того чтобы добавить свой softirq нужно перекомпилировать ядро, но если писать свой softirq, то он должен иметь приоритет выше тасклета (иначе не имеет смысла). Между softirq и тасклетами существует значительное различие связанное с возможностями выполнения. softirq хорошо масштабируется в многопроцессорных системах. Одно и то же softirq может одновременно выполняться на разных процессорах. В силу этого в теле softirq должно выполняться взаимоисключение. Код softirq должен быть реентерабельным. В нём должно реализовываться взаимоисключение, при этом блокироваться softirq не может и использоваться может только команда test_and_set. (обычно используются spinlock, но это макрокоманда в которой используется test_and_set). Тасклеты тоже не могут блокироваться, но tasklet в каждый момент времени может выполняться только в единственном экземпляре. В силу этого к тасклетам не предъявляются такие жесткие требования по реентерабельности. Прервать выполнение softirq может только аппаратное прерывание.
В файле <linux/interrupt.h> определена структура. Эта структура определена начиная с ядра 2.6.37 struct softirq_action.
Начиная с этого ядра в этой структуре одно единственное поле
struct softirq_action{
void (*action)(struct softirq_action*);
};
Когда ядро выполняет обработчик отложенного прерывания, то функция action вызывается с указателем на структуру softirq_action в качестве параметра.
Как мы сказали, везде рекомендуется не создавать собственные softirq, но в системе есть необходимые для этого функции.
void open_softirq(int nr, void (*action)(struct softirq_action *)) //определена в kernel/softirq.c
{
softirq_vec[nr].action = action;
}
int nr - индекс в массиве softirq_vec
void (*action)(struct softirq_action *) - указатель на функцию softirq которая будет выполняться
Массив softirq_vec (из прошлой лекции). Размер определяется константой NR_SOFTIRQS которая равна 10. Была табличка, все перечисленные в таблице имена были представлены нами в массиве softirq_to_name размером NR_SOFTIRQS.
Всё перечисленное можно увидеть с помощью /proc/softirqs. В соответствии со сказанным, функция open_softirq заполняет вектор softirq_vec заданным типом softirq_action. То есть эта фукция фактически выполняет регистрацию отложенного действия софтирку. Эта зарегестрированная софтирку ставится в очередь на выполнение с помощью функции raise_softirq.
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irgoff(nr);
local_irq_restore(flags);
}
макросы local_irq_save и local_irq_restore работают с флагами.
local_irq_save сохраняет значение флага AF(Interrupt Flag) регистра флагов EFLAGS(32 разряда) и отключает прерывание на локальном процессоре.
AF – флаг полупереноса или вспомогательного переноса . Устанавливается в 1, если в результате предыдущей операции произошёл перенос (или заём) из третьего бита в четвёртый.
local_irq_restore восстанавливает значение флага на AF регистра EFLAGS и разрешает прерывания.
Функция raise_softirq_irgoff должна выполняться с разрешенными irq.
inline void raise_softirq_irgoff(unsigned int nr)
{
__raise_softirq_irgoff(nr);
/* если мы в прерывании или в softirq то завершаем. Мы фактически запустим softirq после того как вренемся из irq или softirq иначе пробуждаем демона ksoftirq_daemon чтобы убедиться что мы запланировали softirq в ближайшее время. Фактически это ключевой момент - пробуждение демона. В системе на каждый процесс есть ksoftirq_daemon/<num> Таких демонов будет столько столько процессоров. */
if (!in_interrupt)
makeup_ksoftirqd();
}
Если 4-х ядерная система то это будет ksoftirqd/0, ksoftirqd/1, ksoftirqd/2, ksoftirqd/3. (ksoftirq daemon)
Проверка ожидающих выполнения обработчиков отложенных прерываний осуществляется в следующих случаях:
- при возврате из аппаратного прерывания;
- в контектсе потока ядра ksoftirqd.
- в любом коде ядра в котором явно проверяются и запускаются ожидающие обработки отложенные прерывания, как например это делается в сетевой подсистеме.
Независимо от способа вызова softirq, его выполнение осуществляется в функци do_softirq(). Функция do_softirq в цикле проверяет наличие отложенных прерываний. То есть каждый поток ядра ksoftirqd выполняет функцию run_ksoftirqd(), которая проверяет наличие отложенных прерываний и вызывает функцию do_softirq() в зависимости от результата проверки. Эта функция считывает битовую маску softirq __softirq_pending (Пендинг локального процессора) и выполняет отложенные функции соответсвтующие каждому установленному биту.
Во время выполнения отложенной функции могут возникнуть новые ожидающие softirq. Основная проблема здесь заключается в том, что выполнение кода пространства пользователя (то есть приложений) может быть отложено на длительное время пока функция __do_softirq обрабатывает отложенное прерывание. Для исключения такой ситуации устанавливается предел времени выполнения - MAX_SOFTIRQ_TIMER.
В качестве примера рассмотрим обработку прерываний от сетевого адаптера. У нас с вами есть softirq NET_RX_SOFTIRQ - отложенное действие, предназначенное для приема сетевых пакетов. Есть softirq для отправки сетевых пакетов, но мы рассмотрим прием. Для приемки сетевых пакетов(естественно когда приходит сетевой пакет возникает аппаратное прерывание), обработчик прерывания от сетевого адаптера просто копирует пришедший пакет в ядро. В ядре пакет ставится в так называемую буферную очередь, в которой ожидает обработки соответствующим потоком ядра. Обработчик прерывания от сетевого адаптера завершается вызовом соответствующего softirq и эта softirq закончит обработку данного сетевого пакета. Давайте посмотрим на схеме, какие действия при этом выполняются. Обратить внимаение, что демон, то есть kernel_softirq_daemon запускает соответствующие softirq. То есть каждое отдельное действие в системе выполняется как отдельный поток. Речь идет об SMP архитектурах.
потом добававила в рисунок пунктов
Первое действие - создание демона ядра ksoftirqd. Это поток, создается один для каждого процессора. Например на CPU. Он начинает выполнять циклы обработки с помощью run_ksoftirqd(). Выполняет функцию run_ksoftirq(), которая проверяет наличие отложенных прерываний и в зависимости от результата проверки вызывает функцию do_softirq().
Существует битовое поле которое проверяется этой функцией, в результате под третьим пунктом 3 шаг - создается на каждый процессор лист опроса - poll_list. poll - опрос в цикле.
net_dev_init регистрирует softirq_handler для NET_RX_SOFTIRQ, который вызывает net_rx_action(). Затем так называемый NAPI poller добавляет poll_list и драйвер softirq устанавливает бит softirq_pending. Функция run_ksoftirqd проверяет биты битового поля softirq_pending. Если pending, то вызывается функция __do_softirq() которая выполняет зарегестрированный обработчик для ожидающего softirq (NET_RX_SOFTIRQ).
(та же картинка более детально)
NAPI (network API)
Сетевая подсистема линукс построена на стеке BSD. В этой подсистеме прием и передача данных на транспортном и сетевом уровнях происходят с помощью интерфейса сокетов. Основная цель NAPI - сократить количество прерываний, генерируемых при получени пакета. В NAPI механизм прерываний сочетается с механизмом опроса. В NAPI совместимых драйверах прерывания отключается когда приходит пакет. В этом случае обработчик только вызывает rx_scheduler, то есть планировщих, который гарантирует, что в дальнейшем обработка пакета будет выполнена. Здесь также есть интересные моменты которые следует отразить.
(это действия ядра) Поступление сетевых пакетов
здесь упоминается DMA. DMA - Direct Memory Access(прямой доступ к памяти). То есть сохранение данных в памяти происходит минуя регистры процессора(процессор в это время свободен). Для этого в состав системы входит 1 или несколько контроллеров DMA. Они берут на себя задачу пересылки данных от внешнего устройства в оперативную память.
В нашей диаграмме (рис 1) шаги 5 - 8 относятся к поступлению данных для обработки, то есть относятся к поступлению сетевых пакетов. И как раз поступление сетевых пакетов уточняется второй диаграммой. Как мы видим, когда приходит пакет, сетевая карта использует DMA для того чтобы записать пакет в оперативную память. Для приема пакетов используется кольцевой буфер (но видимо предполагается что из этого кольцевого буфера эти пакеты выбираются). Следует отметить, что NIC сетевая карта является мультиочередной (то есть в ней много очередей), то есть таких кольцевых буферов в оперативной памяти может существовать много. Hас собственно интересует не совсем это. На этом примере мы показываем необходимость отложенной обработи. Как мы видим, это довольно затратное действие в системе, которое не может выполняться на очень высоких уровнях приоритетов(когда запрещены прерывания). Это реализуется с помощью softirq типа NET_RX_SOFTIRQ. Мы также видим роль ksoftirqd (kernel softirq daemon). То есть, именно он выполняет соответствующие softirq.
Еще раз хочу обратить внимание, что функции для работы с тасклетами излагаются на семинаре.
Tasklet ставится в очередь с помощью функции tasklet_schedule().
static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set(TASKLET_STATE_SHED, &t->state))
__tasklet_schedule(t);
}
__tasklet_schedule(t) вызывает __tasklet_schedule_common(), а она в свою очередь вызывает
static void__tasklet_schedule_common(struct tasklet_struct *t, struct tasklet_head_percpu *headp, unsigned int softirq_nr)
{
struct tasklet_head *head;
unsigned long flags;
local_irq_save(flags);
...
raise_softirq_irgoff(softirq_nr);
local_irq_restore(flags);
}
//tasklet_head_percpu -значит что выполняется на том процессоре, на котором выполнялось аппаратное прерывание. Уже здесь мы видим что передаётся индекс вектор softirq и видим что tasklet на самом деле планируется с помощью функции raise_softirq_irgoff(softirq_nr).
Очереди работ являются очень важным механизмом ядра, особенно драйверов. Этот механизм, который получил название lock_queue был добавлен в линукс в версии ядра 2.5. В отличие от тасклетов которые как говорят являются однократной схемой исполнения, очереди работ являются обобщенным механизмом отложенного выполнения в котором функция обработчика рализующая соответствующие действия может блокироваться(или засыпать). Отсюда, очереди работ имеют большие задержки при выполнении (или как говорят латентность), но в силу того что они устроены несколько сложнее, для работы с ними определено большее количество API. В основе выполнения работ в очереди работ лежат две структуры
- workqueue_struct структура которая описывает очередь
2)work_struct - описывает работу.
work_queue_struct фактически описывает пул работ. Это довольно большая структура
struct workqueue_struct
{
struct list_head pwqs;
struct list_head list;
...
struct pool_workqueue *dtl_pwq; /*pw:only for unboard wqs*/
....
struct pool_workqueue __percpu *cpu_pwqs;
....
};
Кроме перечисленного здесь есть еще поля, но мы видим что во первых в системе имеется двухсвязный список всех пулов очередей работ и двухсвязный список очередей работ. Здесь мы также видим присутствует структура pool_workqueue. struct pool_workqueue *dtl_pwq; /pw:only for unboard wqs/ - не привязанные к конкретному процессу. На каждый процессор существует соответствующий пул очередей работ.
struct work_struct
{
atomic_long_t data;
struct list_head entry;
work_func_t func;
...
};
Мы видим что очередь это двухсвязный список и struct work_struct предоставляет нам entry. В этой структуре находится функция которая определена для выполнения действий соответствующей работы (которая собственно и ставится в очередь) и какие-то данные.