---
title: "Процесс и его состояния"
description: "Вы собрали Go-сервис: go build -o api ./cmd/api. На диске появился файл api, несколько мегабайт байтов в формате ELF (на Linux) или Mach-O (на macOS). Пока его никто не запустил…"
canonical: https://graphlms.ru/os/book/process
course: "Операционные системы"
updated: 2026-10-12
lang: ru
publisher: GraphLMS
---

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

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

## Программа и процесс

Вы собрали Go-сервис: `go build -o api ./cmd/api`. На диске появился файл `api`,
несколько мегабайт байтов в формате ELF (на Linux) или Mach-O (на macOS). Пока его
никто не запустил, это просто файл, как картинка или текстовый документ.

Теперь вы запускаете `./api` дважды, в двух терминалах. В `ps` появятся две строки (вывод примерный)
с разными PID (номерами процессов). Файл один, а запущенных экземпляров два, и у
каждого своя память, свои открытые сокеты, своё место в коде. Вот это и есть
**процесс**: запущенная программа вместе со всем её текущим состоянием.

```bash
$ ps -o pid,stat,rss,cmd -C api
  PID STAT   RSS CMD
 4121 Sl   18432 ./api
 4187 Sl   17920 ./api
```

Курс начинается с процесса, потому что почти всё дальнейшее отвечает на один из
двух вопросов: как ОС делит между процессами процессор и как делит между ними
память.

Сразу договоримся о словах. По-русски «ядро» означает две разные вещи:

- **ядро ОС** (kernel) — программа, которая управляет железом и процессами;
- **ядро процессора** (core) — вычислительный блок CPU, который исполняет
  инструкции.

Где может быть путаница, будем писать «ядро ОС» и «ядро CPU».

## Иллюзия собственного процессора

На ноутбуке 8 ядер CPU, а в `ps aux | wc -l` несколько сотен процессов. Каждый
пишется так, будто процессор принадлежит только ему: код идёт строчка за строчкой,
никто его не прерывает. На самом деле ядро ОС быстро переключает ядра CPU между
процессами: десяток миллисекунд один, потом другой, потом третий. Переключения
происходят так часто, что для человека и для самой программы всё выглядит как
одновременная работа.

Этот приём называется **разделением времени** (time sharing), а иллюзию, которую он
создаёт, — **виртуализацией CPU**. Цена иллюзии в том, что ОС должна:

- уметь остановить процесс и потом продолжить с того же места (механизм, глава
  [прямое исполнение с ограничениями](/os/book/limited-direct-execution));
- решать, кого запускать следующим (политика, главы о планировании начиная с
  [метрик и базовых политик](/os/book/scheduling)).

## Что входит в состояние процесса

Чтобы остановить процесс и потом продолжить, нужно сохранить всё, от чего зависит
его дальнейшая работа. Это три группы.

**Память.** Код программы, глобальные переменные, куча, стек. Каждый процесс видит
собственное адресное пространство, и соседи туда не залезут. Как это устроено, мы
разберём в блоке про память, начиная с главы
[адресное пространство](/os/book/address-space).

**Регистры процессора.** Самые важные из них: счётчик команд (program counter, PC),
то есть адрес следующей инструкции, и указатель стека (stack pointer, SP). Если при
остановке не сохранить PC, процесс не узнает, где он был.

**Ресурсы ОС.** Открытые файлы и сокеты (по номерам-дескрипторам 0, 1, 2 и дальше),
текущий каталог, права пользователя. По соглашению Unix у каждого процесса с
рождения открыты три дескриптора: 0 (stdin), 1 (stdout) и 2 (stderr).

Ядро ОС хранит всё это в структуре, которую в учебниках называют **PCB** (process
control block). В Linux это структура `task_struct`. Сохранённые регистры, ссылка на
таблицу памяти, таблица открытых файлов, PID, состояние — всё лежит там.

Горутина в эту картину не входит. Горутины живут внутри одного процесса, и
переключает их рантайм Go без участия ядра ОС. Для ядра ваш Go-сервис — один
процесс с несколькими потоками. Подробно сравним процессы, потоки и горутины в
главе [потоки vs процессы vs горутины](/os/book/threads-intro).

Это легко проверить:

