---
title: "Устройства ввода-вывода: прерывания и DMA"
description: "Драйверы бэкендер не пишет, но почти всё время его сервиса уходит на ввод-вывод: запрос пришёл по сети, данные прочитаны с диска, ответ ушёл обратно. Эта глава отвечает на…"
canonical: https://graphlms.ru/os/book/io-devices
course: "Операционные системы"
updated: 2026-10-12
lang: ru
publisher: GraphLMS
---

# Устройства ввода-вывода: прерывания и DMA

> Источник: https://graphlms.ru/os/book/io-devices — курс «Операционные системы», GraphLMS.

## Зачем бэкендеру устройства

Драйверы бэкендер не пишет, но почти всё время его сервиса уходит на ввод-вывод:
запрос пришёл по сети, данные прочитаны с диска, ответ ушёл обратно. Эта глава
отвечает на вопросы, которые иначе остаются магией. Почему горутина, ждущая
ответа от базы, не ест CPU? Куда уходит время внутри `read()`? Что значит
колонка `si` в `top` и почему одно ядро процессора сервера бывает загружено сетью на 100%?

Начинаем блок про постоянное хранение данных, поэтому сначала разберёмся, как
процессор вообще разговаривает с железом, а в следующих главах — что это за
железо: [жёсткие диски](/os/book/hdd) и [SSD](/os/book/ssd).

## Как устройство выглядит для процессора

Устройства подключены к процессору через **шину** — набор линий связи с
протоколом обмена. В современном сервере это в основном PCI Express: к ней
подключены NVMe-диски, сетевые карты, GPU. Память подключена к процессору
отдельно и быстрее.

```text
        ┌───────────┐     ┌──────────┐
        │    CPU    │═════│   RAM    │   контроллер памяти в процессоре
        └─────┬─────┘     └──────────┘
              │ PCIe
     ┌────────┼──────────┬────────────┐
  ┌──┴───┐ ┌──┴─────┐ ┌──┴───┐   ┌────┴────┐
  │ NVMe │ │ сетевая│ │ GPU  │   │ чипсет: │
  │ диск │ │ карта  │ │      │   │ USB,SATA│
  └──────┘ └────────┘ └──────┘   └─────────┘
```

Для процессора у каждого устройства есть набор **регистров**: небольших ячеек,
которые можно читать и писать. В простейшей учебной модели их три вида:

- **статус** — что сейчас с устройством: занято, готово, ошибка;
- **команда** — что сделать: «прочитай блок 12345», «отправь пакет»;
- **данные** — через них передаются сами байты.

Как процессор до них добирается? Есть два способа.

- **Порты ввода-вывода** (port I/O): отдельные инструкции `in`/`out` на x86 и
  отдельное адресное пространство портов. Это наследие старых ПК, сегодня так
  общаются с немногими legacy-устройствами.
- **Отображение в память** (memory-mapped I/O, MMIO): регистры устройства
  получают физические адреса, как будто это кусок RAM. Процессор пишет в них
  обычной инструкцией `mov`, а контроллер шины отправляет эту запись не в
  память, а в устройство. Так работает всё современное: NVMe, сетевые карты,
  GPU. Доступ к таким адресам разрешён только ядру (и драйверам).

Базовый протокол общения с устройством в учебной модели выглядит так:

```text
1. while (статус == ЗАНЯТО) { }      // ждём, пока устройство свободно
2. записать данные в регистр данных
3. записать команду в регистр команд // устройство начинает работу
4. while (статус == ЗАНЯТО) { }      // ждём, пока закончит
```

Шаги 1 и 4 — это **опрос** (polling): процессор в цикле читает статус. Посмотрим,
чем это плохо.

## Polling: процессор ждёт в цикле

Пока устройство работает, процессор, который его опрашивает, ничего полезного
не делает.

```text
  время →
  CPU   │ A │ p │ p │ p │ p │ p │ A │ A │      p — опрос статуса впустую
  диск  │   │███████ работает ███│   │   │
```

Для медленного устройства это катастрофа. Чтение с жёсткого диска занимает
порядка 10 мс, а процессор на 3 ГГц за это время выполнил бы 30 миллионов тактов
чужой работы.

## Прерывания: устройство само позовёт

Вместо опроса процессор даёт устройству команду и уходит заниматься другим. ОС
переводит процесс, который ждёт данных, в состояние «заблокирован» (см.
[состояния процесса](/os/book/process)) и запускает другой. Когда устройство
закончит, оно посылает процессору **прерывание** (interrupt) — электрический (на
PCIe — специальное сообщение, MSI-X) сигнал «у меня событие». Процессор
приостанавливает текущий код, переходит в режим ядра ОС и выполняет
**обработчик прерывания** (interrupt handler), который смотрит, что случилось, и
будит ждавший процесс.

