namespaces: границы видимости
namespaces: как ядро прячет от процесса остальную систему.
Контейнер глазами хоста
Со стороны хостовой ОС в контейнере нет ничего особенного. Запусти контейнер и посмотри процессы на хосте: там будет обычный процесс с обычным PID, рядом с sshd и остальными.
$ docker run -d --name web nginx
$ ps aux | grep nginx
root 28714 0.0 0.1 10620 6100 Ss 11:02 0:00 nginx: master process
Тот же процесс изнутри контейнера видит себя под номером 1. Один и тот же процесс, два разных взгляда на него. Разницу создаёт namespace.
Что такое namespace
Namespace — это механизм ядра (kernel), который ограничивает, какую часть системы видит процесс. Ядро одно на всю машину, но оно может показать процессу урезанную картину: только его процессы, только его сетевые интерфейсы, только его дерево каталогов.
Процессы в одном namespace видят одни и те же ресурсы. Процесс в другом namespace того же типа этих ресурсов не видит вовсе. Проверить, в каких namespaces сидит процесс, помогает lsns.
$ lsns -p 1
NS TYPE NPROCS PID USER COMMAND
4026531835 cgroup 142 1 root /sbin/init
4026531836 pid 142 1 root /sbin/init
4026531840 net 140 1 root /sbin/init
Каждая строка — отдельный namespace со своим числовым идентификатором. Два процесса с одинаковым идентификатором net сидят в одной сетевой среде.
Типы namespaces
Каждый тип отвечает за свой класс ресурсов.
| Тип | Что изолирует |
|---|---|
| PID | номера процессов; первый процесс внутри получает PID 1 |
| Network | сетевые интерфейсы, адреса, порты, таблицы маршрутизации |
| Mount | дерево смонтированных каталогов |
| UTS | hostname и доменное имя |
| IPC | разделяемую память и очереди сообщений |
| User | маппинг uid и gid |
| cgroup | видимость собственной иерархии cgroup |
PID namespace объясняет фокус с первым процессом. Внутри своего PID namespace процесс контейнера видит нумерацию с единицы, а снаружи у него номер вроде 28714.
$ sudo unshare --pid --fork --mount-proc bash
# ps -e
PID TTY TIME CMD
1 pts/0 00:00:00 bash
2 pts/0 00:00:00 ps
User namespace даёт особенно наглядный эффект. Внутри процесс может быть root с uid 0, а на хосте тот же процесс отображается на обычного непривилегированного пользователя. Так контейнерный root не получает власти над всей машиной.
$ id # на хосте
uid=1000(deploy) gid=1000(deploy)
$ unshare --user --map-root-user id # внутри нового user namespace
uid=0(root) gid=0(root)
На Ubuntu 23.10 и 24.04 эта команда без sudo падает с Permission denied:
непривилегированные user namespaces там ограничены профилями AppArmor.
Проверить и обойти:
$ sysctl kernel.apparmor_restrict_unprivileged_userns
kernel.apparmor_restrict_unprivileged_userns = 1
$ sudo unshare --user --map-root-user id
uid=0(root) gid=0(root)
Единица означает, что ограничение включено. Через sudo команда отработает на любом дистрибутиве, так что в примерах ниже проще сразу писать sudo.
Кто создаёт namespaces
Namespaces создаются системными вызовами clone и unshare. clone порождает новый процесс сразу в новых namespaces, unshare отделяет уже работающий процесс от текущих namespaces. Инструмент командной строки unshare даёт к этому доступ без написания кода.
sudo unshare --net bash # новый сетевой namespace: свои интерфейсы
sudo unshare --uts bash # свой hostname, меняй hostname без вреда хосту
Сетевой и UTS namespace сами по себе требуют прав root, поэтому здесь sudo
обязателен. Без него их можно создать только вместе с --user, а этот путь
на Ubuntu 23.10 и 24.04 закрыт тем же ограничением AppArmor.
Внутри такого сетевого namespace список интерфейсов пуст, кроме локального loopback:
# ip link
1: lo: <LOOPBACK> mtu 65536 ...
Docker и другие среды исполнения контейнеров делают ровно это под капотом: создают набор namespaces через clone и запускают внутри процесс приложения.
Частая ошибка: «контейнер это лёгкая виртуальная машина»
Виртуальная машина несёт собственное ядро и эмулирует железо. Контейнер ядра не несёт: все контейнеры и сам хост работают на одном и том же kernel. Namespaces лишь ограничивают видимость, но код процесса исполняется тем же ядром, что и хостовые процессы.
Проверить легко. Версия ядра внутри контейнера совпадает с хостовой, потому что оно буквально одно.
$ uname -r # на хосте
6.8.0-31-generic
$ docker run --rm nginx uname -r
6.8.0-31-generic
Изоляция ресурсов (сколько CPU и памяти достанется процессу) решается отдельно, через cgroups, а не namespaces. Namespaces определяют, что процесс видит. А сколько ресурсов ему выделить, решают уже cgroups.
Что важно запомнить
С точки зрения хоста контейнер остаётся обычным процессом с изолированными namespaces и урезанными правами.
Namespace ограничивает видимость: PID, Network, Mount, UTS, IPC, User, cgroup.
Внутри PID namespace первый процесс получает PID 1; внутри User namespace root может быть непривилегированным пользователем снаружи.
Создаются вызовами clone и unshare; посмотреть текущие можно через lsns.
Контейнер работает на ядре хоста, отдельной ОС у него нет. За ограничение ресурсов отвечают cgroups.
В следующем уроке — cgroups: как ядро выделяет процессу CPU и память и не даёт съесть больше.
Прошли материал до конца? Отметьте урок пройденным.
Прогресс сохранён в этом браузере
Почистишь историю или откроешь с телефона, и отметки пропадут. Оставь почту, они переедут в аккаунт. Пароль придумывать не нужно.
Готово. теперь в аккаунте: прогресс виден с любого устройства. Мой прогресс