GraphLMS

ОС
Начать

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

Процесс и его состояния

Что вообще такое «процесс»

Процесс — это запущенная программа. Программа на диске мертва: это просто байты в ELF/Mach-O файле. Как только ОС загрузила её в память, выдала кусок адресного пространства, открыла дескрипторы и поставила в очередь на исполнение — появился процесс, живая сущность со своим состоянием.

Главная иллюзия, которую создаёт операционная система, — виртуализация CPU. У вас одно (ну, несколько) физических ядер, а процессов — сотни. ОС делает вид, что каждому достался свой собственный процессор, быстро переключаясь между ними. Этот фокус называется time sharing, и весь раздел про планирование — о том, как ОС решает, кого пустить на ядро следующим.

Если вы пишете на Go, держите в голове важное различие: процесс — это не горутина. Горутины живут внутри одного процесса, их переключает рантайм Go в user-space. Процессы переключает ядро, и это на порядок дороже. Когда на собесе спрашивают «почему горутина дешевле потока ОС» — корень ответа здесь.

Из чего состоит процесс

Состояние процесса — это всё, что нужно сохранить, чтобы потом продолжить с того же места:

  • Адресное пространство — код, данные, куча, стек. Виртуальная память (про неё отдельный раздел) даёт каждому процессу иллюзию, что вся память его.
  • Регистры — в том числе program counter (где мы в коде) и stack pointer. При переключении их сохраняют и восстанавливают.
  • Открытые ресурсы — файловые дескрипторы, сокеты. В Unix у каждого процесса по умолчанию есть 0/1/2 (stdin/stdout/stderr).

Всё это ядро хранит в структуре — Process Control Block (в Linux это task_struct). По сути это и есть «паспорт» процесса.

Состояния процесса

В любой момент процесс находится в одном из состояний. Упрощённая, но рабочая модель — три состояния:

  • Running — прямо сейчас исполняется на ядре.
  • Ready — готов исполняться, ждёт, когда планировщик его пустит.
  • Blocked — ждёт события (чтение с диска, ответ по сети) и не претендует на CPU, пока событие не произошло.
            запланирован
   READY  ───────────────▶  RUNNING
     ▲                         │
     │   снят с ядра           │  ушёл в I/O
     └─────────────────────────┤

                            BLOCKED
        (событие пришло → обратно в READY)

Ключевой момент для понимания производительности: процесс уходит в blocked, когда делает блокирующий системный вызов — например, читает файл. Пока диск вертится, держать его на ядре бессмысленно, поэтому ОС снимает его и пускает кого-то из ready. Это и есть причина, по которой ввод-вывод «прячется» за вычислениями: пока один ждёт диск, другой считает.

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

Context switch

Переключение с одного процесса на другой называется context switch: сохранить регистры текущего в его PCB, загрузить регистры следующего, передать управление. Это не бесплатно — прямые затраты (сотни наносекунд) плюс косвенные: вымывается кэш, сбрасывается TLB. Поэтому «переключать почаще, чтобы было честнее» — плохая идея: на одних переключениях можно сжечь весь полезный такт.

Именно баланс между честностью (каждому дать время) и накладными расходами (не переключаться слишком часто) и есть главная головная боль планировщика — ей посвящены следующие главы.

Проверь себя· процесс и состояния

Чем процесс принципиально отличается от горутины?

В какое состояние уходит процесс, сделавший блокирующее чтение с диска?

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

  • Чем процесс отличается от потока и от горутины? Что у них общее, что раздельное (адресное пространство — общее у потоков, раздельное у процессов).
  • Что происходит при fork() и почему он дешёвый (copy-on-write).
  • Почему context switch дорогой и при чём тут кэш и TLB.
Прямое исполнение с ограничениями