```go play
package main

import (
	"fmt"
	"os"
	"sync"
)

func main() {
	fmt.Println("main: PID", os.Getpid())

	var wg sync.WaitGroup
	for i := 1; i <= 3; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			fmt.Printf("горутина %d: PID %d\n", i, os.Getpid())
		}()
	}
	wg.Wait()
}
```

Вывод будет примерно таким (число PID у вас другое, порядок строк горутин может
меняться от запуска к запуску):

```text
main: PID 48213
горутина 3: PID 48213
горутина 1: PID 48213
горутина 2: PID 48213
```

Все горутины печатают один и тот же PID: процесс один.

## Три состояния

В каждый момент процесс находится в одном из трёх основных состояний:

- **Running (исполняется)** — его инструкции прямо сейчас выполняет ядро CPU.
- **Ready (готов)** — он мог бы исполняться, но все ядра CPU заняты; ждёт, когда
  планировщик его выберет.
- **Blocked (заблокирован)** — он ждёт события: ответа диска, данных из сети,
  завершения дочернего процесса. Пока событие не наступило, давать ему процессор
  бессмысленно.

```text
               планировщик выбрал
     READY  ─────────────────────▶  RUNNING
       ▲    ◀─────────────────────    │
       │      вытеснен (кончился      │ сделал блокирующий вызов
       │      квант времени)          │ (read с диска, ожидание сети)
       │                              ▼
       └──────────────────────────  BLOCKED
             событие произошло
```

Обратите внимание на стрелки. Из BLOCKED процесс попадает не сразу в RUNNING, а в
READY: событие пришло, но ядро CPU может быть занято кем-то другим.

В Linux состояние видно в колонке `STAT` у `ps`:

| Буква | Что значит | В модели выше |
|---|---|---|
| `R` | running или runnable | RUNNING **или** READY |
| `S` | спит, ждёт события, можно прервать сигналом | BLOCKED |
| `D` | спит без прерывания, обычно ждёт диск | BLOCKED |
| `Z` | зомби: завершился, родитель не забрал код возврата | см. [API процессов](/os/book/process-api) |
| `T` | остановлен (например, Ctrl+Z) | отдельное состояние |

Частая ошибка: считать, что `R` в `ps` значит «прямо сейчас считает». Linux не
различает running и ready в этой колонке, `R` означает «хочет процессор». Если у вас
4 ядра CPU, а в `R` стабильно 20 процессов, то 16 из них стоят в очереди. Именно это
число (вместе с процессами в `D`) усредняет load average в `uptime`.

## Зачем нужно состояние BLOCKED: пример с числами

Возьмём одно ядро CPU и два процесса:

- **A** считает 3 мс, затем читает с диска (чтение длится 5 мс), затем считает
  ещё 2 мс;
- **B** просто считает 6 мс.

Сначала плохой вариант: ОС держит A на CPU, пока тот ждёт диск.

```text
время, мс  0    3         8    10        16
CPU        [ A ][ A ждёт ][ A ][   B    ]
                 диск, CPU
                 простаивает
```

Всё закончится к 16 мс, причём 5 мс процессор ничего не делал.

Теперь как на самом деле: при чтении с диска A уходит в BLOCKED, и ОС сразу
запускает B.

```text
время, мс  0    3              9    11
CPU        [ A ][      B      ][ A ]
диск            [ чтение A ]
                3          8
```

В момент 8 мс диск закончил, A перешёл в READY, но процессор занят B. Когда B
закончил в 9 мс, A получил CPU и досчитал к 11 мс. Работа та же, а закончили на
5 мс раньше: ожидание диска спряталось за вычислениями B. На серверах этот эффект
главный: пока один запрос ждёт базу, процессор обслуживает другие.

В Go то же самое происходит на уровне горутин. Когда горутина читает из сокета, а
данных ещё нет, рантайм не держит поток ОС в ожидании: он паркует горутину и
отдаёт поток другой горутине, а сокет отдаёт на слежение механизму netpoller.
Код при этом выглядит синхронным. Подробнее в главе
[событийная модель и epoll](/os/book/event-based-concurrency).

## Переключение контекста

Переход процессора от одного процесса к другому называется **переключением
контекста** (context switch). Ядро ОС сохраняет регистры текущего процесса в его
PCB, загружает регистры следующего из его PCB и возвращает управление уже в
следующий процесс.

