← На главную

Модули ядра Linux: обработка прерываний

Из предыдущей заметки о модулях ядра 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. Как всегда, буду рад вашим вопросам и дополнениям.