Курс Лекція 1
▶ СлайдиСРС 01+02⌨️ команди 13 · 9 нових
Конспект · Лекція 1

ТРИ МЕЖІ КУРСУ: ПРОЦЕС, АДРЕСНИЙ ПРОСТІР, СИСТЕМНІ ВИКЛИКИ

Де закінчується програма й починається машина

Ключова ідея. Процес — це межа між програмою і машиною, яку курс перетинає тричі: контейнер (Docker), безпека пам'яті (Rust), пісочниця виконання (WebAssembly).
Демо. Запустити фоновий процес (sleep 300 &), знайти його PID, показати ps aux, а потім заглянути у /proc/<pid>/status, /proc/<pid>/maps і /proc/<pid>/fd — побачити, з чого насправді складається "процес" з погляду ядра.
🧩 СРС ДО ЦІЄЇ ЛЕКЦІЇ · 2-га година заняття

Після презентації теми — самостійна робота в аудиторії над двома темами. Вони ж готують до захисту та розширення ЛР1. Матеріали публікуються в Google Classroom разом із лекцією.

📚 ТЕОРЕТИЧНІ ВІДОМОСТІ

Вступ: що означає «керувати системою напряму»

Коли ти пишеш print("hello") на Python або рендериш кнопку в React, ти працюєш у зручному, підготовленому для тебе світі: інтерпретатор, збирач сміття, віртуальна машина браузера — усе це шар за шаром ховає від тебе, як насправді влаштована машина під ногами. Системне програмування — це дисципліна, яка знімає ці шари один за одним і працює безпосередньо з тим, що надає операційна система: пам'яттю, процесами, файловими дескрипторами, мережевими сокетами, сирими байтами на диску.

Ціна помилки тут інша, ніж у прикладному коді. Забутий free() у C — це витік пам'яті, який за тижні роботи сервера з'їсть усю оперативку. Неправильно виставлені права контейнера — це діра, крізь яку процес усередині «в'язниці» дотягнеться до файлів хоста. Гонка (race condition) у роботі з файловим дескриптором — це непередбачувана поведінка сотень процесів одночасно, а в гіршому разі — крах ядра. Тому системне програмування вимагає не «запам'ятати синтаксис», а зрозуміти модель: що саме гарантує операційна система, а що — ні.

Курс «Системне програмування» 2026 року побудований навколо однієї наскрізної ідеї — межі. У кожному з трьох модулів ми перетинаємо іншу межу між кодом і машиною: Docker — межу між процесом і операційною системою (модуль 1), Rust — межу між програмою та її власною пам'яттю (модуль 2), WebAssembly — межу між кодом і чужим, недовіреним середовищем виконання (модуль 3). Сьогоднішня лекція закладає фундамент для всіх трьох: що таке процес з погляду ядра, і як через один-єдиний механізм — системний виклик — програма взагалі отримує доступ до світу поза собою.

Процес — це не файл і не програма

У побутовому сенсі ми кажемо «запустив програму» і маємо на увазі одне й те саме — файл на диску й те, що виконується. Але ядро розрізняє ці поняття принципово. Файл програми (наприклад, /bin/ls) — це пасивні байти на диску: машинний код, таблиці символів, метадані ELF-формату. Він нічого не робить, поки лежить на диску.

Процес — це зовсім інша сутність: активний, супроводжуваний ядром об'єкт, що виникає в момент, коли ядро виконує системний виклик (syscall) execve() над цим файлом. З цієї миті ядро веде для процесу окрему бухгалтерію: унікальний ідентифікатор (PID), стан виконання (running, sleeping, zombie, stopped), батьківський процес (PPID), список відкритих файлових дескрипторів, таблицю відображення сторінок пам'яті, набір облікових даних (UID, GID, capabilities) і багато іншого. Усе це в Linux зберігається у структурі ядра task_struct — по одному екземпляру на кожен процес (і навіть на кожен потік: у Linux потік — це «полегшений» процес, який ділить адресний простір з батьківським).

Ключова ідея: кожен процес живе в ілюзії власного, ексклюзивного світу. Він «бачить» адресний простір (address space) 0…2⁶⁴, ніби машина належить лише йому. Він «бачить» процесор, ніби виконується безперервно. Насправді ядро підмінює йому цю картину: планувальник перемикає контекст між сотнями процесів десятки разів на секунду, а MMU (memory management unit) транслює адреси процесу у фізичну пам'ять непомітно для нього. Уся ця ілюзія тримається на одному фундаменті — ядро контролює межу.

Народження процесу: fork() і execve()

У Unix-подібних системах немає окремого системного виклику «запустити нову програму» в один крок. Замість цього використовуються дві незалежні операції, кожна з яких вирішує свою задачу.

fork() створює копію поточного процесу: новий процес (дитина) отримує власний PID, але спершу — практично ідентичний знімок пам'яті батька. Насправді ядро використовує техніку copy-on-write (COW): фізичні сторінки не копіюються одразу, а лише позначаються «тільки для читання» й спільно використовуються батьком і дитиною; реальне копіювання конкретної сторінки відбувається лише в момент, коли хтось із них намагається її змінити. Дитина повертається з fork() з кодом 0, батько — з PID дитини. Це дозволяє одному й тому ж коду розгалужуватись на дві гілки виконання.

execve() — зовсім інша дія: вона повністю замінює образ пам'яті поточного процесу новим виконуваним файлом. PID лишається той самий, але код, дані, стек — усе перезаписується новою програмою. Класична оболонка виконує послідовність fork() + execve(): спершу породжує копію себе, а потім у дитині викликає execve(), щоб перетворити цю копію на нову програму (наприклад, ls). Батьківський процес (shell) тим часом чекає завершення дитини через wait()/waitpid().

# Спостерігаємо це наживо через strace
strace -f -e trace=fork,vfork,clone,execve bash -c "ls > /dev/null"

У сучасних системах прямий fork() частіше реалізовано через гнучкіший системний виклик clone(), який приймає прапорці, що визначають, скільки саме розділяти з батьком: адресний простір, файлові дескриптори, простори імен. Саме clone() з прапорцями CLONE_NEWPID, CLONE_NEWNET тощо — фундамент, на якому побудовані контейнери, про що піде мова в лекції 2.

Системний виклик — єдина легальна межа

Процес не може напряму «попросити» диск прочитати сектор чи мережеву карту відправити пакет. Апаратне забезпечення в сучасних процесорах x86-64 та ARM організоване за принципом кілець захисту (protection rings) — рівнів привілеїв, які апаратно обмежують, які інструкції й регістри доступні коду.

Кільце Назва Хто виконується Приклад
Ring 0 kernel mode Ядро ОС обробники переривань, планувальник, драйвери
Ring 1–2 (майже не використовуються в Linux) історично для гіпервізорів/драйверів
Ring 3 user mode Звичайні програми твій ls, python3, nginx

Код у Ring 3 фізично не має доступу до привілейованих інструкцій — спроба виконати їх викликає апаратний виняток (general protection fault). Єдиний легальний спосіб для user-space коду попросити ядро щось зробити — це системний виклик (syscall): спеціальна інструкція процесора (syscall на x86-64), яка перемикає CPU в Ring 0, передає керування заздалегідь визначеному обробнику ядра з номером бажаної операції та аргументами в регістрах, і після виконання повертає керування назад у Ring 3.

Кожна дія, яку виконує програма поза власною пам'яттю — читання файлу, відкриття сокета, створення процесу, виділення додаткової пам'яті через mmap — це виклик однієї з кількох сотень syscalls, які надає ядро Linux (наразі трохи більше 450 на x86-64). Бібліотека libc ховає це за зручними функціями (printf, malloc, fopen), але зрештою все зводиться до тонкого шару syscalls.

# Побачити реальний перелік syscalls, які виконує проста команда
strace -c ls > /dev/null
# % time     seconds  usecs/call     calls    syscall
# ------ ----------- ----------- --------- ----------------
#  35.20    0.000234          12        19 openat
#  20.11    0.000134           7        19 read
#  ...

Саме тому «межа» в назві курсу — не метафора. Docker обмежує, які syscalls і з яким ефектом дозволені контейнеру (через namespaces, seccomp-профілі, capabilities). Rust у момент компіляції гарантує, що небезпечний доступ до пам'яті неможливий без явного unsafe, тобто контролює межу ще до того, як код узагалі дійде до syscall. WebAssembly в браузері не має прямого доступу до syscalls операційної системи взагалі — лише до того, що явно надав JavaScript-хост через «import»-функції. Три різні технології, одна й та сама ідея контрольованої межі, реалізована на різних рівнях стека.

Адресний простір процесу зсередини

Кожен процес отримує від ядра ілюзію лінійного адресного простору від 0 до 2⁶⁴−1 (на практиці на x86-64 використовується canonical-діапазон, ~128 ТБ користувацького простору). Цей простір поділений на сегменти з різним призначенням:

  • text (code) — машинний код програми, зазвичай доступний тільки для читання й виконання (immutable, щоб унеможливити самомодифікацію коду як клас вразливостей);
  • data / bss — статично ініціалізовані й неініціалізовані глобальні змінні;
  • heap — динамічна пам'ять, яку виділяють malloc/new; росте вгору через syscall brk/mmap;
  • shared libraries — код спільних бібліотек (libc.so тощо), відображений у пам'ять процесу через mmap, часто спільний фізично між процесами, що використовують ту саму бібліотеку;
  • stack — локальні змінні, адреси повернення функцій, росте вниз від верхньої межі користувацького простору.

