Learning Platform
Глоссарий Troubleshooting
Урок 14.02 · 18 мин
Начальный
udevDevicesLinuxHotplugPersistent naming

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.


Жизненный цикл устройства от подключения до /dev
Hardware eventФизическое событие: USB-флешка воткнута, диск подключён, ethernet-кабель воткнут, kernel module загружен. Контроллер шины генерирует прерывание
Kernel driverДрайвер шины (usb-storage, sata, e1000e) распознаёт устройство, создаёт kobject в /sys, генерирует uevent
ueventUniform event: kernel выбрасывает структурированное сообщение через netlink-сокет. Содержит ACTION=add, DEVPATH=/devices/..., SUBSYSTEM=block, MAJOR/MINOR, DEVNAME
systemd-udevdDaemon, который слушает netlink. Получает uevent, читает /sys для атрибутов устройства, ищет matching rules в /lib/udev/rules.d и /etc/udev/rules.d
Apply rulesПрименяет правила: создать /dev/sdc, создать symlinks (/dev/disk/by-uuid/...), запустить helper (e.g. blkid), задать права/группу, отправить hint к Desktop (для automount)
/dev/sdc + symlinksПоявляется device node, persistent symlinks, права. Параллельно systemd-mount может смонтировать в /media. Total time -- сотни миллисекунд

Архитектура простая:

  1. Hardware event. Физическое подключение USB или загрузка драйвера.
  2. Kernel driver probe. Драйвер обнаруживает устройство, регистрирует в /sys/, генерирует uevent через netlink.
  3. systemd-udevd слушает netlink, получает uevent, читает /sys/.../, ищет matching rules.
  4. Применяет правила. Создаёт 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 в /sys
  • N: — имя устройства
  • S: — symlinks, которые создал udev
  • E: — 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
WARNING

ВНИМАНИЕ: при 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 — wwan
  • o<N> — onboard (встроенный, нумерация от BIOS)
  • p<bus>s<slot> — PCI (bus и slot)
  • s<N> — hot-plug slot

В отличие от eth0 — имя enp4s0 физически привязано к слоту PCI и не меняется при добавлении других карт.


Что происходит при kernel module load

Когда вы делаете modprobe e1000e, происходит цепочка:

  1. modprobe загружает .ko-модуль в kernel.
  2. Драйвер регистрируется в подсистеме (PCI bus, USB, etc.).
  3. Подсистема для каждого устройства, которое match-ит этот драйвер, вызывает probe().
  4. Probe создаёт kobject в /sys, генерирует uevent.
  5. 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

Проверка знанийKnowledge check
Перезагрузил сервер -- наша nightly-backup перестал работать. В скрипте написано mount /dev/sdb1 /mnt/backup, но теперь sdb1 -- это not backup-диск, а data-диск. Что произошло и как починить раз и навсегда?
ОтветAnswer
Произошла классическая беда динамического naming. При загрузке kernel опрашивает SATA-контроллер в каком-то порядке, и какой диск стал sda, какой sdb -- зависит от множества факторов (firmware, ACPI-таблицы, версия ядра, время инициализации контроллеров). При предыдущем запуске backup был sdb1, при этом -- стал sdc1, а на sdb1 теперь data-диск. Скрипт смонтировал не тот диск, что мог привести к потере данных (если он что-то записал в data вместо backup) или фейлу. Правильный фикс -- НИКОГДА не использовать /dev/sdX в production-скриптах и /etc/fstab. Использовать persistent symlinks: 1. UUID файловой системы (стабилен через reboot, через шину): sudo blkid /dev/sdb1: UUID=7f8c1d2e-... В скрипте/fstab: mount UUID=7f8c1d2e-... /mnt/backup В fstab формат: UUID=7f8c1d2e-... /mnt/backup ext4 defaults 0 2 2. Label (если задан): sudo e2label /dev/sdb1 BACKUP # одноразово mount LABEL=BACKUP /mnt/backup 3. by-id (стабилен даже если переформатировать ФС): /dev/disk/by-id/usb-Kingston_..._SERIAL Правило: смотри в /dev/disk/ перед написанием fstab/скриптов. Раз поправил -- забыл, через reboot всё работает. Дополнительно: если нужна автоматизация на подключение конкретного диска -- udev rule. Прописать ID_SERIAL_SHORT и RUN+="/usr/local/bin/backup.sh". Тогда даже если флешка втыкается в любой USB-порт, скрипт запустится только для конкретной флешки. Кстати, та же проблема с network interfaces -- решена в systemd через predictable names (enp4s0 вместо eth0). Если у вас старый сервер с eth0 в конфигах -- стоит мигрировать на predictable.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 6. Что физически делает udev при подключении USB-флешки?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4