A guide to programming Linux kernel modules. Part 6

The next part of the latest version of the tutorial on creating kernel modules from July 2, 2022. In it, we will get acquainted with the concept of tty, which is an alternative to a macro

print

we will write a module for blinking the keyboard LEDs, and we will also analyze the topic of task scheduling using tasklets and job queues.

▍ Finished parts of the manual:

13. Replacing the macro Print

▍ 13.1 Replacement

In section 2, I said that programming kernel modules through the X Window System is not desirable. This is true for module development specifically, but in actual use, we need the ability to send messages to any tty from which the command to load the module came from.

The abbreviation tty stands for teletypewriter, a device that in its original form was a keyboard combined with a printer used to interact with a Unix system. In modern terms, teletype is an abstraction of the text stream used by a Unix program, be it a physical terminal, an xterm on an X server, a network connection via ssh, or something similar.

This is implemented using current, a pointer to the currently executing task, allowing you to get the tty structure of this task. This structure is then looked up looking for a pointer to a string function writewhich is used to write a string to the given 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 Flashing keypad LEDs

Under certain conditions, you may prefer a simpler and more direct way to communicate with the outside world. The solution in this case may be to flash the keyboard LEDs. This is a direct way to attract attention or demonstrate a certain state. Any keyboard has LEDs, they are always visible, do not require configuration and are very easy to use when compared to writing to a tty or file.

In v4.14 and v.4.15, a number of changes have been made to the Timer API to improve memory security. Structure Area Buffer Overflow timer_list may cause fields to be overwritten function and data, giving an attacker the ability to call arbitrary functions in the kernel using ROP. In addition, the prototype of the callback function containing the argument unsigned long, will completely eliminate the possibility of type checking. Plus, such a prototype can interfere with the protection of indirect transitions and calls (forward-edge) using the preservation of control flow integrity (CFI).

So it’s better to use a unique prototype to separate from the cluster that receives the argument unsigned long. You must pass a non-argument to the timer callback unsigned longand the pointer to the structure timer_list. Then it will combine all the information it needs, including the structure timer_listinto a larger structure and will be able to use instead of the value unsigned long macro container_of. This topic is described in more detail in the article. Improving the kernel timers API.

Prior to Linux v4.14, timers were initialized with setup_timerand the structure timer_list looked like this:

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);

In Linux v4.14 appeared

timer_setup

and the core gradually rebuilt from

setup_timer

on the

timer_setup

. One of the reasons for changing the API was the need to coexist with the interface of older versions. Moreover, at the beginning

timer_setup

implemented through

setup_timer

.

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

Later in v4.15

setup_timer

removed, which also affected the appearance of the structure

timer_list

:

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

The code below shows a minimal kernel module that, once loaded, flashes its LEDs until it is unloaded.

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");

If none of the examples in this chapter fit your debugging needs, there are surely other solutions. Haven’t thought about what it can be useful for

CONFIG_LL_DEBUG

from

menu menuconfig

? When activated, you get low-level access to the serial port. And although it may not seem particularly useful, this technique allows you to patch

kernel/printk.c

or any other important system call to print ASCII characters, making it possible to trace almost all code actions on the serial port. If you are porting the kernel to a new, previously unsupported architecture, then the implementation of this solution should be one of the first. You can also consider logging via netconsole.

Despite the many debugging techniques discussed here, there are a few things to keep in mind. Debugging is almost always an intrusive procedure. Adding debugging code may cause the error to disappear at first glance. Therefore, such code must be minimized and ensured that it does not end up in the production code.

14. Task scheduling

There are two main ways to perform tasks: tasklets and job queues. Tasklets are a quick and easy way of scheduling the execution of a single function, for example, when it is activated by an interrupt. But job queues, although more complex, are better suited for executing task sequences.

▍ 14.1 Tasklets

Below is an example of a tasklet module. Function

tasklet_fn

takes a few seconds. At the same time, the execution of the function

example_tasklet_init

may continue until the exit point, which will depend on whether it was interrupted

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");

After downloading this example

dmesg

should display the following:

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

While tasklets are easy to use, they have several drawbacks, and there is discussion in the developer community about their possible exclusion from the core. The tasklet callback executes in an atomic context within a software interrupt, meaning it cannot enter sleep mode or access user-space data, resulting in the tasklet handler not being able to do all the work. In addition, the kernel only allows one instance of any given tasklet to run at a time. In this case, several different ones can be performed in parallel.

Recent versions of the kernel have made it possible to replace tasklets with job queues, timers, or threaded interrupts. While tasklet removal continues to be a long-term goal, in its current form the kernel contains over a hundred instances of their use. Now developers continue to make changes to the API, and for compatibility there is a macro DECLARE_TASKLET_OLD. Read more on the page https://lwn.net/Articles/830964/.

▍ 14.2 Job queues

You can add tasks to the scheduler through the task queue. The kernel uses the Completely Fair Scheduler (CFS) to execute the tasks assigned to this queue.

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");

▍ Continued

The next part will be the final one. In it, we will cover topics such as interrupt handling, cryptography, the virtual input device driver, optimization methods, and some others.

▍ Finished parts of the manual:

Telegram channel and cozy chat for clients

Similar Posts

Leave a Reply Cancel reply