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

21 февраля 2022

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

Метки: , .