С появлением таких утилит, как Docker и Linux контейнеры стало возможным очень легко изолировать процессы в Linux системе в их собственное системное окружение. Это дало возможность запускать целый набор приложений на одной Linux машине и быть уверенным в том, что они не будут мешать друг другу, без необходимости использования зоопарка виртуальных машин.
Ключевая функциональность ядра, которая позволяет добиться такой изоляции — Linux Namespaces, появилась в Linux начиная с версии 2.4.19 в 2002 году (CLONE_NEWNS), после чего с последующими обновлениями ядра добавлялись новые:
UTS namespaces (CLONE_NEWUTS, Linux 2.6.19)
IPC namespaces (CLONE_NEWIPC, Linux 2.6.19)
PID namespaces (CLONE_NEWPID, Linux 2.6.24)
Network namespaces (CLONE_NEWNET, добавлен в разработку в Linux 2.4.19, 2.6.24 и завершён в Linux 2.6.29)
User namespaces (CLONE_NEWUSER, добавлен в разработку в Linux 2.6.23 и завершён в Linux 3.8)
Всякий, кто знаком с chroot в Linux уже имеет базовое представление о том, чего можно добиться с помощью Namespaces: аналогично тому, как chroot позволяет процессам видеть только определённую директорию как корень файловой системы — механизмы Linux namespaces позволяют выполнять операции изоляции в других механизмах операционной системы, такими как дерево процессов, сетевые интерфейсы, точки монтирования, IPC и так далее.
PID Namespace
Изначально Linux оддерживало только единое дерево процессов, в котором содержались ссылки на все запущенные в системе процессы в виде иерархического дерева. Любой процесс, при условии наличия соответствующих прав, мог подключиться к любому другому процессу и выполнить его трассировку или даже уничтожить его.
С появлением Linux Namespaces стало возможным иметь несколько «вложенных» друг в друга деревьев процессов: каждый процесс может иметь полностью изолированный набор процессов. С помощью этого достигается реальная изоляция процессов — процессы, принадлежащие к одному дереву процессов не могут подключиться или уничтожить процессы из дерева процессов другого, дочернего или родительского, процесса. По факту — эти процессы даже не имеют представления о том, что где-то есть другие такие процессы.
Каждый раз, когда компьютер с Linux запускается, он стартует с одного процесса с PID 1:
sudo pstree -pn | head
systemd(1)-+-systemd-journal(246)
|-lvmetad(258)
|-systemd-udevd(265)
|-systemd-timesyn(557)---{systemd-timesyn}(558)
|-crond(562)
|-dbus-daemon(563)
|-systemd-logind(565)
|-wpa_supplicant(566)
|-NetworkManager(567)-+-{NetworkManager}(582)
| `-{NetworkManager}(586)
Этот процесс, в данном примере systemd(1), является корнем дерева процессов, и он инициализирует всю систему, запуская все остальные необходимые для её работы процессы, и все такие процессы запускаются в этом дереве ниже первого процесса.
Process Namespace позволяет создать новое дерево, с его собственным процессом с PID 1 — процесс, который создал такое дерево, остаётся в родительском пространстве имён, в основном дереве процессов, но его дочерний процесс становится корневым процессом собственного дерева процесса.
Таким образом, с помощью изоляции PID namespace, процессы в дочернем дереве процесса не имеют никакой возможности узнать о существовании своего родительского процесса, однако процессы в родительском пространстве имён имеют полное представление о дочернем пространстве, как если бы те были обычными процессами в родительском пространстве имён:
Кроме того, таким же образом возможно создавать вложенные простанства имён: один процесс порождает другой, дочерний, в новом PID namespace, а этот дочерний процесс порождает следующий, в ещё одном пространстве PID, и так далее.
С появлением PID namespaces один процесс может иметь несколько PID, по одному на каждый связанное с этим процессом пространство имён.
В исходном коде Linux, в файле pid.h, мы можем увидеть, что структура pid, которая ранее использовалась для отслеживания задач по одному PID, теперь использует структуру upid для отслеживания задач по нескольким PID:
...
/*
* struct upid is used to get the id of the struct pid, as it is
* seen in particular namespace. Later the struct pid is found with
* find_pid_ns() using the int nr and struct pid_namespace *ns.
*/
struct upid {
/* Try to keep pid_chain in the same cacheline as nr for find_vpid */
int nr;
struct pid_namespace *ns;
struct hlist_node pid_chain;
};
struct pid {
atomic_t count;
unsigned int level;
/* lists of tasks that use this pid */
struct hlist_head tasks[PIDTYPE_MAX];
struct rcu_head rcu;
struct upid numbers[1];
};
...
Что бы создать новое пространство имён для PID — используем системный вызов clone() (другой вариант — использовать fork() и unshare()), которому аргументом передадим флаг CLONE_NEWPID.
После вызова clone() — новый процесс будет создан в новом пространстве имён PID, с новым деревом процессов.
Используем следующй код:
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
#define STACK_SIZE (1024 * 1024)
static char child_stack[STACK_SIZE];
static int child_function() {
printf("Child's PID from child_function() from own namespace: %d\n", getpid());
return 0;
}
int main(int argc, char** argv) {
pid_t pid = getpid();
pid_t child_pid = clone(child_function, child_stack + STACK_SIZE, CLONE_NEWPID | SIGCHLD, NULL);
printf("PID for %s process = %d\n", argv[0], pid);
printf("Child process created by clone() from %s have PID = %d\n", argv[0], child_pid);
waitpid(child_pid, NULL, 0);
return 0;
}
Собираем, запускаем:
gcc pid_namespaces.c -opid_namespaces
sudo ./pid_namespaces
PID for ./pid_namespaces process = 29595
Child process created by clone() from ./pid_namespaces have PID = 29596
Child's PID from child_function() from own namespace: 1
Кратко рассмотрим саму программу:
static char child_stack[STACK_SIZE]: определяем размер стека для дочернего процесса
pid_t pid = getpid(): определяем переменную pid типа pid_t, в которую заносим PID процесса, который выполняет саму программу pid_namespaces
pid_t child_pid = clone(...): определяем переменную child_pid типаpid_t, которая получит значение PID из дочернего процесса, который создаётся вызовом clone()
child_function: имя функции, которая будет выполнена дочерним процессом в новом namespace и выведет свой PID с помощью getpid()
child_stack: расположение стека для дочернего процесса
CLONE_NEWPID: собственно флаг для использования нового namespace при создании дочернего процесса
printf("PID for %s process = %d\n", argv[0], pid): выводим имя программы pid_namespaces (argv[0]) и PID процесса программы pid_namespaces
printf("Child process created by clone() from %s have PID = %d\n", argv[0], child_pid): выводим имя программы pid_namespaces, из которой вызывается clone() и PID дочернего процесса, созданного вызовом clone()
Итак — функция clone() порождает новый процесс, клонируя родительский, и начинает выполнение дочернего процесса с вызова функции child_function().
Однако, т.к. мы передали флаг CLONE_NEWPID — новый процесс запускается в собственном пространстве имён и создаёт новое дерево процессов, поэтому getpid() из child_function() возвращает нам Process ID равным единице, т.е. самый первый процесс в этом новом дереве.
Проверяем.
Изменим функцию child_function(), и к вызову getpid() (получить свой PID) — добавим вызов функции getppid() (Get Parent PID), в документации к которой сказано:
The getppid() function shall always be successful and no return value is reserved to indicate an error.
ОК:
...
static int child_function() {
printf("Child's PID from child_function() from own namespace: %d\n", getpid());
printf("Parent's PID from child_function() from own namespace: %d\n", getppid());
return 0;
}
...
Собираем, проверяем:
gcc pid_namespaces.c -o pid_namespaces
sudo ./pid_namespaces
PID for ./pid_namespaces process = 10726
Child process created by clone() from ./pid_namespaces have PID = 10727
Child's PID from child_function() from own namespace: 1
Parent's PID from child_function() from own namespace: 0
Parent’s PID равен нулю, как и ожидали, т.к. child_function() запускается в собственном дереве и не видит родительского процесса вообще.
А теперь — изменим main() и вызов clone() в ней: уберём флаг CLONE_NEWPID:
Внутри созданного пространства доступен только интерфейс lo, при чём он в статусе DOWN, а не Up, как lo на хост-системе.
А для того, что бы получить возможность обращаться к устройствам в созданном NET Namespace — надо создать Point-to-Point туннель между хостом и «гостевой» системой, которая работала бы в в новом пространстве.
Для этого в Linux можно использовать виртуальные интерфейсы (veth — Virtual Ethernet), которые и используются в Docker для проброса трафика между хостом и создаваемыми контейнерами.
veth создаёт два виртуальных интефейса — vent0 и veth1, которые затем связываются с пространством имён хост-системы и пространством имён в контейнере (которого у нас нет, но в Docker это происходит именно так).
Создаём пару veth:
sudo ip link add veth0 type veth peer name veth1
Проверяем:
ip link list | grep veth
8: veth1@veth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
9: veth0@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
Вот наши два туннеля — veth1 => veth0, и обратный — veth0 => veth1.
Подключаем интерфейс veth1 в пространство имён demo:
sudo ip link set veth1 netns demo
Повторяем list из пространства demo, как мы делали в начале:
history | grep "link list"
576 ip link list
579 sudo ip netns exec demo ip link list
!579
sudo ip netns exec demo ip link list
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
8: veth1@if9 — вот и новый интерфейс в пространстве имён demo.
Теперь настроим этот интерфейс, как делали бы это на любом другом.
Активируем loopback:
sudo ip netns exec demo ip link set lo up
Активируем veth1:
sudo ip netns exec demo ip link set veth1 up
Присваиваем IP 169.254.1.2 из сети 169.254.1.0/30 к veth1:
sudo ip netns exec demo ip addr add 169.254.1.2/30 dev veth1
Поднимаем интерфейс veth0 на хосте:
sudo ip link set veth0 up
И присваем ему IP 169.254.1.1 из сети 169.254.1.0/30:
sudo ip addr add 169.254.1.1/30 dev veth0
Проверяем доступ между пространствами:
ping 169.254.1.2 -c 1
PING 169.254.1.2 (169.254.1.2) 56(84) bytes of data.
64 bytes from 169.254.1.2: icmp_seq=1 ttl=64 time=0.077 ms
--- 169.254.1.2 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.077/0.077/0.077/0.000 ms
Таблица маршрутизации для 169.254.1.0/30:
ip route list 169.254.1.0/30
169.254.1.0/30 dev veth0 proto kernel scope link src 169.254.1.1
Удаляем пару veth, что бы продолжить с примером на С:
sudo ip link del veth0
ip link list | grep veth | wc -l
Создание NET Namespace: clone()и CLONE_NEWNET
Теперь повторим всё то же, но из C программы, используя clone(), как в случае с PID namespace в начале поста, только с флагом CLONE_NEWNET вместо CLONE_NEWPID: