Логотип коледжу
Оптико-механічний фаховий коледж

Процеси: адресний простір, PCB і життєвий цикл

Файл виконуваної програми є пасивним набором інструкцій і даних. Коли ОС завантажує програму, надає їй пам'ять, стан процесора та ресурси, виникає процес. Кілька процесів можуть виконувати ту саму програму, але мати різні PID (англ. Process Identifier — ідентифікатор процесу), дані, відкриті файли й етапи життєвого циклу.

Цілі лекції

Після опрацювання матеріалу ви зможете:

  • пояснювати відмінність між програмою і процесом;
  • описувати призначення code, data, heap і stack в адресному просторі;
  • називати основні групи даних PCB (англ. Process Control Block — блок керування процесом);
  • простежувати переходи між станами процесу;
  • пояснювати ролі fork, exec, wait, exit і CreateProcess;
  • читати дерево процесів і пояснювати роль PID 1, init та systemd;
  • створювати й коректно очікувати дочірній процес у C/Linux.

Передумови

Потрібно знати роль ядра, User Mode і Kernel Mode, системні виклики та базові поняття C: функція main, локальна змінна, покажчик, перевірка коду помилки. Приклади розраховані на Linux із POSIX (англ. Portable Operating System Interface — переносний інтерфейс операційної системи) API (англ. Application Programming Interface — прикладний програмний інтерфейс) та не потребують sudo.

1. Програма і процес

Програма - це файл або набір файлів із кодом і даними. Процес - екземпляр програми у виконанні разом із поточним станом та ресурсами.

Одна команда може породити кілька різних процесів:

sleep 20 &
sleep 20 &
jobs -l

Обидва процеси виконують /usr/bin/sleep, але мають різні PID. Команда jobs -l показує завдання поточного shell. Процеси самі завершаться приблизно через 20 секунд; команда не змінює конфігурацію системи.

До процесу належать:

  • унікальний у межах поточного PID namespace ідентифікатор PID;
  • віртуальний адресний простір;
  • стан регістрів і поточна точка виконання;
  • відкриті файлові дескриптори;
  • облікові дані та права;
  • зв'язки з батьківським і дочірніми процесами;
  • інформація для планування та обліку ресурсів.

2. Віртуальний адресний простір

