2.3. Linux namespaces
Цели
После этого материала вы сможете:
- объяснить, что такое namespace и какую задачу он решает;
- перечислить все типы namespaces и сказать, что изолирует каждый;
- посмотреть namespaces любого процесса, включая процессы containers;
- определить, находятся ли два процесса в одном namespace;
- войти в namespace работающего container с host;
- создать изолированное окружение без Docker, средствами
unshare; - объяснить, какие namespaces Docker создаёт по умолчанию, а какие — нет.
Предварительные знания
- 2.1. Процессы и containers;
- понимание файловой системы
/proc; - базовое представление о сетевых интерфейсах.
Ключевые термины
| Термин | Объяснение |
|---|---|
namespace | Механизм ядра, изолирующий представление процесса об одном виде системных ресурсов |
unshare | Системный вызов и одноимённая утилита: отделяет процесс от текущего namespace, создавая новый |
setns | Системный вызов для входа в существующий namespace |
nsenter | Утилита, использующая setns для запуска команды в namespaces другого процесса |
inode | Номер файлового объекта. Namespaces идентифицируются номерами inode |
mount propagation | Правила, по которым монтирования распространяются между mount namespaces |
Теория
Задача, которую решают namespaces
Обычно все процессы Linux видят одну систему: один список процессов, одну файловую систему, один набор сетевых интерфейсов, одно имя хоста. Namespace разрывает эту единственность: процессы в разных namespaces видят разные системы, хотя выполняются на одном ядре.
Формально namespace — это обёртка вокруг глобального ресурса ядра, из-за которой процессам внутри namespace кажется, что они обладают собственным экземпляром этого ресурса.
Каждый процесс принадлежит ровно одному namespace каждого типа. По умолчанию это initial namespace — тот же, что у init.
Типы namespaces
| Тип | Флаг clone() | Что изолирует | Появился |
|---|---|---|---|
| Mount | CLONE_NEWNS | Точки монтирования и дерево файловой системы | Linux 2.4.19 |
| UTS | CLONE_NEWUTS | Имя хоста и доменное имя NIS | Linux 2.6.19 |
| IPC | CLONE_NEWIPC | Очереди сообщений, семафоры, разделяемая память System V; POSIX message queues | Linux 2.6.19 |
| PID | CLONE_NEWPID | Пространство идентификаторов процессов | Linux 2.6.24 |
| Network | CLONE_NEWNET | Сетевые интерфейсы, таблицы маршрутизации, правила фильтрации, порты, /proc/net | Linux 2.6.29 |
| User | CLONE_NEWUSER | Идентификаторы пользователей и групп, capabilities | Linux 3.8 |
| Cgroup | CLONE_NEWCGROUP | Корень иерархии cgroup, видимый процессу | Linux 4.6 |
| Time | CLONE_NEWTIME | Системные часы CLOCK_MONOTONIC и CLOCK_BOOTTIME | Linux 5.6 |
Аббревиатура UTS происходит от структуры utsname («UNIX Time-sharing System»), которую возвращает системный вызов uname().
Что делает каждый тип
Mount namespace. Собственное дерево точек монтирования. Именно он даёт container отдельную файловую систему: сначала создаётся mount namespace, затем внутри него выполняется pivot_root() на распакованные слои образа. Монтирование внутри container не влияет на host, и наоборот — если не настроено обратное через mount propagation.
UTS namespace. Собственное имя хоста. Причина, по которой hostname внутри container возвращает ID container, а не имя вашей машины.
IPC namespace. Собственные механизмы межпроцессного взаимодействия System V и POSIX message queues. Два container не могут случайно использовать один сегмент разделяемой памяти. Флаг --ipc=host отключает эту изоляцию — иногда требуется, например, для PyTorch DataLoader с большим shared_memory.
PID namespace. Собственное пространство PID. Единственный namespace с иерархией: процессы дочернего namespace видны из родительского, но не наоборот. Первый процесс в новом PID namespace получает PID 1 и особый статус — см. раздел 04.
Network namespace. Собственный сетевой стек целиком: интерфейсы, адреса, маршруты, правила iptables/nftables, номера портов, содержимое /proc/net. Порт 8000, занятый в одном namespace, свободен в другом. Именно поэтому три container могут одновременно слушать порт 80. Подробно — в разделе 08.
User namespace. Собственное отображение UID и GID. Позволяет процессу быть root внутри, оставаясь непривилегированным снаружи. Основа rootless mode. Docker по умолчанию не создаёт user namespace для containers — это важно и объясняется ниже.
Cgroup namespace. Скрывает от процесса реальный путь его cgroup в иерархии, показывая его как корень. Предотвращает утечку информации о структуре host.
Time namespace. Позволяет сместить CLOCK_MONOTONIC и CLOCK_BOOTTIME. Нужен в основном для миграции containers (checkpoint/restore). Docker его по умолчанию не использует; системное время (CLOCK_REALTIME) он не изолирует вовсе — оно всегда общее с host.
Что Docker создаёт по умолчанию
При обычном docker run создаются: mount, UTS, IPC, PID, network, cgroup.
Не создаётся: user namespace. Это означает, что root внутри container — это UID 0 и на host. Ограничения накладываются другими механизмами: отбором capabilities (урок 2.5), профилем seccomp и AppArmor. Но UID тот же.
Практическое следствие, которое проявляется в разделе 07: файл, созданный процессом container от root в bind mount, будет принадлежать root на host.
Включить user namespace можно двумя способами: userns-remap на уровне daemon или rootless mode. Оба разбираются в разделе 12.
Time namespace также не используется.
Как namespaces идентифицируются
Каждый namespace представлен файлом в /proc/<pid>/ns/:
/proc/1234/ns/
├── cgroup -> 'cgroup:[4026531835]'
├── ipc -> 'ipc:[4026531839]'
├── mnt -> 'mnt:[4026531841]'
├── net -> 'net:[4026531840]'
├── pid -> 'pid:[4026531836]'
├── user -> 'user:[4026531837]'
└── uts -> 'uts:[4026531838]'
Число в скобках — номер inode. Он служит идентификатором namespace. Два процесса находятся в одном namespace тогда и только тогда, когда номера inode совпадают. Это даёт простой способ проверки принадлежности.
Namespace существует, пока на него есть ссылка: работающий процесс, открытый файловый дескриптор или bind mount файла из /proc/<pid>/ns/. Когда последняя ссылка исчезает, ядро уничтожает namespace.
Внутренний механизм
Три способа работы с namespaces
| Системный вызов | Действие |
|---|---|
clone() с флагами CLONE_NEW* | Создать новый процесс сразу в новых namespaces |
unshare() с теми же флагами | Отделить текущий процесс в новые namespaces |
setns() | Переместить текущий процесс в существующий namespace |
Container runtime использует первый: runc вызывает clone() с нужным набором флагов, порождая процесс уже изолированным.
Утилита nsenter использует третий: открывает файл /proc/<pid>/ns/<type>, передаёт дескриптор в setns() и затем запускает команду.
Особенность PID namespace
PID namespaces образуют дерево. Процесс имеет PID в своём namespace и во всех родительских, вплоть до initial.
Из этого следует асимметрия видимости: родительский namespace видит процессы дочернего, дочерний родительский — нет. Поэтому с host вы видите процессы всех containers, а изнутри container — только свои.
Вторая особенность: смена PID namespace для уже работающего процесса невозможна. setns() с PID namespace влияет только на дочерние процессы, которые будут созданы после вызова. Причина в том, что PID процесса не может измениться на лету — это сломало бы все ссылки на него.
Практическое следствие для nsenter: вход в PID namespace container требует запуска новой команды, а не «перемещения» текущей оболочки.
Команды и примеры
Просмотр namespaces процесса
ls -l /proc/self/ns/
lrwxrwxrwx 1 evg evg 0 Jul 29 14:22 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 evg evg 0 Jul 29 14:22 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 evg evg 0 Jul 29 14:22 mnt -> 'mnt:[4026531841]'
lrwxrwxrwx 1 evg evg 0 Jul 29 14:22 net -> 'net:[4026531840]'
lrwxrwxrwx 1 evg evg 0 Jul 29 14:22 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 evg evg 0 Jul 29 14:22 time -> 'time:[4026531834]'
lrwxrwxrwx 1 evg evg 0 Jul 29 14:22 user -> 'user:[4026531837]'
lrwxrwxrwx 1 evg evg 0 Jul 29 14:22 uts -> 'uts:[4026531838]'
/proc/self — ссылка на каталог текущего процесса, удобно для интерактивной работы.
Сравнение namespaces host и container
docker run -d --name ns-demo alpine sleep 3600
CPID="$(docker inspect ns-demo --format '{{.State.Pid}}')"
echo "=== host ==="
sudo readlink /proc/1/ns/{pid,net,mnt,uts,ipc,user,cgroup}
echo "=== container ==="
sudo readlink /proc/"$CPID"/ns/{pid,net,mnt,uts,ipc,user,cgroup}
=== host ===
pid:[4026531836]
net:[4026531840]
mnt:[4026531841]
uts:[4026531838]
ipc:[4026531839]
user:[4026531837]
cgroup:[4026531835]
=== container ===
pid:[4026532412]
net:[4026532415]
mnt:[4026532410]
uts:[4026532411]
ipc:[4026532413]
user:[4026531837]
cgroup:[4026532473]
Разберём результат:
| Тип | Совпадает? | Вывод |
|---|---|---|
pid, net, mnt, uts, ipc, cgroup | Нет | Docker создал новые namespaces |
user | Да | User namespace не создан — root в container это root на host |
Последняя строка — самое важное наблюдение урока. Она объясняет и проблемы с правами доступа в разделе 07, и модель угроз в разделе 12.
Утилита lsns
sudo lsns -p "$CPID"
NS TYPE NPROCS PID USER COMMAND
4026531834 time 412 1 root /sbin/init
4026531837 user 412 1 root /sbin/init
4026532410 mnt 1 188104 root sleep 3600
4026532411 uts 1 188104 root sleep 3600
4026532412 pid 1 188104 root sleep 3600
4026532413 ipc 1 188104 root sleep 3600
4026532415 net 1 188104 root sleep 3600
4026532473 cgroup 1 188104 root sleep 3600
Колонка NPROCS показывает число процессов в namespace. Для time и user — 412 (весь host), для остальных — 1 (только наш container). Наглядное подтверждение того, какие namespaces созданы, а какие разделяются.
Все namespaces системы:
sudo lsns
Только сетевые:
sudo lsns -t net
Вход в namespace container
sudo nsenter -t "$CPID" -n ip addr show
Разбор флагов:
| Флаг | Значение |
|---|---|
-t PID | Целевой процесс, в namespaces которого входим |
-n | Network namespace |
-m | Mount namespace |
-p | PID namespace |
-u | UTS namespace |
-i | IPC namespace |
-a | Все namespaces сразу |
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
inet 127.0.0.1/8 scope host lo
16: eth0@if17: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
Мы увидели сетевые интерфейсы container, не заходя в него. Это принципиально важно для диагностики: работает даже тогда, когда в образе нет ни оболочки, ни ip, ни ping — используются утилиты host.
Приём подробно применяется в разделе 13.
Проверка сокетов container утилитами host:
sudo nsenter -t "$CPID" -n ss -tlnp
Вход в mount namespace — увидеть файловую систему container:
sudo nsenter -t "$CPID" -m ls /
bin dev etc home lib media mnt opt proc root
run sbin srv sys tmp usr var
Создание namespace без Docker
Namespaces — механизм ядра, Docker к нему лишь обращается. Убедимся напрямую.
UTS namespace:
sudo unshare --uts sh -c 'hostname isolated-host && hostname'
hostname
isolated-host
my-laptop
Внутри нового UTS namespace имя хоста изменено; на host оно прежнее.
PID namespace:
sudo unshare --pid --fork --mount-proc sh -c 'echo "PID: $$"; ps -e'
PID: 1
PID TTY TIME CMD
1 pts/2 00:00:00 sh
3 pts/2 00:00:00 ps
Оболочка получила PID 1 и видит только свои процессы. Разбор флагов:
--pid— создать PID namespace;--fork— обязательно: самunshareне может сменить свой PID, поэтому порождает дочерний процесс, который и становится PID 1;--mount-proc— перемонтировать/procв новом mount namespace, иначеpsбудет читать/prochost и показывать все процессы.
Забытый --mount-proc — классическая ошибка: PID namespace создан, но ps показывает процессы host, и кажется, что изоляция не работает.
Network namespace:
sudo unshare --net ip addr show
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
Новый сетевой namespace содержит только lo, и тот выключен. Именно с такого состояния Docker начинает настройку сети container — добавляет veth-интерфейс и адрес. Разбирается в разделе 08.
Комбинация:
sudo unshare --uts --pid --fork --mount-proc --net \
sh -c 'hostname mini-container; echo "host: $(hostname), PID: $$"; ip -o link show'
Это уже очень близко к тому, что делает container runtime, — не хватает только смены корня и cgroup.
Разделение namespaces между containers
docker run -d --name first alpine sleep 3600
docker run -d --name second --network=container:first alpine sleep 3600
P1="$(docker inspect first --format '{{.State.Pid}}')"
P2="$(docker inspect second --format '{{.State.Pid}}')"
sudo readlink /proc/"$P1"/ns/net
sudo readlink /proc/"$P2"/ns/net
net:[4026532415]
net:[4026532415]
Номера совпали: два container разделяют один network namespace. Это тот же механизм, на котором построены pod в Kubernetes — см. раздел 18.
Проверим:
docker exec second ip addr show eth0 | grep inet
docker exec first ip addr show eth0 | grep inet
Одинаковые адреса — сетевой стек общий.
Уборка
docker rm -f ns-demo first second
Практическое упражнение
Задание. Напишите скрипт ns-report.sh <container>, который для указанного container выводит таблицу всех namespaces с указанием: тип, номер inode, совпадает ли с host, число процессов в этом namespace.
Затем на основании таблицы письменно ответьте: какие namespaces Docker создал, а какие разделяются с host, и почему это важно.
Подсказки
Подсказка 1
Номер namespace: sudo readlink /proc/<pid>/ns/<type>. Для host используйте /proc/1/ns/<type>.
Подсказка 2
Число процессов удобно взять из lsns:
sudo lsns -t net -o NS,NPROCS --noheadings
Решение
Сначала выполните задание самостоятельно.
Показать решение
#!/usr/bin/env bash
# ns-report.sh — отчёт о namespaces container.
set -euo pipefail
NAME="${1:?Использование: $0 <container>}"
CPID="$(docker inspect "$NAME" --format '{{.State.Pid}}')"
if [ "$CPID" = "0" ]; then
echo "Container $NAME не запущен." >&2
exit 1
fi
printf '%-8s %-14s %-14s %-10s %s\n' ТИП HOST CONTAINER СОВПАДАЕТ ПРОЦЕССОВ
printf '%s\n' "----------------------------------------------------------------"
for t in mnt uts ipc pid net user cgroup time; do
host_ns="$(sudo readlink "/proc/1/ns/$t" 2>/dev/null || echo '-')"
cont_ns="$(sudo readlink "/proc/$CPID/ns/$t" 2>/dev/null || echo '-')"
[ "$host_ns" = "-" ] && continue
if [ "$host_ns" = "$cont_ns" ]; then
same="ДА"
else
same="нет"
fi
inode="${cont_ns//[^0-9]/}"
nprocs="$(sudo lsns -t "$t" -o NS,NPROCS --noheadings 2>/dev/null \
| awk -v n="$inode" '$1==n {print $2}')"
: "${nprocs:=?}"
printf '%-8s %-14s %-14s %-10s %s\n' \
"$t" "${host_ns//[^0-9]/}" "$inode" "$same" "$nprocs"
done
echo
echo "СОВПАДАЕТ=ДА означает, что namespace разделяется с host —"
echo "изоляции по этому виду ресурсов нет."
Запуск:
docker run -d --name nstest alpine sleep 300
chmod +x ns-report.sh
./ns-report.sh nstest
docker rm -f nstest
Ожидаемый вывод:
ТИП HOST CONTAINER СОВПАДАЕТ ПРОЦЕССОВ
----------------------------------------------------------------
mnt 4026531841 4026532410 нет 1
uts 4026531838 4026532411 нет 1
ipc 4026531839 4026532413 нет 1
pid 4026531836 4026532412 нет 1
net 4026531840 4026532415 нет 1
user 4026531837 4026531837 ДА 412
cgroup 4026531835 4026532473 нет 1
time 4026531834 4026531834 ДА 412
СОВПАДАЕТ=ДА означает, что namespace разделяется с host —
изоляции по этому виду ресурсов нет.
Ответ на вопрос задания.
Docker создал шесть namespaces: mount, UTS, IPC, PID, network, cgroup. Два разделяются с host: user и time.
Разделение user namespace — практически значимо. Оно означает, что UID 0 внутри container это UID 0 на host. Отсюда следуют два наблюдаемых эффекта:
- Файлы, созданные процессом container в bind mount, принадлежат
rootна host, и обычный пользователь не может их удалить (раздел 07). - Если процесс выйдет за пределы изоляции, он окажется на host под UID 0. Ограничения задаются не namespace, а capabilities, seccomp и AppArmor (раздел 12).
Разделение time namespace означает, что часы у container и host общие. На практике это обычно желательно.
Проверка результата
docker run -d --name check alpine sleep 60
CPID=$(docker inspect check --format '{{.State.Pid}}')
sudo readlink /proc/$CPID/ns/user
sudo readlink /proc/1/ns/user
docker rm -f check
Оба значения должны совпасть — это подтверждает, что user namespace не создан.
Типичные ошибки
| Ошибка | Причина | Исправление |
|---|---|---|
unshare --pid без --fork | Сам unshare не может сменить свой PID | Добавить --fork |
unshare --pid --fork без --mount-proc | /proc остался от host, ps показывает его процессы | Добавить --mount-proc |
Ожидание, что root в container не является root на host | User namespace по умолчанию не создаётся | Использовать userns-remap или rootless mode |
| Попытка войти в PID namespace без запуска новой команды | PID работающего процесса нельзя изменить | nsenter запускает новую команду в целевом namespace |
Диагностика сети container путём установки ip/ping в образ | Раздувает образ, нарушает минимальность | nsenter -t <pid> -n с host, инструменты host |
Вывод об отсутствии изоляции по «одинаковому» ps | Забыт --mount-proc при ручном эксперименте | Проверить, что /proc перемонтирован |
Ожидание, что --ipc=host безопасен | Отключает изоляцию System V IPC между container и host | Применять только при явной необходимости и с пониманием риска |
Контрольные вопросы
На понимание:
- Как по номеру inode определить, находятся ли два процесса в одном namespace?
- Почему PID namespace — единственный с иерархией?
- Почему Docker по умолчанию не создаёт user namespace, и какие два следствия это даёт?
- Что произойдёт с namespace, когда завершится последний процесс в нём?
- Почему три container могут одновременно слушать порт 80?
На применение:
- Как посмотреть сетевые интерфейсы container, если в его образе нет утилиты
ip? - Как проверить, что два container разделяют один network namespace?
- Как создать процесс с изолированным списком процессов, не используя Docker?
На диагностику:
- Файл, созданный приложением в container, нельзя удалить с host обычным пользователем. Какой namespace за это отвечает и почему?
- В экспериментальном PID namespace команда
psпоказывает все процессы host. Что забыто?
Краткое резюме
- Namespace изолирует представление процесса об одном виде системных ресурсов.
- Восемь типов: mount, UTS, IPC, PID, network, user, cgroup, time.
- Docker по умолчанию создаёт шесть; user и time разделяются с host.
- Отсутствие user namespace означает, что
rootв container — этоrootна host. - Namespaces идентифицируются номерами inode в
/proc/<pid>/ns/. - Совпадение номеров означает принадлежность одному namespace.
- PID namespaces образуют дерево: родитель видит процессы потомка, но не наоборот.
nsenterпозволяет войти в namespace container с host, используя инструменты host.unshareсоздаёт namespaces без Docker — механизм принадлежит ядру, а не Docker.- Разделение namespace между containers (
--network=container:name) — механизм, лежащий в основе pod в Kubernetes.
Официальные источники
| Источник | Ссылка | Что подтверждает |
|---|---|---|
namespaces(7) man page | https://man7.org/linux/man-pages/man7/namespaces.7.html | Полный список типов namespaces, флаги CLONE_NEW*, файлы в /proc/<pid>/ns/ |
pid_namespaces(7) man page | https://man7.org/linux/man-pages/man7/pid_namespaces.7.html | Иерархия PID namespaces, особый статус PID 1 |
user_namespaces(7) man page | https://man7.org/linux/man-pages/man7/user_namespaces.7.html | Отображение UID и GID, capabilities внутри namespace |
network_namespaces(7) man page | https://man7.org/linux/man-pages/man7/network_namespaces.7.html | Что входит в сетевой namespace |
mount_namespaces(7) man page | https://man7.org/linux/man-pages/man7/mount_namespaces.7.html | Дерево монтирований и mount propagation |
unshare(1) man page | https://man7.org/linux/man-pages/man1/unshare.1.html | Флаги --fork, --mount-proc и их назначение |
nsenter(1) man page | https://man7.org/linux/man-pages/man1/nsenter.1.html | Вход в namespaces другого процесса |
lsns(8) man page | https://man7.org/linux/man-pages/man8/lsns.8.html | Просмотр namespaces системы |
| Isolate containers with a user namespace | https://docs.docker.com/engine/security/userns-remap/ | Включение user namespace в Docker |
Навигация
← Предыдущий материал
Вернуться к разделу
Следующий материал → Cgroups
Главное оглавление