Ядро підтримує для кожного процесу таблицю сторінок (page table), яка відображає ці віртуальні адреси на реальні фізичні сторінки пам'яті. Саме завдяки цій непрямій адресації два різні процеси можуть мати абсолютно різний фізичний вміст за однаковою віртуальною адресою — і не бачити один одного, якщо ядро цього не дозволило.

/proc: файлова система, яка показує внутрішній стан ядра

Linux надає віртуальну файлову систему /proc, у якій кожен активний процес представлений каталогом /proc/<pid>/. Це не файли на диску — вони генеруються ядром «на льоту» при читанні й показують реальний внутрішній стан:

sleep 300 &
PID=$!
cat /proc/$PID/status   # стан, використання пам'яті, батько
cat /proc/$PID/maps     # список сегментів адресного простору з правами доступу
ls  /proc/$PID/fd       # відкриті файлові дескриптори (символьні посилання)
cat /proc/$PID/cmdline  # аргументи запуску

/proc/<pid>/maps особливо показовий: кожен рядок — це один регіон адресного простору з правами r/w/x і файлом, з якого він відображений (сам бінарник, спільна бібліотека, heap, stack або анонімна mmap-область). Це буквально та сама структура, яку ми щойно розглянули теоретично, побачена наживо для конкретного процесу.

Куди це веде: три модулі курсу

Розуміння процесу й syscall-межі — не самоціль, а фундамент для трьох модулів курсу:

  1. Docker (модуль 1, лекції 1–3, лабораторні 1–3) — контейнер це процес, якому ядро через namespaces підмінило видимість світу, а через cgroups обмежило споживання ресурсів. Ти вже знаєш, що таке PID і адресний простір — залишилось зрозуміти, як ядро може дати різним процесам різні «версії» цих понять.
  2. Rust (модуль 2) — мова, яка на етапі компіляції математично доводить, що доступ до пам'яті (heap, stack, посилання) безпечний, ще до того, як програма взагалі викличе хоч один syscall.
  3. WebAssembly (модуль 3) — байт-код, який виконується в пісочниці браузера й не має прямого доступу до syscalls операційної системи взагалі — лише до того, що явно «імпортовано» з JavaScript-хоста.

Підводні камені

  • Плутанина PID і task_struct. PID — це просто число; реальний стан процесу живе в структурі ядра task_struct, до якої PID є лише ключем пошуку.
  • fork() не копіює «миттєво» все. Через copy-on-write фізичні сторінки не дублюються одразу — тому fork() дешевший, ніж здається, але перше ж записування в дочірньому процесі спричиняє реальне копіювання сторінки.
  • strace показує syscalls, а не «все». Виклики бібліотечних функцій (printf) не видно напряму — strace перехоплює лише межу user↔kernel, тому один printf може розкластися на кілька або жодного syscall залежно від буферизації.
  • /proc — не статичний знімок. Значення там змінюються в реальному часі; повторний cat того самого файлу за секунду може дати інші цифри.
  • Ring 3 не означає «без пам'яті ядра». Код ядра теж відображений в адресний простір кожного процесу (для швидкого перемикання в syscall), просто недоступний для читання/запису з Ring 3 — саме цю межу експлуатували вразливості на кшталт Meltdown.

Підсумок

Процес — не файл, а активна абстракція, яку ядро підтримує через task_struct, адресний простір і таблицю дескрипторів. Єдиний легальний спосіб для процесу вийти за межі власної пам'яті — системний виклик, апаратно захищений переходом між Ring 3 і Ring 0. /proc дає змогу побачити цю внутрішню бухгалтерію ядра як звичайні файли. Розуміння цієї межі — фундамент для всіх трьох модулів курсу: Docker ізолює процес через namespaces і cgroups, Rust контролює межу пам'яті на етапі компіляції, WebAssembly обмежує доступ до syscalls до явно імпортованого хостом. Наступна лекція заглиблюється в перший з трьох механізмів — простори імен.

Розбір за слайдами

План лекції

Фокус: одна ідея — межа середовища виконання

Що таке системне програмування

Мотивація: чому це важливо саме зараз

Три модулі — три межі

