Block vs char devices — два мира /dev
Откройте ls /dev на любой Linux-машине, и вы увидите странный зоопарк. sda1, tty3, null, random, input/mouse0, nvme0n1p2. Что это такое? Это файлы? Не совсем. Это “ручки”, через которые userspace-программы говорят с железом. Их называют device nodes, и они — одна из самых красивых абстракций Unix: “всё есть файл”, даже мышь, диск и генератор случайных чисел.
В этом уроке разберём два больших мира /dev: block devices (диски, разделы, RAID, LVM) и character devices (терминалы, мыши, аудио, /dev/null, /dev/random). Поймём, чем они отличаются физически, как kernel понимает, какой драйвер вызывать через major/minor numbers, и зачем уметь читать ls -l /dev/.
Зачем устройство выглядит как файл
В классических ОС (DOS, ранний Windows) каждое устройство было особым — открываешь дискету через одни API, принтер — через другие. Unix в 1970-е принял радикальное решение: всё, что не процесс — файл. Диск — файл, который можно read()/write(). Терминал — тоже файл, можно read() нажатые клавиши, write() — символ появится на экране. Сетевая карта чуть выбилась (сокеты — отдельный API), но всё остальное действительно файлы.
Это даёт мощную вещь: одни и те же команды работают везде. cp file1 /dev/sda запишет содержимое в диск (не делайте этого!). cat /dev/urandom | head -c 16 прочитает 16 случайных байт. echo hello > /dev/tty3 — если на третьей виртуальной консоли сидит пользователь, он увидит “hello”.
Когда вы делаете open("/dev/sda1", O_RDONLY), kernel не открывает файл на диске. Он находит драйвер по major number и говорит ему: “клиент хочет читать тебя, готовься”. Дальше read() уходит прямо в драйвер, минуя файловую систему.
Block device: блоки по 4K, random access
Block device — это устройство, которое отдаёт данные блоками фиксированного размера (обычно 512 байт или 4K) и поддерживает random access (можно прыгнуть на любое смещение). Это классические “хранилища”: HDD, SSD, NVMe, USB-флешки, CD/DVD, loopback-устройства, LVM volumes, RAID arrays.
$ ls -l /dev/sda /dev/sda1 /dev/nvme0n1 /dev/nvme0n1p1
brw-rw---- 1 root disk 8, 0 May 18 09:12 /dev/sda
brw-rw---- 1 root disk 8, 1 May 18 09:12 /dev/sda1
brw-rw---- 1 root disk 259, 0 May 18 09:12 /dev/nvme0n1
brw-rw---- 1 root disk 259, 1 May 18 09:12 /dev/nvme0n1p1
Обратите внимание на первый символ: b. Это маркер block device. Дальше — права (rw для root и группы disk), потом два числа: major (8 для SCSI/SATA, 259 для NVMe) и minor (0, 1, 2 — номер раздела). И disk — это группа, члены которой могут читать-писать диск напрямую (опасный privilege!).
Главная особенность block-устройств — page cache. Когда вы читаете /dev/sda1, kernel не идёт сразу в диск — он сначала смотрит в page cache. Если страница уже в RAM (потому что её недавно читал другой процесс) — отдаёт оттуда. Write тоже идёт через cache — сначала в RAM, потом kernel асинхронно сбрасывает на диск. Это огромное ускорение, но и источник проблем (durability, fsync — модуль 10).
На блочное устройство можно “положить” файловую систему: mkfs.ext4 /dev/sda1. После этого файлы внутри FS попадают на диск через VFS, который дёргает драйвер блочного устройства. Если же делать dd if=/dev/sda1 of=backup.img — читаются raw-блоки, минуя FS. Это и есть мощь Unix: один и тот же интерфейс read/write работает на двух уровнях.
Character device: байт за байтом, sequential
Character device — это устройство, которое отдаёт поток байт без понятия “блок” и часто без random access. Терминал, мышь, клавиатура, последовательный порт, /dev/null, /dev/random, аудио. Когда вы нажимаете клавишу, в драйвер прилетают конкретные байты (например, “27 91 65” для стрелки вверх) — они доступны через read(/dev/tty0).
$ ls -l /dev/tty0 /dev/null /dev/zero /dev/random /dev/urandom /dev/input/mouse0 2>/dev/null
crw--w---- 1 root tty 4, 0 May 18 09:12 /dev/tty0
crw-rw-rw- 1 root root 1, 3 May 18 09:12 /dev/null
crw-rw-rw- 1 root root 1, 5 May 18 09:12 /dev/zero
crw-rw-rw- 1 root root 1, 8 May 18 09:12 /dev/random
crw-rw-rw- 1 root root 1, 9 May 18 09:12 /dev/urandom
crw-r----- 1 root input 13, 32 May 18 09:12 /dev/input/mouse0
Первый символ — c. Это character device. И опять major/minor: major 1 — “memory” (null, zero, random живут в одном драйвере mem); major 4 — tty; major 13 — input subsystem.
Главное отличие от block: нет page cache. Каждый read() идёт прямо в драйвер. Это логично — ну какой смысл кэшировать вывод /dev/random или нажатие клавиши? Поэтому character devices обычно медленнее по latency, но bandwidth их редко интересует — мышь генерирует 100 байт в секунду.
Major и minor: как kernel диспатчит вызов
Major number — это идентификатор драйвера. Когда драйвер загружается (например, sd_mod для SCSI/SATA дисков), он регистрирует свой major у kernel: “я отвечаю за major 8”. С этого момента любой open() на устройство с major 8 уходит в этот драйвер.
Minor number — это конкретный инстанс внутри драйвера. Для дисков: minor 0 — /dev/sda, minor 1 — /dev/sda1, minor 16 — /dev/sdb, minor 32 — /dev/sdc. Драйвер сам решает, как интерпретировать minor.
Посмотреть, какие major зарегистрированы прямо сейчас:
$ cat /proc/devices
Character devices:
1 mem
4 tty
5 /dev/tty
7 vcs
10 misc
13 input
21 sg
29 fb
116 alsa
180 usb
189 usb_device
226 drm
Block devices:
7 loop
8 sd
9 md
11 sr
65 sd
66 sd
253 device-mapper
254 mdp
259 blkext
Это динамический список. Когда загружаете модуль (modprobe) — появится новый major. Когда выгружаете (rmmod) — исчезнет.
Файлы, которые не файлы: /dev/null, /dev/zero, /dev/random
Эти “псевдоустройства” живут в kernel и не имеют физического железа. Но они character devices с major/minor — и работают через те же абстракции.
/dev/null (major 1, minor 3) — “чёрная дыра”. read() всегда возвращает 0 (EOF). write() всегда успешен, но данные выбрасываются. Используется чтобы выкинуть вывод: command > /dev/null 2>&1.
/dev/zero (1, 5) — бесконечный нулевой поток. read() всегда возвращает запрошенное количество нулевых байт. Используется для создания пустых файлов нужного размера или для wipe данных: dd if=/dev/zero of=zerofile bs=1M count=100 — создаст 100 МБ нулей.
/dev/random (1, 8) — криптографический PRNG. До Linux 5.6 блокировал, если “энтропии не хватало”. С 5.6 ведёт себя как /dev/urandom.
/dev/urandom (1, 9) — криптографический PRNG, никогда не блокирует. Источник случайности для ssh-keygen, openssl, всего криптографического. По современному консенсусу — используйте urandom всегда.
# Создать файл 100 МБ нулей:
dd if=/dev/zero of=zeros.bin bs=1M count=100
# Прочитать 16 случайных байт в hex:
dd if=/dev/urandom bs=16 count=1 2>/dev/null | xxd
# Выкинуть весь вывод:
make 2>&1 > /dev/null
# Замерить скорость диска через /dev/zero (write benchmark):
dd if=/dev/zero of=/tmp/test bs=1M count=1024 conv=fdatasync
dd if=/dev/zero of=/dev/sda — сотрёт ваш диск. dd if=/dev/sda of=/dev/sdb — забэкапит sda в sdb (точная копия байтов). Эти команды легко уничтожают данные — работайте под root только с двойной проверкой of=.
/dev/tty, /dev/pts: терминалы
Терминалы — отдельная вселенная. Когда вы логинитесь по SSH, ваша shell привязана к pseudo-terminal (pty), который представлен в /dev/pts/N. Когда вы переключаетесь Alt-F1…F6 в локальной консоли, вы переключаете виртуальные терминалы /dev/tty1…/dev/tty6.
$ tty # на какой терминал привязан текущий процесс
/dev/pts/0
$ ls -l /dev/pts/
crw--w---- 1 lev tty 136, 0 May 18 09:30 /dev/pts/0
crw--w---- 1 lev tty 136, 1 May 18 09:31 /dev/pts/1
Major 136 — pty slave. Через эти файлы terminal emulator (gnome-terminal, alacritty, mosh) общается с shell. write(/dev/pts/0, "hello\n") — буквы появятся в окне.
Попробуйте: echo "Hello from other terminal" > /dev/pts/0 из другого окна — получатель увидит сообщение.
Попробуй сам
# 1. Посмотрите major/minor всех block-устройств:
ls -l /dev/sd* /dev/nvme* 2>/dev/null
# Первая буква 'b' -- block; колонки с числами -- это major,minor
# 2. Посмотрите registered drivers:
cat /proc/devices
# 3. /dev/null в действии:
echo "this disappears" > /dev/null
cat /dev/null # ничего не выводит -- EOF сразу
# 4. /dev/zero в действии:
head -c 32 /dev/zero | xxd
# 00000000: 0000 0000 ... -- 32 нулевых байта
# 5. Создайте свой character device через mknod (требует root):
sudo mknod /tmp/mynull c 1 3 # такой же major/minor как /dev/null
ls -l /tmp/mynull
echo "test" > /tmp/mynull # работает как настоящий /dev/null
sudo rm /tmp/mynull
# 6. Узнайте свой текущий tty:
tty
# 7. Посмотрите все pty (другие активные сессии):
ls -l /dev/pts/
# 8. Отправьте сообщение другой сессии (нужно знать её tty):
# В другом окне: tty -> /dev/pts/1
# В первом окне: echo "Привет из tty 0" > /dev/pts/1
Главные различия в одной таблице
Когда вы дальше будете дебажить “почему диск медленно работает” — вы будете смотреть на /dev/sda и его очереди (urok 14). Когда “почему ssh-сессия зависла” — на /dev/pts/N. Знание, какой тип устройства перед вами, экономит время.