Главная » Ядро Linux » Очереди  отложенных действий

1

Очереди  отложенных действий (work queue)  — это еще  один  способ  реализации отложенных операций, который отличается от рассмотренных ранее.  Очереди  действий  позволяют откладывать некоторые операции для  последующего выполнения потоком пространства ядра — отложенные действия всегда выполняются в контексте процесса. Поэтому код, выполнение которого отложено с помощью постановки в  очередь  отложенных действий, получает  все  преимущества, которыми обладает код, выполняющийся в контексте процесса. Наиболее важное свойство — это то, что выполнение очередей действий управляется планировщиком процессов и, соответственно, выполняющийся код может переходить в состояние ожидания (sleep).

Обычно принять решение о том, что необходимо использовать: очереди  отложенных  действий или  отложенные прерывания/тасклеты,  достаточно просто.  Если отложенным действиям необходимо переходить в состояние ожидания, то следует использовать очереди  действий. Если  же отложенные операции не могут переходить в состояние ожидания, то воспользуйтесь тасклетами или отложенными прерываниями.  Обычно альтернатива использованию очередей отложенных действий — это создание новых потоков пространства ядра.  Поскольку при введении новых  потоков пространства ядра разработчики ядра обычно  хмурят брови  (а у некоторых народов это  означает смертельную обиду), настоятельно рекомендуется использовать очереди отложенных действий. Их действительно очень просто  использовать.

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

7 Отсутствие строгой  последовательности выполнени я хорошо  сказывается   на производительности, но  приводит  к усложнению  программирования . Конвертаци я обработчико в ВН  в обработчики  тасклетов,   например, требует тщательного  осмысления того,  безопасно  ли  выполнять этот  же код  в то же самое  время  другим  тасклетом?  Однако  после  того  как  конвертаци я выполнена, это  окупается повышением производительности.

Реализация очередей отложенных действий

В своей наиболее общей форме подсистема очередей отложенных действий — это интерфейс для создания потоков пространства ядра, которые выполняют некоторые действия, где-то поставленные в очередь. Эти потоки ядра называются рабочими потоками (worker threads). Очереди действий позволяют драйверам создавать специальные рабочие потоки ядра для того, чтобы выполнять  отложенные действия. Кроме того, подсистема  очередей действий  содержит рабочие  потоки  ядра,  которые  работают по умолчанию.  Поэтому в своей общей форме очереди отложенных действий — это простой интерфейс  пользователя для откладывания  работы, которая будет выполнена потоком ядра.

Рабочие  потоки,  которые выполняются  по умолчанию,  называются  events/n , где п—номер процессора.  Для каждого процессора  выполняется  один такой поток. Например,  в однопроцессорной системе выполняется  один поток events/0 . Б двухпроцессорной системе добавляется  еще один поток— events/1 . Рабочие  потоки, которые  выполняются   по умолчанию,  обрабатывают отложенные  действия,  которые приходят из разных мест. Многие драйверы,  которые работают в режиме ядра, откладывают обработку своих нижних половин  с помощью потоков,  работающих по умолчанию.  Если для драйвера или подсистемы  нет строгой необходимости  в создании своего собственного  потока ядра, то использование  потоков,  работающих по умолчанию,  более предпочтительно.

Тем не менее ничто не запрещает коду ядра создавать собственные  потоки.  Это может понадобиться,  если в рабочем потоке выполняется  большое количество вычислительных  операций.  Операции,  критичные  к процессорным ресурсам или к высокой  производительности, могут получить преимущества  от использования отдельного выделенного  потока.  Это также уменьшает нагрузку на потоки,  работающие по умолчанию,  и предотвращает  нехватку ресурсов для остальных отложенных действий.

Структуры данных для представления потоков

Рабочие потоки представлены с помощью следующей структуры workqueue_

struct .

/*

* Внешне видимая абстракция для представления очередей отложенных действий представляет собой массив очередей для каждого процессора:

*/

struct workqueue_struct {

struct cpu_workqueue_struct cpu_wq [NR_CPUS] ;

const char* name;

struct list_head list;

};

Эта структура содержит  массив  структур  struc t    cpu_workqueue_struct ,  пo одному экземпляру на каждый возможный процессор в системе. Так как рабочий поток существует для каждого процессора  в системе,  то для каждого рабочего потока, работающего на каждом процессоре  машины,  существует такая структура.

Структура cpu_workqueue_struct определена в  файле kernel/workqueue.с и является основной. Эта структура показана ниже.

/*

* Очередь отложенных действий, связанная с процессором:

*/

