Содержание


Обслуживание периферии в коде модулей ядра: Часть 60. Тасклеты и очереди отложенных действий

Comments

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

Тасклеты

Схема, показанная в предыдущей статье, является достаточно громоздкой, и во многих случаях её подменяют тасклеты— механизм на базе тех же softirq с двумя фиксированными индексами HI_SOFTIRQ или TASKLET_SOFTIRQ. Тасклеты — это частный случай реализации общего механизма softirq для этих фиксированных уровней. Тасклеты представляются в файле <linux/interrupt.h> с помощью структуры struct tasklet_struct:

struct tasklet_struct {
   struct tasklet_struct *next; /* указатель на следующий тасклет в списке */
   unsigned long state;         /* текущее состояние тасклета */
   atomic_t count;              /* счетчик ссылок */
   void (*func)(unsigned long); /* функция-обработчик тасклета*/
   unsigned long data;          /* аргумент функции-обработчика тасклета */
};

Поле state может принимать только одно из следующих значений: 0, TASKLET_STATE_SCHED, TASKLET_STATE_RUN. Значение TASKLET_STATE_SCHED указывает на то, что тасклет запланирован на выполнение, а значение TASKLET_STATE_RUN— что тасклет уже выполняется.

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

Схематически использование тасклета в коде полностью повторяет структуру кода для softirq. Инициализация тасклета выполняется во время инициализации модуля:

struct xxx_device_struct { /* структура с данными, специфичными для устройства */
  /* ... */
  struct tasklet_struct tsklt;
  /* ... */
}
void __init xxx_init() {
   struct xxx_device_struct *dev_struct;
   /* ... */
   request_irq( irq, xxx_interrupt, 0, "xxx", NULL );
   /* Initialize tasklet */
   tasklet_init( &dev_struct->tsklt, xxx_analyze, dev );
}

Для статического создания тасклета и обеспечения прямого доступа к нему могут использоваться два макроса:

DECLARE_TASKLET( name, func, data )
DECLARE_TASKLET_DISABLED( name, func, data );

Оба макроса статически создают экземпляр структуры struct tasklet_struct с указанным именем name. Второй макрос создает тасклет, но устанавливает для него значение поля count, равное единице, и, соответственно, этот тасклет будет запрещен для исполнения. Макрос DECLARE_TASKLET( name, func, data ) эквивалентен следующему фрагменту кода:

struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data };

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

Для того чтобы запланировать тасклет на выполнение, должна быть вызвана функция tasklet_schedule() (обычно это происходит в обработчике прерывания), которой в качестве аргумента передаётся указатель на соответствующий экземпляр структуры struct tasklet_struct:

/* обработчик прерывания */
static irqreturn_t xxx_interrupt( int irq, void *dev_id ) {
   struct xxx_device_struct *dev_struct;
   /* ... */
   /* отметить тасклет как запланированный для выполения */
   tasklet_schedule( &dev_struct->tsklt );
   return IRQ_HANDLED;
}

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

Если вместо стандартного тасклета нужно использовать высокоприоритетный тасклет (уровень HI_SOFTIRQ), то вместо функции tasklet_schedule() следует вызвать функцию планирования tasklet_hi_schedule().

Уже запланированный тасклет может быть запрещен к исполнению (временно) с помощью функции tasklet_disable(). Если тасклет в данный момент уже начал выполнение, то функция не возвратит управление, пока тасклет не закончит своё выполнение. Как альтернативу можно использовать функцию tasklet_disable_nosync(), которая запрещает указанный тасклет, но возвращается сразу, не ожидая, пока тасклет завершит выполнение. Это может быть небезопасно, так как в данном случае нельзя гарантировать, что выполнение тасклета завершилось. Вызов функции tasklet_enable() снова разрешает ранее запрещённый тасклет. Эта функция также должна быть вызвана, чтобы можно было выполнить тасклет, созданный с помощью макроса DECLARE_TASKLET_DISABLED(). Из очереди тасклетов, ожидающих выполнения, тасклет может быть удален с помощью функции tasklet_kill().

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

Демон ksoftirqd

Обработкой отложенных прерываний (softirq) и тасклетов занимаются отдельные потоки пространства ядра— по одному потоку на каждый процессор. Потоки пространства ядра помогают обрабатывать отложенные прерывания, когда система перегружена большим количеством отложенных прерываний. С помощью следующей команды ps можно посмотреть потоки ядра (их имена в листинге заключены в квадратные скобки [...]):

$ ps -ALf | head -n12
UID        PID  PPID   LWP  C NLWP STIME TTY          TIME CMD
root         1     0     1  0    1 08:55 ?        00:00:01 /sbin/init
...
root         4     2     4  0    1 08:55 ?        00:00:00 [ksoftirqd/0]
...
root         7     2     7  0    1 08:55 ?        00:00:00 [ksoftirqd/1]
...

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

