2.1. Процессы и containers
Цели
После этого материала вы сможете:
- объяснить, что такое process в Linux и какими атрибутами он обладает;
- сформулировать, что container — это процесс host-системы, а не отдельная машина;
- найти процесс container в списке процессов host и объяснить, почему PID различаются;
- объяснить, что именно ядро подменяет процессу, чтобы получился container;
- назвать ресурсы, которые container разделяет с host, и те, что изолирует;
- доказать командами, что kernel у host и container общий.
Предварительные знания
- Раздел 01. Подготовка среды — работающий Docker;
- базовое понимание процессов: PID, родительский процесс,
ps,kill; - умение читать вывод команд в терминале.
Ключевые термины
| Термин | Объяснение |
|---|---|
process | Выполняющаяся программа со своим адресным пространством, дескрипторами файлов и контекстом выполнения |
PID | Process ID — числовой идентификатор процесса, уникальный в пределах своего PID namespace |
PPID | Parent PID — идентификатор родительского процесса |
namespace | Механизм ядра, изолирующий представление процесса об одном из видов системных ресурсов |
cgroup | Механизм ядра, ограничивающий потребление ресурсов группой процессов |
rootfs | Корневая файловая система, которую процесс видит как / |
kernel | Ядро операционной системы. У host и всех его containers оно одно |
Теория
Что такое process
Процесс в Linux — это выполняющаяся программа плюс всё, что ядро хранит о ней. У процесса есть:
| Атрибут | Что это |
|---|---|
| PID | Номер процесса |
| PPID | Номер родителя |
| UID / GID | От чьего имени выполняется |
| Рабочий каталог | Где он «находится» в файловой системе |
| Корневой каталог | Что процесс считает корнем / |
| Дескрипторы файлов | Открытые файлы, сокеты, каналы |
| Адресное пространство | Выделенная память |
| Переменные окружения | PATH, HOME и остальные |
| Capabilities | Какие привилегированные операции разрешены |
Всё это ядро хранит в структурах, доступных через файловую систему /proc. Каталог /proc/<pid>/ — это окно в состояние процесса.
Ключевое наблюдение: большинство атрибутов процесса — не свойства программы, а свойства записи в ядре. Программа не решает, какой у неё PID; ядро назначает его. Программа не решает, что считать корнем файловой системы; ядро сообщает ей это.
Отсюда следует главная идея контейнеризации.
Идея контейнеризации
Если ядро само сообщает процессу, что он видит, то ядро может сообщить другое.
Container — это обычный процесс, которому ядро подменило ответы на несколько вопросов:
| Вопрос процесса | Обычный ответ | Ответ для container |
|---|---|---|
| Какие процессы есть в системе? | Все процессы host | Только процессы этого container |
| Какой у меня PID? | Настоящий, например 184213 | 1 |
Что является корнем /? | Корень host | Распакованные слои образа |
| Какие сетевые интерфейсы есть? | Интерфейсы host | Собственный eth0 и lo |
| Как называется хост? | my-laptop | ID container |
| Сколько памяти доступно? | Вся память машины | Столько, сколько задано лимитом |
Механизмы, реализующие эту подмену:
- namespaces — изолируют то, что процесс видит (урок 2.3);
- cgroups — ограничивают то, что процесс потребляет (урок 2.4);
- capabilities — ограничивают то, что процессу разрешено (урок 2.5);
- смена корня — определяет, какую файловую систему процесс видит как
/.
Никакой отдельной сущности «container» в ядре Linux нет. Есть процесс с определённым набором namespaces, помещённый в определённую cgroup, с определённым корневым каталогом и ограниченным набором capabilities. «Container» — это термин уровня инструментов, а не уровня ядра.
Что это означает практически
Из «container — это процесс» следуют выводы, объясняющие большую часть поведения Docker.
1. Container живёт, пока живёт его главный процесс. Не «пока не выключим», а именно пока процесс не завершится. Отсюда — «container сразу упал» для команд, которые просто отработали и вышли.
2. Запуск container стоит примерно столько же, сколько запуск процесса. Миллисекунды. Никакой загрузки операционной системы не происходит, потому что операционная система уже загружена — это ваш host.
3. Kernel общий. Container не может использовать другую версию ядра, загрузить свой модуль или изменить системные параметры (без специальных привилегий). Всё, что делает ядро — оно делает одинаково для host и для containers.
4. Изоляция ограничена возможностями ядра. Если в ядре есть ошибка, она есть и для containers. Это фундаментальное отличие от виртуальных машин и основание для раздела 12.
5. Обычные инструменты Linux применимы. ps, top, strace, lsof на host видят процессы containers. Это основа диагностики в разделе 13.
Внутренний механизм
Что происходит при запуске container
Упрощённая последовательность (полная — в уроке 2.6):
- Собирается корневая файловая система: слои образа объединяются в единое дерево.
- Создаются namespaces нужных типов.
- Создаётся cgroup и в неё записываются лимиты.
- Вызывается
clone()илиunshare()— порождается процесс, помещённый в созданные namespaces. - В новом процессе выполняется
pivot_root(): корнем становится подготовленное дерево. - Отбираются лишние capabilities, применяется профиль seccomp.
- Выполняется
execve()— образ процесса заменяется на программу из container.
Шаг 7 важен: процесс не порождает приложение как дочернее, а становится им. Поэтому приложение получает PID 1 внутри своего namespace — со всеми последствиями, которые разбираются в разделе 04.
Два PID у одного процесса
После создания PID namespace процесс имеет два номера одновременно:
- PID внутри своего namespace (обычно 1 для главного процесса container);
- PID в namespace host (обычное большое число).
Это не копия и не отображение — это один процесс, у которого ядро хранит идентификатор в каждом namespace, к которому он принадлежит. PID namespaces вложены: host видит все процессы, container — только свои.
PID namespace host
┌────────────────────────────────────────────┐
│ PID 1 systemd │
│ PID 1842 dockerd │
│ PID 184213 sleep 3600 ◄─────┐ │
│ │ один и │
│ PID namespace container │ тот же │
│ ┌───────────────────────────┼────────┐ │
│ │ PID 1 sleep 3600 ◄──┘ │ │
│ └────────────────────────────────────┘ │
└────────────────────────────────────────────┘
Практическое следствие: kill 184213 на host и kill 1 внутри container адресуют один процесс. Но результат различается — PID 1 внутри namespace имеет особый статус, о чём в разделе 04.
Команды и примеры
Процесс на host
Запустим долгоживущий процесс обычным способом:
sleep 3600 &
[1] 187422
Число — PID процесса. Посмотрим его:
ps -o pid,ppid,user,comm -p 187422
PID PPID USER COMMAND
187422 186930 evg sleep
Что видно: PID, родитель (ваша оболочка), пользователь, имя команды.
Завершим:
kill 187422
Тот же процесс в container
docker run -d --name proc-demo alpine sleep 3600
c8f4a1b2d3e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1
Что видит процесс внутри container:
docker exec proc-demo ps -o pid,ppid,user,comm
PID PPID USER COMMAND
1 0 root sleep
7 0 root ps
Обратите внимание: sleep имеет PID 1, а список содержит только процессы container. Всей остальной системы для него не существует.
Теперь найдём тот же процесс на host:
HOST_PID="$(docker inspect proc-demo --format '{{.State.Pid}}')"
echo "PID на host: $HOST_PID"
ps -o pid,ppid,user,comm -p "$HOST_PID"
PID на host: 188104
PID PPID USER COMMAND
188104 188082 root sleep
Один процесс, два номера. Внутри container — 1, снаружи — 188104. Это ключевая демонстрация всего урока.
Убедимся, что это действительно один процесс, а не два разных:
docker exec proc-demo cat /proc/1/cmdline | tr '\0' ' '; echo
cat "/proc/$HOST_PID/cmdline" | tr '\0' ' '; echo
sleep 3600
sleep 3600
Команда tr '\0' ' ' заменяет нулевые байты пробелами: в /proc/<pid>/cmdline аргументы разделены нулями.
Проверим, что процесс на host действительно принадлежит нашему container:
cat "/proc/$HOST_PID/cgroup"
0::/system.slice/docker-c8f4a1b2d3e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1.scope
В пути cgroup видно полный ID container. Связь однозначна.
Дерево процессов
ps -eo pid,ppid,comm --forest | grep -A2 -B2 'sleep' | head -20
Или нагляднее:
pstree -p "$HOST_PID" -s
systemd(1)───containerd-shim(188082)───sleep(188104)
Родитель нашего sleep — не dockerd, а containerd-shim. Почему именно так, разбирается в уроке 2.6 и подробно в разделе 17.
Общий kernel
uname -r
docker exec proc-demo uname -r
6.8.0-88-generic
6.8.0-88-generic
Одинаково — и не может быть иначе. Container не содержит ядра и не может его содержать: он использует ядро host.
Проверим на образе другого дистрибутива:
docker run --rm ubuntu:24.04 uname -r
docker run --rm alpine uname -r
Оба вернут ядро вашего host, а не «ядро Ubuntu» и «ядро Alpine». Образ содержит пользовательское окружение — библиотеки, утилиты, файловую структуру дистрибутива, — но не ядро.
Это объясняет, почему на Linux с ядром 6.8 работают и Ubuntu-образы, и Alpine-образы, и Debian-образы одновременно: им всем нужно ядро Linux, а какое именно — определяет host.
Что разделяется, а что изолируется
Проверим по пунктам.
Разделяется: kernel.
docker exec proc-demo uname -r # то же, что на host
Разделяется: время системы.
date +%s
docker exec proc-demo date +%s
Значения совпадут (с точностью до секунды выполнения). Time namespace существует, но Docker его по умолчанию не использует для смещения часов.
Разделяется: информация о CPU и памяти в /proc.
nproc
docker exec proc-demo nproc
8
8
Container видит все CPU host, даже если ему выставлен лимит. Это ровно та проблема, из-за которой Gunicorn с автоматическим расчётом worker-процессов создаёт их по числу CPU машины, а не по выделенной доле. Разбирается в разделе 06.
Изолируется: список процессов.
ps -e | wc -l
docker exec proc-demo sh -c 'ps -e | wc -l'
412
3
Изолируется: файловая система.
ls /
docker exec proc-demo ls /
Разные деревья. У container — содержимое образа Alpine.
Изолируется: имя хоста.
hostname
docker exec proc-demo hostname
my-laptop
c8f4a1b2d3e5
По умолчанию именем хоста в container становится сокращённый ID.
Изолируется: сеть.
ip -o addr show | awk '{print $2, $4}'
docker exec proc-demo ip -o addr show | awk '{print $2, $4}'
lo 127.0.0.1/8
wlp0s20f3 192.168.1.14/24
docker0 172.17.0.1/16
---
lo 127.0.0.1/8
eth0 172.17.0.2/16
У container собственный сетевой стек. Подробно — в разделе 08.
Уборка
docker rm -f proc-demo
Практическое упражнение
Задание. Напишите скрипт compare-process.sh, который наглядно демонстрирует, что container — это процесс host.
Скрипт должен:
- Запустить container с долгоживущим процессом.
- Показать PID процесса внутри container и на host.
- Доказать, что это один и тот же процесс (сравнением командной строки и через cgroup).
- Показать родителя процесса на host.
- Сравнить версию ядра внутри и снаружи.
- Сравнить количество видимых процессов.
- Удалить container за собой.
Скрипт должен работать при любом имени container и не оставлять мусора даже при ошибке.
Подсказки
Подсказка 1
PID процесса на host извлекается так:
docker inspect <name> --format '{{.State.Pid}}'
Подсказка 2
Чтобы гарантировать уборку даже при ошибке, используйте trap:
trap 'docker rm -f "$NAME" >/dev/null 2>&1 || true' EXIT
Подсказка 3
Связь процесса с container видна в /proc/<pid>/cgroup — там присутствует полный ID container. Сравните его с выводом docker inspect --format '{{.Id}}'.
Решение
Сначала выполните задание самостоятельно.
Показать решение
#!/usr/bin/env bash
# compare-process.sh — демонстрация того, что container является процессом host.
set -euo pipefail
NAME="proc-compare-$$"
trap 'docker rm -f "$NAME" >/dev/null 2>&1 || true' EXIT
echo "1. Запуск container"
docker run -d --name "$NAME" alpine sleep 3600 > /dev/null
CID="$(docker inspect "$NAME" --format '{{.Id}}')"
HOST_PID="$(docker inspect "$NAME" --format '{{.State.Pid}}')"
echo " container: ${CID:0:12}"
echo
echo "2. PID процесса"
IN_PID="$(docker exec "$NAME" sh -c 'echo $$; ' >/dev/null; docker exec "$NAME" ps -o pid,comm | awk '$2=="sleep"{print $1}')"
printf ' внутри container: %s\n' "$IN_PID"
printf ' на host: %s\n' "$HOST_PID"
echo
echo "3. Это один процесс?"
in_cmd="$(docker exec "$NAME" tr '\0' ' ' < /proc/1/cmdline 2>/dev/null || docker exec "$NAME" sh -c "tr '\\0' ' ' < /proc/1/cmdline")"
host_cmd="$(tr '\0' ' ' < "/proc/$HOST_PID/cmdline")"
printf ' cmdline внутри: %s\n' "$in_cmd"
printf ' cmdline на host: %s\n' "$host_cmd"
if grep -q "$CID" "/proc/$HOST_PID/cgroup"; then
echo " [+] /proc/$HOST_PID/cgroup содержит ID container — связь подтверждена"
else
echo " [!] связь через cgroup не подтверждена"
fi
echo
echo "4. Родитель процесса на host"
ppid="$(awk '{print $4}' "/proc/$HOST_PID/stat")"
printf ' PPID %s: %s\n' "$ppid" "$(tr -d '\0' < "/proc/$ppid/comm" 2>/dev/null || echo '?')"
echo
echo "5. Версия ядра"
printf ' host: %s\n' "$(uname -r)"
printf ' container: %s\n' "$(docker exec "$NAME" uname -r)"
echo
echo "6. Видимых процессов"
printf ' host: %s\n' "$(ps -e --no-headers | wc -l)"
printf ' container: %s\n' "$(docker exec "$NAME" sh -c 'ps -e --no-headers 2>/dev/null | wc -l')"
echo
echo "Вывод: один и тот же процесс имеет PID 1 внутри своего namespace"
echo "и обычный PID на host. Ядро общее. Изолирован список процессов."
Запуск:
chmod +x compare-process.sh
./compare-process.sh
Ожидаемый вывод:
1. Запуск container
container: c8f4a1b2d3e5
2. PID процесса
внутри container: 1
на host: 188104
3. Это один процесс?
cmdline внутри: sleep 3600
cmdline на host: sleep 3600
[+] /proc/188104/cgroup содержит ID container — связь подтверждена
4. Родитель процесса на host
PPID 188082: containerd-shim
5. Версия ядра
host: 6.8.0-88-generic
container: 6.8.0-88-generic
6. Видимых процессов
host: 412
container: 2
trap ... EXIT гарантирует удаление container, даже если скрипт прервётся посередине.
Проверка результата
Убедитесь, что можете ответить на три вопроса про свой вывод:
- Почему PID внутри и снаружи различаются, хотя процесс один?
- Почему версии ядра одинаковы, хотя образ Alpine, а host — Ubuntu?
- Почему родителем процесса на host является
containerd-shim, а неdockerd?
Первые два ответа есть в этом уроке, третий — в уроке 2.6.
Типичные ошибки
| Ошибка | Причина | Исправление |
|---|---|---|
| «Container — это лёгкая виртуальная машина» | Аналогия удобна, но ведёт к неверным ожиданиям об изоляции и безопасности | Container — процесс. Разница подробно разбирается в уроке 2.2 |
| Ожидание, что в container можно загрузить модуль ядра | Ядро общее, и модули загружаются на уровне host | Загружать модуль на host; container им воспользуется |
Расчёт worker-процессов по nproc внутри container | Container видит все CPU host, а не свой лимит | Читать лимит из cgroup — см. раздел 06 |
Поиск процессов container в ps внутри самого container при диагностике зависаний | Внутри виден только собственный список | Смотреть с host: docker top или ps по PID из docker inspect |
| Попытка «выключить» container вместо завершения процесса | Container не имеет отдельного состояния «выключен» | Завершается главный процесс — завершается container |
| Ожидание, что образ Alpine даст «ядро Alpine» | Образ содержит пользовательское окружение, но не ядро | uname -r всегда покажет ядро host |
Контрольные вопросы
На понимание:
- Почему в ядре Linux нет объекта «container»?
- Почему главный процесс container обычно имеет PID 1?
- Почему запуск container занимает миллисекунды, а запуск виртуальной машины — секунды?
- Что означает утверждение «kernel разделяется» и какие практические последствия у него есть?
- Почему
nprocвнутри container может показать больше CPU, чем ему выделено?
На применение:
- Как найти PID процесса container на host?
- Как убедиться, что конкретный процесс на host принадлежит конкретному container?
- Как посмотреть командную строку главного процесса container, не заходя внутрь?
На диагностику:
- Container «завис» и не отвечает на
docker exec. Как посмотреть, что делает его процесс, средствами host? - Приложение внутри container сообщает о 16 доступных CPU, хотя запущено с
--cpus=2. Объясните причину.
Краткое резюме
- Процесс в Linux — это программа плюс запись о ней в ядре; большинство «свойств» процесса задаёт ядро.
- Container — обычный процесс, которому ядро подменило представление о системе.
- Подмена реализуется namespaces (что видно), cgroups (сколько можно), capabilities (что разрешено) и сменой корня.
- Отдельной сущности «container» в ядре нет — это термин уровня инструментов.
- Один процесс имеет PID 1 внутри своего namespace и обычный PID на host.
- Kernel у host и всех containers общий; образ не содержит ядра.
- Container живёт ровно столько, сколько живёт его главный процесс.
- Изоляция ограничена возможностями ядра — это основание для отдельного разговора о безопасности.
- Часть информации (
/proc/cpuinfo,/proc/meminfo) не изолируется и отражает host. - Обычные инструменты Linux на host видят процессы containers — это база диагностики.
Официальные источники
| Источник | Ссылка | Что подтверждает |
|---|---|---|
| Docker overview | https://docs.docker.com/get-started/docker-overview/ | Определение container и его отличие от VM |
namespaces(7) man page | https://man7.org/linux/man-pages/man7/namespaces.7.html | Механизм namespaces и их вложенность |
pid_namespaces(7) man page | https://man7.org/linux/man-pages/man7/pid_namespaces.7.html | Два PID у одного процесса, особый статус PID 1 |
proc(5) man page | https://man7.org/linux/man-pages/man5/proc.5.html | Содержимое /proc/<pid>/: cmdline, cgroup, stat |
clone(2) man page | https://man7.org/linux/man-pages/man2/clone.2.html | Флаги создания namespaces при порождении процесса |
| docker inspect reference | https://docs.docker.com/reference/cli/docker/inspect/ | Поле .State.Pid и шаблоны --format |
| docker top reference | https://docs.docker.com/reference/cli/docker/container/top/ | Просмотр процессов container с точки зрения host |
Навигация
Вернуться к разделу
Следующий материал → Containers против virtual machines
Главное оглавление