Привет, Хабр! Сегодня рассмотрим изоляции процессов и управления ресурсами в Linux, изучив возможности cgroups и namespaces. Разберёмся, как работают контейнеры изнутри и научимся создавать собственное изолированное окружение без Docker.

Немного теории

Namespaces

Namespaces в Linux — это механизм, который изолирует и виртуализирует системные ресурсы для групп процессов. По сути, это как если бы каждый процесс жил в своём собственном мире, не подозревая о существовании других. Рассмотрим основные виды namespaces:

  • PID Namespace: изолирует идентификаторы процессов. Процессы внутри этого пространства видят свои собственные PID, начиная с 1, и не могут видеть процессы вне этого пространства..

  • Mount Namespace: изолирует точки монтирования файловой системы. Процессы внутри видят собственную файловую систему или её часть, не затрагивая хост-систему.

  • UTS Namespace: позволяет изолировать имена узлов и доменов, т.е. hostname и domainname.

  • Network Namespace: изолирует сетевые интерфейсы, маршруты, таблицы ARP и т.д. Процессы внутри имеют свой собственный сетевой стек.

  • IPC Namespace: изолирует межпроцессное взаимодействие с помощью System V IPC.

  • User Namespace: изолирует идентификаторы пользователей и групп.

  • Cgroup Namespace: иззолирует видимость cgroups. Процессы видят только свои собственные cgroups.

  • Time Namespace: позволяет изолировать системное время, чтобы процессы могли видеть другое время, нежели хост-система.

Cgroups

Cgroups — это механизм ядра Linux, который позволяет ограничивать, учитывать и изолировать использование системных ресурсов для групп процессов. Допустим, вы на афтепати и хотите, чтобы каждый гость съел не больше определённого количества пиццы. Cgroups делают то же самое, только вместо пиццы — CPU, память, диск и другие ресурсы.

Основные контроллеры cgroups:

  • CPU: конролирует использование процессорного времени. Можно установить лимиты на процент использования CPU или распределять время между группами процессов.

  • Memory: ограничивает использование оперативной памяти и swap.

  • IO: контролирует ввод-вывод на блочные устройства.

  • Devices: управляет доступом к устройствам. Позволяет разрешить или запретить определённым процессам доступ к конкретным устройствам.

  • PIDs: ограничивает количество процессов в группе.

Стоит отметить, что существует две версии cgroups: v1 и v2. Вторая версия объединяет все контроллеры в единую иерархию и упрощает управление. В современных дистрибутивах Linux по дефолту используется cgroups v2.

Cgroups используют псевдофайловую систему, которая монтируется в /sys/fs/cgroup/.

Практическая часть

Сначала убедимся, какая версия cgroups используется в системе:

mount | grep cgroup

Если в выводе есть cgroup2, значит используется cgroups v2.

Создание cgroup и ограничение ресурсов

Создадим директорию для нашей группы:

sudo mkdir /sys/fs/cgroup/my_group

Установим лимит использования CPU до 20%:

echo "20000 100000" | sudo tee /sys/fs/cgroup/my_group/cpu.max

Здесь 20000 — квота в микросекундах, 100000 — период, т.е процесс может использовать CPU не более 20% времени.

Установим лимит памяти в 50 МБ:

echo $((50*1024*1024)) | sudo tee /sys/fs/cgroup/my_group/memory.max

Добавим текущий процесс в cgroup:

echo $$ | sudo tee /sys/fs/cgroup/my_group/cgroup.procs

Создание изолированного окружения

Используем утилиту unshare для запуска процессов в новых пространствах имён.

sudo unshare --fork --pid --mount --uts --ipc --net --user --map-root-user --mount-proc /proc /bin/bash

Разберём ключи команды:

  • --fork: форкает процесс после создания пространств имён.

  • --pid: создаёт новый PID namespace.

  • --mount: создаёт новый Mount namespace.

  • --uts: создаёт новый UTS namespace.

  • --ipc: создаёт новый IPC namespace.

  • --net: создаёт новый Network namespace.

  • --user: создаёт новый User namespace.

  • --map-root-user: маппинг root пользователя внутри пространства имён.

  • --mount-proc: монтирует новую файловую систему /proc внутри нового Mount namespace.

Теперь мы находимся в изолированной среде. Проверим это.

Выполним:

ps aux

Можно увидеть минимальный список процессов. PID bash будет равен 1.

На хосте выполняем:

ps aux | grep bash

Вы увидите, что процесс имеет другой PID на хосте.

Изменим hostname внутри изолированного окружения:

hostname new_container

Проверим:

hostname
# Вывод: new_container

На хосте hostname останется прежним.

Посмотрим сетевые интерфейсы:

ip link

Скорее всего, вы увидите только интерфейс lo, т.к в новом Network namespace нет других интерфейсов.

Подготовка файловой системы

Создадим директорию для нашей файловой системы:

mkdir -p ~/my_container/rootfs

BusyBox — это единый бинарник, включающий множество стандартных утилит Unix.

Установим его:

sudo apt-get update
sudo apt-get install -y busybox-static

Копируем BusyBox в файловую систему:

mkdir -p ~/my_container/rootfs/bin
cp /bin/busybox ~/my_container/rootfs/bin/

