Ввод-вывод: шины, прерывания и DMA

Урок объясняет, как процессор обменивается данными с клавиатурой, диском, сетью и другими устройствами.

Шина — общий набор проводов для передачи данных, адресов и управляющих сигналов между процессором, памятью и устройствами. Прерывание — сигнал устройства процессору «я готов, обрати внимание».

Зачем особый механизм для ввода-вывода

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

Шины

Устройства подключены к процессору и памяти через шины. Классически выделяют три: шина данных (передаёт значения), шина адреса (указывает, куда/откуда), шина управления (сигналы чтения/записи, прерываний). Ширина шины данных (например, 64 бита) определяет, сколько передаётся за раз.

   ┌─────────┐   шина адреса   ┌─────────┐
   │   CPU   │════════════════→│ ПАМЯТЬ  │
   │         │←═══ шина данных ═│         │
   └────┬────┘   шина управл.   └─────────┘
        ║════════╦═══════════╦══════════╗
     ┌──╨──┐  ┌──╨──┐    ┌───╨──┐   ┌───╨──┐
     │ диск│  │сеть │    │клав. │   │ GPU  │
     └─────┘  └─────┘    └──────┘   └──────┘

Опрос против прерываний

Есть два способа узнать, готово ли устройство. Опрос (polling): процессор в цикле спрашивает «готов? готов?» — просто, но тратит такты впустую. Прерывание (interrupt): процессор занимается своими делами, а устройство само подаёт сигнал, когда готово; процессор бросает текущую работу, обрабатывает событие и возвращается. Сравним «потраченные такты»:

def polling_cycles(wait_cycles, poll_cost):
    # процессор крутит цикл ожидания всё время
    return wait_cycles * poll_cost

def interrupt_cycles(wait_cycles, handler_cost):
    # процессор занят полезным делом, тратит такты лишь на обработчик
    return handler_cost          # сама работа во время ожидания полезна

wait = 1000      # устройство готовится 1000 тактов
print("опрос:      потрачено", polling_cycles(wait, 1), "тактов впустую")
print("прерывание: потрачено", interrupt_cycles(wait, 50), "тактов (только обработчик)")
print("за время ожидания CPU при прерывании сделал ~", wait, "тактов полезной работы")

Вывод:

опрос:      потрачено 1000 тактов впустую
прерывание: потрачено 50 тактов (только обработчик)
за время ожидания CPU при прерывании сделал ~ 1000 тактов полезной работы

Как работает под капотом: DMA

Даже с прерываниями есть проблема: при передаче большого файла процессор должен копировать каждый байт «диск → процессор → память». Это снова занятость впустую. Решение — DMA (Direct Memory Access): специальный контроллер копирует данные между устройством и памятью напрямую, минуя процессор. Процессор лишь говорит «перенеси N байт оттуда сюда» и получает прерывание по завершении.

БЕЗ DMA:  диск -> CPU -> память   (CPU копирует каждый байт)

С DMA:    диск ===DMA===> память  (CPU свободен!)
          CPU только: 1) задал задачу DMA
                      2) получил прерывание "готово"
МеханизмНагрузка на CPUКогда применяют
Опрос (polling)высокая (крутит цикл)очень частые быстрые события
Прерываниясредняя (обработчик)редкие события (клавиша, пакет)
DMAминимальнаябольшие передачи (диск, сеть, видео)

Глубже в тему

Корень всех проблем ввода-вывода — чудовищная разница в скоростях. Процессор оперирует тактами в доли наносекунды, а механический диск отвечает за миллисекунды, клавиатура — за десятки миллисекунд между нажатиями. Разница достигает миллионов раз: если бы процессор просто стоял и ждал диск, это было бы всё равно что человеку замереть на несколько месяцев в ожидании одного письма. Поэтому вся подсистема ввода-вывода построена вокруг одной цели — не дать быстрому процессору простаивать, пока медленные устройства делают своё дело. Три механизма из урока (опрос, прерывания, DMA) — это три последовательных шага по освобождению процессора: сначала от активного ожидания, затем от ручного копирования данных.

