Модули ядра Linux: обработка прерываний
21 февраля 2022
Из предыдущей заметки о модулях ядра Linux мы узнали, что такое контекст прерывания, и что в нем нельзя делать блокирующие вызовы. Но что, если я хочу повесить прерывание на нажатие кнопки, а в обработчике делать что-то блокирующее? Давайте разберемся.
Воспользуемся макетной платой, кнопкой, и подтягивающим резистором:
… а также драйвером, который вешает прерывание на нажатие кнопки:
#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
мы увидим:
[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. Как всегда, буду рад вашим вопросам и дополнениям.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.