```text
  время →
  CPU   │ A │ B │ B │ B │ B │ B │ A │ A │
  диск  │   │███████ работает ███│
                                 ↑ прерывание: обработчик будит A
```

Пока диск работает, процессор исполняет процесс B. Это тот же механизм, что
таймерное прерывание из главы про
[ограниченное прямое исполнение](/os/book/limited-direct-execution): управление
передаётся ядру по сигналу железа.

### Прерывания не бесплатны

Принять прерывание — значит прервать текущий код, сохранить его регистры, войти
в ядро, выполнить обработчик и вернуться. Это порядка микросекунды. Если
обработчик разбудил процесс, планировщик может ещё и переключить контекст. Для
медленного диска это копейки, но в двух случаях цена становится заметной.

**Очень быстрое устройство.** Если ответ придёт через несколько микросекунд,
ушедший спать процесс проснётся позже, чем если бы просто подождал. Поэтому
Linux умеет для быстрых NVMe-дисков опрашивать очередь завершений вместо
ожидания прерывания (режим polled I/O). Та же дилемма «крутиться или спать», что
у [спинлока против сна](/os/book/locks-hardware).

**Очень частые события.** Сетевая карта под нагрузкой получает сотни тысяч
пакетов в секунду. Прерывание на каждый пакет — и процессор только и делает, что
входит в обработчики. Два лекарства:

- **объединение прерываний** (interrupt coalescing): карта копит несколько
  пакетов или ждёт несколько микросекунд и шлёт одно прерывание на пачку;
- **NAPI** в Linux: получив прерывание, драйвер отключает дальнейшие прерывания
  и забирает пакеты опросом, пачками, пока очередь не опустеет. Только потом
  включает прерывания обратно.

Обработку пакетов ядро делает в **softirq** (отложенная часть обработки
прерывания). Время, потраченное на неё, в `top` показывает колонка `si`. Если
у одного ядра процессора `si` под 100%, а остальные свободны — вероятно, все прерывания
сетевой карты приходят на одно ядро процессора. Распределение видно в `/proc/interrupts`,
его меняют настройками RSS (несколько очередей на карте) и `irqbalance`.

## DMA: данные идут мимо процессора

Осталась ещё одна потеря. Допустим, с диска нужно прочитать 1 МБ. Даже с
прерываниями кто-то должен перенести байты из устройства в RAM. Если это делает
процессор, читая регистр данных слово за словом, — это **программный
ввод-вывод** (programmed I/O, PIO), и процессор снова занят тупой работой.

**DMA** (Direct Memory Access, прямой доступ к памяти) — устройство само пишет
данные в RAM и само читает их оттуда, без участия процессора. Процессору нужно
только сказать, где в памяти лежат буферы и сколько байт перенести.

```text
   PIO: процессор носит байты            DMA: устройство пишет в RAM само

   диск ──► CPU ──► RAM                  диск ═══════════════► RAM
          (CPU занят копированием)              │
                                                └─ прерывание «готово» ─► CPU
```

В учебниках DMA часто рисуют как отдельный контроллер на материнской плате. Так
было в старых ПК. Сегодня почти каждое быстрое устройство само умеет
обращаться к памяти по шине (это называется bus mastering). NVMe-диск и
сетевая карта работают через **очереди в памяти**: драйвер кладёт в кольцевой
буфер в RAM описание команды («прочитай блоки с 1000 по 1007 в физические
страницы X и Y») и записывает номер последней команды в регистр-«звонок»
(doorbell) устройства через MMIO. Устройство само забирает команду из памяти,
само пишет данные в указанные страницы, кладёт запись о завершении в очередь
завершений и шлёт прерывание. Между устройством и памятью обычно стоит
**IOMMU** — аналог MMU для устройств: он ограничивает, в какие страницы
устройство имеет право писать (важно для безопасности и для виртуальных машин).

Сравним загрузку процессора:

```text
   PIO:   CPU │ A │ c c c c c │ B │       c — процессор копирует данные
   DMA:   CPU │ A │ B B B B B │ A │       процессор свободен
          диск│   │▒ DMA пишет в RAM ▒│
```

## Путь read() от программы до диска и обратно

