← Linux: изоляция
12 мин · средне · Урок 1 из 4

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дерево смонтированных каталогов
UTShostname и доменное имя
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 и память и не даёт съесть больше.

Прошли материал до конца? Отметьте урок пройденным.