cgroups и namespaces в Linux: как это работает?

от автора

Привет, Хабр! Сегодня рассмотрим изоляции процессов и управления ресурсами в 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. На странице курса вы сможете бесплатно посмотреть записи последних вебинаров.


ссылка на оригинал статьи https://habr.com/ru/articles/858780/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *