Процессы в операционной системе: жизненный цикл, состояния и планирование🔗
Что такое процесс: от статического кода к исполнению🔗
Процесс — это динамическая абстракция ОС, представляющая собой программу в состоянии выполнения с выделенным адресным пространством и ресурсами. Через него ядро распределяет процессорное время и изолирует задачи друг от друга.
Программа — всего лишь пассивный набор байтов на диске. Процесс, напротив, является «ожившим» кодом. Возьмём пример из кулинарии: программа подобна рецепту, а процесс — самому приготовлению блюда. Кухня здесь выступает в роли ЦП, а ингредиенты становятся данными из оперативной памяти. Из одного рецепта можно запустить три готовки одновременно, как и одну программу — развернуть в несколько независимых задач.
Отличия программы от процесса🔗
Программа хранится в файловой системе как статический объект. Чтобы она начала работать, ядро создает специальные структуры данных, выделяет виртуальную память и открывает доступ к устройствам ввода-вывода.
| Характеристика |
Программа |
Процесс |
| Природа |
Статический файл (пассивный) |
Экземпляр в памяти (активный) |
| Место хранения |
Вторичная память (HDD/SSD) |
Оперативная память (RAM) |
| Жизненный цикл |
Постоянный (до удаления файла) |
Временный (от запуска до конца работы) |
| Ресурсы |
Занимает только место на диске |
Использует ЦП, ОЗУ, дескрипторы файлов |
Архитектурная иерархия управления🔗
Процесс существует в жестко ограниченных рамках. Ядро ОС выступает посредником: оно гарантирует, что один поток вычислений не получит доступ к данным другого. Изоляция реализуется через системные вызовы — единственный легальный способ для прикладного кода получить ресурсы от «железа».
Логика взаимодействия:
Диаграмма загружается…
Каждый запуск порождает новый процесс с уникальным идентификатором (PID). Код остается прежним, но состояние регистров процессора и содержимое памяти у каждого экземпляра будут свои. Для отслеживания этих данных ядро использует специальную структуру — Process Control Block (PCB). Подумайте, какая информация должна храниться в PCB, чтобы ОС могла в любой момент поставить процесс «на паузу», а затем возобновить его без ошибок?
Структура Process Control Block (PCB): как ядро хранит данные о процессе🔗
Process Control Block (PCB) — это служебная структура в оперативной памяти ядра, которая служит «паспортом» процесса и содержит все данные для управления им. Через этот блок операционная система идентифицирует задачу, отслеживает её ресурсы и восстанавливает состояние при переключении контекста.
При запуске программы ядро создает новый экземпляр PCB. В Linux за это отвечает структура task_struct. Когда работа завершается, дескриптор удаляется, а занятая им память возвращается системе.
Анатомия PCB: содержимое дескриптора🔗
PCB — это динамический объект, к которому планировщик обращается тысячи раз в секунду. Параметры внутри него сгруппированы по ролям:
| Категория |
Поля (примеры) |
Назначение |
| Идентификация |
PID (Process ID), PPID |
Уникальный номер процесса и ID «родителя». |
| Состояние |
Running, Ready, Waiting |
Текущий этап жизненного цикла задачи. |
| Контекст ЦП |
Program Counter (PC), Регистры |
Данные для восстановления работы процессора. |
| Память |
Page Tables, Segment Table |
Ссылки на адресное пространство для MMU. |
| Ресурсы и I/O |
Таблица дескрипторов (FD) |
Список открытых файлов, сокетов и устройств. |
Изоляция через абстракцию данных🔗
Наличие PCB позволяет реализовать механизм изоляции. Процесс «не догадывается» о существовании соседей, так как ядро ограничивает его видимость только теми ресурсами, что прописаны в дескрипторе. Любая попытка выйти за эти рамки блокируется ОС на основе прав доступа из структуры.
Упрощенная модель PCB на языке C выглядит так:
struct pcb_t {
uint32_t pid; // Идентификатор
enum state process_state; // Текущее состояние
uint32_t program_counter; // Адрес следующей инструкции
uint32_t registers[16]; // Содержимое регистров общего назначения
struct memory_limits mem; // Границы виртуальной памяти
int file_descriptors[1024]; // Таблица открытых файлов
};
При этом При переключении задач ядро инициирует сохранение состояния. Текущие значения из физических регистров процессора копируются в поля registers и program_counter блока активного процесса. Вслед за этим в CPU загружаются данные из PCB новой задачи. Именно эта процедура создает иллюзию одновременной работы приложений, хотя физически процессор обрабатывает их по очереди.
PCB остается единственным источником правды для системы. Если ресурс не зафиксирован в дескрипторе, процесс не сможет им воспользоваться. Такая строгость исключает хаос в среде, где одновременно сосуществуют сотни активных сущностей.
Как вы думаете, сильно ли вырастет нагрузка на систему, если увеличить размер структуры PCB в два раза?
Жизненный цикл процесса: от рождения до терминации🔗
Жизненный цикл процесса — это цепочка сменяющих друг друга состояний задачи, которая показывает её текущую активность и доступ к ресурсам CPU. Ядро переключает эти фазы, чтобы эффективно распределять вычислительные мощности между сотнями конкурирующих запросов.
ОС отслеживает положение задачи в этом цикле, обновляя статус в блоке управления (PCB). В классической архитектуре принято выделять пять универсальных стадий.
Диаграмма загружается…
Анатомия пяти состояний🔗
При этом 1. New (Создание). Процесс только что инициирован — допустим, через системный вызов fork() в Unix. Ядро выделяет память под служебные структуры данных, но исполнение кода ещё не началось.
2. Ready (Готовность). Программа полностью загружена в оперативную память и готова к работе. Она ожидает в очереди планировщика момента, когда система выделит ей квант времени на процессоре.
3. Running (Выполнение). Инструкции кода физически исполняются процессором. В классической одноядерной системе в этом статусе может пребывать строго одна задача.
4. Waiting / Blocked (Ожидание). Выполнение приостановлено до наступления конкретного события: завершения чтения файла, ответа от сети или освобождения мьютекса. В этой фазе процесс не потребляет ресурсы CPU, освобождая место другим.
5. Terminated (Завершение). Работа окончена штатно (вызов exit()) или прервана из-за ошибки (к примеру, Segmentation fault). ОС освобождает память, но запись в таблице процессов может сохраняться, пока родительская задача не получит код возврата.
Логика и триггеры переходов🔗
Смена состояний происходит под влиянием системных вызовов или аппаратных прерываний. Если задача в статусе Running исчерпала лимит времени, таймер генерирует сигнал, и планировщик принудительно возвращает её в очередь Ready.
Часто возникает вопрос: может ли процесс из Waiting сразу прыгнуть в Running? Нет, это нарушило бы принцип справедливости. Когда операция ввода-вывода завершается, задача получает право на работу, но не может самовольно вытеснить текущий процесс с процессора. Она встает в общую очередь, позволяя алгоритмам планирования соблюсти приоритеты и избежать хаоса.
Краткая карта перемещений🔗
- Running → Waiting: добровольная уступка CPU ради ожидания внешних данных.
- Running → Ready: принудительная остановка по сигналу таймера (вытеснение).
- Waiting → Ready: сигнал о том, что внешняя операция завершена и можно двигаться дальше.
Каждый такой переход требует от ядра выполнения процедуры переключения контекста. Как именно значения регистров. Указатели стека подменяются «на лету» без потери данных? Ответ на этот вопрос кроется в механизмах сохранения контекста, которые мы изучим в следующем блоке.
Как работает переключение контекста (Context Switch) в ядре ОС🔗
Переключение контекста — это операция ядра по сохранию состояния текущего процесса и восстановлению данных другого для запуска на CPU. Этот механизм позволяет процессору переключаться между задачами так быстро, что создается иллюзия их одновременной работы.
Когда управление переходит от Процесса А к Процессу Б, ядро «замораживает» текущую вычислительную среду. В контекст входят значения регистров, указатель стека (SP), счетчик команд (PC) и настройки виртуальной памяти. Все эти данные фиксируются в структуре PCB.
Алгоритм переключения: шаг за шагом🔗
Процедура запускается либо добровольно через системный вызов, либо принудительно по сигналу аппаратного прерывания от таймера или периферии.
Диаграмма загружается…
Также 1. Переход в режим ядра. Базовый набор регистров сохраняется в системном стеке.
2. Фиксация состояния. Текущие значения CPU записываются в task_struct (в Linux) или аналогичный блок дескриптора.
3. Выбор задачи. Ядро обращается к планировщику, чтобы определить следующую очередь согласно приоритетам.
4. Обновление памяти. Переключаются указатели на таблицы страниц (скажем, регистр CR3 в x86), что открывает доступ к адресному пространству новой программы.
5. Восстановление. Данные из PCB выбранного процесса загружаются обратно в аппаратные регистры.
Цена многозадачности🔗
Переключение контекста — накладная операция. Она не выполняет полезных вычислений, но активно потребляет такты процессора. Основные потери делятся на два типа:
- Прямые затраты. Ресурсы, затраченные на работу кода ядра по копированию данных и смене таблиц страниц.
- Косвенные затраты (Cold Cache). Новый процесс сталкивается с «холодным» кэшем L1/L2 и пустым буфером TLB. Процессору приходится заново подтягивать данные из медленной оперативной памяти, что снижает производительность в первые микросекунды после старта.
Слишком частая смена задач (thrashing) заставляет систему тратить на административные нужды больше времени, чем на сами приложения. Поэтому эффективность ОС зависит от того, как планировщик балансирует между отзывчивостью интерфейса и пропускной способностью CPU.
При этом Как вы считаете, может ли увеличение количества ядер в процессоре полностью нивелировать затраты на переключение контекста?
Практика в POSIX: системные вызовы fork(), exec() и wait() на C🔗
fork(), exec() и wait() — это базовый интерфейс POSIX для управления жизненным циклом процессов. С его помощью система создает копии текущих задач, заменяет их код и синхронизирует иерархию «родитель — потомок».
При этом В этой связке fork() отвечает за порождение нового процесса, exec() — за смену «личности», а wait() — за очистку ресурсов после завершения работы. В отличие от многих других системных подходов, создание задачи в UNIX-подобных ОС реализовано как двухэтапный механизм.
Сначала вызов fork() создает полный дубликат текущего процесса. В системе возникают две идентичные задачи с разными идентификаторами (PID), которые продолжают выполнение с одной и той же строки кода. Различается только возвращаемое значение функции: родитель получает PID ребенка, а ребенок — ноль.
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("Ошибка fork");
return 1;
} else if (pid == 0) {
// Код выполняется в процессе-потомке
printf("[CHILD] Мой PID: %d, мой родитель: %d\n", getpid(), getppid());
char *args[] = {"/bin/ls", "-l", NULL};
execv(args[0], args); // Заменяем образ процесса
} else {
// Код выполняется в родительском процессе
printf("[PARENT] Создан потомок с PID: %d\n", pid);
wait(NULL); // Ожидаем завершения потомка
printf("[PARENT] Потомок завершен. Я продолжаю работу.\n");
}
return 0;
}
Механика exec() устроена иначе: она не порождает новую сущность, а замещает тело текущей программы (код, данные и стек) новым исполняемым файлом. Запись в таблице процессов и PID сохраняются, но старый код бесследно исчезает. Если вызов прошел успешно, управление в изначальную функцию никогда не вернется.
Иерархия, зомби и сироты🔗
Завершение процесса не означает его мгновенное исчезновение. Потомок переходит в состояние Zombie (Z). Он перестает выполняться и освобождает память, но запись о нем хранится в системе до тех пор, пока родитель не подтвердит получение кода завершения через wait().
Кроме того, 1. Процесс-зомби (Zombie): появляется, когда ребенок закончил работу, а родитель еще не выполнил wait(). Если игнорировать этот этап, таблица процессов переполнится «мертвыми» записями.
2. Процесс-сирота (Orphan): возникает, когда родитель завершается раньше ребенка. Таких сирот «усыновляет» процесс init (PID 1), который берет на себя роль диспетчера и очищает их ресурсы.
Схема взаимодействия в иерархии выглядит так:
Диаграмма загружается…
Подобный механизм разделения гарантирует изоляцию: критическая ошибка в дочернем процессе не обрушит родительскую программу. Как вы считаете, какие риски несет возможность процесса бесконечно плодить «зомби» в реальной серверной системе?