Embedded HandbookПриложения
Все разделы › XVII. Системное программирование на C
· уровень продвинутый · 14 мин чтения
Открыть в интерактивном справочнике

147. Ядро Linux: сборка, модуль, символьное устройство, ioctl

Всё, что выше в этой части, — программы в пользовательском режиме: они просят ядро через системные вызовы. Драйвер живёт по другую сторону: внутри ядра, с его памятью, без libc, без защиты от собственных ошибок. Раздел показывает, из чего состоит ядро как проект, как собрать его модуль и как выглядит драйвер простейшего устройства — кольцевого буфера, доступного через /dev/ringdev, с read, write и ioctl.

Модуль собран против заголовков ядра Debian 6.1 в stm-sys:1 (_verify/sys147/) — сборка, modinfo, символы и секции настоящие. Загрузить его в ядро контейнера нельзя: ядро хоста другой версии (6.6, WSL2) и без прав, insmod отвечает Operation not permitted. Загрузка — на настоящей машине или виртуальной с тем же ядром.

Как это называется в документации

Оригинал Перевод
kernel space, user space пространство ядра, пространство пользователя
loadable kernel module (.ko) загружаемый модуль ядра
Kbuild, obj-m система сборки ядра, цель «модуль»
vermagic отпечаток версии и настроек ядра, с которым собран модуль
character device, major/minor символьное устройство, старший/младший номер
file_operations таблица операций над файлом устройства
copy_to_user, copy_from_user копирование между ядром и пользователем
ioctl команда устройству вне read/write
printk, dmesg печать ядра и её журнал
tainted kernel ядро «запятнано» несвободным или сбойным модулем
ftrace, kprobes трассировка функций ядра

147.1 Ядро — это программа

Ядро — один большой исполняемый файл на C: vmlinux — обычный ELF, bzImage — он же сжатый для загрузчика. Ядро всегда в памяти, всегда запущено и одно на всех: планирует процессы, делит память, говорит с железом. Всё остальное — драйверы — либо вкомпилировано в него, либо подгружается модулями .ko по требованию.

Дерево исходников: arch/ — процессоры, drivers/ — драйверы (больше половины всего объёма), fs/ — файловые системы, net/ — сеть, mm/ — память, kernel/ — планировщик и системные вызовы, include/ — заголовки. В одном include/linux — 1464 заголовка; fs.h — 3776 строк. Ядро исполняется без libc: свои kmalloc вместо malloc, printk вместо printf, mutex_lock вместо pthread.

Сборка всего ядра: make defconfig или make localmodconfig (только модули, загруженные сейчас, — сборка в разы короче), правка .config через make menuconfig, make -j$(nproc); на Debian — make deb-pkg и dpkg -i. Модуль собирается без сборки ядра — против его заголовков.

Путь read(): программа → файл устройства → file_operations → ring_read → буфер в ядре через copy_to_user


147.2 Модуль: вход, выход, параметры

Модуль — файл .ko с двумя обязательными точками: что делать при загрузке и при выгрузке:

