linux-interrupt-handler/

Из предыдущей заметки о модулях ядра Linux мы узнали, что такое контекст прерывания, и что в нем нельзя делать блокирующие вызовы. Но что, если я хочу повесить прерывание на нажатие кнопки, а в обработчике делать что-то блокирующее? Давайте разберемся.

Воспользуемся макетной платой, кнопкой, и подтягивающим резистором:

Обработка прерываний в модулях ядра Linux

… а также драйвером, который вешает прерывание на нажатие кнопки:

#include <linux/delay.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/module.h>

static int button_irq = 1 ;

static struct gpio buttons [ ] = {
{ 14 /* pin number */ , GPIOF_IN , «BUTTON1» } ,
} ;

static void bottomhalf_work_fn ( struct work_struct * work ) {
pr_info ( «Bottom half work starts n » ) ;
msleep ( 500 ) ;
pr_info ( «Bottom half work ends n » ) ;
}

DECLARE_WORK ( buttonwork , bottomhalf_work_fn ) ;

static irqreturn_t button_isr ( int irq , void * data ) {
if ( irq == button_irq ) {
schedule_work ( & buttonwork ) ;
}

return IRQ_HANDLED ;
}

int init_module ( void ) {
int ret ;
pr_info ( «%s n » , __func__ ) ;

ret = gpio_request_array ( buttons , ARRAY_SIZE ( buttons ) ) ;
if ( ret ) {
pr_err ( «gpio_request_array() failed: %d n » , ret ) ;
return ret ;
}

ret = gpio_to_irq ( buttons [ 0 ] . gpio ) ;
if ( ret < 0 ) {
pr_err ( «gpio_to_irq() failed: %d n » , ret ) ;
gpio_free_array ( buttons , ARRAY_SIZE ( buttons ) ) ;
return ret ;
}

button_irq = ret ;
ret = request_irq ( button_irq , button_isr ,
IRQF_TRIGGER_RISING ,
«gpiomod#button1» , NULL ) ;
if ( ret ) {
pr_err ( «request_irq() failed: %d n » , ret ) ;
gpio_free_array ( buttons , ARRAY_SIZE ( buttons ) ) ;
return ret ;
}

return 0 ;
}

void cleanup_module ( void ) {
pr_info ( «%s n » , __func__ ) ;

cancel_work_sync ( & buttonwork ) ;
free_irq ( button_irq , NULL ) ;
gpio_free_array ( buttons , ARRAY_SIZE ( buttons ) ) ;
}

MODULE_LICENSE ( «GPL» ) ;

Если теперь загрузить модуль и понажимать кнопку, то в dmesg мы увидим:

[3161764.904979] init_module
[3161770.817940] Bottom half work starts
[3161771.318026] Bottom half work ends
[3161771.318142] Bottom half work starts
[3161771.818216] Bottom half work ends
[3161855.764062] cleanup_module

Fun fact! Попробуйте сделать cat / proc / interrupts , пока модуль загружен.

Думается, что процедуры gpio_* в особых пояснениях не нуждаются. Здесь мы говорим, что по переднему фронту сигнала ( IRQF_TRIGGER_RISING ) на пине 14 должен вызываться обработчик button_isr() . С этого момента начинается самое интересное. Блокироваться в обработчике прерывания нельзя, и вернуться из него нужно как можно быстрее. Что делать?

Решение заключается в том, чтобы запланировать выполнение некоторой работы, но в самом обработчике ее не делать. Эта запланированная работа называется нижней половиной (bottom half) обработчика. Соответственно, то, что делает button_isr() , называется верхней половиной (top half). Нижняя половина может быть реализована при помощи одного из двух примитивов, либо тасклетов (tasklets) , либо очередей задач (workqueues) .

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

В отличие от тасклетов, очереди задач выполняются в контексте процесса (process context). Они не имеют названных ранее ограничений. Приведенный пример использует именно очереди задач. Процедура schedule_work() является неблокирующей и добавляет задачу (work task) в глобальную очередь задач (global workqueue). При помощи alloc_workqueue() можно создать собственную очередь. Под капотом очередь задач реализована, как потоки (kthreads), разгребающие очередь из struct work_struct * . Эти потоки называется workers.

Таким образом, обработчик прерывания просто добавляет bottomhalf_work_fn() в глобальную очередь задач и завершает свою работу. Из очереди ее забирает worker и выполняет в контексте процесса. А в этом контексте мы можем смело делать msleep() и другие блокирующие вызовы. Все гениальное просто.

Подробнее о workqueues можно почитать в Documentation/core-api/workqueue.rst . Перечень всех доступных процедур с комментариями к ним вы найдете в include/linux/workqueue.h , а детали реализации — в kernel/workqueue.c .

Как-то так происходит обработка прерываний в Linux. Полную версию исходников к посту вы найдете в этом репозитории на GitHub . Как всегда, буду рад вашим вопросам и дополнениям.

EnglishRussianUkrainian