Как ОС разговаривает с устройствами, зачем нужны драйверы и в каком порядке обслуживать запросы к диску
lspcidisk.py.Устройства ввода-вывода очень разные: клавиатура передаёт десяток байт в секунду, SSD — гигабайты. ОС делит их на три вида:
| Вид | Как работают | Примеры |
|---|---|---|
| Блочные | Читают и пишут блоками по номеру, можно обратиться к любому блоку | Диски, SSD, флешки |
| Символьные | Поток байтов, по порядку, без адресов | Клавиатура, мышь, последовательный порт, принтер |
| Сетевые | Пакеты, свой интерфейс (сокеты) | Сетевые карты, Wi-Fi |
Процессор не управляет механикой диска напрямую. У каждого устройства есть контроллер — микросхема со своими регистрами: в одни процессор записывает команды («прочитать блок 1000»), из других читает состояние («готово», «ошибка»). Драйвер — часть ядра, которая знает язык конкретного контроллера и даёт остальной ОС единый интерфейс: «прочитать», «записать». Поэтому файловой системе всё равно, SATA-диск под ней или NVMe.
Устройства в Windows разбиты на классы. Какие классы есть в системе и сколько в каждом устройств:
PS> Get-PnpDevice -PresentOnly | Group-Object Class | Sort-Object Count -Descending | Select-Object -First 10 Count, Name
Count Name
----- ----
35 System
16 USB
4 Volume
4
2 SoftwareDevice
2 Net
2 SCSIAdapter
2 HDC
2 Mouse
2 Processor
PS> Get-PnpDevice -PresentOnly -Class DiskDrive, Net, Display | Select-Object Class, FriendlyName, Status
Class FriendlyName Status
----- ------------ ------
DiskDrive QEMU HARDDISK OK
Net Microsoft Kernel Debug Network Adapter OK
Net Intel(R) PRO/1000 MT Network Connection OK
Display Microsoft Basic Display Adapter OK
В виртуальной машине устройства тоже виртуальные: диск «QEMU HARDDISK», сетевая карта, которую гипервизор изображает как Intel PRO/1000, видеоадаптер «Microsoft Basic Display Adapter» — стандартный драйвер Windows, который ставится, когда своего драйвера видеокарты нет.
Как процессор узнаёт, что устройство закончило работу? Три способа:
| Способ | Как | Когда годится |
|---|---|---|
| Программный опрос | Процессор в цикле читает регистр состояния: «готово? готово?» | Очень быстрые устройства, где ждать недолго; иначе процессор впустую занят |
| Прерывания | Процессор занимается другим; устройство, закончив, подаёт сигнал прерывания, процессор откладывает текущую работу и выполняет обработчик драйвера | Почти всё: клавиатура, сеть, диск |
| DMA | Контроллер прямого доступа к памяти сам копирует блок данных между устройством и памятью, процессор получает одно прерывание в конце | Большие объёмы: диски, сеть, звук, видео |
Без DMA процессор копировал бы каждый байт сам: чтение 1 ГБ с диска заняло бы его полностью. С DMA он только даёт команду «прочитать 1 МБ в такой-то адрес памяти» и узнаёт о завершении по прерыванию.
Номера прерываний называют IRQ. В Linux счётчики прерываний по каждому ядру — в /proc/interrupts:
$ head -1 /proc/interrupts; grep -E "timer|virtio|ata|eth|ens" /proc/interrupts | head -6
CPU0 CPU1
0: 34 0 IO-APIC 2-edge timer
10: 0 2873354 IO-APIC 10-fasteoi virtio0
14: 0 0 IO-APIC 14-edge ata_piix
15: 0 5662968 IO-APIC 15-edge ata_piix
24: 0 0 PCI-MSIX-0000:00:12.0 0-edge virtio1-config
25: 0 5411376 PCI-MSIX-0000:00:12.0 1-edge virtio1-input.0
virtio1-input — приём пакетов виртуальной сетевой картой, ata_piix на линии 15 — контроллер диска. Миллионы прерываний — это нормально для сервера, который работает месяцами. Видно и то, что прерывания устройств обрабатывает в основном одно ядро (CPU1). В Windows то же показывает Диспетчер устройств → Вид → Ресурсы по типу → Запрос на прерывание (IRQ).
Plug and Play — «подключи и работай». Когда устройство подключается, шина (USB, PCI Express) сообщает ОС его идентификатор: код производителя и модели (VID и PID для USB, Vendor и Device для PCI). ОС ищет драйвер с этим идентификатором — в Windows в хранилище драйверов, в Linux среди модулей ядра — загружает его, и устройство готово.
Какие драйверы обслуживают устройства, в PowerShell:
PS> Get-CimInstance Win32_PnPSignedDriver -Filter "DeviceClass='NET' or DeviceClass='DISKDRIVE'" | Select-Object DeviceName, DriverVersion, DriverProviderName, InfName
DeviceName DriverVersion DriverProviderName InfName
---------- ------------- ------------------ -------
Intel(R) PRO/1000 MT Network Connection 8.4.13.0 Microsoft nete1g3e.inf
Disk drive 10.0.26100.9711 Microsoft disk.inf
Microsoft Kernel Debug Network Adapter 10.0.26100.8521 Microsoft kdnic.inf
InfName — файл описания драйвера (.inf): какие устройства он поддерживает и какие файлы ставить. В 64-разрядной Windows драйвер режима ядра должен иметь цифровую подпись Microsoft — защита от вредоносных драйверов (раздел 02: драйвер работает с правами ядра).
Диспетчер устройств (Win+X → Диспетчер устройств или devmgmt.msc) — главный инструмент:
Из командной строки драйверы ставит pnputil /add-driver путь\driver.inf /install; pnputil /enum-drivers перечисляет добавленные сторонние пакеты драйверов (на чистой Windows список пуст).
В Linux устройства на шине PCI показывает lspci, USB — lsusb (пакет usbutils), а что сказало ядро при подключении — sudo dmesg | tail:
$ lspci
00:00.0 Host bridge: Intel Corporation 440FX - 82441FX PMC [Natoma] (rev 02)
00:01.0 ISA bridge: Intel Corporation 82371SB PIIX3 ISA [Natoma/Triton II]
00:01.1 IDE interface: Intel Corporation 82371SB PIIX3 IDE [Natoma/Triton II]
00:01.2 USB controller: Intel Corporation 82371SB PIIX3 USB [Natoma/Triton II] (rev 01)
00:01.3 Bridge: Intel Corporation 82371AB/EB/MB PIIX4 ACPI (rev 03)
00:02.0 VGA compatible controller: Device 1234:1111 (rev 02)
00:03.0 Unclassified device [00ff]: Red Hat, Inc. Virtio memory balloon
00:05.0 PCI bridge: Red Hat, Inc. QEMU PCI-PCI bridge
00:12.0 Ethernet controller: Red Hat, Inc. Virtio network device
00:1e.0 PCI bridge: Red Hat, Inc. QEMU PCI-PCI bridge
00:1f.0 PCI bridge: Red Hat, Inc. QEMU PCI-PCI bridge
01:01.0 SCSI storage controller: Red Hat, Inc. Virtio SCSI
Здесь видно, что ВМ работает на KVM: устройства «Red Hat, Inc. Virtio» — паравиртуальные. Гостевая ОС знает, что они виртуальные, и общается с гипервизором напрямую, без имитации настоящего оборудования, — это быстрее. Драйверы virtio встроены в ядро Linux; для Windows их ставят отдельно.
buff/cache из раздела 07. Повторное чтение файла идёт из памяти, а не с диска.Отсюда «Безопасное извлечение устройства»: команда сбрасывает на флешку всё, что ещё лежит в кэше, и отключает её. Выдернуть флешку без этого — потерять последние изменения и, на FAT32 без журнала, испортить файловую систему. В Windows для съёмных дисков по умолчанию включена политика «Быстрое удаление» — отложенная запись для них отключена, но и она не спасает, если флешку выдернули во время копирования. В Linux сбросить кэш на диски — sync, отключить — umount.
У жёсткого диска (HDD) чтение блока складывается из перемещения головки к нужной дорожке-цилиндру (время поиска, миллисекунды), ожидания, пока нужный сектор подъедет под головку (задержка вращения), и самой передачи. Поиск — самое дорогое. Когда в очереди много запросов, ОС может выбирать порядок, чтобы головка проходила меньше.
Пример: цилиндры 0–199, головка на цилиндре 53, очередь запросов 98, 183, 37, 122, 14, 124, 65, 67. Сравнивают общий путь головки — сумму перемещений.
FCFS — по порядку поступления. Головка мечется через весь диск:
$ python3 calc/disk.py fcfs 53 up 199 "98 183 37 122 14 124 65 67"
FCFS; головка 53, цилиндры 0–199
Путь: 53 → 98 → 183 → 37 → 122 → 14 → 124 → 65 → 67
Перемещения: 45 + 85 + 146 + 85 + 108 + 110 + 59 + 2 = 640
SSTF (shortest seek time first) — следующим ближайший к головке запрос. Путь резко короче, но это аналог SJF из раздела 05: запросы на краях диска могут ждать бесконечно, если рядом с головкой всё время появляются новые:
$ python3 calc/disk.py sstf 53 up 199 "98 183 37 122 14 124 65 67"
SSTF; головка 53, цилиндры 0–199
Путь: 53 → 65 → 67 → 37 → 14 → 98 → 122 → 124 → 183
Перемещения: 12 + 2 + 30 + 23 + 84 + 24 + 2 + 59 = 236
SCAN («лифт») — головка идёт в одну сторону до края диска, обслуживая запросы по пути, потом разворачивается. LOOK — то же, но разворачивается у последнего запроса, а не у края:
$ python3 calc/disk.py scan 53 down 199 "98 183 37 122 14 124 65 67"
SCAN, направление к меньшим; головка 53, цилиндры 0–199
Путь: 53 → 37 → 14 → 0 → 65 → 67 → 98 → 122 → 124 → 183
Перемещения: 16 + 23 + 14 + 65 + 2 + 31 + 24 + 2 + 59 = 236
$ python3 calc/disk.py look 53 down 199 "98 183 37 122 14 124 65 67"
LOOK, направление к меньшим; головка 53, цилиндры 0–199
Путь: 53 → 37 → 14 → 65 → 67 → 98 → 122 → 124 → 183
Перемещения: 16 + 23 + 51 + 2 + 31 + 24 + 2 + 59 = 208
C-SCAN (циклический) — обслуживает только в одну сторону; дойдя до края, головка быстро возвращается к началу и снова идёт вверх. Путь длиннее (возврат 199 → 0 тоже считается), зато время ожидания равномернее: запросы в начале диска не ждут двойного прохода:
$ python3 calc/disk.py cscan 53 up 199 "98 183 37 122 14 124 65 67"
C-SCAN, направление к большим; головка 53, цилиндры 0–199
Путь: 53 → 65 → 67 → 98 → 122 → 124 → 183 → 199 → 0 → 14 → 37
Перемещения: 12 + 2 + 31 + 24 + 2 + 59 + 16 + 199 + 14 + 23 = 382
| Алгоритм | Путь | Особенность |
|---|---|---|
| FCFS | 640 | Честно, но медленно |
| SSTF | 236 | Быстро, возможно голодание дальних запросов |
| SCAN | 236 | Без голодания, доезжает до края |
| LOOK | 208 | SCAN без лишнего пути до края |
| C-SCAN | 382 (с возвратом) | Самое равномерное ожидание |
Как вручную: выпишите запросы по возрастанию — 14, 37, 65, 67, 98, 122, 124, 183, — отметьте головку 53 и проведите путь по правилу алгоритма. Для SCAN, LOOK и C-SCAN важно направление движения: оно задаётся в условии задачи.
У SSD нет головки, время доступа к любому блоку одинаковое, и алгоритмы «лифта» теряют смысл. Современные ОС для SSD и быстрых виртуальных дисков используют простые планировщики. В Linux планировщик ввода-вывода диска виден в /sys, выбранный — в квадратных скобках:
$ lsblk -d -o NAME,SIZE,ROTA,TYPE,MODEL
NAME SIZE ROTA TYPE MODEL
sda 32G 1 disk QEMU HARDDISK
sr0 3,7G 1 rom QEMU DVD-ROM
$ cat /sys/block/sda/queue/scheduler
[none] mq-deadline
none — без переупорядочивания, запросы сразу уходят устройству; mq-deadline — сортирует запросы, но гарантирует каждому срок исполнения. ROTA 1 — ядро считает диск вращающимся: гипервизор не сообщил, что под ним SSD. Windows в той же ВМ тоже не знает тип диска:
PS> Get-PhysicalDisk | Select-Object FriendlyName, MediaType, BusType, @{n="ГБ"; e={[math]::Round($_.Size/1GB)}}
FriendlyName MediaType BusType ГБ
------------ --------- ------- --
QEMU HARDDISK Unspecified SATA 64
У SSD своя особенность: перезаписать занятую ячейку нельзя, сначала её нужно стереть, а стирается она большими блоками. Если удалённые файлом блоки не отметить свободными, SSD со временем замедляется. Команда TRIM сообщает SSD, какие блоки ФС освободила. В Windows TRIM включён, когда DisableDeleteNotify = 0:
PS> fsutil behavior query DisableDeleteNotify
NTFS DisableDeleteNotify = 0 (Позволяет отправлять операции TRIM на запоминающее устройство)
ReFS DisableDeleteNotify = 0 (Позволяет отправлять операции TRIM на запоминающее устройство)
Windows отправляет TRIM при удалении файлов и раз в неделю в задаче «Оптимизация дисков» (dfrgui), которая для HDD делает дефрагментацию, а для SSD — только TRIM: дефрагментировать SSD бесполезно и вредно, это лишние циклы записи. В Debian то же делает служба fstrim раз в неделю — она мелькала в списке systemd-analyze blame в разделе 02.
lspci, dmesgsync