module_init(ring_init);
module_exit(ring_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Кольцевой буфер как символьное устройство");
MODULE_VERSION("1.0");

Параметр задаётся при загрузке — insmod ringdev.ko ring_size=256:

static int ring_size = 64;                              /* параметр: insmod ringdev.ko ring_size=256 */
module_param(ring_size, int, 0444);
MODULE_PARM_DESC(ring_size, "размер кольцевого буфера в байтах");

Сборка — три строки Makefile, всё остальное делает Kbuild ядра:

obj-m := ringdev.o
KDIR ?= /lib/modules/$(shell uname -r)/build
all:
    $(MAKE) -C $(KDIR) M=$(PWD) modules

Готовый модуль читается modinfo:

version:        1.0
description:    Кольцевой буфер как символьное устройство
license:        GPL
name:           ringdev
vermagic:       6.1.0-53-amd64 SMP preempt mod_unload modversions 
parm:           ring_size:размер кольцевого буфера в байтах (int)

vermagic — ключ: настройки ядра (SMP preempt mod_unload modversions) должны совпасть. Номер 6.1.0-53-amd64 при modversions не сравнивается — вместо него ядро сверяет контрольные суммы каждого символа, которым пользуется модуль. Ядро 6.6 его отвергнет: суммы разойдутся, и в dmesg будет disagrees about version of symbol …. Поэтому модули собирают под каждое ядро отдельно (DKMS делает это при обновлении). Файл .ko — 279 672 байта, из них код и данные — 14 080 (strip --strip-debug), остальное отладочная информация.

Секции модуля: .init.text — код ring_init, выбрасывается после загрузки; .exit.text — код выгрузки; .modinfo — то, что печатает modinfo; __param — параметры. Символы, которые модуль требует от ядра (nm -u): alloc_chrdev_region, cdev_add, _copy_to_user, _printk, __kmalloc, mutex_lock, device_create… Большинство из них экспортированы для любых модулей, и только четыре — device_create, device_destroy, class_destroy и __class_create (за ним стоит макрос class_create) — помечены EXPORT_SYMBOL_GPL: для несвободного модуля их не существует. Модуль с другой лицензией даже не соберётся — modpost остановит сборку сообщением «GPL-incompatible module … uses GPL-only symbol». Тип экспорта каждого символа виден в Module.symvers рядом с заголовками ядра.


147.3 Символьное устройство и file_operations

Устройство в Unix — файл в /dev. У символьного устройства байты идут потоком (терминал, порт, наш буфер), у блочного — блоками с произвольным доступом (диск). Файлу соответствует пара номеров: старший выбирает драйвер, младший — устройство внутри драйвера. Драйвер отдаёт ядру таблицу операций — тот же приём, что таблица функций в разделе 146:

static const struct file_operations ring_fops = {       /* «класс» устройства: таблица функций (раздел 146) */
    .owner = THIS_MODULE,
    .read = ring_read,
    .write = ring_write,
    .unlocked_ioctl = ring_ioctl,
};

В ядре 6.1 struct file_operations объявлена в fs.h на строке 2174 и начинается с owner, llseek, read, write; всего в ней несколько десятков указателей, и незаполненные означают «операция не поддерживается» (-EINVAL для read на устройстве без .read).

Регистрация в четыре шага, с откатом при любой ошибке:

    rc = alloc_chrdev_region(&devno, 0, 1, "ringdev");   /* ядро выдаёт старший номер */
    if (rc) goto err_buf;
    cdev_init(&cdev, &ring_fops);
    rc = cdev_add(&cdev, devno, 1);
    if (rc) goto err_region;
    device_create(cls, NULL, devno, NULL, "ringdev");     /* udev создаст /dev/ringdev */

goto err_… — стиль ядра для освобождения в обратном порядке: в ядре нет исключений и нет второго шанса. class_create + device_create сообщают о новом устройстве udev, и файл /dev/ringdev появляется сам; иначе его пришлось бы делать mknod по номерам из /proc/devices.


147.4 read, write, ioctl

Пользователь вызывает read(fd, buf, n) — ядро зовёт ring_read с теми же аргументами, но buf указывает в чужое адресное пространство. Писать туда напрямую нельзя: только copy_to_user, которая проверяет адрес и возвращает, сколько не скопировалось:

static ssize_t ring_read(struct file *f, char __user *ubuf, size_t n, loff_t *off)
{
    size_t done = 0;
    mutex_lock(&lock);
    while (done < n && count > 0) {
        if (copy_to_user(ubuf + done, &buf[tail], 1)) { mutex_unlock(&lock); return -EFAULT; }   /* в пользователя — только copy_to_user */
        tail = (tail + 1) % ring_size; count--; done++;
    }
    mutex_unlock(&lock);
    return done;                                        /* 0 — «конец файла» для cat */
}

Пометка __user — для статического анализатора (sparse): смешать указатели ядра и пользователя — ошибка. Мьютекс — потому что read и write могут идти из двух процессов одновременно; в ядре гонки такие же, как в разделе 139, только последствия — паника всей машины. Именно мьютекс, а не спин-блокировка: copy_to_user может заснуть, пока ядро подкачивает страницу пользователя, а под спин-блокировкой спать нельзя. Здесь пустой буфер отдаёт 0 — «конец файла»; настоящие драйверы в этом месте усыпляют читателя через wait_event_interruptible и будят его из write, как канал. Возврат: число байт, 0 — конец, отрицательный errno — ошибка. write в полный буфер возвращает -ENOSPC, а не 0: при нуле echo повторял бы запись вечно.

ioctl — команды, не укладывающиеся в поток байтов: очистить буфер, узнать заполнение. Номер команды кодирует направление, «магическое» число, порядковый номер и размер данных, и заголовок с ними общий у модуля и программы:

#define RING_IOC_MAGIC 'R'
#define RING_IOC_CLEAR _IO(RING_IOC_MAGIC, 0)              /* без данных */
#define RING_IOC_COUNT _IOR(RING_IOC_MAGIC, 1, int)        /* ядро → пользователь: сколько байт в буфере */
#define RING_IOC_SIZE  _IOR(RING_IOC_MAGIC, 2, int)
RING_IOC_CLEAR = 0x5200, RING_IOC_COUNT = 0x80045201, RING_IOC_SIZE = 0x80045202

0x52 — код буквы R; старший бит 0x8000… — «читать из ядра», 04 — четыре байта данных. Чужую команду драйвер отвергает -ENOTTY. Пользовательская сторона — обычные open, write, ioctl(fd, RING_IOC_COUNT, &n), read.

Номер команды ioctl по полям: направление, размер данных, буква и порядковый номер — на трёх командах модуля


147.5 Загрузка, журнал, отладка

Команда Что делает
insmod файл.ko [параметры] загрузить по пути, без зависимостей
modprobe имя загрузить из /lib/modules/$(uname -r)/ с зависимостями
rmmod имя, modprobe -r выгрузить
lsmod что загружено, кто кем пользуется
dmesg -w журнал ядра, вживую

printk печатает в журнал ядра с уровнем: pr_err, pr_warn, pr_info, pr_debug — обёртки над printk(KERN_ERR …); восемь уровней от KERN_EMERG до KERN_DEBUG — это строка-префикс перед сообщением. pr_debug печатает, только если модуль собран с -DDEBUG (ccflags-y += -DDEBUG в Makefile) или сообщения включены на лету: echo 'module ringdev +p' > /sys/kernel/debug/dynamic_debug/control. Сообщение модуля ringdev: major N, буфер 64 байт (pr_info в ring_init) появляется в dmesg при загрузке.

Отладчик к работающему ядру подключают иначе, чем к процессу. Ядро в QEMU запускают с ключами -s -S — сервер отладчика на порту 1234, процессор стоит до команды c — и с nokaslr в командной строке ядра, иначе адреса не совпадут с vmlinux. Тогда gdb vmlinux (ядро с отладочной информацией) после target remote :1234 ставит точки останова и в ядре, и в модуле. Символы модулей подгружает команда lx-symbols из scripts/gdb — для неё ядро собирают с CONFIG_GDB_SCRIPTS. На железе то же даёт kgdb через последовательный порт (CONFIG_KGDB). В ядре Debian оба ключа выключены, поэтому чаще обходятся без отладчика: printk, ftrace (/sys/kernel/tracing: какая функция ядра сколько раз вызвана и за сколько), kprobes (точка останова на функции ядра с обработчиком), perf. Ошибка в модуле — Oops с трассировкой стека в dmesg, а часто и зависание всей машины: модули отлаживают в виртуальной машине, не на рабочем компьютере.

Безопасная загрузка. Ядро Debian собрано с CONFIG_LOCK_DOWN_IN_EFI_SECURE_BOOT: если на машине включена безопасная загрузка (Secure Boot), оно уходит в режим lockdown и неподписанный модуль не берёт и от root — insmod отвечает тем же Operation not permitted, что в контейнере, а в dmesg ядро пишет, что загрузку запретил lockdown. Ядро, которое требует подписи всегда (module.sig_enforce=1), отвечает Key was rejected by service. Модуль подписывают своим ключом — парой из openssl req -x509:

/usr/src/linux-headers-$(uname -r)/scripts/sign-file sha256 key.priv key.der ringdev.ko

После этого modinfo показывает sig_hashalgo: sha256, а ключ регистрируют в прошивке: mokutil --import key.der и подтверждение при следующей перезагрузке. Для опытов проще виртуальная машина без безопасной загрузки.


147.6 Грабли

Симптом Причина
insmod: Invalid module format vermagic не совпал — собрать под своё ядро
insmod: Operation not permitted нет прав, контейнер или безопасная загрузка: нужен root на настоящей машине и подписанный модуль (147.5)
insmod: Key was rejected by service ядро требует подписи, а модуль не подписан или подписан чужим ключом
Unknown symbol in module функция EXPORT_SYMBOL_GPL, а лицензия модуля не GPL
class_create не собирается с 6.4 один аргумент, до — два (THIS_MODULE, имя)
cat /dev/ringdev не завершается read не возвращает 0 при пустом буфере
echo x > /dev/ringdev зациклился write вернул 0 при полном буфере — нужен -ENOSPC
Oops при read запись в пользовательский буфер напрямую вместо copy_to_user
ioctl отвечает ENOTTY номер команды у программы и модуля разный — общий заголовок
после rmmod осталась запись в /dev не вызваны device_destroy/class_destroy
.ko 280 КБ отладочная информация; код — 14 КБ

147.7 Итоги

Полные исходники

ringdev.h — команды ioctl — общий заголовок модуля и программы.

/* Раздел 147: команды ioctl — общий заголовок модуля и пользовательской программы. */
#ifndef RINGDEV_H
#define RINGDEV_H
#include <linux/ioctl.h>
#define RING_IOC_MAGIC 'R'
#define RING_IOC_CLEAR _IO(RING_IOC_MAGIC, 0)              /* без данных */
#define RING_IOC_COUNT _IOR(RING_IOC_MAGIC, 1, int)        /* ядро → пользователь: сколько байт в буфере */
#define RING_IOC_SIZE  _IOR(RING_IOC_MAGIC, 2, int)
#endif

ringdev.c — модуль ядра: символьное устройство с кольцевым буфером.

/* Раздел 147: модуль ядра — символьное устройство /dev/ringdev с кольцевым буфером:
   read/write/ioctl, параметр модуля, регистрация через cdev и класс устройств.
   Собирается против заголовков ядра; загрузка требует настоящего ядра той же версии. */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/mutex.h>
#include <linux/version.h>
#include "ringdev.h"

static int ring_size = 64;                              /* параметр: insmod ringdev.ko ring_size=256 */
module_param(ring_size, int, 0444);
MODULE_PARM_DESC(ring_size, "размер кольцевого буфера в байтах");

static char *buf;
static size_t head, tail, count;
static DEFINE_MUTEX(lock);
static dev_t devno;
static struct cdev cdev;
static struct class *cls;

static ssize_t ring_read(struct file *f, char __user *ubuf, size_t n, loff_t *off)
{
    size_t done = 0;
    mutex_lock(&lock);
    while (done < n && count > 0) {
        if (copy_to_user(ubuf + done, &buf[tail], 1)) { mutex_unlock(&lock); return -EFAULT; }   /* в пользователя — только copy_to_user */
        tail = (tail + 1) % ring_size; count--; done++;
    }
    mutex_unlock(&lock);
    return done;                                        /* 0 — «конец файла» для cat */
}

static ssize_t ring_write(struct file *f, const char __user *ubuf, size_t n, loff_t *off)
{
    size_t done = 0;
    mutex_lock(&lock);
    while (done < n && count < (size_t)ring_size) {
        if (copy_from_user(&buf[head], ubuf + done, 1)) { mutex_unlock(&lock); return -EFAULT; }
        head = (head + 1) % ring_size; count++; done++;
    }
    mutex_unlock(&lock);
    return done ? done : -ENOSPC;                       /* полон — ошибка, а не 0 (иначе echo зациклится) */
}

static long ring_ioctl(struct file *f, unsigned int cmd, unsigned long arg)
{
    switch (cmd) {
    case RING_IOC_CLEAR: mutex_lock(&lock); head = tail = count = 0; mutex_unlock(&lock); return 0;
    case RING_IOC_COUNT: return put_user((int)count, (int __user *)arg);
    case RING_IOC_SIZE:  return put_user(ring_size, (int __user *)arg);
    }
    return -ENOTTY;                                     /* чужая команда */
}

static const struct file_operations ring_fops = {       /* «класс» устройства: таблица функций (раздел 146) */
    .owner = THIS_MODULE,
    .read = ring_read,
    .write = ring_write,
    .unlocked_ioctl = ring_ioctl,
};

static int __init ring_init(void)
{
    int rc;
    buf = kmalloc(ring_size, GFP_KERNEL);
    if (!buf) return -ENOMEM;
    rc = alloc_chrdev_region(&devno, 0, 1, "ringdev");   /* ядро выдаёт старший номер */
    if (rc) goto err_buf;
    cdev_init(&cdev, &ring_fops);
    rc = cdev_add(&cdev, devno, 1);
    if (rc) goto err_region;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0)
    cls = class_create("ringdev");                       /* с 6.4 — без владельца */
#else
    cls = class_create(THIS_MODULE, "ringdev");
#endif
    if (IS_ERR(cls)) { rc = PTR_ERR(cls); goto err_cdev; }
    device_create(cls, NULL, devno, NULL, "ringdev");     /* udev создаст /dev/ringdev */
    pr_info("ringdev: major %d, буфер %d байт\n", MAJOR(devno), ring_size);
    return 0;
err_cdev:   cdev_del(&cdev);
err_region: unregister_chrdev_region(devno, 1);
err_buf:    kfree(buf);
    return rc;
}

static void __exit ring_exit(void)
{
    device_destroy(cls, devno);
    class_destroy(cls);
    cdev_del(&cdev);
    unregister_chrdev_region(devno, 1);
    kfree(buf);
    pr_info("ringdev: выгружен\n");
}

module_init(ring_init);
module_exit(ring_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Кольцевой буфер как символьное устройство");
MODULE_VERSION("1.0");

Дальше: раздел 148 — базы данных из C.

Открыть в интерактивном справочнике