udev — кто создаёт /dev на лету
Вы воткнули USB-флешку, и через секунду в /dev/sdc появилось устройство, а /media/user/Kingston сама смонтировалась. Никто не запускал mknod, никто не редактировал /dev руками. Кто это сделал? udev — userspace-демон, который слушает события kernel о hotplug-устройствах и динамически создаёт device nodes, применяя ваши правила.
В этом уроке разберём: что было до udev (статический /dev из 10000 файлов), как udev работает с kernel через netlink-сокет, как читать и писать udev rules, и зачем нужен persistent naming — стабильные имена дисков и сетевых карт, не зависящие от порядка обнаружения.
История: статический /dev и его проблемы
До 2003 года Linux имел статический /dev — директория содержала файлы для всех возможных устройств заранее, “на всякий случай”. Дистрибутивы поставляли скрипт MAKEDEV, который создавал примерно 10000 device nodes: /dev/hda до /dev/hdz, /dev/sda до /dev/sdz, /dev/tty0 до /dev/tty63, аудио, видео, всё подряд.
Это работало, но имело проблемы:
- Засорение. 10000 файлов в /dev, большинство мёртвые (нет железа).
- Hot-plug сложен. Воткнули новую USB-флешку — надо создать new node, но имени нет.
- Имена нестабильные. Воткнули две флешки, перезагрузились — порядок sdc/sdd может поменяться.
В 2003 пришёл devfs (kernel-генерирует /dev), а в 2004 его заменил udev — userspace-демон, гораздо гибче.
С 2012 года udev объединён с systemd (systemd-udevd), и это стандартный механизм на всех современных дистрибутивах: Ubuntu, Debian, RHEL, Fedora, Arch.
Архитектура: kernel -> netlink -> udevd -> /dev
Архитектура простая:
- Hardware event. Физическое подключение USB или загрузка драйвера.
- Kernel driver probe. Драйвер обнаруживает устройство, регистрирует в
/sys/, генерируетueventчерез netlink. - systemd-udevd слушает netlink, получает uevent, читает
/sys/.../, ищет matching rules. - Применяет правила. Создаёт device node, symlinks, ставит права, запускает helper-программы.
Посмотреть, что прямо сейчас приходит:
# Включить мониторинг udev событий (нужны права):
sudo udevadm monitor --kernel --udev
# Теперь воткните USB-флешку -- увидите поток событий:
KERNEL[1234.567] add /devices/.../usb1/1-2 (usb)
KERNEL[1234.589] add /devices/.../host3/target3:0:0/3:0:0:0 (scsi)
KERNEL[1234.601] add /devices/.../block/sdc (block)
KERNEL[1234.602] add /devices/.../block/sdc/sdc1 (block)
UDEV [1234.689] add /devices/.../block/sdc (block)
UDEV [1234.702] add /devices/.../block/sdc/sdc1 (block)
Два потока: KERNEL — что отправляет kernel; UDEV — что обработал udev daemon (с задержкой, после применения правил). Между ними может пройти 50-200 мс.
/sys — источник атрибутов устройства
Когда udev получает событие, ему нужны атрибуты устройства: vendor, model, серийный номер, размер. Эти данные kernel экспортирует в /sys/. udev читает /sys/.../ и использует значения в правилах.
# Полная информация об устройстве:
udevadm info /dev/sdc
P: /devices/pci0000:00/0000:00:14.0/usb1/1-4/1-4:1.0/host3/target3:0:0/3:0:0:0/block/sdc
N: sdc
L: 0
S: disk/by-id/usb-Kingston_DataTraveler_3.0_60A44C413..._0:0
S: disk/by-path/pci-0000:00:14.0-usb-0:4:1.0-scsi-0:0:0:0
E: DEVPATH=/devices/.../block/sdc
E: DEVNAME=/dev/sdc
E: DEVTYPE=disk
E: MAJOR=8
E: MINOR=32
E: SUBSYSTEM=block
E: USEC_INITIALIZED=1234567
E: ID_VENDOR=Kingston
E: ID_MODEL=DataTraveler_3.0
E: ID_SERIAL_SHORT=60A44C413FCC
E: ID_BUS=usb
Здесь:
P:— devpath в /sysN:— имя устройстваS:— symlinks, которые создал udevE:— environment variables (атрибуты, доступные правилам)
В правилах вы можете писать: “если ID_VENDOR=Kingston, создай symlink /dev/my-flash”. udev подставит соответствующее имя.
udev rules: язык правил
udev rules — это файлы с расширением .rules в двух местах:
/lib/udev/rules.d/(системные, ставятся пакетами)/etc/udev/rules.d/(admin custom, override)
Файлы обрабатываются в лексикографическом порядке имени. Поэтому файлы называют 70-mydev.rules — число впереди задаёт приоритет.
Синтаксис правила — одна строка: пары ключ=значение через запятую. Ключи делятся на:
- Matching keys (что искать):
SUBSYSTEM,KERNEL,ACTION,ATTR{...},ENV{...}— используются с==или!=. - Assignment keys (что делать):
NAME,SYMLINK,OWNER,GROUP,MODE,RUN— используются с=или+=.
# Пример: для всех USB-флешек Kingston, создать symlink /dev/kingston
SUBSYSTEM=="block", ENV{ID_VENDOR}=="Kingston", SYMLINK+="kingston"
# Пример: для конкретной мышки -- сделать читаемой обычным юзерам
SUBSYSTEM=="input", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c534", MODE="0666"
# Пример: при подключении конкретного диска -- запустить backup
SUBSYSTEM=="block", ACTION=="add", ENV{ID_SERIAL_SHORT}=="ABC123", RUN+="/usr/local/bin/auto-backup.sh"
После добавления правила — перезагрузить udev:
sudo udevadm control --reload-rules
sudo udevadm trigger # перепрогнать события для существующих
Persistent naming: почему /dev/disk/by-uuid важнее /dev/sda
Главная боль динамических имён: порядок обнаружения нестабилен. Воткнули две флешки — одна стала sdb, другая sdc. Перезагрузились — порядок поменялся. Это катастрофа для /etc/fstab — если вы написали “монтировать /dev/sdb1 в /data”, после reboot туда может смонтироваться флешка.
Решение: persistent symlinks под /dev/disk/. udev автоматически создаёт несколько вариантов:
$ ls -l /dev/disk/by-uuid/
lrwxrwxrwx 1 root root 10 May 18 09:12 5d3e0f4a-... -> ../../sda1
lrwxrwxrwx 1 root root 10 May 18 09:12 7f8c1d2e-... -> ../../sdb1
$ ls -l /dev/disk/by-id/
lrwxrwxrwx 1 root root 9 May 18 09:12 ata-Samsung_SSD_870_EVO_1TB_S5RXNF0... -> ../../sda
lrwxrwxrwx 1 root root 9 May 18 09:12 usb-Kingston_DataTraveler_3.0_60A44C413... -> ../../sdc
$ ls -l /dev/disk/by-label/
lrwxrwxrwx 1 root root 10 May 18 09:12 BACKUP -> ../../sdb1
$ ls -l /dev/disk/by-path/
lrwxrwxrwx 1 root root 9 May 18 09:12 pci-0000:00:17.0-ata-1 -> ../../sda
Четыре варианта стабильных имён:
- by-uuid — UUID файловой системы (создаётся при mkfs). Лучший выбор для /etc/fstab.
- by-id — ID железа (модель + серийник). Стабилен даже если перешили ФС.
- by-label — метка файловой системы (если задана).
- by-path — путь по физической шине (полезно если железо не меняется, но FS пересоздаётся).
В /etc/fstab правильно писать так:
# Плохо -- может сломаться:
/dev/sda1 / ext4 defaults 0 1
# Хорошо -- стабильно:
UUID=5d3e0f4a-1234-5678-9abc-def012345678 / ext4 defaults 0 1
Узнать UUID:
sudo blkid
/dev/sda1: UUID="5d3e0f4a-..." TYPE="ext4"
/dev/sdb1: UUID="7f8c1d2e-..." TYPE="ext4" LABEL="BACKUP"
# или через udev:
lsblk -f
ВНИМАНИЕ: при mkfs.ext4 /dev/sdX1 — генерируется НОВЫЙ UUID. Если вы переформатировали диск, старый UUID в /etc/fstab больше не сработает — mount будет фейлиться. Поэтому после mkfs всегда обновляйте fstab и/или используйте tune2fs -U <new-uuid> /dev/sdX1 чтобы вернуть прежний UUID.
Persistent naming для сетевых интерфейсов
Та же проблема с сетевыми картами. В старых дистрибутивах было eth0, eth1, eth2 — и порядок зависел от того, в каком порядке kernel обнаружил PCI-устройства. Перезагрузились с другим железом или другой версией ядра — порядок поменялся, /etc/network перестал работать.
С 2015 systemd ввёл predictable network interface names. Имя выводится из физического расположения карты:
$ ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 ...
2: eno1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ... # onboard NIC #1
3: enp4s0: <BROADCAST,MULTICAST> mtu 1500 ... # PCI bus 4, slot 0
4: wlp3s0: <BROADCAST,MULTICAST,UP> mtu 1500 ... # Wireless, PCI bus 3
Схема:
en— ethernet,wl— wireless,ww— wwano<N>— onboard (встроенный, нумерация от BIOS)p<bus>s<slot>— PCI (bus и slot)s<N>— hot-plug slot
В отличие от eth0 — имя enp4s0 физически привязано к слоту PCI и не меняется при добавлении других карт.
Что происходит при kernel module load
Когда вы делаете modprobe e1000e, происходит цепочка:
- modprobe загружает .ko-модуль в kernel.
- Драйвер регистрируется в подсистеме (PCI bus, USB, etc.).
- Подсистема для каждого устройства, которое match-ит этот драйвер, вызывает
probe(). - Probe создаёт kobject в /sys, генерирует uevent.
- udev получает uevent, применяет правила, создаёт /dev/eth0 или /dev/enp4s0.
То же самое при rmmod (с ACTION=remove). Устройство исчезает из /sys, udev удаляет node.
Дебаг udev: что делать, если правило не работает
Самая частая ошибка: правило написано, но не применяется.
# 1. Перезагрузить правила:
sudo udevadm control --reload-rules
# 2. Симулировать событие add для конкретного устройства:
sudo udevadm test /sys/class/block/sdc 2>&1 | head -40
# Покажет, какие правила match-ятся
# 3. Посмотреть текущие свойства устройства:
udevadm info -a /dev/sdc
# Длинный вывод с ATTR{...} -- можно использовать в матчинге
# 4. Триггер всех событий заново:
sudo udevadm trigger
# 5. Мониторинг live-событий:
sudo udevadm monitor --property
Частые грабли:
==для матчинга,=для assignment — путают.ATTR{...}— только текущего устройства;ATTRS{...}— ищет в родительских устройствах. Для USB — часто нужныATTRS.- Кавычки строгие:
KERNEL=="sdc"не сматчится сsdc1.
Попробуй сам
# 1. Посмотреть live-события udev:
sudo udevadm monitor --kernel --udev
# В другом окне -- воткнуть/вынуть USB, нажать клавишу
# 2. Полная info о вашем основном диске:
udevadm info /dev/sda
# 3. Найти все symlinks для дисков:
ls -l /dev/disk/by-uuid/
ls -l /dev/disk/by-id/
# 4. Сравнить UUID в /etc/fstab с blkid:
sudo blkid
cat /etc/fstab
# 5. Тестировать правило:
# Создаём файл /etc/udev/rules.d/99-test.rules:
echo 'KERNEL=="sdc", SYMLINK+="my-usb"' | sudo tee /etc/udev/rules.d/99-test.rules
sudo udevadm control --reload-rules
sudo udevadm trigger
# Воткнуть USB -- проверить /dev/my-usb
# 6. Удалить тест:
sudo rm /etc/udev/rules.d/99-test.rules
sudo udevadm control --reload-rules
# 7. Посмотреть на predictable names сетевых:
ip link
# Сравнить с тем, как было бы в 2010-х -- eth0/eth1