GraphLMS

ОС
Начать

ОС · Виртуализация CPU · 10 мин

Прямое исполнение с ограничениями

Дилемма: быстро, но под контролем

Чтобы программа работала быстро, её нужно исполнять прямо на железе — никаких интерпретаторов между кодом и процессором. Это называется direct execution. Но если просто отдать процессу ядро, возникают две проблемы:

  1. Контроль. Как ОС вернёт себе управление, если процесс зациклился или просто не хочет уступать? Как запретить ему лезть в чужую память и диск?
  2. Безопасность. Что мешает процессу выполнить инструкцию «прочитай весь диск» или «обратись к памяти ядра»?

Решение — limited direct execution: исполняем напрямую, но с аппаратными ограничениями. Два механизма делают всю работу: режимы процессора и прерывания.

User mode и kernel mode

Процессор умеет работать в двух режимах:

  • User mode — в нём бежит ваш код. Привилегированные инструкции (прямой доступ к железу, к памяти ядра) запрещены: попытка → аппаратное исключение.
  • Kernel mode — в нём бежит ядро. Можно всё.

Как тогда процессу прочитать файл, если прямой доступ к диску запрещён? Через системный вызов (syscall). Процесс выполняет специальную инструкцию (syscall на x86-64), которая аккуратно, через заранее заданную точку входа, переключает процессор в kernel mode и прыгает в код ядра. Ядро проверяет аргументы, делает работу и возвращает управление обратно в user mode.

  user mode                       kernel mode
  ──────────                      ───────────
  read(fd, buf, n)
      │  syscall (trap)
      └──────────────────────────▶ обработчик read:
                                     проверить права,
                                     запустить I/O,
                                     ...
      ◀──────────────────────────┘ return-from-trap
  продолжаем с результатом

Этот «прыжок» называется trap, а адреса обработчиков ядро задаёт один раз при загрузке (trap table). Процесс не может выбрать, куда прыгнуть, — только сказать «вызов номер N». Поэтому syscall — единственная легальная дверь из user-кода в ядро, и именно поэтому она дороже обычного вызова функции: смена режима, сохранение контекста, проверки.

В Go каждый сетевой/файловый вызов под капотом — это syscall. Понимание их цены объясняет, почему батчинг (один большой write вместо тысячи мелких) так заметно ускоряет код: вы платите за переход в ядро один раз, а не тысячу.

Как ОС возвращает себе управление

Syscall — это когда процесс сам позвал ядро. Но что если он завис в бесконечном цикле и ничего не зовёт? Если бы ОС ждала доброй воли процесса (кооперативный подход), один баг вешал бы всю систему.

Поэтому используется прерывание по таймеру. ОС перед запуском процесса программирует таймер: «дёрни меня через 10 мс». Когда время вышло, железо принудительно прерывает процесс, переключается в kernel mode и отдаёт управление обработчику. Это вытесняющая (preemptive) многозадачность — ОС всегда может отобрать ядро.

Дальше планировщик решает: продолжить тот же процесс или переключиться на другой. Если переключиться — сделать context switch (сохранить регистры в PCB одного, загрузить из PCB другого) и через return-from-trap вернуться уже в другой процесс.

Интересная параллель: до Go 1.14 горутины переключались в основном кооперативно (в точках вызова функций), и «горячий» цикл без вызовов мог застопорить планировщик. В 1.14 завезли асинхронное вытеснение через сигналы — ровно та же идея «принудительно отобрать управление», что и таймер в ОС.

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

  • Чем syscall отличается от обычного вызова функции и почему он дороже.
  • Кооперативная vs вытесняющая многозадачность — плюсы и риски каждой.
  • Как именно ОS отбирает CPU у зациклившегося процесса (таймер + прерывание).
  • Зачем нужны два режима процессора.
Процесс и его состоянияПланирование: метрики и базовые политики