Операционные системы
Процессы
Современный сервер PostgreSQL под нагрузкой - это 100+ процессов. Chrome в браузере - 30-50 процессов. Сбой одного процесса Chrome не роняет остальные вкладки. PostgreSQL-воркер, упавший на плохом запросе, не уносит весь кластер. Изоляция через процессы - архитектурное решение, которое делает современные системы стабильными. Понять процесс - значит понять, почему это работает именно так.
- **Chrome**: каждая вкладка - отдельный процесс (Site Isolation). Падение Flash-плагина в 2015-м роняло страницу, но не браузер - именно благодаря изоляции процессов
- **Nginx**: master-процесс управляет конфигурацией, worker-процессы (по одному на CPU-ядро) обрабатывают запросы. 1 million connections с минимальными context switches
- **PostgreSQL**: backend-процесс на каждое соединение. При 1000 соединениях - 1000 PCB в памяти ядра. Именно поэтому PgBouncer (connection pooler) критичен
- **Docker контейнеры** - это процессы с namespace isolation. `docker run` вызывает fork() + clone() с флагами CLONE_NEWPID, CLONE_NEWNET и другими
- **Node.js cluster** модуль: master делает fork() для каждого worker - те же системные вызовы, что и в C-программах 40 лет назад
Цели урока
- Объяснить процесс: программа + execution context + resources + address space
- Перечислить состояния (New, Ready, Running, Waiting, Terminated) и переходы между ними
- Знать содержимое PCB: PID, state, PC, registers, memory pointers, open FDs
- Применять fork/exec/wait, понять copy-on-write и зачем разделение fork и exec
- Оценить cost: process create ~1ms, context switch ~5µs, exit ~100µs
Деннис Ритчи и рождение процессов в Unix
В 1969 году Кен Томпсон и Деннис Ритчи писали Unix на PDP-7 в Bell Labs. Концепция процесса была революционной: вместо того чтобы запускать одну программу за раз, система поддерживала несколько «живых» программ одновременно, переключаясь между ними. fork() появился как лаконичное решение проблемы создания процессов - скопировать себя, потом заменить копию на нужную программу через exec(). Эта комбинация fork+exec прожила 55 лет и до сих пор лежит в основе каждого запуска терминальной команды.
Концепция процесса
**Процесс** - это программа в стадии выполнения. Не просто код на диске, а живая сущность с собственной памятью, регистрами процессора и состоянием. Chrome на компьютере - это не одна программа. Это 30-50 процессов: отдельный для каждой вкладки, отдельный для GPU, отдельный для сетевых операций. Сбой одной вкладки не роняет остальные именно потому, что процессы изолированы.
Каждый процесс имеет собственное **адресное пространство**, которое включает: **код программы** (text section), **данные** (data section), **стек** (stack) для временных переменных и **кучу** (heap) для динамической памяти.
**Ключевое отличие:** Программа - пассивная сущность (файл на диске), процесс - активная (выполняется в памяти). Одна программа может порождать множество процессов. Nginx запускает один master-процесс и N worker-процессов - по одному на каждое ядро CPU.
Операционная система управляет процессами через специальную структуру данных - **Process Control Block (PCB)**, которая хранит всю информацию о процессе. В Linux это структура `task_struct` размером около 1.7 КБ. Десятки тысяч процессов в системе - десятки тысяч таких структур в памяти ядра.
Что НЕ является частью адресного пространства процесса?
Состояния процесса
В течение своей жизни процесс проходит через несколько **состояний**. На типичном сервере с 4 ядрами может быть 2000 процессов - но только 4 одновременно находятся в состоянии Running. Остальные ждут: очереди на CPU, завершения I/O операций, сигналов.
**Пять основных состояний:** - **New** - процесс создаётся - **Ready** - процесс готов к выполнению, ждёт процессора - **Running** - процесс выполняется на процессоре - **Waiting** - процесс ждёт события (I/O, сигнала) - **Terminated** - процесс завершил выполнение
**Переходы между состояниями:** - **New -> Ready:** ОС завершила инициализацию процесса - **Ready -> Running:** Планировщик выбрал процесс для выполнения - **Running -> Ready:** Истекло время кванта (time slice) или пришло прерывание - **Running -> Waiting:** Процесс запросил I/O операцию - **Waiting -> Ready:** I/O операция завершена, процесс снова готов - **Running -> Terminated:** Процесс завершил работу
**Context Switch** - переключение контекста происходит при смене процесса на процессоре. ОС сохраняет состояние старого процесса и загружает состояние нового. Это затратная операция: 0.1-1 мкс, включая сброс TLB (Translation Lookaside Buffer). На сервере с тысячами процессов и частыми I/O это может занимать 5-10% CPU только на переключения. Node.js и Nginx построены на event loop именно чтобы минимизировать context switches.
Важно понимать: в системе обычно много процессов в состоянии **Ready**, но только один (или несколько на многоядерных системах) может быть в состоянии **Running** на каждом ядре процессора.
Процесс завершил чтение файла с диска. В какое состояние он переходит?
Process Control Block (PCB)
**Process Control Block (PCB)** - структура данных в ядре ОС, которая содержит всю информацию о процессе. PCB - это «паспорт» процесса для операционной системы. Без PCB невозможно контекстное переключение: ядро не знало бы, что сохранить и что восстановить.
**Основные поля PCB:** - **Process ID (PID)** - уникальный идентификатор процесса - **Process State** - текущее состояние (New, Ready, Running, Waiting, Terminated) - **Program Counter** - адрес следующей инструкции - **CPU Registers** - значения всех регистров процессора - **Memory Management Info** - таблицы страниц, границы сегментов - **Accounting Info** - время CPU, приоритет, лимиты - **I/O Status** - открытые файлы, устройства
Когда происходит **переключение контекста** (context switch), ОС должна: 1. Сохранить состояние текущего процесса в его PCB 2. Загрузить состояние нового процесса из его PCB 3. Переключить адресное пространство (таблицы страниц)
В Linux PCB называется `task_struct` и занимает около 1.7 КБ памяти. При 100 000 процессов - 170 МБ только на PCB. Команда `ps aux` показывает информацию из PCB всех процессов: PID, состояние, время CPU, использование памяти.
Что происходит с PCB при переключении контекста с процесса A на процесс B?
Операции с процессами
Операционная система предоставляет механизмы для **создания** и **завершения** процессов. Процессы образуют иерархию: каждый процесс (кроме init с PID=1) имеет родителя. В Linux эта иерархия видна через `pstree` - systemd (PID 1) является прародителем всего.
**Создание процесса в UNIX/Linux:** Процесс-родитель создаёт процесс-потомок системным вызовом **fork()**. Новый процесс получает копию адресного пространства родителя через механизм copy-on-write - страницы физически не копируются пока не будет записи.
**Как работает fork():** 1. Создаётся копия PCB родительского процесса 2. Выделяется новое адресное пространство (copy-on-write) 3. Дочерний процесс получает копию данных родителя 4. fork() возвращает 0 в дочернем процессе и PID дочернего в родительском
**Важно:** После exec() новая программа полностью заменяет код, данные и стек процесса. PID остаётся прежним, но выполняется уже другая программа. Это позволяет shell запускать любые команды не создавая новый PID с нуля.
**Завершение процесса:** Процесс завершается явно (вызов exit()) или при возврате из main(). ОС освобождает ресурсы: память, файлы, освобождает PCB. Если родитель не вызвал wait(), процесс становится **zombie** - PCB висит в памяти, хотя процесс уже завершён. На production-серверах утечка zombie-процессов - реальная проблема.
После fork() дочерний процесс начинает выполнение с начала программы (с main)
После fork() и родительский, и дочерний процессы продолжают выполнение с точки ПОСЛЕ вызова fork()
fork() не перезапускает программу, а создаёт копию процесса в текущем состоянии. Оба процесса продолжают с той же инструкции, но с разными возвращаемыми значениями fork() (0 в дочернем, PID в родительском).
Что вернёт fork() в дочернем процессе?
Ключевые идеи
- **Процесс** - программа в стадии выполнения с собственной памятью, регистрами и состоянием. Программа - пассивный файл, процесс - активная сущность
- **Адресное пространство**: text (код) + data (глобальные) + heap (динамическая память) + stack (локальные переменные). Регистры CPU - не часть address space, но часть контекста
- **Состояния**: New -> Ready -> Running -> Waiting -> Terminated. Context switch (0.1-1 мкс) происходит при смене процесса на CPU
- **PCB (task_struct в Linux)** - 1.7 КБ на процесс, хранит всё: PID, состояние, регистры, указатели на memory/files. Основа для context switch
- **fork()** создаёт копию через copy-on-write, **exec()** заменяет программу, **wait()** ожидает дочерний. Zombie-процессы - реальная проблема на серверах без wait()
- **Chrome, Nginx, PostgreSQL, Docker** - все используют изоляцию процессов как архитектурный примитив стабильности
Связанные темы
Процессы - фундамент, на котором строится всё остальное в операционных системах:
- Потоки (Threads) — Легковесные процессы внутри одного адресного пространства. Разделяют память, но имеют свои стеки и регистры.
- Планирование CPU — Алгоритмы выбора процесса из очереди Ready: FCFS, SJF, Round Robin, CFS (Linux).
- Синхронизация — Мьютексы, семафоры и атомарные операции для координации процессов и потоков.
- Управление памятью — Как ОС выделяет и изолирует адресные пространства процессов через виртуальную память.
Вопросы для размышления
- Почему переключение контекста затратно? Что именно происходит с TLB при смене процесса?
- В чём преимущество copy-on-write при fork() перед полным копированием памяти?
- Почему в UNIX процессы создаются через fork() + exec(), а не одним системным вызовом?
- Что такое zombie-процесс и почему он опасен на production-сервере?
Связанные уроки
- os-01-intro — Базовые концепции ОС и роль ядра в управлении ресурсами
- os-03-threads — Потоки - облегчённые процессы, разделяющие адресное пространство
- os-04-scheduling — Планировщик выбирает процессы из очереди Ready
- os-05-sync — Синхронизация нужна когда несколько процессов работают с общими данными
- arch-04-cpu — Контекстное переключение - операция на уровне регистров процессора
- os-07-memory — Виртуальная память изолирует адресные пространства процессов
- db-25-connection-pooling