Соберём всё вместе. Программа вызывает `read(fd, buf, 4096)` для файла на
NVMe-диске. Сначала вспомним **page cache** из главы
[о памяти в проде](/os/book/memory-cloud): ядро держит недавно прочитанные куски
файлов в свободной RAM.

```text
 ПРОГРАММА
   read(fd, buf, 4096)
   ──── системный вызов, переход в ядро ────
 ЯДРО: виртуальная ФС
   есть ли нужная страница файла в page cache?
     ├─ да (cache hit): скопировать 4 КБ в buf, вернуться.  ~1 мкс
     └─ нет (cache miss):
 ЯДРО: файловая система
   по inode найти, в каком блоке диска лежат эти байты
   выделить страницу в page cache под данные
 ЯДРО: блочный слой и драйвер NVMe
   сформировать команду «прочитать блок N в физическую страницу P»
   положить в очередь отправки, позвонить в doorbell
   процесс заблокирован, CPU отдан другим
 ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
 ЖЕЛЕЗО
   диск читает данные, DMA пишет их в страницу P    ~50–100 мкс (NVMe)
   запись в очередь завершений, прерывание
 ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
 ЯДРО: обработчик прерывания
   отметить страницу как прочитанную, разбудить процесс
 ЯДРО (уже в контексте процесса)
   скопировать 4 КБ из страницы P в buf
   ──── возврат из системного вызова ────
 ПРОГРАММА
   read вернул 4096
```

Три вещи, которые стоит из этого вынести.

- **Блокирующий `read()` не ест CPU.** Между отправкой команды и прерыванием
  процесс снят с процессора. «Заблокирован» значит «спит», а не «крутится».
- **Данные копируются дважды**: устройство → page cache (DMA), page cache →
  ваш буфер (процессор). Зато следующий `read` того же места не тронет диск.
  Базы данных, которые держат свой кэш, иногда открывают файлы с флагом
  `O_DIRECT`: тогда DMA пишет прямо в буфер программы, а page cache не
  используется. Об этом подробнее в главе про
  [fsync](/os/book/fsync-durability).
- **Ядро читает с запасом.** Заметив последовательное чтение, оно заранее
  запрашивает следующие блоки (readahead), чтобы следующий `read` попал в кэш.

## А что с горутинами

Когда горутина ждёт ответа от базы по сети, путь похож, только устройство —
сетевая карта. Карта DMA-ет пакеты в кольцевые буферы в RAM, прерывание и NAPI
передают их сетевому стеку ядра, тот раскладывает данные по сокетам, и сокет
становится «готов к чтению». Дальше вступает [netpoller](/os/book/event-based-concurrency):
горутина, ждавшая этот сокет, снова становится готовой к запуску. Ни один поток
ОС всё это время не спал ради этой горутины и не крутился в цикле.

С файлами иначе: `os.File.Read` — блокирующий системный вызов, и поток ОС спит
внутри него, пока не придёт прерывание от диска. Рантайм Go на это время отдаёт
логический процессор P другому потоку, чтобы остальные горутины не стояли.

## Сравнение

| | Polling | Прерывания | DMA |
|---|---|---|---|
| Кто узнаёт о готовности | процессор в цикле | устройство сигналит | устройство сигналит в конце |
| Кто переносит данные | процессор | процессор | устройство |
| CPU во время ожидания | занят на 100% | свободен | свободен |
| Накладные расходы | нет входов в ядро | вход в обработчик, пробуждение | настройка команды, одно прерывание |
| Когда выгодно | ответ через микросекунды | редкие или медленные события | большие объёмы |

Реальные системы их комбинируют: NVMe-диск переносит данные через DMA, о
завершении сообщает прерыванием, а под высокой нагрузкой драйвер может
перейти на опрос очереди завершений.

> Квиз по разделу: см. вопросы в конце документа или на https://graphlms.ru/os/book/io-devices

## Частые ошибки

- Считать, что блокирующий `read` «грузит процессор». Процесс спит до прерывания.
- Забывать про page cache: «каждый `read` ходит на диск».
- Думать, что `O_DIRECT` делает запись долговечной. Он только обходит page cache.
- Объяснять высокий `si` в `top` проблемами с диском: это обработка сетевых прерываний, часто на одном ядре процессора.
- Путать прерывание с переключением контекста: прерывание — вход в ядро ОС, переключение случается, только если обработчик кого-то разбудил.

## Проверьте себя

