Модули ядра Linux: пример символьного устройства

15 ноября 2021

Недавно мы научились основам написания модулей ядра Linux. Впрочем, рассмотренные тогда примеры были совсем простые, можно даже сказать, что игрушечные. Сегодня мы напишем модуль поинтереснее. Он будет создавать в каталоге /dev символьное устройство, с которым можно взаимодействовать из юзерспейса.

Кода не много, так что привожу его целиком:

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/irq.h>

#define DEVICE_NAME "chardev"

static int sensor_value = 0;
static DEFINE_MUTEX(sensor_value_mtx);

static int major;
static struct class *cls;

typedef struct ChardevPrivateData {
    char buff[32];
    int cnt;
} ChardevPrivateData;

static int device_open(struct inode *, struct file *);
static int device_release(struct inode *, struct file *);
static ssize_t device_read(struct file *, char __user *,
                           size_t, loff_t *);
static ssize_t device_write(struct file *, const char __user *,
                            size_t, loff_t *);

static struct file_operations chardev_fops = {
    .read = device_read,
    .write = device_write,
    .open = device_open,
    .release = device_release,
};

int init_module(void) {
    struct device* dev;
    major = register_chrdev(0, DEVICE_NAME, &chardev_fops);

    if(major < 0) {
        pr_alert("register_chrdev() failed: %d\n", major);
        return -EINVAL;
    }

    pr_info("major = %d\n", major);

    cls = class_create(THIS_MODULE, DEVICE_NAME);
    if(IS_ERR(cls)) {
        pr_alert("class_create() failed: %ld\n", PTR_ERR(cls));
        return -EINVAL;
    }

    dev = device_create(cls, NULL, MKDEV(major, 0), NULL, DEVICE_NAME);
    if(IS_ERR(dev)) {
        pr_alert("device_create() failed: %ld\n", PTR_ERR(dev));
        return -EINVAL;
    }

    pr_info("/dev/%s created\n", DEVICE_NAME);
    return 0;
}

void cleanup_module(void) {
    device_destroy(cls, MKDEV(major, 0));
    class_destroy(cls);
    unregister_chrdev(major, DEVICE_NAME);
}

static int device_open(struct inode *inode, struct file *file) {
    ChardevPrivateData* pd;
    int val;

    if(!try_module_get(THIS_MODULE)) {
        pr_alert("try_module_get() failed\n");
        return -EINVAL;
    }

    pd = kmalloc(sizeof(ChardevPrivateData), GFP_KERNEL);
    if(pd == NULL) {
        pr_alert("kmalloc() failed\n");
        module_put(THIS_MODULE);
        return -EINVAL;
    }

    mutex_lock(&sensor_value_mtx);
    val = sensor_value;
    sensor_value++;
    mutex_unlock(&sensor_value_mtx);

    sprintf(pd->buff, "Dummy sensor value: %d\n", val);
    pd->cnt = 0;
    file->private_data = pd;
    return 0;
}

static int device_release(struct inode *inode, struct file *file) {
    kfree(file->private_data);
    module_put(THIS_MODULE);
    return 0;
}

static ssize_t device_read(struct file *file,
                           char __user *buffer,
                           size_t length,
                           loff_t *offset) {
    int bytes_read = 0;
    ChardevPrivateData* pd = file->private_data;

    while(length && (pd->buff[pd->cnt] != '\0')) {
        if(put_user(pd->buff[pd->cnt], buffer++) != 0)
            return -EINVAL;
        pd->cnt++;
        bytes_read++;
        length--;
    }

    return bytes_read;
}

static ssize_t device_write(struct file *filp,
                            const char __user *buff,
                            size_t len,
                            loff_t *off) {
    pr_alert("device_write() is not implemented\n");
    return -EINVAL;
}

MODULE_LICENSE("GPL");

А вот пример использования модуля:

$ sudo insmod chardev.ko
$ dmesg | cut -d ' ' -f 2- | tail -n 2
major = 240
/dev/chardev created
$ sudo cat /dev/chardev
Dummy sensor value: 0
$ sudo cat /dev/chardev
Dummy sensor value: 1
$ sudo cat /dev/chardev
Dummy sensor value: 2
$ sudo rmmod chardev

