ЛЕКЦІЯ 2
ПРОСТОРИ ІМЕН, ГРУПИ КЕРУВАННЯ (CGROUPS), ПРИВІЛЕЇ
Ізоляція ядром без віртуалізації заліза
ВТФК · Системне програмування · 2026
План лекції
- Контейнер — це процес, а не машина
- Простори імен (namespaces): що ізолює кожен
- clone / unshare / setns — три системні виклики
- cgroups v2: обмеження ресурсів
- Привілеї: capabilities і user namespace
- Як це збирається у Docker
Фокус: ізоляція ядром
Віртуальна машина ↔ контейнер
Віртуальна машина
- Гіпервізор віртуалізує залізо
- Окреме ядро гостьової ОС
- Старт — секунди, образ — гігабайти
- Ізоляція — на межі заліза
vs
Контейнер
- Звичайний процес на ядрі хоста
- Ядро одне — спільне з хостом
- Старт — мілісекунди, образ — мегабайти
- Ізоляція — namespaces + cgroups + capabilities
Контейнеру змінили «видимість світу», а не дали окрему машину
Простори імен (*namespaces*): що ізолює кожен
PIDвласна нумерація процесів; усередині — PID 1
NETвласний мережевий стек: інтерфейси, IP, маршрути
MNTвласне дерево точок монтування
UTSвласне ім'я хоста (hostname)
IPCчерги повідомлень, семафори, спільна пам'ять System V
USERвідображення UID: root усередині ≠ root на хості
Кожен простір — окрема «версія» глобального ресурсу ядра
Сьомий простір і успадкування
- CGROUP namespace ізолює видимість власної ієрархії cgroup
- Процес бачить своє піддерево як корінь /sys/fs/cgroup
- Docker за замовчуванням: PID, NET, MNT, UTS, IPC, CGROUP; USER — з --userns-remap
- Є ще TIME (ядро 5.6+); новий процес успадковує простори батька
Три системні виклики для просторів імен
docker exec — це setns() у простори вже запущеного контейнера, не новий clone()
unshare: власний PID-простір
bash
sudo unshare --pid --fork --mount-proc bash
# усередині:
ps aux
echo $$
Той самий /bin/bash, але echo $$ покаже PID 1 — процеси хоста невидимі
Що робить unshare --pid --fork --mount-proc bash
- 1
unshare(CLONE_NEWPID)поточний процес відв'язано у новий PID-простір
- 2
fork()перша дитина в новому просторі стає PID 1
- 3
mount -t procновий /proc, інакше ps читав би /proc хоста
- 4
execve(bash)той самий бінарник — інша картина світу
- 5
echo $$ → 1процеси хоста невидимі; exit повертає назад
Без --mount-proc ps показує «чужі» процеси — типова помилка
Живе демо: unshare проти хоста
- На хості: ps aux | wc -l (десятки процесів)
- sudo unshare --pid --fork --mount-proc bash
- Усередині: ps aux — видно тільки bash і ps
- echo $$ — PID дорівнює 1
- exit — повернення в звичайний простір, хост незмінний
Очікувано: Один і той самий бінарник bash поводиться по-різному залежно від простору імен, у якому він запущений
setns(): як docker exec заходить у контейнер
bash
PID=$(docker inspect -f '{{.State.Pid}}' b7)
ls -l /proc/$PID/ns/
# pid -> pid:[4026532xxx] net -> net:[4026532yyy] ...
nsenter --target $PID --pid --net --mount ps aux
nsenter обгортає setns() — заходить у namespaces процесу за його PID
Простори імен ↔ групи керування
namespaces
- Що процес БАЧИТЬ
- Власні PID, мережа, ФС, hostname
- Не обмежують кількість ресурсів
- Механізм: clone / unshare / setns
+
cgroups
- Скільки процес МОЖЕ спожити
- CPU, пам'ять, I/O, кількість процесів
- Не ховають нічого від процесу
- Механізм: файли у /sys/fs/cgroup
Без cgroups ізольований процес міг би з'їсти всю пам'ять хоста
cgroups v2 — контроль апетиту
- Групи керування (*cgroups*) обмежують і обліковують CPU, пам'ять, I/O, процеси
- Ієрархія у /sys/fs/cgroup/: група — каталог, ліміт — файл (memory.max, cpu.max)
- cgroups v2 — єдина уніфікована ієрархія; v1 мала окрему на кожен контролер
- Docker записує --memory / --cpus у ці файли перед execve()
Контролери cgroups v2
Значення — звичайний текст; ліміт змінюється echo у файл
cgroups наживо
bash
docker run -d --name b7 --memory=100m --cpus=0.5 nginx
cat /sys/fs/cgroup/system.slice/docker-*.scope/memory.max
docker stats --no-stream
docker run --memory / --cpus транслюється у файли cgroups v2
cgroups v2: файли лімітів зсередини
bash
cat /sys/fs/cgroup/.../memory.max # 104857600 (100 МБ)
cat /sys/fs/cgroup/.../cpu.max # 50000 100000 (0.5 CPU)
cat /sys/fs/cgroup/.../pids.max # захист від fork-бомб
cpu.max записаний як "quota period" у мікросекундах: 50000/100000 = половина ядра
Привілеї: root у контейнері ≠ root на хості
- Linux capabilities дроблять «суперправа root» на окремі дозволи
- Docker за замовчуванням відкидає більшість capabilities навіть для root
- USER namespace відображає UID 0 контейнера на непривілейований UID хоста
- Два незалежні механізми — подвійний захист при втечі з ізоляції
USER-простір: чому root у контейнері небезпечний
Docker не вмикає USER-простір за замовчуванням — це і є та «сьома брама», що лишається відчиненою
Capabilities: приклади
--cap-add / --cap-drop — точково; --privileged — вимикає майже все одразу
Привілеї в цифрах
~40
capabilities у ядрі Linux
замість бінарного root / не-root
14
лишає Docker за замовчуванням
CAP_SYS_ADMIN, CAP_NET_ADMIN — відкинуті
0 → 100000
UID при userns-remap
root усередині — звичайний користувач зовні
1
прапорець --privileged
усі caps + вимкнений seccomp + пристрої хоста
Разом: з чого складається "контейнер"
- namespaces — що процес БАЧИТЬ (ізоляція видимості)
- cgroups — скільки процес МОЖЕ споживати (ізоляція ресурсів)
- capabilities — що процес МАЄ ПРАВО робити (ізоляція привілеїв)
- Docker — це зручна обгортка над цими трьома примітивами ядра Linux
Як docker run збирає контейнер
docker runCLI → dockerd
→containerd → runcOCI-рантайм
→clone(flags)нові простори імен
→cgroups + capsзапис лімітів, скидання прав
→execve()процес контейнера, PID 1
Три механізми незалежні й не знають один про одного — комбінує їх runc
Кожна програма і кожен користувач системи мають працювати з найменшим набором привілеїв, необхідним для виконання роботи.
— Джером Зальцер, Майкл Шредер — принцип найменших привілеїв (1975)
Наперед: з чого складається образ
- Сьогодні — про запущений контейнер (runtime-ізоляція)
- Наступного разу — про сам образ: шари, Dockerfile, кеш, багатоетапна збірка
- Питання на подив: чому образ ubuntu:24.04 важить ~80 МБ, а не гігабайти?
Підводні камені: namespaces і привілеї
- unshare --pid без --mount-proc лишає старий /proc — ps показує «чужі» процеси
- --privileged вимикає майже всю ізоляцію, а не додає одну можливість
- root без user namespace — реальний root на хості при втечі з ізоляції
- cgroups v1 і v2 мають різні шляхи й формат файлів
Підсумок
- Контейнер = namespaces (видимість) + cgroups (ресурси) + capabilities (привілеї)
- unshare показує: та сама програма поводиться по-різному в іншому просторі імен
- root у контейнері — не root на хості, якщо налаштовано user namespace
- Наступна лекція: як усе це пакується в образ
🥚 У прикладах контейнер названо b7, а процес усередині нього завжди бачить себе як PID 1. Двосимвольне ім'я обране не випадково — воно ще трапиться.