ПРОЦЕС межа середовища ПАМʼЯТЬ межа безпеки ІНСТРУКЦІЇ межа набору Docker · namespaces Rust · borrow checker asm → WebAssembly ТРИ МЕЖІ МІЖ ПРОГРАМОЮ І МАШИНОЮ

Файл на диску ↔ процес у пам'яті

Файл програми (/bin/sleep)
  • Пасивні байти: код, таблиці символів, метадані ELF
  • Нічого не робить, доки лежить на диску
  • Ідентифікатор — шлях у файловій системі
  • Один файл → скільки завгодно процесів
execve()
Процес
  • Активне виконання, яке ядро супроводжує
  • PID, адресний простір, дескриптори, UID/GID
  • Живе в ілюзії «я сам на машині»
  • Народжується через execve(), завершується з кодом

Ілюзію «я сам на машині» підтримує ядро: планувальник і MMU

З чого складається процес у ядрі (task_struct)

PID і станrunning · sleeping · stopped · zombie
Батько (PPID)дерево процесів від PID 1
Адресний простірtext · data · heap · stack; таблиця сторінок
Дескриптори0 stdin · 1 stdout · 2 stderr · далі файли, сокети
Облікові даніUID, GID, capabilities — що дозволено
Простори іменякий «світ» бачить процес → Лекція 2

Усе це видно через каталог /proc/<pid>/

Процес під мікроскопом: /proc

bash
sleep 300 &
PID=$!                # напр. 3101
cat /proc/$PID/status | head -5
cat /proc/$PID/maps | head -5
ls /proc/$PID/fd

/proc — файлова проекція внутрішнього стану ядра для кожного процесу

/proc/<pid>/ — що де лежить

ФайлЩо показуєЧастина task_struct
statusстан, PPID, пам'ять, UID/GIDстан, батько, облікові дані
mapsрегіони адресного простору і праватаблиця сторінок (mm)
fd/відкриті дескриптори як символьні посиланнятаблиця файлів
cmdlineаргументи запускуобраз програми
ns/простори імен процесу→ Лекція 2
cgroupгрупа керування ресурсами→ Лекція 2

Файли генеруються ядром «на льоту» при читанні — це не диск

/proc/<pid>/maps докладно

bash
cat /proc/$PID/maps
# приклад рядка:
# 55f2a1000000-55f2a1001000 r--p 00000000 08:01 123 /bin/sleep
# 7f9c4b000000-7f9c4b028000 r-xp 00000000 08:01 456 /lib/x86_64-linux-gnu/libc.so.6

Кожен рядок — один регіон адресного простору: адреси, права rwx, файл-джерело

Народження процесу: fork → execve → wait

  1. 1
    fork() / clone()копія батька; дитина отримує власний PID
  2. 2
    execve() у дитиніобраз пам'яті замінено новою програмою; PID той самий
  3. 3
    wait4() у батькаshell блокується, доки дитина працює
  4. 4
    exit() дитиникод завершення повертається батькові
  5. 5
    Zombie → прибранопісля wait ядро звільняє task_struct

Так shell запускає кожну команду: ls, cat, docker

Хто є хто після clone() і execve()

НАРОДЖЕННЯ ПРОЦЕСУ: CLONE → EXECVE → WAIT БАТЬКО · PID 4021 bash PID 4021 bash wait4() батько живий увесь час ДИТИНА · PID 4022 bash (копія) той самий код /bin/ls образ підмінено clone() execve() exit(0) clone: copy-on-write — сторінки спільні, поки ніхто не пише · execve: PID той самий, новий лише вміст clone — копіяexecve — підмінаwait4 — код

На демо покажіть: PID дитини той самий до і після execve — змінився лише вміст пам'яті

fork() ↔ execve()

fork()
  • Створює НОВИЙ процес — копію поточного
  • Повертає двічі: 0 дитині, PID батькові
  • Пам'ять спільна через copy-on-write
  • Код лишається той самий
+
execve()
  • Процес той самий — PID не змінюється
  • Повертає лише при помилці
  • Пам'ять повністю замінена новим образом
  • Код — уже інша програма

Два незалежні кроки замість одного «запустити програму» — дизайн Unix

clone() і copy-on-write

strace: fork, execve наживо

bash
strace -f -e trace=fork,vfork,clone,execve,wait4 bash -c "ls > /dev/null"

Видно послідовність clone() (нова дитина) → execve() (заміна образу) → wait4() (батько чекає)

Системний виклик (*syscall*) — єдина легальна межа

Кільця захисту (Ring 0 / Ring 3)

USER SPACE · кільце 3 ваша програма read() write() KERNEL SPACE · кільце 0 ядро драйвери, ФС syscall єдина межа кожен доступ до заліза проходить через syscall-шлюз

