Очередная часть последней версии пособия по созданию модулей ядра от 2 июля 2022 года. В ней мы познакомимся с понятием tty, представляющим альтернативу макросу print, напишем модуль для мигания светодиодами клавиатуры, а также разберем тему планирования задач с помощью тасклетов и очередей заданий.

▍ Готовые части руководства:



13. Замена макроса Print


▍ 13.1 Замена


В разделе 2 я говорил, что программировать модули ядра через X Window System не желательно. Это верно относительно именно разработки модулей, но при фактическом использовании нам нужна возможность отправлять сообщения на любой tty, с которого поступила команда на загрузку модуля.

Аббревиатура tty означает телетайп, устройство, которое в своем изначальном виде представляло совмещенную с принтером клавиатуру, используемую для взаимодействия с системой Unix. В современном же представлении телетайп является абстракцией текстового потока, используемой программой Unix, будь то физический терминал, xterm на X-сервере, сетевое подключение через ssh или нечто аналогичное.

Реализуется это с помощью current, указателя на выполняемую в данный момент задачу, позволяющего получить tty-структуру этой задачи. Затем эта структура просматривается в поиске указателя на строковую функцию write, которая используется для записи строки в данный tty.

print_string.c
/* 
 * print_string.c – отправляет вывод на tty, с которого мы работаем, 
 * будь то через X11, telnet и т.д. Для этого мы выводим строку на 
 * tty, связанный с текущей задачей. 
 */ 
#include <linux/init.h> 
#include <linux/kernel.h> 
#include <linux/module.h> 
#include <linux/sched.h> /* Для current. */ 
#include <linux/tty.h> /* Для объявлений tty. */ 
 
static void print_string(char *str) 
{ 
    /* tty для текущей задачи. */ 
    struct tty_struct *my_tty = get_current_tty(); 
 
    /* Если my_tty равен NULL, значит у текущей задачи нет tty, куда 
     * можно было бы произвести вывод (например, если это демон). В таком  
     * случае ничего не поделаешь.
     */ 
    if (my_tty) { 
        const struct tty_operations *ttyops = my_tty->driver->ops; 
        /* my_tty->driver – это структура, где расположены функции tty,
         * одна из которых (write) используется для записи строк в tty. 
         * С помощью нее можно извлекать строки из сегментов пространства памяти ядра или 
         * пользователя. 
         * 
         * Первый параметр этой функции устанавливает tty, куда нужно
         * производить запись, потому что одна и та же функция служит
         * для записи во все tty определенного типа. 
         * Второй параметр – это указатель на строку. 
         * Третий параметр устанавливает длину строки. 
         * 
         * Как вы увидите ниже, иногда необходимо использовать функционал 
         * препроцессора, чтобы получить код, работающий для различных
         * версий ядра. Реализованный нами здесь наивный подход плохо 
         * масштабируется. Правильный способ решения этой проблемы описан 
         * в разделе 2 документации: linux/Documentation/SubmittingPatches 
         */ 
        (ttyops->write)(my_tty, /* Сам tty. */ 
                        str, /* Строка. */ 
                        strlen(str)); /* Длина. */ 
 
        /* Изначально телетайпы были аппаратными и, как правило, строго 
         * следовали стандарту ASCII. В ASCII для перехода на новую строку 
         * необходимо два символа, возврат каретки и перевод строки. В 
         * Unix перевод строки ASCII задействуется для того и другого,
         * поэтому нельзя просто использовать \n, так как возврата
         * каретки не произойдет, и следующая строка начнется в столбце, 
         * идущим сразу за переводом строки.
         *
         * Именно поэтому в Unix и MS Windows текстовые файлы отличаются.      
         * В CP/M и ее производных вроде MS-DOS и MS Windows текст строго 
         * подчиняется стандарту ASCII, в связи с чем для перехода на 
         * новую строку требуется и LF, и CR. 
         */ 
        (ttyops->write)(my_tty, "\015\012", 2); 
    } 
} 
 
static int __init print_string_init(void) 
{ 
    print_string("The module has been inserted.  Hello world!"); 
    return 0; 
} 
 
