7.6. Очереди задач | |
Очереди задач (workqueue) поверхностно похожи на тасклеты; они позволяют коду ядра запросить, какая функция будет вызвана в будущем. Есть, однако, некоторые существенные различия между ними, в том числе:
• | Тасклеты работают в контексте программного прерывания, в результате чего весь код тасклета должен быть атомарным. Вместо этого, функции очереди задач выполняются в контексте специального процесса ядра; в результате чего они имеют больше гибкости. В частности, функции очереди задач могут спать. |
• | Тасклеты всегда работают на процессоре, который их изначально запланировал. Очереди задач по умолчанию работают таким же образом. |
• | Код ядра может дать запрос, чтобы отложить выполнение функций очереди задач на заданный интервал. |
Ключевым различием между ними двумя является то, что тасклеты выполняются быстро, в течение короткого периода времени и в атомарном режиме, а функции очереди задач могут иметь более высокие задержки, но не должны быть атомарными. Каждый механизм имеет ситуации, когда он уместен.
Очереди задач имеют тип struct workqueue_struct, которая определена в . Очередь задач должна быть явно создана перед использованием, используя одну из двух следующих функций:
struct workqueue_struct *create_workqueue(const char *name);
struct workqueue_struct *create_singlethread_workqueue(const char *name);
Каждая очередь задач имеет один или более специализированных процессов ("потоки ядра"), которые запускают функции, помещённые в очередь. Если вы используете create_workqueue, вы получите очередь задач, которая имеет специальный поток для каждого процессора в системе. Во многих случаях все эти потоки являются просто излишними; если одного рабочего потока будет достаточно, вместо этого создайте очередь задач с помощью create_singlethread_workqueue.
Чтобы поместить задачу в очередь задач, необходимо заполнить структуру work_struct. Этот можно сделать во время компиляции следующим образом:
DECLARE_WORK(name, void (*function)(void *), void *data);
Где name является именем структуры, которая должна быть объявлена, function является функцией, которая будет вызываться из очереди задач, и data является значением для передачи в эту функцию. Если необходимо создать структуру work_struct во время выполнения, используйте следующие два макроса:
INIT_WORK(struct work_struct *work, void (*function)(void *), void *data);
PREPARE_WORK(struct work_struct *work, void (*function)(void *), void *data);
INIT_WORK делает более серьёзную работу по инициализации структуры; вы должны использовать его в первый раз при создании структуры. PREPARE_WORK делает почти такую же работу, но он не инициализирует указатели, используемые для подключения в очередь задач структуры work_struct. Если есть любая возможность, что в настоящее время структура может быть помещена в очередь задач и вы должны изменить эту структуру, используйте PREPARE_WORK вместо INIT_WORK.
Для помещения работы в очередь задач cуществуют две функции:
int queue_work(struct workqueue_struct *queue, struct work_struct *work);
int queue_delayed_work(struct workqueue_struct *queue, struct work_struct *work, unsigned long delay);
Любая из них добавляет work к данной очереди. Однако, если используется queue_delayed_work, фактическая работа не выполняется, пока по крайней мере не пройдёт delay тиков. Возвращаемым значением этих функций является 0, если work была успешно добавлена в очередь; ненулевой результат означает, что эта структура work_struct уже ожидает в очереди и не была добавлена во второй раз.
В некоторое время в будущем функция work будет вызвана с заданным значением data. Эта функция будет работать в контексте рабочего потока, поэтому он может заснуть в случае необходимости, хотя вы должны знать, как этот сон может повлиять на любые другие задачи, находящиеся в той же очереди задач. Однако, такая функция не может получить доступ в пользовательское пространство. Так как она работает внутри в потоке ядра, просто нет доступа в пользовательское пространства.
Если вам необходимо отменить ожидающую запись в очереди задач, можно вызвать:
int cancel_delayed_work(struct work_struct *work);
Возвращаемое значение отлично от нуля, если запись была отменена ещё до начала исполнения. Ядро гарантирует, что выполнение данной записи не будет начато после вызова cancel_delayed_work. Однако, если cancel_delayed_work возвращает 0, запись уже может работать на другом процессоре и может всё ещё быть запущена после вызова cancel_delayed_work. Чтобы иметь абсолютную уверенность, что функция work не работает нигде в системе после того, как cancel_delayed_work вернула 0, вы должны затем сделать вызов:
void flush_workqueue(struct workqueue_struct *queue);
После возвращения flush_workqueue, никакая из функций, помещённых перед этим для вызова в очередь, больше нигде в системе не работает.
Когда вы закончите с очередью задач, можно избавиться от неё:
void destroy_workqueue(struct workqueue_struct *queue);
Драйвер устройства во многих случаях не нуждается в своей собственной очереди задач. Если вы только изредка помещаете задачи в очередь, может оказаться более эффективным просто использовать общую очередь задач по умолчанию, предоставляемую ядром. Однако, при использовании этой очереди, вы должны знать, что будете делить её с другими. Среди прочего, это означает, что вы не должны монополизировать очередь на длительные периоды времени (не использовать длительные засыпания) и вашим задачам может потребоваться больше времени для получения ими процессора.
Модуль jiq ("только в очередь") экспортирует два файла, которые демонстрируют использование общей очереди задач. Они используют одну структуру work_struct, которая создана таким образом:
static struct work_struct jiq_work;
/* это строка в jiq_init( ) */
INIT_WORK(&jiq_work, jiq_print_wq, &jiq_data);
Когда процесс читает /proc/jiqwq, модуль начинает серию перемещений по общей очереди задач без каких-либо задержек. Функция использует это:
int schedule_work(struct work_struct *work);
Обратите внимание, что при работе с общей очередью используется другая функция; в качестве аргумента требуется только структура work_struct. Фактический код в jiq выглядит следующим образом:
prepare_to_wait(&jiq_wait, &wait, TASK_INTERRUPTIBLE);
schedule_work(&jiq_work);
schedule( );
finish_wait(&jiq_wait, &wait);
Реальная рабочая функция выводит строку так же, как делает модуль jit, затем, в случае необходимости, повторно помещает структуру work_struct в очередь задач. Вот jiq_print_wq полностью:
static void jiq_print_wq(void *ptr)
{
struct clientdata *data = (struct clientdata *) ptr;
if (! jiq_print (ptr))
return;
if (data->delay)
schedule_delayed_work(&jiq_work, data->delay);
else
schedule_work(&jiq_work);
}
Если пользователь читает устройство с задержкой (/proc/jiqwqdelay), рабочая функция повторно помещает себя в режиме задержки с помощью schedule_delayed_work:
int schedule_delayed_work(struct work_struct *work, unsigned long delay);
Если вы посмотрите на вывод из этих двух устройств, он выглядит примерно так:
% cat /proc/jiqwq
time delta preempt pid cpu command
1113043 0 0 7 1 events/1
1113043 0 0 7 1 events/1
1113043 0 0 7 1 events/1
1113043 0 0 7 1 events/1
1113043 0 0 7 1 events/1
% cat /proc/jiqwqdelay
time delta preempt pid cpu command
1122066 1 0 6 0 events/0
1122067 1 0 6 0 events/0
1122068 1 0 6 0 events/0
1122069 1 0 6 0 events/0
1122070 1 0 6 0 events/0
Когда /proc/jiqwq читается, между печатью каждой строки нет очевидной задержки. Когда вместо этого читается /proc/jiqwqdelay, есть задержка ровно на один тик между каждой строкой. В любом случае мы видим одинаковое печатаемое имя процесса; это имя потока ядра, который выполняет общую очередь задач. Номер процессора напечатан после косой черты; никогда не известно, какой процессор будет работать при чтении /proc файла, но рабочая функция в последующий период всегда будет работать на том же процессоре.
Если необходимо отменить работающую запись, помещённую в общую очередь, можно использовать cancel_delayed_work, как описано выше. Однако, очистка (flushing) общей очереди задач требует отдельной функции:
void flush_scheduled_work(void);
Поскольку не известно, кто ещё может использовать эту очередь, никогда не известно, сколько времени потребуется для возвращения flush_scheduled_work.
Комментариев нет:
Отправить комментарий
Примечание. Отправлять комментарии могут только участники этого блога.