struct cpu_workqueue_struct {

spinlock_t lock; /* Очередь для защиты данной структуры */

long rernove_sequence;  /* последний добавленный элемент

(следующий для запуска ) */

long insert sequence;  /* следующий элемент для добавления */ struct list_head worklist;    /* список действий */ wait_queue_head_t more_work;

wait_queue_head_t work_done;

struct workqueue_struct *wq;  /* соответствующая структура workqueue_struct */

task_t *thread; /* соответствующий поток */

int run_depth;  /* глубина рекурсии функции run_workqueue() */

};

Заметим,   что  каждый   тип  рабочих  потоков   имеет  одну,  связанную   с  этим  типом структуру  workqueue_struct .   Внутри   этой   структуры  имеется   по  одному  экземпляру структуры cpu_workqueue_struc t  для  каждого рабочего потока и,  следовательно, для  каждого   процессора  в  системе,   так  как  существует  только   один  рабочий   поток каждого  типа  на  каждом   процессоре.

Структуры для представления действий

Все  рабочие   потоки   реализованы  как  обычные   потоки   пространства  ядра,   которые  выполняют  функцию   worker_threa d  ().  После   начальной   инициализации  эта функция  входит  в  бесконечный  цикл  и  переходит  в  состояние   ожидания.  Когда  какие-либо  действия   ставятся   в  очередь,   поток  возвращается  к  выполнению  и  выполняет  эти  действия.   Когда  в  очереди  не  остается   работы,   которую  нужно  выполнять, поток   снова  возвращается  в  состояние   ожидания.  Каждое  действие   представлено   с помощью    структуры   work_struct ,   определенной   в   файле    <linux/workqueue.h> . Эта  структура  показана   ниже.

struct work_struct {

unsigned long pending; /* ожидает ли это действие на выполнение?*/ struct list_head entry; /* связанный список всех действий */ void (*func)(void *) ;  /* функция-обработчик */

void *data;    /* аргумент функции-обработчика */

void *wq_data;  /* для внутреннего использования */

struct timer_list timer; /* таймер, который используется для очередей отложенных действий с задержками */

};

Эти структуры объединены в  связанный список,  по одному списку на каждый тип очереди для каждого процессора.  Например,  для каждого процессора существует  список отложенных действий, которые выполняются потоками,  работающими по умолчанию. Когда рабочий поток возвращается к выполнению, он начинает выполнять все  действия, которые находятся в  его  списке. После завершения работы рабочий  поток удаляет соответствующие  структуры work_struc t из списка. Когда список становится пустым, поток переходит в  состояние ожидания.

Давайте рассмотрим упрощенную основную часть функции  worker_threa d () .

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);

}

Эта функция выполняет следующие действия в бесконечном цикле.

• Поток переводит себя в  состояние ожидания (флаг состояния  устанавливается в значение TASK_INTERRUPTIBLE),  и текущий поток добавляется в очередь ожидания.

• Если связанный список действий пуст, то  поток вызывает функцию schedule ()

и переходит в  состояние ожидания.

• Если список не пуст, то  поток не переходит в состояние ожидания. Вместо  этого  он устанавливает свое состояние в значение TASK_RUNNING  и удаляет  себя из очереди ожидания.

• Если список не пустой, то  вызывается функция  run_workqueue ()  для  выполнения отложенных действий.

Функция run_workqueue ()

Функция  run_workqueue ()  в свою очередь выполняет сами отложенные действия, как показано ниже.

while (!list_empty(&cwq->worklist)) {

struct work_struct *work;

void (*f) (void *) ;

void *data;

work = list_entry(cwq->worklist.next, struct work_struct, entry);

f = work->func; data = work->data; list_del_init(cwq->worklist.next); clear_bit(0, &work->pending); f(data);

}

Эта  функция просматривает в цикле все  элементы списка отложенных действий и выполняет для  каждого элемента функцию,  на  которую указывает поле  fun c  соответствующей  структуры  workqueue_struct .  Последовательность действий  следующая.

• Если  список не  пустой, получить следующий элемент списка.

• Получить указатель на  функцию (поле   func) , которую необходимо вызвать,  и аргумент  этой  функции (поле  data) .

• Удалить  полученный элемент из  списка и  обнулить бит  ожидания в  структуре элемента.

• Вызвать  полученную функцию.

• Повторить указанные действия.

Извините,еслинепонятно

Взаимоотношения между  различными, рассмотренными в  этом  разделе  структурами  достаточно запутанные.  На  рис.  7.1  показана диаграмма, которая эти  взаимоотношения поясняет.