Очереди отложенных действий (workqueue)

Очереди отложенных действий (workqueue) — это еще один, совершенно другой, способ реализации отложенных операций. Очереди отложенных действий позволяют откладывать некоторые операции для последующего выполнения отдельным потоком пространства ядра (эти потоки ядра называют рабочими потоками — worker threads). Такие отложенные действия всегда выполняются в контексте процесса (что принципиально отличает этот метод от рассмотренного ранее). Поэтому код, выполнение которого отложено с помощью постановки в очередь отложенных действий, получает все преимущества, которыми обладает код, выполняющийся в контексте процесса, главное из которых — это возможность переходить в блокированные состояния. Рабочие потоки, которые выполняются по умолчанию, называются events/n, где n— номер процессора, например:

$ ps -ALf | grep '\['events
root        15     2    15  0    1 17:08 ?        00:00:00 [events/0]
root        16     2    16  0    1 17:08 ?        00:00:00 [events/1]

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

typedef void (*work_func_t)( struct work_struct *work );
struct work_struct {
   atomic_long_t data;      /* аргумент функции-обработчика */
   struct list_head entry;  /* связанный список всех действий */
   work_func_t func;        /* функция-обработчик */
...
};

Для статического создания (на этапе компиляции) экземпляра действия необходимо использовать макрос:

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

В этом выражении создаётся экземпляр struct work_struct с именем name, с функцией-обработчиком func() и аргументом функции-обработчика data.

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

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

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

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

Сначала при инициализации модуля необходимо создать отложенное действие:

#include <linux/workqueue.h>
struct work_struct *hardwork;
void __init xxx_init() {
   /* ... */
   request_irq( irq, xxx_interrupt, 0, "xxx", NULL );
   hardwork = kmalloc( sizeof(struct work_struct), GFP_KERNEL );
   /* инициализировать экземпляр действия */
   INIT_WORK( hardwork, xxx_analyze, data );
}

Экземпляр действия можно определить и статически:

#include <linux/workqueue.h>
DECLARE_WORK( hardwork, xxx_analyze, data );
void __init xxx_init() {
   /* ... */
   request_irq( irq, xxx_interrupt, 0, "xxx", NULL );
}

Самое интересное начинается, когда нужно запланировать отложенное действие. При использовании для этого рабочего потока ядра по умолчанию (events/n) применяются следующие функции:

  • schedule_work( struct work_struct *work )— действие планируется для немедленного выполнения и будет выполнено, как только рабочий поток events, работающий на данном процессоре, перейдёт в состояние выполнения.
  • schedule_delayed_work( struct delayed_work *work, unsigned long delay )— в этом случае запланированное действие не будет выполнено, пока не пройдёт хотя бы заданное в параметре delay количество импульсов системного таймера.

В обработчике прерывания планирование действия выглядит так:

static irqreturn_t xxx_interrupt( int irq, void *dev_id ) {
   /* ... */
   schedule_work( hardwork );
   /* или schedule_work( &hardwork ); - для статической инициализации */
   return IRQ_HANDLED;
}

Часто бывает необходимо дождаться, пока очередь отложенных действий очистится (все отложенные действия завершатся), для чего используется функция:

void flush_scheduled_work( void );

Для отмены незавершённых отложенных действий с задержками используется функция:

int cancel_delayed_work( struct work_struct *work );

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

struct workqueue_struct *create_workqueue( const char *name );
struct workqueue_struct *create_singlethread_workqueue( const char *name );

Планирование на выполнение в этом случае осуществляют функции:

int queue_work( struct workqueue_struct *wq, struct work_struct *work );
int queue_delayed_work( struct workqueue_struct *wq,
                        struct wesrk_struct *work, unsigned long delay);

Они аналогичны рассмотренным выше schedule_*(), но работают с созданной очередью, указанной в первом параметре. С вновь созданными потоками предыдущий фрагмент может выглядеть так:

struct workqueue_struct *wq;
/* инициализация модуля драйвера */
static int __init xxx_init( void ) {
   /* ... */
   request_irq( irq, xxx_interrupt, 0, "xxx", NULL );
   hardwork = kmalloc( sizeof(struct work_struct), GFP_KERNEL );
   /* инициализация экземпляра действия */
   INIT_WORK( hardwork, xxx_analyze, data );
   wq = create_singlethread_workqueue( "xxxdrv" );
   return 0;
}
static irqreturn_t xxx_interrupt( int irq, void *dev_id ) {
   /* ... */
   queue_work( wq, hardwork );
   return IRQ_HANDLED;
}

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

void flush_workqueue( struct workqueue_struct *wq );

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

Заключение

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


Ресурсы для скачивания


Похожие темы


Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Open source
ArticleID=932895
ArticleTitle=Обслуживание периферии в коде модулей ядра: Часть 60. Тасклеты и очереди отложенных действий
publish-date=06062013