Что такое драйвер — kernel modules и hardware abstraction
Когда вы делаете cat /etc/hostname, происходит огромная цепочка: open(), VFS, ext4-driver читает inode с диска, scsi-driver посылает команду чтения SATA-контроллеру, SATA-контроллер дёргает физический SSD. Каждый “переходник” между уровнями — это драйвер. Без драйверов kernel бы умел общаться только с CPU и RAM — никакого диска, сети, экрана, клавиатуры.
В этом уроке: что такое драйвер физически, почему Linux — “monolithic but modular” kernel, как работают kernel modules (.ko файлы), и команды lsmod, modprobe, insmod, rmmod для управления ими.
Драйвер — кусок кода в kernel, говорящий с железом
Драйвер — это код, который знает протокол общения с конкретным железом и предоставляет kernel API. Драйвер NVMe знает: куда писать в регистры NVMe-контроллера, как организовать submission/completion queues, как читать сектор. Driver e1000e знает: как разговаривать с Intel 1Gbit Ethernet через DMA-кольца. Driver xpad знает: как декодировать USB-пакеты с Xbox-контроллера.
С точки зрения kernel, драйвер делает три вещи:
- Register. “Я драйвер для железа с этим vendor ID и product ID” — сообщает шине (PCI, USB).
- Probe. Когда железо обнаружено, kernel вызывает
probe()драйвера — инициализация устройства. - Provide ops. Драйвер регистрирует функции (file_operations или подобное), которые kernel вызовет при
open/read/write/ioctlот userspace.
Кодом драйвера может быть, например (упрощённый псевдо-C для драйвера character device):
static int my_read(struct file *f, char __user *buf, size_t len, loff_t *off) {
// Прочитать с железа в kernel buffer
char data = read_from_device();
// Скопировать в userspace
copy_to_user(buf, &data, 1);
return 1;
}
static struct file_operations my_fops = {
.read = my_read,
.write = my_write,
.open = my_open,
};
// При загрузке:
register_chrdev(major, "my_device", &my_fops);
userspace делает read(fd, ...) — kernel находит этот fd привязан к /dev/my_device с major X, вызывает my_fops.read, оттуда read_from_device — регистры железа.
Linux kernel — monolithic, но modular
В мире OS-архитектур есть два полюса:
- Monolithic kernel. Всё в одном адресном пространстве kernel: VFS, scheduler, драйверы. Плюсы: быстро (всё рядом, нет IPC между подсистемами). Минусы: упал драйвер -> упало ядро.
- Microkernel. Минимальное ядро (IPC, scheduling). Драйверы — userspace-процессы, общаются с kernel через сообщения. Плюсы: упал драйвер — ядро живо. Минусы: медленнее (overhead на IPC).
Linux monolithic, но с подвохом — модульный. Драйверы компилируются в kernel modules — отдельные .ko-файлы. Можно загрузить в kernel в runtime через modprobe и выгрузить через rmmod. Это даёт гибкость microkernel (drivers загружаются по требованию) при производительности monolithic (загруженный модуль работает в kernel space).
# Все загруженные сейчас модули:
$ lsmod | head
Module Size Used by
nvme 57344 3
nvme_core 184320 4 nvme
e1000e 338944 0
i915 4136960 18
snd_hda_intel 61440 1
btrfs 1843200 0
xfs 2228224 0
ext4 954368 3
fuse 217088 9
В колонке Size — размер в байтах. Used by — сколько других модулей зависят от него (3 для nvme значит, что 3 другие модуля используют этот).
Где живут модули
Все собранные модули лежат в /lib/modules/$(uname -r)/:
$ ls /lib/modules/$(uname -r)/kernel/drivers/net/ | head
amd
arcnet
bonding
broadcom
cisco
dummy.ko
ethernet
hyperv
intel
...
$ find /lib/modules/$(uname -r) -name "nvme.ko*"
/lib/modules/6.5.0-21-generic/kernel/drivers/nvme/host/nvme.ko.zst
.ko (kernel object) — скомпилированный бинарь модуля. .zst (zstandard) — сжатый. Размер обычно 50-500 КБ для простых драйверов, мегабайты для сложных (i915 для Intel GPU).
Каждая kernel версия имеет свой /lib/modules/<version>/. Поэтому apt upgrade linux-image создаст новую версию ядра и новую папку с модулями. Старая остаётся (можно загрузиться, если новая упадёт).
modprobe vs insmod: загрузка модулей
Есть два способа загрузить модуль.
insmod — “тупо загрузи этот файл”:
sudo insmod /lib/modules/$(uname -r)/kernel/drivers/net/dummy.ko
Минусы: вы должны указать точный путь и сами установить зависимости (если модуль требует другой модуль — insmod не загрузит его).
modprobe — умный, понимает зависимости и алиасы:
sudo modprobe dummy
sudo modprobe e1000e
modprobe смотрит в файл /lib/modules/<version>/modules.dep (карта зависимостей, генерируется depmod) и автоматически загружает все нужные модули. Так что если у модуля есть зависимости — modprobe их сначала подгружает.
Также modprobe умеет:
- Алиасы (
alias snd-card-0 snd-hda-intelв /etc/modprobe.d/*). - Параметры (
modprobe my_driver verbose=1). - Blacklist (запрет на автозагрузку:
blacklist nouveau).
Конфиги modprobe — в /etc/modprobe.d/ и /usr/lib/modprobe.d/.
rmmod — выгрузить модуль
$ sudo rmmod dummy
# Если модуль used by кем-то:
$ sudo rmmod nvme_core
rmmod: ERROR: Module nvme_core is in use by: nvme
Нельзя выгрузить, если есть зависимости. Сначала надо выгрузить зависимые модули. Или использовать modprobe -r (умное удаление):
sudo modprobe -r nvme
# выгрузит nvme и nvme_core если nvme был последним пользователем
ВНИМАНИЕ: выгружать модули в running production-системе опасно. Если выгрузить nvme на системе, где / на NVMe — IO зависнет, kernel может паниковать. Выгружать модули осмысленно: только те, которые не критичны (например, экспериментальный driver USB-устройства).
Информация о модуле: modinfo
$ modinfo nvme
filename: /lib/modules/6.5.0-21-generic/kernel/drivers/nvme/host/nvme.ko.zst
version: 1.0
license: GPL
author: Matthew Wilcox <[email protected]m>
srcversion: ABC123...
depends: nvme-core
intree: Y
name: nvme
vermagic: 6.5.0-21-generic SMP preempt mod_unload modversions
parm: use_threaded_interrupts:bool
parm: use_cmb_sqes:use controller's memory buffer for I/O SQes (bool)
parm: default_ps_max_latency_us:max power saving latency for new devices; use PM QOS to change per device (ulong)
Это даёт:
- filename — где модуль лежит.
- license — GPL обычно (нужно для использования некоторых kernel API). Проприетарные драйверы (Nvidia legacy) имеют другую лицензию — kernel пометит это.
- depends — от каких модулей зависит.
- vermagic — для какой версии kernel собран. Если не совпадает — модуль не загрузится (kernel ABI меняется).
- parm — доступные параметры модуля.
Параметры модулей: настройка драйверов
# Передать параметр при загрузке:
sudo modprobe i915 enable_psr=0
# Сделать постоянным -- в /etc/modprobe.d/i915.conf:
options i915 enable_psr=0
# Посмотреть текущие параметры загруженного модуля:
$ cat /sys/module/nvme/parameters/use_threaded_interrupts
N
/sys/module/<name>/parameters/ — live-просмотр параметров. Некоторые параметры можно менять в runtime (echo в файл), другие — только при загрузке.
Кто и когда загружает модули автоматически
Большинство модулей в обычной системе загружаются автоматически:
- Hotplug + udev. Воткнули USB-устройство — kernel создал uevent, udev (или kmod) вызвал
modprobe usb-storage. Драйвер загружен. - Boot-time. initramfs (initrd) содержит модули для дисков/файловых систем, нужные чтобы смонтировать root FS. После mount root — остальные подгружаются по мере необходимости.
- systemd-modules-load.service. Читает
/etc/modules-load.d/*.confи/usr/lib/modules-load.d/*.conf, загружает что прописано. Полезно для серверных модулей, не привязанных к hardware (br_netfilter для k8s, nf_conntrack для firewall).
Посмотреть, кто загрузил данный модуль — сложнее. Можно dmesg | grep -i <module> — увидеть, когда был probe.
Built-in vs modular
Некоторые драйверы скомпилированы прямо в kernel image (vmlinuz) — “built-in”. Они грузятся при boot вместе с kernel и не могут быть выгружены. Другие — “modular” (.ko).
Решение built-in vs module принимается при сборке kernel:
- Built-in (
yв Kconfig) — всегда есть. - Module (
m) — собирается отдельно, загружается по требованию. - Disabled (
n) — нет.
Дистрибутивы обычно билдят почти всё как модули, а в kernel image оставляют только то, что нужно для boot (ext4, scsi/ahci/nvme базовый). Это уменьшает RAM (модуль занимает RAM только когда загружен).
Посмотреть config своей kernel:
# Часто доступен:
zcat /proc/config.gz | grep CONFIG_NVME
# CONFIG_NVME_CORE=m (modular)
# CONFIG_NVME_TCP=m
# Или:
cat /boot/config-$(uname -r) | grep CONFIG_EXT4
# CONFIG_EXT4_FS=m
Если =y — built-in; =m — module; # ... is not set — disabled.
Дебаг: проблемы с модулями
Главные граблями:
1. modprobe говорит “Module not found”. Возможно, опечатка в имени. Проверьте find /lib/modules/$(uname -r) -name "<name>*".
2. modprobe говорит “version magic mismatch”. Модуль собран под другую kernel. Часто бывает после kernel upgrade без перезагрузки. Решение: reboot. Или dkms для пересборки driver под новый kernel.
3. Драйвер загружается, но устройство не работает. Смотреть dmesg — драйвер логирует ошибки initialization туда.
4. Driver A vs Driver B для одного устройства. Иногда два драйвера могут обслужить одно железо (e.g. nouveau vs nvidia для Nvidia GPU). Тогда — blacklist одного:
echo "blacklist nouveau" | sudo tee /etc/modprobe.d/blacklist-nouveau.conf
sudo update-initramfs -u
reboot
Попробуй сам
# 1. Все загруженные модули:
lsmod | wc -l # обычно 50-150
lsmod | head
# 2. Информация о ключевом модуле:
modinfo $(lsmod | head -2 | tail -1 | awk '{print $1}')
# 3. Где живут все модули:
ls /lib/modules/$(uname -r)/kernel/
du -sh /lib/modules/$(uname -r)/
# обычно 200-500 МБ
# 4. Сколько модулей сейчас загружено:
lsmod | tail -n +2 | wc -l
# 5. Параметры текущего kernel:
ls /sys/module/
# каждая папка -- загруженный модуль или built-in subsystem
# 6. Сколько RAM занимают модули:
lsmod | awk 'NR>1 {sum += $2} END {print sum/1024/1024 " MB"}'
# 7. Безопасный тест загрузки/выгрузки (dummy module безвреден):
sudo modprobe dummy numdummies=2
ip link # увидеть dummy0, dummy1
sudo rmmod dummy
# 8. Конфиг kernel (если доступен):
cat /boot/config-$(uname -r) | grep -c '=m' # модулей в конфиге
cat /boot/config-$(uname -r) | grep -c '=y' # built-in
Drivers and Linux kernel community
Один из странных фактов о Linux: 80% драйверов идут “in-tree” — прямо в исходниках kernel. Когда выходит новая kernel, обновляются и драйверы. Это и плюс (драйверы поддерживаются вместе с kernel), и минус (нужно убедить maintainer’а принять ваш driver — может занять годы).
“Out-of-tree” драйверы — те, что собираются отдельно. Примеры: Nvidia proprietary, ZFS, некоторые vendor drivers. Их недостаток — ломаются на новых kernel версиях, требуют пересборки (dkms помогает).
В общем, для Junior DE задача проста: понимать, что драйвер — это kernel module, уметь читать lsmod/modprobe/modinfo, и не пугаться dmesg-сообщений о probe/initialization.