Верхние и нижние половины
Одной из основных проблем при обработке прерывания является выполнение в обработчике длительных задач. Часто в ответ на прерывание устройства должна быть проделана значительная часть работы, но обработчику прерывания необходимо завершиться быстро и не держать прерывания надолго заблокированными. Эти две потребности (работа и скорость) конфликтуют друг с другом, оставляя автора драйвера немного связанным.
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, могут генерировать много прерываний за короткий срок, поэтому нередко поступает несколько до выполнения нижней половины. Драйверы должны всегда быть готовы к этому и должны быть в состоянии определить объём работы на основе информации, оставленной верхней половиной.
Напомним, что очереди задач вызывают функцию когда-нибудь в будущем в контексте специального рабочего процесса. Поскольку функция очереди задач выполняется в контексте процесса, она может заснуть, если это будет необходимо. Однако, вы не можете копировать данные из очереди задач в пользовательское пространство, если вы не используете современные методики, которые мы покажем в Главе 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.
Комментариев нет:
Отправить комментарий
Примечание. Отправлять комментарии могут только участники этого блога.