Кожний процес працює зі своїми віртуальними адресами. Ядро разом із MMU (англ. Memory Management Unit — блок керування пам'яттю) відображає їх на фізичну пам'ять або інші об'єкти. Через це однакова числова віртуальна адреса у двох процесах зазвичай не означає спільні дані.

Навчальна модель адресного простору має такі області:

ОбластьТиповий вмістВластивість
code / textмашинні інструкції програмизазвичай доступна для виконання і не для запису
read-only dataконстанти, рядкові літераличитання без запису
data / BSS (англ. Block Started by Symbol — сегмент неініціалізованих даних)глобальні та статичні змінніініціалізовані дані та нульові початкові значення
heapдинамічні виділення mallocзмінюється за запитами програми й allocator
memory mappingsбібліотеки, файли, анонімні відображеннястворюються через механізми на кшталт mmap
stackкадри викликів, локальні змінні, адреси поверненняокремий стек для кожного потоку

Адресний простір процесу, PCB і переходи між станами

Схема є концептуальною. Точні адреси, порядок і напрям росту областей залежать від архітектури, формату програми, завантажувача та ASLR (англ. Address Space Layout Randomization — рандомізація розташування адресного простору). Не можна писати переносний код, спираючись на те, що heap «завжди росте вгору», а stack «завжди вниз».

Спостереження адрес у C

Створіть address_space.c:

#include <stdio.h>
#include <stdlib.h>

int global_initialized = 7;
int global_zero;

static void marker(void) {
}

int main(void) {
    int local = 11;
    int *dynamic = malloc(sizeof(*dynamic));
    if (dynamic == NULL) {
        perror("malloc");
        return 1;
    }

    *dynamic = 13;
    marker();
    printf("initialized data:  %p\n", (void *)&global_initialized);
    printf("zero-initialized:  %p\n", (void *)&global_zero);
    printf("heap allocation:   %p\n", (void *)dynamic);
    printf("stack local:       %p\n", (void *)&local);

    free(dynamic);
    return 0;
}

Компіляція і запуск:

cc -std=c11 -Wall -Wextra -pedantic address_space.c -o address_space
./address_space
nm -n address_space | grep ' marker$'

Програма друкує адреси об'єктів data, BSS, heap і stack. Команда nm окремо знаходить символ функції marker у секції коду; її значення у PIE-файлі (англ. Position-Independent Executable — позиційно-незалежний виконуваний файл) не слід ототожнювати зі сталою runtime-адресою. Переносний ISO (англ. International Organization for Standardization — Міжнародна організація зі стандартизації) C не гарантує перетворення покажчика на функцію до void *, тому приклад навмисно не друкує його через %p. Виведені адреси об'єктів можуть змінитися після нового запуску через ASLR. Завдання прикладу - побачити різні категорії, а не запам'ятати конкретні числа.

3. PCB: як ядро представляє процес

ОС потребує власного запису, щоб зупинити, відновити, планувати й завершити процес. Узагальнену структуру називають Process Control Block (PCB), або блоком керування процесом.

PCB зазвичай охоплює:

  • PID, стан і зв'язки з іншими процесами;
  • збережений процесорний контекст;
  • дані планування: пріоритет, належність до черг, використаний CPU (англ. Central Processing Unit — центральний процесор) time;
  • посилання на структури адресного простору;
  • відкриті файли та поточний каталог;
  • облікові дані, сигнали й ліміти ресурсів;
  • статистику та статус завершення.

PCB не є ділянкою, яку звичайна програма безпосередньо редагує у своєму адресному просторі. Це керівні дані ядра. У Linux немає буквально однієї структури з назвою PCB: центральну роль для задачі виконує task_struct, пов'язана з іншими структурами. У Windows представлення процесу пов'язане, зокрема, з EPROCESS. Термін PCB є корисною загальною моделлю.

Під час context switch ядро зберігає достатній стан поточного потоку та відновлює стан наступного. Частина інформації розташована в керівних структурах, частина - у стеку ядра або архітектурно залежних структурах.

4. Стани та переходи

Базова модель містить п'ять станів:

СтанЗначення
NewОС створює керівні структури та ресурси
Readyпроцес готовий виконуватися й очікує CPU
Runningінструкції процесу зараз виконуються на CPU
Blocked / Waitingпроцес чекає події, наприклад даних або завершення нащадка
Terminatedвиконання завершене; ОС звільняє або вже звільнила ресурси

Типові переходи:

  • New -> Ready: створення завершено;
  • Ready -> Running: диспетчер надав CPU;
  • Running -> Ready: витіснення або завершення кванта;
  • Running -> Blocked: потрібна подія, якої ще немає;
  • Blocked -> Ready: подія сталася;
  • Running -> Terminated: процес виконав exit, повернувся з main або був завершений.

Процес у стані Blocked не «стоїть у черзі за CPU»: навіть отримавши CPU, він не може продовжити без очікуваної події. Після події він спочатку стає Ready, а момент переходу до Running визначає планувальник.

Реальні ОС мають детальніші стани. Наприклад, Linux-утиліти можуть показувати R, S, D, T, Z та інші коди. Навчальна п'ятистанова модель пояснює загальну логіку, але не замінює документацію конкретного ядра.

5. POSIX-модель: fork, exec, wait, exit

Unix-подібні системи розділяють створення процесу та заміну його програми.

fork: створити нащадка

fork() створює дочірній процес, який продовжує виконання з точки повернення fork. Успішний виклик повертається двічі:

  • у батьківському процесі - PID дочірнього процесу, значення більше нуля;
  • у дочірньому процесі - 0;
  • за помилки в батьківському процесі - -1, і нащадка немає.

Логічно нащадок отримує знімок стану батька. Сучасні системи зазвичай оптимізують пам'ять через Copy-on-Write: фізичні сторінки не копіюються негайно, а розділяються до спроби запису.

exec: замінити програмний образ

Сімейство exec не створює ще один процес. Успішний exec замінює code, data, heap і stack поточного процесу новою програмою. PID зберігається. Успішний exec не повертається до старого коду; повернення означає помилку.

wait і waitpid: отримати завершення

Батьківський процес викликає wait або waitpid, щоб дочекатися зміни стану нащадка й забрати статус завершення. Якщо нащадок уже завершився, але статус ще не отримано, у Unix-моделі залишається мінімальний запис zombie. Він не виконує код, але займає запис у таблиці процесів до wait або до обробки його новим батьком.

exit: завершити процес

Повернення з main еквівалентне виклику exit із цим кодом для нормального C-завершення. ОС закриває ресурси процесу, а код завершення стає доступним батьківському процесу. За домовленістю 0 означає успіх, ненульове значення - певну помилку, але точний зміст визначає програма.

6. Безпечний приклад fork і waitpid

Створіть fork_wait.c:

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main(void) {
    pid_t child = fork();

    if (child == -1) {
        perror("fork");
        return 1;
    }

    if (child == 0) {
        printf("Дочірній процес: pid=%ld, ppid=%ld\n",
               (long)getpid(), (long)getppid());
        fflush(stdout);
        _exit(7);
    }

    printf("Батьківський процес: pid=%ld, child=%ld\n",
           (long)getpid(), (long)child);

    int status;
    pid_t waited;
    do {
        waited = waitpid(child, &status, 0);
    } while (waited == -1 && errno == EINTR);

    if (waited == -1) {
        perror("waitpid");
        return 1;
    }

    if (WIFEXITED(status)) {
        printf("Нащадок завершився з кодом %d\n", WEXITSTATUS(status));
    } else {
        printf("Нащадок завершився не через звичайний exit\n");
    }

    return 0;
}

Компіляція й запуск:

cc -std=c11 -Wall -Wextra -pedantic fork_wait.c -o fork_wait
./fork_wait

У результаті будуть PID і код 7. Однак перший рядок може надрукувати як батьківський, так і дочірній процес. Після fork обидва є Ready або Running за рішенням планувальника; стандарт POSIX не гарантує порядок їхнього виведення.

waitpid гарантує інше: підсумковий рядок зі статусом друкується батьком після завершення конкретного нащадка. Саме синхронізація через waitpid, а не припущення про швидкість процесів, задає потрібний порядок.

fflush(stdout) перед _exit явно передає рядок бібліотеці/ядру. _exit завершує дочірній процес без повторної обробки успадкованих буферів stdio. Для звичайного завершення окремої програми найчастіше використовують повернення з main або exit.

7. Комбінація fork + exec

Shell зазвичай створює нащадка, а нащадок замінює себе потрібною програмою. Безпечний фрагмент запускає /bin/echo:

pid_t child = fork();
if (child == -1) {
    perror("fork");
    return 1;
}

if (child == 0) {
    execl("/bin/echo", "echo", "Запущено через exec", (char *)NULL);
    perror("execl");
    _exit(127);
}

int status;
if (waitpid(child, &status, 0) == -1) {
    perror("waitpid");
    return 1;
}

Рядок після execl виконається лише за помилки. Код 127 часто використовують оболонки для невдалого запуску команди, але це домовленість, а не автоматична властивість exec.

8. Windows: CreateProcess

Windows API не використовує пару fork + exec як основну модель. CreateProcessW одним викликом створює новий процес і його первинний потік та завантажує задану програму. Батьківський код отримує handles процесу й потоку та їхні ідентифікатори.

Порівнювати варто за метою, а не механічно:

ЗавданняPOSIXWinAPI
Створити копію поточного процесуforkнемає прямого загального аналога
Замінити образ поточного процесуexecінша модель запуску
Створити процес із новою програмоючасто fork + execCreateProcessW
Дочекатися завершенняwaitpidWaitForSingleObject для process handle
Отримати код завершеннямакроси WIFEXITED, WEXITSTATUSGetExitCodeProcess

Handles після використання потрібно закривати через CloseHandle. PID та handle не є одним і тим самим: PID ідентифікує, а handle є посиланням із правами доступу до об'єкта ядра.

9. Дерево процесів, init і systemd

У Unix-подібній моделі процес має батьківський PID, тому зв'язки утворюють дерево. Подивитися фрагмент дерева поточного сеансу можна без привілеїв:

ps -o pid,ppid,state,comm -p $$
ps -o pid,ppid,state,comm --forest -u "$USER"

Виведення залежить від системи. $$ у shell означає PID поточної оболонки.

Перший userspace-процес у системному PID namespace зазвичай має PID 1. Історично його називають init. У багатьох сучасних Linux-дистрибутивах цю роль виконує systemd: він запускає й контролює системні служби, організовує залежності та приймає осиротілі процеси для подальшого прибирання їхніх статусів.

У контейнері PID 1 може бути не systemd, а сама програма контейнера або мінімальний init. Тому завжди відрізняйте загальну роль процесу PID 1 від конкретної реалізації.

Якщо батьківський процес завершується раніше за живого нащадка, нащадок стає orphan і перепризначається відповідному процесу-прибирачу, часто PID 1 або subreaper. Orphan не те саме, що zombie: orphan може продовжувати виконання, zombie вже завершився й очікує отримання статусу.

Практичні завдання

Завдання 1. Дослідіть адреси

Скомпілюйте address_space.c і запустіть його двічі. Позначте, яка адреса відповідає data, BSS, heap і stack, а за виведенням nm знайдіть символ marker у code. Поясніть, чому показані значення не слід вважати сталими runtime-адресами.

Завдання 2. Простежте два шляхи після fork

Скомпілюйте fork_wait.c. Запустіть не менше трьох разів і запишіть порядок перших двох рядків. Не робіть висновку, що один процес «завжди перший», навіть якщо у трьох запусках порядок випадково збігся.

Завдання 3. Побудуйте керований запуск

Розширте приклад: у дочірній гілці виконайте /bin/echo через execl, у батьківській - waitpid, перевірку WIFEXITED та WEXITSTATUS. Обробіть помилки fork, exec і waitpid. Намалюйте послідовність станів обох процесів від fork до завершення та вкажіть місце, де батько може бути Blocked.

Підсумок

  • Процес є програмою у виконанні зі станом, адресним простором і ресурсами.
  • Code, data, heap і stack мають різне призначення; точне розміщення залежить від системи.
  • PCB є моделлю керівних даних ядра про процес, а не частиною вихідного коду.
  • Ready означає готовність до CPU, Blocked - очікування іншої події.
  • fork створює нащадка, exec замінює поточний образ, waitpid отримує завершення, exit завершує процес.
  • Порядок виконання батька й нащадка після fork не визначений; потрібний порядок створюють механізмами синхронізації.
  • CreateProcessW реалізує Windows-модель створення процесу без прямого повторення fork + exec.
  • Дерево процесів відображає батьківські зв'язки; PID 1 виконує особливу роль init, але не завжди є systemd.

Завдання

1. Що найточніше відрізняє процес від файла програми?

2. Де зазвичай розміщують динамічно виділені через `malloc` об'єкти?

3. Яке призначення PCB?

4. Який перехід типовий, коли процес у стані Running викликає `read` і мусить чекати даних?

5. Яке значення повертає успішний `fork()` у дочірньому процесі? Запишіть лише число.

6. Навіщо батьківський процес викликає `wait` або `waitpid`?