Выбор между опросом и прерываниями не так однозначен, как кажется на первый взгляд, и зрелый инженер понимает оба компромисса. Прерывание кажется безусловно лучше — процессор занят полезной работой и реагирует лишь по сигналу. Но у прерывания есть накладные расходы: сохранить состояние, переключиться в обработчик, восстановить состояние — это десятки-сотни тактов на каждое событие. Если события идут очень часто (например, высокоскоростная сетевая карта на 10 гигабит, где пакеты сыплются непрерывно), лавина прерываний может «утопить» процессор накладными расходами — это явление называют interrupt storm. Тогда выгоднее вернуться к опросу или гибридным схемам: современные сетевые драйверы (механизм NAPI в Linux) при высокой нагрузке отключают прерывания и переходят на опрос, а при затишье — обратно. Так «устаревший» опрос оказывается оптимальным в условиях, где прерывания захлёбываются.

DMA — третий и решающий шаг освобождения процессора, и стоит понять, почему даже прерываний недостаточно. Представьте копирование большого файла с диска: даже если процессор узнаёт о готовности данных по прерыванию, ему всё равно приходится самому переписывать каждый байт по маршруту «устройство → регистр процессора → память». Для мегабайтов это миллионы команд копирования — процессор снова занят впустую, просто перекладывая данные. DMA-контроллер берёт эту черновую работу на себя: процессор лишь говорит «перенеси N байт оттуда сюда» и возвращается к полезным делам, а специализированный контроллер копирует данные напрямую между устройством и памятью, минуя процессор, и шлёт прерывание лишь по завершении всей передачи. Так процессор тратит на гигабайтную передачу не миллионы команд, а буквально две: задать задачу и принять рапорт.

У DMA есть тонкая, но важная цена — проблема когерентности кэша, о которой легко забыть. DMA-контроллер пишет данные прямо в ОЗУ, в обход процессора, а значит, и в обход его кэша. Что произойдёт, если процессор держит в кэше старую копию той области памяти, которую DMA только что обновил свежими данными с диска? Процессор увидит устаревшее значение из кэша, а не настоящее из памяти — классическая рассогласованность. Поэтому система должна согласовать кэш с памятью после DMA-передачи: либо аппаратно (когерентный DMA, где контроллер участвует в протоколе когерентности кэшей), либо программно (драйвер явно сбрасывает или инвалидирует соответствующие кэш-строки до и после передачи). Эта проблема — частный случай той же темы когерентности, что возникает между ядрами в многоядерных процессорах, и она показывает, что кэш, ускоряя обычную работу, добавляет хлопот всякий раз, когда память меняет кто-то «со стороны». Понимание этого тонкого момента отличает того, кто умеет писать корректные драйверы, от того, кто ловит загадочные ошибки порчи данных.

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

  • Считать опрос всегда плохим. Для очень частых событий опрос дешевле, чем накладные расходы на прерывания.
  • Думать, что DMA «обходит» память. Наоборот, DMA пишет прямо в память, обходя именно процессор.
  • Забывать про когерентность. После DMA данные в памяти могли измениться, а в кэше CPU — устареть; это требует согласования.

Итог

  • Шины (данных/адреса/управления) связывают процессор, память и устройства.
  • Прерывания освобождают процессор от опроса: устройство само сигналит о готовности.
  • DMA переносит большие объёмы между устройством и памятью напрямую, разгружая CPU.
Проверьте себя
1. В чём преимущество прерываний над опросом (polling)?
AПрерывания проще программировать
BПроцессор не тратит такты на ожидание: устройство само сигналит о готовности
CПрерывания не требуют шины
DОпрос невозможен на современных CPU
2. Что делает DMA-контроллер?
AУскоряет АЛУ
BПереносит данные между устройством и памятью напрямую, минуя процессор
CПредсказывает переходы
DУправляет кэшем
3. Для чего нужна шина адреса?
AПередавать значения данных
BУказывать, по какому адресу читать или писать
CПодавать питание
DХранить команды
py
Курс по теме
Пройдите курс «Python с нуля» — по шагам, с проверкой
8 уроков · ~14 ч · теория, упражнения и экзамен с бейджем
Открыть курс →