Как же это работает? При загрузке модуля вызывается процедура init_module(). В ней при помощи register_chrdev() происходит регистрация символьного устройства. В качестве аргументов нужно передать major-номер устройства, имя устройства и структуру с указателями на кучу колбэков. В приведенном примере в качестве major-номера указан 0. Это означает, что номер выберет ОС среди никем не занятых. Колбэков в примере используется четыре, но на самом деле их доступно куда больше.

Fun fact! Код ядра Linux можно склонировать себе на машину и искать по нему при помощи какого-нибудь Sublime Text. Это позволяет быстро находить определение всех макросов и описание всех структур, а также документацию к ним.

Далее создается класс устройства (device class), а затем и конкретный экземпляр устройства, через device_create(). Один модуль может обслуживать много устройств одного класса. Чтобы модуль мог отличить одно устройство от другого, используются minor-номера. В нашем примере создается одно устройство с minor-номером 0, см макрос MKDEV.

Еще раз подчеркнем разницу между major- и minor-номерами устройства. Major-номера используются системой, чтобы понять, какой модуль обслуживает заданное устройство. Поэтому не должно быть двух модулей, использующих один и тот же major-номер. До minor-номеров ОС нет никакого дела. Эти номера используются только модулем, чтобы отличить один экземпляр устройства от другого.

При выходе из init_module() в системе будет создано символьное устройство /dev/chardev:

$ ls -la /dev/chardev
crw------- 1 root root 240, 0 Aug  7 12:34 /dev/chardev

Здесь 240 — это выбранный системой major-номер, а 0 представляет собой minor-номер. Буква c указывает на то, что это символьное устройство (character device). Блочным устройствам, таким, как жесткие диски, соответствует букв b.

Fun fact! В приведенном коде в случае любой ошибки возвращается EINVAL. С полным списком доступных кодов ошибок и их описанием можно ознакомиться в man errno.

При открытии устройства управление передается колбэку device_open(). Колбэк первым делом взывает try_module_get(). Эта процедура инкрементирует счетчик использования модуля. Значение счетчика отличное от нуля говорит системе о том, что модуль кем-то используется, и не может быть выгружен. Если счетчик невозможно увеличить (например, пользователь прямо сейчас просит систему выгрузить модуль), процедура возвращает false. Декремент счетчика осуществляется вызовом module_put(). Этот вызов всегда завершается успешно.

Текущее значение счетчика для данного модуля ядра можно увидеть через sysfs:

cat /sys/module/chardev/refcnt

Далее в device_open() инициализируется структура ChardevPrivateData и указатель на нее записывается в file->private_data. Это специальное поле, где можно сохранить указатель на какие-то интересные модулю данные. Память под структуру выделяется при помощи kmalloc().

Флаг GFP_KERNEL в аргументах kmalloc() указывает на то, что при выделении памяти допускается приостановить исполнение потока. Пользоваться этим флагом в обработчиках прерываний нельзя. Альтернативным аргументом является GFP_NOWAIT. При его использовании гарантируется, что память будет выделена без остановки потока. Есть и другие флаги. Процедура kfree(), предназначенная для освобождения памяти, никогда не останавливает исполнение потока.

Устройство может быть открыто в параллель несколькими процессами. Чтобы при доступе к глобальной переменной sensor_value не случилось гонки, в device_open() применены мьютексы. Из прочих примитивов синхронизации ядро Linux предлагает спинлоки и атомарные переменные.

При чтении из символьного устройства управление передается в device_read(). Это очень простая процедура, если не считать одного момента. Дело в том, что buffer находится в пользовательском адресном пространстве, и писать в него напрямую не допускается. Вместо этого нужно использовать макрос put_user(). Аналогично, для чтения предусмотрен get_user(). Оба макроса работают с типами, имеющими размер 8, 16, 32 или 64 бита. Для копирования данных большего объема есть процедуры copy_to_user() и copy_from_user(). Названные макросы и процедуры возвращают 0 в случае успеха и иное значение в случае ошибки.

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

Дополнение: В продолжение темы см заметки Модули ядра Linux: таймеры и GPIO и Модули ядра Linux: обработка прерываний.

Метки: , , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.