Межа в цифрах

450+
системних викликів
ядро Linux на x86-64
2
кільця, що реально використовує Linux
Ring 0 · Ring 3
1
інструкція переходу
syscall — і назад sysret
0
прямих доступів до заліза з Ring 3
диск, мережа, MMU — лише через ядро

libc ховає syscalls за printf, malloc, fopen — але знизу завжди вони

Побачити syscalls наживо

bash
strace -c ls > /dev/null
# short summary: скільки викликів read/write/openat/execve...

strace показує реальний діалог програми з ядром

Живе демо: анатомія процесу

  1. Запустити sleep 300 & і зберегти PID
  2. Показати процес у ps aux | grep sleep
  3. cat /proc/$PID/status — стан, пам'ять, батько
  4. cat /proc/$PID/maps — сегменти адресного простору
  5. ls /proc/$PID/fd — відкриті дескриптори

Очікувано: Студенти бачать: процес у ps — це рядок, а в /proc — цілий каталог зі структурою пам'яті й дескрипторів

Адресний простір процесу

ВІРТУАЛЬНИЙ АДРЕСНИЙ ПРОСТІР Ядро (недоступне)Стек ↓⋮ вільно ⋮Купа ↑ (heap)BSS / DataText (код) 0x7fff… 0x0000…

Сегменти адресного простору

СегментВмістПраваРосте
textмашинний код програмиr-x
data / bssглобальні змінніrw-
heapmalloc / new; brk, mmaprw-↑ вгору
shared libslibc.so та інші, через mmapr-x
stackлокальні змінні, адреси поверненняrw-↓ вниз

Ті самі регіони — рядки у /proc/<pid>/maps; права видно в другій колонці

Чому Docker — перша межа курсу

Мета абстракції — не бути розпливчастим, а створити новий семантичний рівень, на якому можна бути абсолютно точним. — Едсгер Дейкстра, «The Humble Programmer» (1972)

Наперед: перший контейнер

bash
docker run --rm -it ubuntu:24.04 ps aux
# у контейнері PID 1 — це наш процес, а не системний init хоста

Анонс Л2: той самий процес, інший PID-простір

Підводні камені: процес і syscalls

Підсумок

Джерела

🥚 ПАСХАЛКА
У прикладах /proc фоновий процес має PID 3101 — шістнадцятково 0xC1D. Це не випадковість: таке число трапиться в курсі ще не раз.
⌨️ КОМАНДИ НА ЦІЙ СТОРІНЦІ 13 · 9 нових

Кожна команда терміналу з цієї сторінки — одним реченням. Позначка нове — команда зустрічається в курсі вперше; далі вважаємо її знайомою.

sleepнове
Чекає вказану кількість секунд (sleep infinity — назавжди; зручно, щоб контейнер «жив», поки ми його вивчаємо).
cat
Виводить вміст файлу в термінал повністю (від concatenate); зручно для коротких файлів і /proc/....
head
Показує перші рядки файлу або потоку (head -5 — перші п'ять).
lsнове
Показує список файлів у каталозі; ls -la — з правами, власником, розміром і прихованими файлами.
docker run
Створює й запускає контейнер з образу (docker run -it ubuntu:24.04 bash — інтерактивно з терміналом; -d — у фоні; --rm — видалити після виходу; -p 8080:80 — прокинути порт).
docker
Клієнт Docker: усі дії з образами й контейнерами — через його підкоманди (docker <підкоманда> --help — довідка).
psнове
Список процесів; ps aux — усі процеси з користувачем, PID і командою (у контейнері видно лише його власні).
straceнове
Показує системні виклики, які програма робить до ядра (strace -f -e trace=execve bash -c ls).
echoнове
Друкує рядок або значення змінної (наприклад echo $$ — PID поточної оболонки).
grepнове
Фільтрує текст: виводить лише рядки, що містять шаблон (ps aux | grep bash).
unshareнове
Запускає програму в нових просторах імен (namespaces) ядра — те, з чого «зшитий» контейнер (unshare --pid --fork --mount-proc bash).
python3нове
Інтерпретатор Python; тут — python3 -m http.server 80, найкоротший спосіб підняти тестовий HTTP-сервер.
printfнове
Форматований вивід, як у C; зручно записати у файл точні байти (printf '\xff' > bad.bin).

💡 Прочитати README прямо в терміналі: cat README.md (виведе весь файл) або less README.md (посторінково; вихід — клавіша q).
Довідка з будь-якої команди: man curl (повний посібник, вихід — q) або коротко curl --help; для підкоманд — docker run --help, cargo build --help.