1. Почему polling выгоден для очень быстрых устройств и невыгоден для HDD?
2. Чем MMIO отличается от портов ввода-вывода?
3. Сколько раз копируются данные при обычном `read` файла с промахом page cache?
4. Сервис читает один и тот же конфиг-файл на 4 КБ на каждый запрос, 10 000 раз в секунду. Сколько раз в секунду ядро ОС будет обращаться к диску и сколько раз копировать данные? Что изменится после перезагрузки сервера?

Ответы. (1) Ответ быстрого устройства приходит раньше, чем окупится вход в обработчик прерывания и пробуждение; HDD отвечает миллисекунды, за которые процессор сделал бы миллионы тактов полезной работы. (2) При MMIO регистры устройства отображены на физические адреса и доступны обычными инструкциями чтения и записи; порты — отдельное адресное пространство и отдельные инструкции `in`/`out`. (3) Дважды: устройство → page cache через DMA, page cache → буфер программы процессором. (4) После первого чтения обращений к диску нет, файл в page cache; копирований — 10 000 в секунду. После перезагрузки кэш пуст: первое чтение снова пойдёт на диск, дальше всё как прежде.

## Что спрашивают на собесе

- **Polling или прерывания?** Polling жжёт CPU, но не платит за вход в
  обработчик. Прерывания освобождают CPU, но каждое стоит порядка микросекунды.
  Для очень быстрых устройств и очень частых событий выгоднее опрос (NVMe polled
  I/O, NAPI в сети).
- **Что такое DMA?** Устройство само переносит данные в RAM и из неё;
  процессор только описывает буферы и получает прерывание в конце.
- **Как процессор пишет в регистры устройства?** В основном через MMIO:
  регистры отображены на физические адреса, запись — обычная инструкция.
- **Почему блокирующий `read()` не грузит CPU?** Процесс спит до прерывания о
  готовности данных; процессор в это время исполняет другие задачи.
- **Опишите путь `read()`.** Системный вызов → проверка page cache → при
  промахе ФС находит блок → драйвер ставит команду в очередь устройства → процесс
  спит → устройство DMA-ет данные в page cache → прерывание → процесс просыпается
  → копирование в буфер → возврат.
- **Что такое interrupt coalescing и NAPI?** Способы не утонуть в прерываниях
  под сетевой нагрузкой: объединять события и переходить на опрос пачками.
- **Какие три вида регистров у устройства в учебной модели?** Статус, команда,
  данные.

## Вопросы для самопроверки

**Квиз «Ввод-вывод: прерывания и DMA»**

1. В чём главный недостаток опроса (polling) для медленного устройства?
   - Данные копируются дважды
   - Невозможен на многоядерных системах
   - Требует отдельный контроллер
   - CPU крутится в цикле и жжёт такты впустую (верно)
   - Пояснение: При polling CPU в цикле читает регистр статуса и ничего полезного не делает — для медленного устройства это миллионы потерянных тактов.
2. Что делает DMA-контроллер?
   - Ускоряет шину процессора
   - Кэширует прерывания
   - Перекачивает данные между устройством и памятью без участия CPU (верно)
   - Опрашивает статус устройства за CPU
   - Пояснение: DMA сам переносит байты устройство↔память; CPU только инициирует операцию и получает одно прерывание в конце.
3. Почему для очень быстрого устройства прерывание может быть невыгодным?
   - Быстрые устройства не имеют регистра статуса
   - Прерывания не работают с быстрыми устройствами
   - DMA отключает прерывания
   - Цена переключения контекста может превысить время самого ожидания (верно)
   - Пояснение: Прерывание стоит переключения контекста (сотни-тысячи тактов). Если устройство отвечает быстрее, дешевле подождать опросом.
4. Какие из перечисленных — типы регистров устройства?
   - Данные (верно)
   - Статус (верно)
   - Прерывание
   - Команда (верно)
   - Пояснение: Стандартная модель: регистр статуса (читать состояние), команды (отдать приказ) и данных (гонять байты). 'Прерывание' — это сигнал, а не регистр.
5. Почему блокирующий read() обычно не держит CPU на 100%?
   - Процесс засыпает и снимается с ядра до прерывания о готовности (верно)
   - Ядро ускоряет CPU на время ожидания
   - Данные уже лежат в кэше
   - read() выполняется на GPU
   - Пояснение: На время ожидания ОС снимает процесс с CPU; пробуждает его прерывание о готовности. Блокировка — это сон, а не активное ожидание.
6. Сколько прерываний в идеале шлёт DMA на одну крупную передачу данных?
   - Ответ: 1
   - Пояснение: DMA перекачивает весь блок самостоятельно и дёргает CPU одним прерыванием 'готово' в конце, а не на каждый байт.