static void __exit print_string_exit(void) 
{ 
    print_string("The module has been removed.  Farewell world!"); 
} 
 
module_init(print_string_init); 
module_exit(print_string_exit); 
 
MODULE_LICENSE("GPL");


▍ 13.2 Мигание светодиодами клавиатуры


В определенных условиях вы можете предпочесть более простой и непосредственный способ связи с внешним миром. Решением в таком случае может стать мигание светодиодами клавиатуры. Это прямой способ привлечь внимание или продемонстрировать некое состояние. Светодиоды есть у любой клавиатуры, они всегда на виду, не требуют настройки и очень просты в использовании, если сравнивать с записью в tty или файл.

В v4.14 и v.4.15 в API таймера произошел ряд изменений, нацеленных на повышение безопасности памяти. Переполнение буфера в области структуры timer_list может привести к перезаписи полей function и data, предоставив атакующему возможность с помощью возвратно-объектного программирование вызывать произвольные функции в ядре. Кроме того, прототип функции обратного вызова, содержащий аргумент unsigned long, полностью исключит возможность проверки типов. Плюс такой прототип может помешать защитить косвенные переходы и вызовы (forward-edge) с помощью сохранения целостности потока управления (CFI).

Поэтому лучше использовать уникальный прототип, чтобы отделиться от кластера, который получает аргумент unsigned long. В обратный вызов таймера необходимо передавать не аргумент unsigned long, а указатель на структуру timer_list. Тогда он объединит всю необходимую ему информацию, включая структуру timer_list, в более обширную структуру и сможет использовать вместо значения unsigned long макрос container_of. Более развернуто эта тема описана в статье Improving the kernel timers API.

До Linux v4.14 инициализация таймеров производилась с помощью setup_timer, а структура timer_list выглядела так:

struct timer_list { 
    unsigned long expires; 
    void (*function)(unsigned long); 
    unsigned long data; 
    u32 flags; 
    /* ... */ 
}; 
 
void setup_timer(struct timer_list *timer, void (*callback)(unsigned long), 
                 unsigned long data);

В Linux v4.14 появилась timer_setup, и ядро постепенно перестроилось с setup_timer на timer_setup. Одна из причин изменения API заключалась в потребности сосуществования с интерфейсом старых версий. Более того, по началу timer_setup реализовывалась через setup_timer.

void timer_setup(struct timer_list *timer, 
                 void (*callback)(struct timer_list *), unsigned int flags);

Позднее в v4.15 setup_timer удалили, что также отразилось на облике структуры timer_list:

struct timer_list { 
    unsigned long expires; 
    void (*function)(struct timer_list *); 
    u32 flags; 
    /* ... */ 
};

Приведенный ниже код демонстрирует минимальный модуль ядра, который после загрузки начинает мигать светодиодами до тех пор, пока не будет выгружен.

kbleds.c
/* 
 * kbleds.c – мигает светодиодами клавиатуры, пока не будет выгружен. 
 */ 
 
#include <linux/init.h> 
#include <linux/kd.h> /* Для KDSETLED. */ 
#include <linux/module.h> 
#include <linux/tty.h> /* Для tty_struct. */ 
#include <linux/vt.h> /* Для MAX_NR_CONSOLES. */ 
#include <linux/vt_kern.h> /* Для fg_console. */ 
#include <linux/console_struct.h> /* Для vc_cons. */ 
 
MODULE_DESCRIPTION("Example module illustrating the use of Keyboard LEDs."); 
 
static struct timer_list my_timer; 
static struct tty_driver *my_driver; 
static unsigned long kbledstatus = 0; 
 
#define BLINK_DELAY HZ / 5 
#define ALL_LEDS_ON 0x07 
#define RESTORE_LEDS 0xFF 
 