Прямая цена переключения — порядка микросекунды. Есть и косвенная, обычно
больше прямой: новый процесс работает с другими данными, поэтому кэши процессора
и TLB (кэш трансляции адресов, глава [TLB](/os/book/tlb)) оказываются «холодными»,
и первые микросекунды после переключения он работает медленнее. Сводка цифр — в
главе [цена syscall и переключения контекста](/os/book/syscall-cost).

Отсюда главный конфликт планирования. Переключаться часто хорошо для честности и
быстрого отклика: никто не ждёт долго. Переключаться редко хорошо для пропускной
способности: меньше времени уходит на сами переключения. Следующие главы о том, как
ОС ищет баланс между этими целями.

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

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

1. Вы запустили один и тот же бинарник три раза. Сколько процессов, сколько
   программ?
2. Процесс читает файл, данных в памяти нет, ОС пошла на диск. В каком он
   состоянии? Куда он перейдёт, когда диск ответит?
3. В `ps` у Go-сервиса один PID, хотя в нём 10 000 горутин. Почему?
4. В примере с A и B поменяйте длительность чтения с диска на 10 мс. Когда
   закончится вся работа при правильном поведении ОС?

Ответы. (1) Три процесса, одна программа. (2) BLOCKED, а после ответа диска READY,
не сразу RUNNING. (3) Горутины переключает рантайм внутри процесса; ядро ОС видит
один процесс и несколько его потоков. (4) A считает 0–3, B считает 3–9, диск
закончит в 13, A досчитает 13–15. Всё закончится к 15 мс; процессор простаивал
с 9 до 13.

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

- Чем процесс отличается от программы и от потока? Процесс — запущенный экземпляр
  со своим адресным пространством; потоки одного процесса делят адресное
  пространство, у каждого свой стек и регистры.
- Чем процесс отличается от горутины, почему горутина дешевле.
- Какие состояния бывают и какие переходы между ними возможны; почему из BLOCKED
  попадают в READY, а не в RUNNING.
- Почему context switch дорогой и при чём тут кэши и TLB.
- Что происходит при `fork()` и почему он дешёвый (copy-on-write) — разбирается в
  главе [API процессов](/os/book/process-api).

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

**Квиз «Процесс и его состояния»**

1. Чем процесс принципиально отличается от горутины?
   - горутина переключается ядром, а процесс — рантаймом
   - процесс переключает ядро (дорого), горутины переключает рантайм Go в user-space (дёшево) (верно)
   - ничем, это синонимы
   - Пояснение: Горутины живут внутри одного процесса и мультиплексируются рантаймом на потоки ОС. Context switch процесса дороже из-за смены адресного пространства, вымывания кэша и TLB.
2. В какое состояние уходит процесс, сделавший блокирующее чтение с диска?
   - blocked (верно)
   - ready
   - running
   - Пояснение: Пока крутится диск, держать процесс на ядре бессмысленно — ОС снимает его в blocked и пускает кого-то из ready. Так I/O прячется за вычислениями.
3. Что делает ОС, когда процесс в состоянии running исчерпал свой квант времени?
   - Завершает процесс
   - Переводит в ready и ставит в очередь, а на CPU запускает другой процесс (верно)
   - Переводит в blocked до конца секунды
   - Оставляет на CPU, пока он сам не закончит
   - Пояснение: Квант кончился: по прерыванию таймера ОС снимает процесс с CPU и возвращает в ready. Он не ждёт никакого события, ему просто нужно дождаться своей очереди. blocked это состояние для процессов, которые ждут I/O или другого события.
4. Какое из состояний процесса означает «готов работать, но CPU сейчас занят другим»?
   - zombie
   - running
   - blocked
   - ready (верно)
   - Пояснение: ready: процесс может исполняться немедленно, как только планировщик даст ему ядро. В running он уже на CPU, в blocked ждёт события (например, диска), а zombie уже завершился и ждёт, пока родитель заберёт код возврата.
5. Сколько процессов могут одновременно находиться в состоянии running на машине с 4 ядрами (по одному потоку на ядро, без гиперпоточности)?
   - Ответ: 4
   - Пояснение: Не больше четырёх: running означает «прямо сейчас исполняется на ядре», а ядро в каждый момент занято одним процессом. Остальные готовые процессы сидят в ready и ждут очереди, а ждущие события в blocked.
