Очереди задач

Предыдущая  Содержание  Следующая V*D*V

Очереди задач (workqueue) поверхностно похожи на микрозадачи; они позволяют коду ядра запросить, какая функция будет вызвана в будущем. Есть, однако, некоторые существенные различия между ними, в том числе:

 

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

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

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

 

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

 

Очереди задач имеют тип struct workqueue_struct, которая определена в <linux/workqueue.h>. Очередь задач должна быть явно создана перед использованием, используя одну из двух следующих функций:

 

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 тиков. Возвращаемое значение этих функций является ненулевым, если 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 (“just in queue”, "только в очередь") экспортирует два файла, которые демонстрируют использование общей очереди задач. Они используют одну структуру 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.

Предыдущая  Содержание  Следующая