/* Функция my_timer_func периодически мигает светодиодами, 
 * вызывая для драйвера клавиатуры команду управления вводом-выводом  
 * KDSETLED. Дополнительную информацию по командам ввода-вывода 
 * смотрите в функции vt_ioctl() файла drivers/tty/vt/vt_ioctl.c. 
 * 
 * Аргумент KDSETLED попеременно устанавливается то на 7 (что приводит к 
 * активации режима LED_SHOW_IOCTL и загоранию всех светодиодов), то на 
 * 0xFF (любое значение выше 7 переключает режим обратно на 
 * LED_SHOW_FLAGS, в результате чего светодиоды отображают фактический 
 * статус клавиатуры). Подробности смотрите в функции setledstate() файла 
 * drivers/tty/vt/keyboard.c.
  */ 
static void my_timer_func(struct timer_list *unused) 
{ 
    struct tty_struct *t = vc_cons[fg_console].d->port.tty; 
 
    if (kbledstatus == ALL_LEDS_ON) 
        kbledstatus = RESTORE_LEDS; 
    else 
        kbledstatus = ALL_LEDS_ON; 
 
    (my_driver->ops->ioctl)(t, KDSETLED, kbledstatus); 
 
    my_timer.expires = jiffies + BLINK_DELAY; 
    add_timer(&my_timer); 
} 
 
static int __init kbleds_init(void) 
{ 
    int i; 
 
    pr_info("kbleds: loading\n"); 
    pr_info("kbleds: fgconsole is %x\n", fg_console); 
    for (i = 0; i < MAX_NR_CONSOLES; i++) { 
        if (!vc_cons[i].d) 
            break; 
        pr_info("poet_atkm: console[%i/%i] #%i, tty %p\n", i, MAX_NR_CONSOLES, 
                vc_cons[i].d->vc_num, (void *)vc_cons[i].d->port.tty); 
    } 
    pr_info("kbleds: finished scanning consoles\n"); 
 
    my_driver = vc_cons[fg_console].d->port.tty->driver; 
    pr_info("kbleds: tty driver magic %x\n", my_driver->magic); 
 
    /* Первая настройка таймера мигания светодиодов. */ 
    timer_setup(&my_timer, my_timer_func, 0); 
    my_timer.expires = jiffies + BLINK_DELAY; 
    add_timer(&my_timer); 
 
    return 0; 
} 
 
static void __exit kbleds_cleanup(void) 
{ 
    pr_info("kbleds: unloading...\n"); 
    del_timer(&my_timer); 
    (my_driver->ops->ioctl)(vc_cons[fg_console].d->port.tty, KDSETLED, 
                            RESTORE_LEDS); 
} 
 
module_init(kbleds_init); 
module_exit(kbleds_cleanup); 
 
MODULE_LICENSE("GPL");


Если ни один из приведенных в этой главе примеров не подходит под ваши отладочные нужды, то наверняка есть другие решения. Не задумывались, для чего может быть полезна CONFIG_LL_DEBUG из menu menuconfig? В случае ее активации вы получаете низкоуровневый доступ к последовательному порту. И хотя это может не показаться особо полезным, такой прием позволяет пропатчить kernel/printk.c или любой другой важный системный вызов для печати символов ASCII, делая возможным отслеживание практически всех действий кода на последовательном порту. Если вы займетесь портированием ядра на новую, ранее неподдерживаемую архитектуру, то реализация этого решения должна идти одной из первых. Также можно рассмотреть вариант логирования через netconsole.

Несмотря на множество рассмотренных здесь отладочных приемов, кое-что нужно иметь ввиду. Отладка практически всегда оказывается интрузивной процедурой. Добавление отладочного кода может привести к тому, что ошибка, на первый взгляд, исчезнет. Поэтому такой код нужно минимизировать и следить, чтобы он не попал в продакшн-код.

14. Планирование задач


Есть два основных способа выполнения задач: тасклеты и очереди заданий. Тасклеты – это быстрый и простой способ планирования выполнения одной функции, например, при ее активации прерыванием. А вот очереди заданий хоть и более сложны, но зато лучше подходят для выполнения последовательностей задач.

▍ 14.1 Тасклеты


Ниже показан пример модуля тасклета. Функция tasklet_fn выполняется несколько секунд. При этом выполнение функции example_tasklet_init может продолжаться до точки выхода, что будет зависеть от того, была ли она прервана softirq.