Создание символических ссылок на утилиты:

cd ~/my_container/rootfs/bin
for cmd in sh ls ps mkdir mount uname hostname cat echo sleep; do
    ln -s busybox $cmd
done

Создадим необходимые директории:

mkdir -p ~/my_container/rootfs/{proc,sys,dev,etc,tmp}

Настройка устройств

Создадим устройства в dev:

sudo mknod -m 666 ~/my_container/rootfs/dev/null c 1 3
sudo mknod -m 666 ~/my_container/rootfs/dev/zero c 1 5
sudo mknod -m 666 ~/my_container/rootfs/dev/tty c 5 0
sudo mknod -m 666 ~/my_container/rootfs/dev/random c 1 8
sudo mknod -m 666 ~/my_container/rootfs/dev/urandom c 1 9

Создадим файл passwd:

echo "root:x:0:0:root:/root:/bin/sh" > ~/my_container/rootfs/etc/passwd

Монтирование файловых систем внутри контейнера

Внутри изолированного окружения (после запуска unshare) монтируем псевдофайловые системы:

mount -t proc proc ~/my_container/rootfs/proc
mount -t sysfs sysfs ~/my_container/rootfs/sys
mount -t devtmpfs devtmpfs ~/my_container/rootfs/dev

Войдём в изолированную систему с chroot:

chroot ~/my_container/rootfs /bin/sh

Теперь находимся внутри изолированной файловой системы.

Проверим, что всё работает:

ls /
# Вывод: bin dev etc proc sys tmp

Проверим hostname:

hostname
# Вывод: new_container

Попробуем создать файл:

echo "Hello from container" > /tmp/hello.txt
cat /tmp/hello.txt
# Вывод: Hello from container

Ограничение ресурсов с cgroups

Если cgroups не смонтирован, монтируем его:

mount -t cgroup2 none /sys/fs/cgroup

Создадим cgroup для контейнера:

mkdir /sys/fs/cgroup/my_container

Установим лимит CPU до 20%:

echo "20000 100000" > /sys/fs/cgroup/my_container/cpu.max

Установим лимит памяти в 50 МБ:

echo $((50*1024*1024)) > /sys/fs/cgroup/my_container/memory.max

Получим PID текущего процесса:

echo $$ > /sys/fs/cgroup/my_container/cgroup.procs

Тестирование ограничений

Создадим скрипт cpu_stress.sh:

#!/bin/sh
while true; do :; done

Сделаем его исполняемым:

chmod +x cpu_stress.sh

Запустим скрипт внутри контейнера:

./cpu_stress.sh &

Проверим использование CPU:

top

Можно будет увидеть, что использование CPU ограничено примерно 20%.

Теперь создадим скрипт для нагрузки на память.

Создадим файл mem_stress.sh:

#!/bin/sh
mem=()
while true; do
    mem+=($(head -c 1048576 /dev/zero))
    echo "Allocated ${#mem[@]} MB"
    sleep 1
done

Этот скрипт будет выделять 1 МБ памяти каждую секунду.

Сделаем его исполняемым:

chmod +x mem_stress.sh

Запустим скрипт:

./mem_stress.sh

Когда суммарное потребление памяти превысит 50 МБ, процесс будет убит cgroups, и вы увидите сообщение об ошибке.

Автоматизация процесса с помощью скрипта

Создадим скрипт start_container.sh для автоматизации всех шагов:

#!/bin/bash

# Шаг 1: Создание изолированного окружения
sudo unshare --fork --pid --mount --uts --ipc --net --user --map-root-user --mount-proc=/proc bash <<EOF

# Шаг 2: Монтирование файловых систем
mount -t proc proc ~/my_container/rootfs/proc
mount -t sysfs sysfs ~/my_container/rootfs/sys
mount -t devtmpfs devtmpfs ~/my_container/rootfs/dev

# Шаг 3: Вход в chroot
exec chroot ~/my_container/rootfs /bin/sh

EOF

Сделаем скрипт исполняемым:

chmod +x start_container.sh

Теперь можно запускать контейнер командой:

/start_container.sh

Заключение

Важно помнить, что при работе с низкоуровневыми возможностями ядра можно легко навредить системе. Всегда проверяйте права доступа и не запускайте незнакомый код с правами суперпользователя.

Если у вас есть вопросы или вы хотите поделиться своим опытом, смело пишите в комментариях.

Полезные ссылки:

Статья подготовлена в преддверии старта курса Computer Science. На странице курса вы сможете бесплатно посмотреть записи последних вебинаров.

Комментарии (2)


  1. seyko2
    19.11.2024 01:29

    Ну что ж.. Полезная статья. Именно по данной теме. Ибо контейнеры в линукс не так уж давно устаканились (относительно всего остального). Ещё бы про KVM и его связь с qemu. Это хоть и пораньше было разработано. Но разработчики - особо не прилагают усилий для просвещения кого либо кроме Линуса.


  1. TIEugene
    19.11.2024 01:29

    контейнеры в линукс не так уж давно устаканились

    Ну как сказать... Chroot в Linux с самого начала, наверное. Но таки да, не так и давно. Годиков 30+ примерно