На  самом   верхнем уровне   находятся рабочие потоки.  Может   существовать несколько типов  рабочих  потоков. Для  каждого типа  рабочих  потоков существует один рабочий поток  для  каждого процессора.  Различные  части  ядра  при  необходимости могут  создавать рабочие потоки.  По  умолчанию выполняются  только   рабочие потоки   events  (события).  Каждый рабочий поток   представлен с  помощью  структуры cpu_workqueue_struct .  Структура  workqueue_struc t  представляет  все  рабочие потоки одного  типа.

Рабочий поток                                   Структура

cpu_workqueue_struct

Структура

workqueue_struct

По одному экземпляру на процессор

Один экземпляр на каждый тип рабочих потоков

Структура

work_struct

Один экземпляр

на каждую функцию отложенного действия

Рис.   7.1.  Соотношения между отложенными действиями,  очередями, действий  и  рабочими  потоками

Например, давайте   будем  считать,  что  в дополнение  к  обычному типу  рабочих потоков events был  создан   еще  один  тип  рабочих  потоков — falcon.  Также  имеется в распоряжении  четырехпроцессорный компьютер. Следовательно, выполняется четыре  потока типа  events (соответственно,  определено четыре  экземпляра структуры cpu_workqueue_struct )  и  четыре   потока типа  falcon  (для  которых тоже  определены   другие  четыре   экземпляра  структуры  cpu_workqueue_struct) .  Для  потоков типа  events определен один  экземпляр  структуры  workqueue_struct ,  а для  потоков типа falcon — другой  экземпляр этой  структуры.

На  самом  нижнем уровне  находятся отложенные действия.  Драйвер создает  отложенное действие,  которой должно выполниться  позже.   Действия представлены структурами work_struct .  Кроме других  полей, эта  структура содержит указатель на функцию,  которая должна  обработать отложенное  действие.  Отложенное  действие отправляется на  выполнение  определенному  потоку. Соответствующий поток  переводится  в состояние выполнения и выполняет отложенную работу.

Большинство  драйверов использует существующие по  умолчанию рабочие  потоки, которые называются events. Они  просты в реализации и в использовании. Однако в некоторых,  более  серьезных ситуациях необходимо  создавать новые   специальные рабочие потоки. Например,  драйвер файловой системы XFS  создает  два новых  типа рабочих   потоков.

Использование очередей отложенных действий

Использовать очереди действий просто.  Сначала мы  рассмотрим рабочие потоки, используемые по умолчанию, — events,  а затем  опишем создание новых  типов  рабочих  потоков.

Создание отложенных действий

Первый этап  — это  создание самого  действия,  которое должно быть  отложено. Для  создания статической структуры на этапе  компиляции  необходимо использовать следующий макрос.

DECLARE_WORK(name, void (*func) (void *), void *data);

Это  выражение  создает   структуру   works_truc t   с  именем  name,  с  функциейобработчиком  fun c  и  аргументом функции-обработчика  data .

Во  время  выполнения  отложенное действие можно создать  с помощью передачи указателя на  структуру, используя следующий макрос.

INIT_WORK(struct work_struct *work, void (*func)(void *),void *data);

Этот  макрос динамически  инициализирует отложенное действие,  на  структуру которого указывает указатель work, устанавливая  функцию-обработчик   fun c  и  аргумент  data .

Обработчик отложенного действия

Прототип  обработчика отложенного действия имеет  следующий вид.

void work_handler (void *data)

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

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

Планирование действий на выполнение

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

г

schedule_work(&work);

Действие планируется на выполнение  немедленно и будет выполнено,  как только рабочий поток  events , работающий  на данном  процессоре,  перейдет в состояние выполнения.

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

schedule_delayed_work (&work, delay);

В этом  случае действие,  представленное  структурой  work_struct ,  с адресом

&work, не будет выполнено,  пока не пройдет хотя бы заданное  в параметре dela y количество  импульсов таймера.  О том, как использовать  импульсы таймера для измерения  времени,  рассказывается  в главе 10, "Таймеры и управление временем".

Ожидание завершения действий

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

Для этого есть следующая функция,  которая  позволяет ждать, пока очередь действий events не будет очищена.

void  flush_scheduled_work(void);

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

Заметим, что  эта  функция не  отменяет никаких отложенных действий с  задержками.  Любые  действия, которые запланированы на выполнение с помощью функции schedule_delayed_wor k ()  и задержки которых еще  не  закончены, — не  очищаются с  помощью  функций   flush_scheduled_wor k () .  Для  отмены  отложенных действий с  задержками следует  использовать функцию

int cancel_delayed_work(struct work_struct *work);

Эта  функция  отменяет отложенное действие, которое связано  с данной структурой  work_struct , если  оно  запланировано.

Создание новых очередей отложенных действий

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

