Верхние и нижние половины

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

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

 

Linux (наряду со многими другими системами) решает эту проблему разделяя обработчик прерывания на две половины. Так называемая верхняя половина является процедурой, которая на самом деле отвечает на прерывание, той, которую вы зарегистрировали с помощью request_irq. Нижняя половина является процедурой, которая планируется верхней половиной, чтобы быть выполненной позднее, в более безопасное время. Большая разница между верхней половиной обработчика и нижней половиной в том, что во время выполнения нижней половины все прерывания разрешены, вот почему она работает в более безопасное время. В типичном сценарии верхняя половина сохраняет данные устройства в зависимый от устройства буфер, планирует свою нижнюю половину и выходит: эта операция очень быстрая. Затем нижняя половина выполняет всё то, что требуется, такое как пробуждение процессов, запуск другой операции ввода/вывода и так далее. Эта установка позволяет верхней половине обслужить новое прерывание, пока нижняя половина всё ещё работает.

 

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

 

Ядро Linux имеет два различных механизма, которые могут быть использованы для реализации обработки в нижней половине, оба они были представлены в Главе 7. Предпочтительным механизмом для обработки в нижней половине часто являются микрозадачи (tasklets), они очень быстры, но весь код микрозадачи должен быть атомарным. Альтернативой микрозадачам являются очереди задач (workqueues), могущие иметь большую задержку, но которые разрешают засыпать.

 

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

Микрозадачи

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

 

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

 

Микрозадачи должны быть объявлены с помощью макроса DECLARE_TASKLET:

 

DECLARE_TASKLET(name, function, data);

 

name является именем для передачи микрозадаче, function является функцией, которая вызывается для выполнения микрозадачи (она получает аргумент unsigned long и возвращает void) и data является значением unsigned long, которое будет передано функции микрозадачи.

 

Драйвер short декларирует свою микрозадачу следующим образом:

 

void short_do_tasklet(unsigned long);

DECLARE_TASKLET(short_tasklet, short_do_tasklet, 0);

 

Чтобы запланировать микрозадачу для работы, используется функция tasklet_schedule. Если short загружен с tasklet=1, он устанавливает другой обработчик прерывания, который сохраняет данные и планирует микрозадачу следующим образом:

 

irqreturn_t short_tl_interrupt(int irq, void *dev_id, struct pt_regs *regs)

{

    do_gettimeofday((struct timeval *) tv_head); /* приведение для остановки предупреждения 'volatile' */

    short_incr_tv(&tv_head);

    tasklet_schedule(&short_tasklet);

    short_wq_count++; /* запоминаем, что поступило прерывание */

    return IRQ_HANDLED;

}

 

Фактическая процедура микрозадачи, short_do_tasklet, будет выполнена в ближайшее время (скажем так), удобное для системы. Как упоминалось ранее, эта процедура выполняет основную работу обработки прерывания; она выглядит следующим образом:

 

void short_do_tasklet (unsigned long unused)

{

    int savecount = short_wq_count, written;

    short_wq_count = 0; /* мы уже удалены из очереди */

    /*

     * Нижняя половина читает массив tv, заполненный верхней половиной,

     * и печатает его в круговой буфер, который затем опустошается

     * читающими процессами

     */

 

    /* Сначала запишем число произошедших прерываний перед этой нижней половиной (bh) */

    written = sprintf((char *)short_head,"bh after %6i\n",savecount);

    short_incr_bp(&short_head, written);

 

    /*

     * Затем запишем значения времени. Пишем ровно 16 байт за раз,

     * так что запись выровнена с PAGE_SIZE

     */

 

    do {

        written = sprintf((char *)short_head,"%08u.%06u\n",

                            (int)(tv_tail->tv_sec % 100000000),

                            (int)(tv_tail->tv_usec));

        short_incr_bp(&short_head, written);

        short_incr_tv(&tv_tail);

    } while (tv_tail != tv_head);

 

    wake_up_interruptible(&short_queue); /* пробудить любой читающий процесс */

}

 

Среди прочего, этот такслет делает отметку о том, сколько пришло прерываний с момента последнего вызова. Устройства, такие как short, могут генерировать много прерываний за короткий срок, поэтому нередко поступает несколько до выполнения нижней половины. Драйверы должны всегда быть готовы к этому и должны быть в состоянии определить объём работы на основе информации, оставленной верхней половиной.

 

В приведённом выше примере есть ошибка:

 

void short_do_tasklet (unsigned long unused)

{

   int savecount = short_wq_count, written;

 

Очищать переменную <short_wq_count> таким способом небезопасно, если в это время произойдёт аппаратное прерывание, мы потеряем один (или может быть больше) тиков для <short_wq_count>. Для решения этой проблемы следует использовать <spin_lock_irqsave>

 

short_wq_count = 0; /* мы уже удалены из очереди */        

 

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

 

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

Напомним, что очереди задач вызывают функцию когда-нибудь в будущем в контексте специального рабочего процесса. Поскольку функция очереди задач выполняется в контексте процесса, она может заснуть, если это будет необходимо. Однако, вы не можете копировать данные из очереди задач в пользовательское пространство, если вы не используете современные методики, которые мы покажем в Главе 15; рабочий процесс не имеет доступа к адресному пространству любого другого процесса.

 

Драйвер short, если он загружен с опцией wq, установленной в ненулевое значение, для обработки в его нижней половине использует очередь задач. Он использует системную очередь задач по умолчанию, так что не требуется особого кода установки; если ваш драйвер имеет специальные требования латентности (задержки) (или может спать в течение длительного времени в функции очереди задач), вы можете создать свою собственную, предназначенную для этого очередь задач. Нам необходима структура work_struct, которая объявлена и проинициализирована так:

 

static struct work_struct short_wq;

 

    /* это строка в short_init( ) */

    INIT_WORK(&short_wq, (void (*)(void *)) short_do_tasklet, NULL);

 

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

 

При работе с очередью задач short устанавливает ещё один обработчик прерывания, который выглядит следующим образом:

 

irqreturn_t short_wq_interrupt(int irq, void *dev_id, struct pt_regs *regs)

{

    /* Получаем информацию о текущем времени. */

    do_gettimeofday((struct timeval *) tv_head);

    short_incr_tv(&tv_head);

 

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

    schedule_work(&short_wq);

 

    short_wq_count++; /* запоминаем, что поступило прерывание */

    return IRQ_HANDLED;

}

 

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

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