example_tasklet.c
/* 
 * example_tasklet.c 
 */ 
#include <linux/delay.h> 
#include <linux/interrupt.h> 
#include <linux/kernel.h> 
#include <linux/module.h> 
 
/* Макрос DECLARE_TASKLET_OLD присутствует для совместимости. 
 * См. https://lwn.net/Articles/830964/.
 */ 
#ifndef DECLARE_TASKLET_OLD 
#define DECLARE_TASKLET_OLD(arg1, arg2) DECLARE_TASKLET(arg1, arg2, 0L) 
#endif 
 
static void tasklet_fn(unsigned long data) 
{ 
    pr_info("Example tasklet starts\n"); 
    mdelay(5000); 
    pr_info("Example tasklet ends\n"); 
} 
 
static DECLARE_TASKLET_OLD(mytask, tasklet_fn); 
 
static int example_tasklet_init(void) 
{ 
    pr_info("tasklet example init\n"); 
    tasklet_schedule(&mytask); 
    mdelay(200); 
    pr_info("Example tasklet init continues...\n"); 
    return 0; 
} 
 
static void example_tasklet_exit(void) 
{ 
    pr_info("tasklet example exit\n"); 
    tasklet_kill(&mytask); 
} 
 
module_init(example_tasklet_init); 
module_exit(example_tasklet_exit); 
 
MODULE_DESCRIPTION("Tasklet example"); 
MODULE_LICENSE("GPL");


После загрузки этого примера dmesg должна отобразить следующее:

tasklet example init
Example tasklet starts
Example tasklet init continues...
Example tasklet ends

И хотя использовать тасклеты легко, они имеют несколько недостатков, и в среде разработчиков обсуждается их возможное исключение из ядра. Обратный вызов тасклета выполняется в атомарном контексте внутри программного прерывания, то есть он не может входить в режим ожидания или получать доступ к данным пользовательского пространства, в результате чего в обработчике тасклетов не получится выполнить всю работу. Кроме того, ядро разрешает одновременно выполнять только один экземпляр любого конкретного тасклета. При этом несколько разных могут выполняться параллельно.

В последних версиях ядра появилась возможность заменить тасклеты очередями заданий, таймерами или прерываниями, выносимыми в отдельные потоки (threaded interrupts). Пока удаление тасклетов продолжает оставаться долгосрочной целью, в своем текущем виде ядро содержит более сотни случаев их использования. Сейчас разработчики продолжают вносить изменения в API, и для совместимости существует макрос DECLARE_TASKLET_OLD. Подробнее читайте на странице https://lwn.net/Articles/830964/.

▍ 14.2 Очереди заданий


Добавлять задачи в планировщик можно через очередь заданий. Для выполнения прописанных в этой очереди задач ядро использует Completely Fair Scheduler (CFS).

sched.c
/* 
 * sched.c 
 */ 
#include <linux/init.h> 
#include <linux/module.h> 
#include <linux/workqueue.h> 
 
static struct workqueue_struct *queue = NULL; 
static struct work_struct work; 
 
static void work_handler(struct work_struct *data) 
{ 
    pr_info("work handler function.\n"); 
} 
 
static int __init sched_init(void) 
{ 
    queue = alloc_workqueue("HELLOWORLD", WQ_UNBOUND, 1); 
    INIT_WORK(&work, work_handler); 
    schedule_work(&work); 
    return 0; 
} 
 
static void __exit sched_exit(void) 
{ 
    destroy_workqueue(queue); 
} 
 
module_init(sched_init); 
module_exit(sched_exit); 
 
MODULE_LICENSE("GPL"); 
MODULE_DESCRIPTION("Workqueue example");



▍ Продолжение


Следующая часть будет завершающей. В ней мы рассмотрим такие темы, как обработка прерываний, криптография, драйвер виртуального устройства ввода, способы оптимизации и некоторые другие.

▍ Готовые части руководства:



Telegram-канал и уютный чат для клиентов

Комментарии (0)