Новая очередь  действий и связанные с ней  рабочие потоки создаются с помощью простого вызова  функции.

struct workqueue_struct *create_workqueue(const char *name);

Параметр name   используется для  того,  чтобы  присваивать имена   потокам ядра. Например, очередь  events , которая используется по умолчанию,  создается с помощью  следующего  вызова.

structworkqueue_struct*keventd_wq=create_workqueue("events");

При  этом  также  создаются все  рабочие потоки (по  одному  на каждый процессор), которые подготавливаются к  выполнению работы.

Создание  отложенных действий выполняется  одинаково,  независимо  от  тина очереди. После  того  как  действия созданы, могут  быть  использованы функции,  аналогичные  функциям   schedule_work( )   и  schedule_delayed_work() ,  которые  отличаются тем,  что  работают с  заданной  очередью действий,  а  не  с  очередью,  используемой по  умолчанию.

int queue_work struct workqueue_struct *wq, struct work_struct *work);

intqueue_delayed_work(structworkqueue_struct*wq,

struct wesrk_struct *work, unsigned long delay);

И  наконец,  ожидание завершения действий в заданной очереди может  быть  выполнено с помощью функции

flush_workqueue(struct workqueue_struct *wq);

Эта  функция  работает   по  аналогии с  функцией   flush_scheduled_wor k () , как описывалось ранее, за  исключением того, что  она  ожидает, пока  заданная очередь не  станет  пустой.

Старый механизм очередей заданий

Так же как и в случае интерфейса ВН, который дал начало  интерфейсам отложенных прерываний (softirq)  и тасклетов (tasklet), интерфейс очередей действий возник благодаря недостаткам интерфейса очередей заданий (task queue).  Интерфейс очередей  заданий (который еще называют просто  tq), так же как  и тасклеты, не имеет ничего  общего  с заданиями (task), в смысле  с процессами8. Все подсистемы, которые использовали механизм очередей заданий, были  разбиты  на две группы  еще во времена  разработки серии  ядер  2.5.  Первая группа  была  переведена на использование тасклетов, а вторая—  продолжала использовать интерфейс очередей заданий. Все, что осталось  от интерфейса очередей заданий, перешло в интерфейс очередей отложенных  действий. Краткое рассмотрение очередей заданий, которым пользовались в течение  некоторого времени, — это хорошее  упражнение по истории.

Интерфейс  очередей заданий позволял определять набор  очередей. Очереди имели  имена, такие  как  scheduler  queue  (очередь  планировщика),  immediate queue (немедленная очередь)  или  timer  queue  (очередь  таймера). Каждая очередь  выполнялась  в определенных местах  в ядре.  Поток пространства ядра keventd выполнял работу, связанную с очередью  планировщика. Эта очередь  была предшественником интерфейса очередей отложенных действий. Очередь  таймера  выполнялась при каждом импульсе системного таймера, а немедленная очередь  выполнялась в нескольких местах, чтобы  гарантировать "немедленное" выполнение. Были  также  и другие очереди.  Кроме того, можно  было динамически создавать  новые  очереди.

Все это  может  показаться полезным, но  на практике интерфейс очередей заданий  приносил только  неприятности. Все очереди  были, по сути, оторваны от действительности. Единственной ценной очередью  оказалась очередь  планировщика, которая предоставляла единственную возможность, чтобы  выполнять отложенные действия в контексте процесса.

Еще  одним  преимуществом механизма очередей заданий была  простота интерфейса.  Несмотря на большое  количество очередей и разнообразие правил, по которым они выполнялись, интерфейс был максимально прост.  Все остальное, что касается очередей заданий, необходимо было убрать.

Различные использования очередей заданий были  заменены другими  механизмами обработки нижних половин; большинство — тасклетами. Осталось только  то, что касалось очереди  планировщика. В конце  концов, код демона  keventd был обобщен в отличный механизм очередей действий, который мы имеем  сегодня, а очереди  заданий  были  полностью удалены  из ядра.

Источник: Лав,  Роберт. Разработка ядра  Linux, 2-е  издание. : Пер.  с англ.  — М.  : ООО  «И.Д.  Вильяме» 2006. — 448 с. : ил. — Парал. тит. англ.

По теме:

  • Комментарии

1 комментарий

  1. Вадим says:

    У меня create_workqueue создает только один поток независимо от реального количество ядер процессора. Пробовал собирать для ядра 3.10 (x86_64) и 3.16 (i386), пробовал на 4х и 8ми ядерном железе — результат один, всегда создается лишь один поток и никакого реального спараллеливания не происходит. В чем может быть причина? Какие-то параметры компиляции или что-то ещё заинклюдить в исходник модуля?