Linux namespace что это

Обновлено: 04.07.2024

В этой серии постов мы внимательно рассмотрим один из главных ингредиентов в контейнере – namespaces. В процессе мы создадим более простой клон команды docker run – нашу собственную программу, которая будет принимать на входе команду (вместе с её аргументами, если таковые имеются) и разворачивать контейнер для её выполнения, изолированный от остальной системы, подобно тому, как вы бы выполнили docker run для запуска из образа.

Что такое namespace?

Linux namespace – это абстракция над ресурсами в операционной системе. Мы можем думать об namespace, как о ящике. В этом ящике находятся системные ресурсы, которые точно зависят от типа ящика (namespace). В настоящее время существует семь типов пространств имён (namespaces): Cgroups, IPC, Network, Mount, PID, User, UTS.

Например, Network namespace включает в себя системные ресурсы, связанные с сетью, такие как сетевые интерфейсы (например, wlan0 , eth0 ), таблицы маршрутизации и т.д., Mount namespace включает файлы и каталоги в системе, PID содержит ID процессов и так далее. Таким образом, два экземпляра Network namespace A и B (соответствующие двум ящикам одного типа в нашей аналогии) могут содержать различные ресурсы – возможно, A содержит wlan0 , тогда как B содержит eth0 и отдельную копию таблицы маршрутизации.

Пространства имён (namespaces) – не какая-то дополнительная фича или библиотека, которую вам нужно установить, например, с помощью пакетного менеджера apt. Они предоставляются самим ядром Linux и уже являются необходимостью для запуска любого процесса в системе. В любой данный момент времени любой процесс P принадлежит ровно одному экземпляру namespace каждого типа. Поэтому, когда ему требуется сказать «обнови таблицу маршрутизации в системе», Linux показывает ему копию таблицы маршрутизации namespace, к которому он принадлежит в этот момент.

Для чего это нужно?

Абсолютно ни для чег… конечно, я просто пошутил. Одним их замечательных свойств ящиков является то, что вы можете добавлять и удалять вещи из ящика и это никак не повлияет на содержимое других ящиков. Тут та же идея с namespaces – процесс P может «сойти с ума» и выполнить sudo rm –rf / , но другой процесс Q, принадлежащий другому Mount namespace, не будет затронут, поскольку они они используют отдельные копии этих файлов.

Обратите внимание, что содержащийся в namespace ресурс не обязательно представляет собой уникальную копию. В ряде случаев, возникших намеренно или ввиду бреши в безопасности, два и более namespaces будут содержать одну и ту же копию, например одного и того же файла. Таким образом, изменения, внесенные в этот файл в одном Mount namespace, фактически будут видны во всех других Mount namespaces, которые также ссылаются на него. Поэтому мы тут откажемся от нашей аналогии с ящиком, поскольку предмет не может одновременно находиться в двух разных ящиках.

Ограничение — это забота

Мы можем видеть namespaces, которым принадлежит процесс! Типичным для Linux образом они отображаются как файлы в каталоге /proc/$pid/ns данного процесса с process id $pid :

Вы можете открыть другой терминал, выполнить ту же команду и это должно дать вам тот же результат. Это потому, что как мы упоминали ранее, процесс обязательно должен принадлежать некоторому пространству имён (namespace) и до тех пор, пока мы мы явно не зададим к какому, Linux добавляет его в namespaces по умолчанию.

Давайте немного вмешаемся в это. Во втором терминале мы можем выполнить что-то вроде этого:

Команда unshare запускает программу (опционально) в новом namespace. Флаг -u говорит ей запустить bash в новом UTS namespace. Обратите внимание, что наш новый процесс bash указывает на другой файл uts , тогда как все остальные остаются прежними.

Создание новых namespaces обычно требует доступа с правами суперпользователя. Здесь и далее мы будем считать, что как unshare , так и наша реализация выполняются с помощью sudo .

Одним из следствий того, что мы только что проделали, является то, что теперь мы можем изменить системный hostname из нашего нового процесса bash и это не повлияет ни на какой другой процесс в системе. Вы можете проверить это, выполнив hostname в первом терминале и увидев, что имя хоста там не изменилось.

Но что, например, такое контейнер?

Надеюсь, теперь у вас есть некоторое представление о том, что может делать namespace. Вы можете предположить, что контейнеры по своей сути — обыкновенные процессы с отличающимися от других процессов namespaces, и вы будете правы. Фактически это квота. Контейнер без квот не обязан принадлежать уникальному namespace каждого типа — он может совместно использовать некоторые из них.

Например, когда вы набираете docker run --net=host redis , всё, что вы делаете — говорите докеру не создавать новый Network namespace для процесса redis. И, как мы видели, Linux добавит этот процесс участником дефолтного Network namespace, как и любой другой обычный процесс. Таким образом, с точи зрения сети процесс redis точно такой же, как и все остальные. Это возможность настройки не только сети, docker run позволяет вам делать такие изменения для большей части существующих namespaces. Тут возникает вопрос, что же такое контейнер? Остаётся ли контейнером процесс, использующий все, кроме одного, общие namespace? ¯\_(ツ)_/¯ Обычно контейнеры идут вместе с понятием изоляции, достигаемой через namespaces: чем меньше количество namespaces и ресурсов, которые процесс делит с остальными, тем более он изолирован и это всё, что действительно имеет значение.

Изолирование

В оставшейся части этого поста мы заложим основу для нашей программы, которую назовём isolate . isolate принимает команду в качестве аргументов и запускает её в новом процессе, изолированном от остальной системы и в ограниченном собственными namespaces. В следующих постах мы рассмотрим добавление поддержки отдельных namespaces для команды процесса, запускаемого isolate .

В зависимости от области применения, мы сфокусируемся на User, Mount, PID и Network namespaces. Остальные же будут относительно тривиальны для реализации после того, как мы закончим (фактически, мы добавим поддержку UTS здесь в первичной реализации программы). А рассмотрение, например, Cgroups, выходит за рамки этой серии (изучение cgroups — другого компонента контейнеров, используемого для управления тем, сколько ресурсов может использовать процесс).

Пространства имён могут очень быстро оказаться сложными и есть много разных путей, которыми можно воспользоваться при изучении каждого namespace, но мы не можем выбрать их все разом. Мы будем обсуждать только те пути, которые имеют отношение к разрабатываемой нами программе. Каждый пост будет начинаться с некоторых экспериментов в консоли над рассматриваемым namespace с целью разобраться с действиями, требуемыми для настройки этого namespace. В результате у нас уже будет представление о том, чего мы хотим достичь, а затем последует и соответствующая реализация в isolate .

Чтобы избежать перегрузки постов кодом, мы не будем включать такие вещи, как вспомогательные функции, не являющиеся необходимыми для понимания реализации. Вы можете найти полный исходный код здесь на Github.

Реализация

Исходный код для этого поста можно найти здесь. Наша реализация isolate будет простой программой, которая считывает строку с командой из stdin и клонирует новый процесс, выполняющий её с указанными аргументами. Клонированный процесс с командой будет выполняться в собственном UTS namespace точно также, как мы делали это ранее с unshare . В следующих постах мы увидим, что namespaces не обязательно работают (или хотя бы обеспечивают изоляцию) из коробки и нам нужно будет выполнить некоторую настройку после их создания (но перед реальным запуском команды), чтобы команда действительно выполнялась изолированной.

Эта комбинация создания-настройки namespace потребует некоторого взаимодействия между основным процессом isolate и дочерним процессом запускаемой команды. В результате часть основной работы здесь будет заключаться в настройке связующего канала между обоими процессами — в нашем случае мы будем использовать Linux pipe из-за его простоты.

Нам нужно сделать три вещи:

  1. Создать основной процесс isolate , читающий данные из stdin.
  2. Клонировать новый процесс, который будет запускать команду в новом UTS namespace.
  3. Настроить пайп таким образом, чтобы процесс выполнения команды начинал её запуск только после получения сигнала от основного процесса о завершении настройки namespace.

Вот основной процесс:

Обратите внимание на clone_flags , которые мы передаем в наш вызов clone . Видите, как просто создать процесс в его собственном namespace? Всё, что нам нужно сделать, это установить флаг для типа namespace ( CLONE_NEWUTS флаг соответствует UTS namespace), а Linux позаботится об остальном.

Далее процесс команды ожидает сигнала перед её запуском:

Наконец, мы может попробовать это запустить:

Сейчас isolate — это немногим больше, чем программа, которая просто форкает команду (у нас есть UTS, работающий для нас). В следующем посте мы сделаем еще один шаг, рассмотрев User namespaces заставим isolate выполнять команду в собственном User namespace. Там мы увидим, что на самом деле надо проделать некоторую работу, чтобы иметь пригодный к использованию namespace, в котором может выполняться команда.

В этой серии постов мы внимательно рассмотрим один из главных ингредиентов в контейнере – namespaces. В процессе мы создадим более простой клон команды docker run – нашу собственную программу, которая будет принимать на входе команду (вместе с её аргументами, если таковые имеются) и разворачивать контейнер для её выполнения, изолированный от остальной системы, подобно тому, как вы бы выполнили docker run для запуска из образа.

Что такое namespace?

Linux namespace – это абстракция над ресурсами в операционной системе. Мы можем думать об namespace, как о ящике. В этом ящике находятся системные ресурсы, которые точно зависят от типа ящика (namespace). В настоящее время существует семь типов пространств имён (namespaces): Cgroups, IPC, Network, Mount, PID, User, UTS.

Например, Network namespace включает в себя системные ресурсы, связанные с сетью, такие как сетевые интерфейсы (например, wlan0 , eth0 ), таблицы маршрутизации и т.д., Mount namespace включает файлы и каталоги в системе, PID содержит ID процессов и так далее. Таким образом, два экземпляра Network namespace A и B (соответствующие двум ящикам одного типа в нашей аналогии) могут содержать различные ресурсы – возможно, A содержит wlan0 , тогда как B содержит eth0 и отдельную копию таблицы маршрутизации.

Пространства имён (namespaces) – не какая-то дополнительная фича или библиотека, которую вам нужно установить, например, с помощью пакетного менеджера apt. Они предоставляются самим ядром Linux и уже являются необходимостью для запуска любого процесса в системе. В любой данный момент времени любой процесс P принадлежит ровно одному экземпляру namespace каждого типа. Поэтому, когда ему требуется сказать «обнови таблицу маршрутизации в системе», Linux показывает ему копию таблицы маршрутизации namespace, к которому он принадлежит в этот момент.

Для чего это нужно?

Абсолютно ни для чег… конечно, я просто пошутил. Одним их замечательных свойств ящиков является то, что вы можете добавлять и удалять вещи из ящика и это никак не повлияет на содержимое других ящиков. Тут та же идея с namespaces – процесс P может «сойти с ума» и выполнить sudo rm –rf / , но другой процесс Q, принадлежащий другому Mount namespace, не будет затронут, поскольку они они используют отдельные копии этих файлов.

Обратите внимание, что содержащийся в namespace ресурс не обязательно представляет собой уникальную копию. В ряде случаев, возникших намеренно или ввиду бреши в безопасности, два и более namespaces будут содержать одну и ту же копию, например одного и того же файла. Таким образом, изменения, внесенные в этот файл в одном Mount namespace, фактически будут видны во всех других Mount namespaces, которые также ссылаются на него. Поэтому мы тут откажемся от нашей аналогии с ящиком, поскольку предмет не может одновременно находиться в двух разных ящиках.

Ограничение — это забота

Мы можем видеть namespaces, которым принадлежит процесс! Типичным для Linux образом они отображаются как файлы в каталоге /proc/$pid/ns данного процесса с process id $pid :

Вы можете открыть другой терминал, выполнить ту же команду и это должно дать вам тот же результат. Это потому, что как мы упоминали ранее, процесс обязательно должен принадлежать некоторому пространству имён (namespace) и до тех пор, пока мы мы явно не зададим к какому, Linux добавляет его в namespaces по умолчанию.

Давайте немного вмешаемся в это. Во втором терминале мы можем выполнить что-то вроде этого:

Команда unshare запускает программу (опционально) в новом namespace. Флаг -u говорит ей запустить bash в новом UTS namespace. Обратите внимание, что наш новый процесс bash указывает на другой файл uts , тогда как все остальные остаются прежними.

Создание новых namespaces обычно требует доступа с правами суперпользователя. Здесь и далее мы будем считать, что как unshare , так и наша реализация выполняются с помощью sudo .

Одним из следствий того, что мы только что проделали, является то, что теперь мы можем изменить системный hostname из нашего нового процесса bash и это не повлияет ни на какой другой процесс в системе. Вы можете проверить это, выполнив hostname в первом терминале и увидев, что имя хоста там не изменилось.

Но что, например, такое контейнер?

Надеюсь, теперь у вас есть некоторое представление о том, что может делать namespace. Вы можете предположить, что контейнеры по своей сути — обыкновенные процессы с отличающимися от других процессов namespaces, и вы будете правы. Фактически это квота. Контейнер без квот не обязан принадлежать уникальному namespace каждого типа — он может совместно использовать некоторые из них.

Например, когда вы набираете docker run --net=host redis , всё, что вы делаете — говорите докеру не создавать новый Network namespace для процесса redis. И, как мы видели, Linux добавит этот процесс участником дефолтного Network namespace, как и любой другой обычный процесс. Таким образом, с точи зрения сети процесс redis точно такой же, как и все остальные. Это возможность настройки не только сети, docker run позволяет вам делать такие изменения для большей части существующих namespaces. Тут возникает вопрос, что же такое контейнер? Остаётся ли контейнером процесс, использующий все, кроме одного, общие namespace? ¯\_(ツ)_/¯ Обычно контейнеры идут вместе с понятием изоляции, достигаемой через namespaces: чем меньше количество namespaces и ресурсов, которые процесс делит с остальными, тем более он изолирован и это всё, что действительно имеет значение.

Изолирование

В оставшейся части этого поста мы заложим основу для нашей программы, которую назовём isolate . isolate принимает команду в качестве аргументов и запускает её в новом процессе, изолированном от остальной системы и в ограниченном собственными namespaces. В следующих постах мы рассмотрим добавление поддержки отдельных namespaces для команды процесса, запускаемого isolate .

В зависимости от области применения, мы сфокусируемся на User, Mount, PID и Network namespaces. Остальные же будут относительно тривиальны для реализации после того, как мы закончим (фактически, мы добавим поддержку UTS здесь в первичной реализации программы). А рассмотрение, например, Cgroups, выходит за рамки этой серии (изучение cgroups — другого компонента контейнеров, используемого для управления тем, сколько ресурсов может использовать процесс).

Пространства имён могут очень быстро оказаться сложными и есть много разных путей, которыми можно воспользоваться при изучении каждого namespace, но мы не можем выбрать их все разом. Мы будем обсуждать только те пути, которые имеют отношение к разрабатываемой нами программе. Каждый пост будет начинаться с некоторых экспериментов в консоли над рассматриваемым namespace с целью разобраться с действиями, требуемыми для настройки этого namespace. В результате у нас уже будет представление о том, чего мы хотим достичь, а затем последует и соответствующая реализация в isolate .

Чтобы избежать перегрузки постов кодом, мы не будем включать такие вещи, как вспомогательные функции, не являющиеся необходимыми для понимания реализации. Вы можете найти полный исходный код здесь на Github.

Реализация

Исходный код для этого поста можно найти здесь. Наша реализация isolate будет простой программой, которая считывает строку с командой из stdin и клонирует новый процесс, выполняющий её с указанными аргументами. Клонированный процесс с командой будет выполняться в собственном UTS namespace точно также, как мы делали это ранее с unshare . В следующих постах мы увидим, что namespaces не обязательно работают (или хотя бы обеспечивают изоляцию) из коробки и нам нужно будет выполнить некоторую настройку после их создания (но перед реальным запуском команды), чтобы команда действительно выполнялась изолированной.

Эта комбинация создания-настройки namespace потребует некоторого взаимодействия между основным процессом isolate и дочерним процессом запускаемой команды. В результате часть основной работы здесь будет заключаться в настройке связующего канала между обоими процессами — в нашем случае мы будем использовать Linux pipe из-за его простоты.

Нам нужно сделать три вещи:

  1. Создать основной процесс isolate , читающий данные из stdin.
  2. Клонировать новый процесс, который будет запускать команду в новом UTS namespace.
  3. Настроить пайп таким образом, чтобы процесс выполнения команды начинал её запуск только после получения сигнала от основного процесса о завершении настройки namespace.

Вот основной процесс:

Обратите внимание на clone_flags , которые мы передаем в наш вызов clone . Видите, как просто создать процесс в его собственном namespace? Всё, что нам нужно сделать, это установить флаг для типа namespace ( CLONE_NEWUTS флаг соответствует UTS namespace), а Linux позаботится об остальном.

Далее процесс команды ожидает сигнала перед её запуском:

Наконец, мы может попробовать это запустить:

Сейчас isolate — это немногим больше, чем программа, которая просто форкает команду (у нас есть UTS, работающий для нас). В следующем посте мы сделаем еще один шаг, рассмотрев User namespaces заставим isolate выполнять команду в собственном User namespace. Там мы увидим, что на самом деле надо проделать некоторую работу, чтобы иметь пригодный к использованию namespace, в котором может выполняться команда.

namespaces

Последние несколько лет отмечены ростом популярности «контейнерных» решений для ОС Linux. О том, как и для каких целей можно использовать контейнеры, сегодня много говорят и пишут. А вот механизмам, лежащим в основе контейнеризации, уделяется гораздо меньше внимания.

Все инструменты контейнеризации — будь то Docker, LXC или systemd-nspawn,— основываются на двух подсистемах ядра Linux: namespaces и cgroups. Механизм namespaces (пространств имён) мы хотели бы подробно рассмотреть в этой статье.

Начнём несколько издалека. Идеи, лежащие в основе механизма пространств имён, не новы. Ещё в 1979 году в UNIX был добавлен системный вызов chroot() — как раз с целью обеспечить изоляцию и предоставить разработчикам отдельную от основной системы площадку для тестирования. Нелишним будет вспомнить, как он работает. Затем мы рассмотрим особенности функционирования механизма пространств имён в современных Linux-системах.

Chroot(): первая попытка изоляции

Название chroot представляет собой сокращение от change root, что дословно переводится как «изменить корень». С помощью системного вызова chroot() и соответствующей команды можно изменить корневой каталог. Программе, запущенной с изменённым корневым каталогом, будут доступны только файлы, находящиеся в этом каталоге.

Файловая система UNIX представляет собой древовидную иерархию:

chroot

Вершиной этой иерархии является каталог /, он же root. Все остальные каталоги — usr, local, bin и другие, — связаны с ним.

С помощью chroot в систему можно добавить второй корневой каталог, который с точки зрения пользователя ничем не будет отличаться от первого. Файловую систему, в которой присутствует изменённый корневой каталог, можно схематично представить так:

chroot

Файловая система разделена на две части, и они никак не влияют друг на друга. Как работает chroot? Сначала обратимся к исходному коду. В качестве примера рассмотрим реализацию chroot в OC 4.4 BSD-Lite.

Системный вызов chroot описан в файле vfs_syscall.c:


Самое главное происходит в предпоследней строке приведённого нами фрагмента: текущая директория становится корневой.
В ядре Linux системный вызов chroot реализован несколько сложнее (фрагмент кода взят отсюда):


Рассмотрим особенности работы chroot в Linux на практических примерах. Выполним следующие команды:


Ошибка заключается в следующем: не была найдена командная оболочка. Обратим внимание на этот важный момент: с помощью chroot мы создаём новую, изолированную файловую систему, которая не имеет никакого доступа к текущей. Попробуем снова:


После этого выполним следующие команды:


Теперь получилось! Попробуем выполнить в новой файловой системе, например, команду ls:


Причина понятна: в новой файловой системе команда ls отсутствует. Нужно опять копировать исполняемый файл и динамические библиотеки, как это уже было показано выше. В этом и заключается серьёзный недостаток chroot: все необходимые файлы нужно дублировать. Есть у chroot и ряд недостатков с точки зрения безопасности.

Попытки усовершенствовать механизм chroot и обеспечить более надёжную изоляцию предпринимались неоднократно: так, в частности, появились такие известные технологии, как FreeBSD Jail и Solaris Zones.
В ядре Linux изоляция процессов была усовершенствована благодаря добавлению новых подсистем и новых системных вызовов. Некоторые из них мы разберём ниже.

Механизм пространств имён

PID: изоляция PID процессов

Исторически в ядре Linux поддерживалось только одно дерево процессов. Дерево процессов представляет собой иерархическую структуру, подобную дереву каталогов файловой системы.

С появлением механизма namespaces стала возможной поддержка нескольких деревьев процессов, полностью изолированных друг от друга.

При загрузке в Linux сначала запускается процесс с идентификационным номером (PID) 1. В дереве процессов он является корневым. Он запускает другие процессы и службы. Механизм namespaces позволяет создавать отдельное ответвление дерева процессов с собственным PID 1. Процесс, который создаёт такое ответвление, являются частью основного дерева, но его дочерний процесс уже будет корневым в новом дереве.

Процессы в новом дереве никак не взаимодействуют с родительским процессом и даже не «видят» его. В то же время процессам в основном дереве доступны все процессы дочернего дерева. Наглядно это показано на следующей схеме:

PID Namespace

Можно создавать несколько вложенных пространств имён PID: один процесс запускает дочерний процесс в новом пространстве имён PID, a тот в свою очередь порождает новый процесс в новом пространстве и т.п.

Один и тот же процесс может иметь несколько идентификаторов PID (отдельный идентификатор для отдельного пространства имён).

Для создания новых пространств имён PID используется системный вызов clone() c флагом CLONE_NEWPID. С помощью этого флага можно запускать новый процесс в новом пространстве имён и в новом дереве. Рассмотрим в качестве примере небольшую программу на языке C (здесь и далее примеры кода взяты отсюда и незначительно нами изменены):


Скомпилируем и запустим эту программу. По завершении её выполнения мы увидим следующий вывод:


Во время выполнения такой маленькой программы в системе произошло много интересного. Функция clone() создала новый процесс, клонировав текущий, и начала его выполнение. При этом она отделила новый процесс от основного дерева и создала для него отдельное дерево процессов.

Попробуeм теперь изменить код программы и узнать родительский PID с точки зрения изолированного процесса:


Вывод изменённой программы будет выглядет так:


Строка «Родительский PID: 0» означает, что у рассматриваемого нами процесса родительского процесса нет. Внесём в программу ещё одно изменение и уберём флаг CLONE_NEWPID из вызова clone():


Системный вызов clone в этом случае сработал практически так же, как fork() и просто создал новый процесс. Между fork() и clone(), однако, есть существенное отличие, которое следует разобрать детально.

Fork() создаёт дочерний процесс, который представляет копию родительского. Родительский процесс копируется вместе со всем контекстом исполнения: выделенной памятью, открытыми файлами и т.п.

В отличие от fork() вызов clone() не просто создаёт копию, но позволяет разделять элементы контекста выполнения между дочерним и родительским процессами. В приведённом выше примере кода с функцией clone используется аргумент child_stack, который задаёт положение стека для дочернего процесса. Как только дочерний и родительский процессы могут разделять память, дочерний процесс не может выполняться в том же стеке, что и родительский. Поэтому родительский процесс должен установить пространство памяти для дочернего и передать указатель на него в вызове clone(). Ещё один аргумент, используемый с функцией clone() — это флаги, которые указывают, что именно нужно разделять между родительским и дочерним процессами. В приведённом нами примере использован флаг CLONE_NEWPID, который указывает, что дочерний процесс должен быть создан в новом пространстве имён PID. Примеры использования других флагов будут приведены ниже.

Итак, изоляцию на уровне процессов мы рассмотрели. Но это — всего лишь первый шаг. Запущенный в отдельном пространстве имён процесс все равно будет иметь доступ ко всем системным ресурсам. Если такой процесс будет слушать, например, 80-й порт, это этот порт будет заблокирован для всех остальных процессов. Избежать таких ситуаций помогают другие пространства имён.

NET: изоляция сетей

Благодаря пространству имён NET мы можем выделять для изолированных процессов собственные сeтевые интерфейсы. Даже loopback-интерфейс для каждого пространства имён будет отдельным.

Сетевые пространства имён можно создавать с помощью системного вызова clone() с флагом CLONE_NEWNET. Также это можно сделать с помощью iproute2:


Воспользуемся strace и посмотрим, что произошло в системе во время приведённой команды:


Обратим внимание: здесь для создания нового пространстве имён использован системный вызов unshare(), а не уже знакомый нам clone. Unshare() позволяет процессу или треду отделять части контекста исполнения, общие с другими процессами (или тредами).

Как можно помещать процессы в новое сетевое пространство имён?

Во-первых, процесс, создавший новое пространство имён, может порождать другие процессы, и каждый из этих процессов будет наследовать сетевое пространство имён родителя.

Во-вторых, в ядре имеется специальный системный вызов — setns(). С его помощью можно поместить вызывающий процесс или тред в нужное пространство имён. Для этого требуется файловый дескриптор, который на это пространство имён ссылается. Он хранится в файле /proc/<PID процесса>/ns/net. Открыв этот файл, мы можем передать файловый дескриптор функции setns().

Можно пойти и другим путём. При создании нового пространства имён с помощью команды ip создаётся файл в директории /var/run/netns/ (см. в выводе трассировки выше). Чтобы получить файловый дескриптор, достаточно просто открыть этот файл.

Сетевое пространство имён нельзя удалить при помощи какого-либо системного вызова. Оно будет существовать, пока его использует хотя бы один процесс.

MOUNT: изоляция файловой системы

Об изоляции на уровне файловой системы мы уже упоминали выше, когда разбирали системный вызов chroot (). Мы отметили, что системный вызов chroot() не обеспечивает надёжной изоляции. С помощью же пространств имён MOUNT можно создавать полностью независимые файловые системы, ассоциируемые с различными процессами:

MOUNT namespace

Для изоляции файловой системы используется системный вызов clone() c флагом CLONE_NEWNS:


Сначала дочерний процесс «видит» те же точки монтирования, что и родительский. Как только дочерний процесс перенесён в отдельное пространство имён, к нему можно примонтировать любую файловую системы, и это никак не затронет ни родительский процесс, ни другие пространства имён.

Другие пространства имён

Изолированный процесс также может быть помещён в другие пространства имён: UID, IPC и PTS. UID позволяет процессу получать привилегии root в пределах определённого пространства имён. С помощью пространства имён IPC можно изолировать ресурсы для коммуникации между процессами.

UTS используется для изоляции системных идентификаторов: имени узла (nodename) и имени домена (domainame), возвращаемых системным вызовом uname(). Рассмотрим ещё одну небольшую программу:


Вывод этой программы будет выглядеть так:


Как видим, функция child_fn() выводит имя узла, изменяет его, а затем выводит уже новое имя. Изменение происходит только внутри нового пространства имён.

Заключение

В этой статье мы в общих чертах рассмотрели, как работает механизм namespaces. Надеемся, она поможет вам лучше понять принципы работы контейнеров. По традиции приводим ссылки на интересные дополнительные материалы:

Если вы по тем или иным причинам не можете оставлять комментарии здесь — приглашаем в наш блог.

Пространства имен Linux

Что такое пространства имен?

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

Один контейнер не должен иметь возможность получить контроль над ресурсами другого, потому что, если этот контейнер затем скомпрометирован, это может поставить под угрозу всю систему. Этот метод атаки похож на то, как ошибка CPU Meltdown работает; разные потоки процессора должны быть изолированы друг от друга. Точно так же процессы, запущенные в разных виртуальных системах (контейнерах), должны быть изолированы от других контейнеров.

Пространства имен достигают этой изоляции на уровне ядра. Подобно тому, как работает приложение chroot, которое блокирует процесс в другом корневом каталоге, пространства имен разделяют другие аспекты системы. Доступно семь пространств имен:

По умолчанию любой процесс, который вы запускаете, использует глобальные пространства имен, и большинство процессов в вашей системе тоже, если не указано иное.

Работа с пространствами имен

Вы можете использовать lsns command (ls-namespaces) для просмотра текущих пространств имен, которые активны в вашей системе. Эту команду нужно запускать от имени пользователя root, иначе список может быть неполным.

Используйте команду lsns (ls-namespaces) для просмотра текущих пространств имен

Если бы вы работали с контейнерами, этот список был бы намного длиннее. Вы можете вывести этот список в формате JSON с помощью -J flag, который гораздо проще использовать с языком сценариев.

Вы можете изменить свое текущее пространство имен с помощью nsenter утилита. Эта команда позволяет вам «войти» в пространство имен другого процесса, обычно в целях отладки. Фактически он может запускать любую команду в этом пространстве имен, но по умолчанию он просто пытается загрузить оболочку ( /bin/bash обычно).

Вы указываете идентификатор процесса, а затем каждое пространство имен, которое хотите ввести:

Например, при попытке ввести пространство имен монтирования для kdevtmpfs загрузит вас в это пространство имен, но впоследствии потерпит неудачу, потому что не может найти /bin/bash , что на самом деле означает, что он сработал, потому что видимый корневой каталог был изменен.

Попытка ввести пространство имен монтирования для kdevtmpfs загружает вас в это пространство имен, но впоследствии терпит неудачу, потому что не может найти / bin / bash

Если ваш ребенок mnt пространство имен включено /bin/bash , вы можете войти в него и загрузить оболочку. Это можно сделать вручную, но через привязать крепления, который может управлять деревом каталогов и связывать файлы между mnt пространства имен. Это может привести к некоторым интересным вариантам использования, например к созданию двух процессов читать разное содержимое из одного файла.

Чтобы создать новые пространства имен, вам необходимо выполнить ответвление от существующего (обычно глобального) и указать, какие пространства имен вы хотите изменить. Это делается с помощью unshare command, которая запускает команду с новым пространством имен, «не имеющим общего доступа» от мастера.

Чтобы отменить совместное использование пространства имен хоста, используйте:

Если команда оставлена ​​пустой, unshare по умолчанию запускает bash. Это создает новое пространство имен, которое будет отображаться в lsns вывод:

Если команда пуста, по умолчанию unshare запускает bash.

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

Если вы не занимаетесь программированием на очень низком уровне, вам, вероятно, не придется самостоятельно трогать пространства имен. Программы контейнеризации, такие как Docker, будут управлять деталями за вас, и в большинстве случаев, когда вам нужна изоляция процесса, вы должны просто использовать существующий инструмент. Тем не менее, важно понимать, как пространства имен работают в контексте контейнеризации, особенно если вы выполняете низкоуровневую настройку своих контейнеров Docker или выполняете отладку вручную.

Механизмы контейнеризации: namespaces

Последние несколько лет отмечены ростом популярности «контейнерных» решений для ОС Linux. О том, как и для каких целей можно использовать контейнеры, сегодня много говорят и пишут. А вот механизмам, лежащим в основе контейнеризации, уделяется гораздо меньше внимания.

Все инструменты контейнеризации — будь то Docker, LXC или systemd-nspawn,— основываются на двух подсистемах ядра Linux: namespaces и cgroups. Механизм namespaces (пространств имён) мы хотели бы подробно рассмотреть в этой статье.

Начнём несколько издалека. Идеи, лежащие в основе механизма пространств имён, не новы. Ещё в 1979 году в UNIX был добавлен системный вызов chroot() — как раз с целью обеспечить изоляцию и предоставить разработчикам отдельную от основной системы площадку для тестирования. Нелишним будет вспомнить, как он работает. Затем мы рассмотрим особенности функционирования механизма пространств имён в современных Linux-системах.

Chroot(): первая попытка изоляции

Название chroot представляет собой сокращение от change root, что дословно переводится как «изменить корень». С помощью системного вызова chroot() и соответствующей команды можно изменить корневой каталог. Программе, запущенной с изменённым корневым каталогом, будут доступны только файлы, находящиеся в этом каталоге.

Файловая система UNIX представляет собой древовидную иерархию:

chroot

Вершиной этой иерархии является каталог /, он же root. Все остальные каталоги — usr, local, bin и другие, — связаны с ним.
С помощью chroot в систему можно добавить второй корневой каталог, который с точки зрения пользователя ничем не будет отличаться от первого. Файловую систему, в которой присутствует изменённый корневой каталог, можно схематично представить так:

chroot

Файловая система разделена на две части, и они никак не влияют друг на друга. Как работает chroot? Сначала обратимся к исходному коду. В качестве примера рассмотрим реализацию chroot в OC 4.4 BSD-Lite.

Системный вызов chroot описан в файле vfs_syscall.c :

Самое главное происходит в предпоследней строке приведённого нами фрагмента: текущая директория становится корневой.
В ядре Linux системный вызов chroot реализован несколько сложнее (фрагмент кода взят отсюда ):

Рассмотрим особенности работы chroot в Linux на практических примерах.
Выполним следующие команды:

Ошибка заключается в следующем: не была найдена командная оболочка. Обратим внимание на этот важный момент: с помощью chroot мы создаём новую, изолированную файловую систему, которая не имеет никакого доступа к текущей. Попробуем снова:

После этого выполним следующие команды:

Теперь получилось! Попробуем выполнить в новой файловой системе, например, команду ls:

Причина понятна: в новой файловой системе команда ls отсутствует. Нужно опять копировать исполняемый файл и динамические библиотеки, как это уже было показано выше. В этом и заключается серьёзный недостаток chroot: все необходимые файлы нужно дублировать. Есть у chroot и ряд недостатков с точки зрения безопасности.

Попытки усовершенствовать механизм chroot и обеспечить более надёжную изоляцию предпринимались неоднократно: так, в частности, появились такие известные технологии, как FreeBSD Jail и Solaris Zones .
В ядре Linux изоляция процессов была усовершенствована благодаря добавлению новых подсистем и новых системных вызовов. Некоторые из них мы разберём ниже.

Механизм пространств имён

Пространство имён (англ. namespace) — это механизм ядра Linux, обеспечивающий изоляцию процессов друг от друга. Работа по его реализации была начата в версии ядра 2.4.19.
На текущий момент в Linux поддерживается шесть типов пространств имён:

Все эти типы используются современными системами контейнеризации (Docker, LXC и другими) при запуске программ.

PID: изоляция PID процессов

Исторически в ядре Linux поддерживалось только одно дерево процессов. Дерево процессов представляет собой иерархическую структуру, подобную дереву каталогов файловой системы.
С появлением механизма namespaces стала возможной поддержка нескольких деревьев процессов, полностью изолированных друг от друга.

При загрузке в Linux сначала запускается процесс с идентификационным номером (PID) 1. В дереве процессов он является корневым. Он запускает другие процессы и службы. Механизм namespaces позволяет создавать отдельное ответвление дерева процессов с собственным PID 1. Процесс, который создаёт такое ответвление, являются частью основного дерева, но его дочерний процесс уже будет корневым в новом дереве.

Процессы в новом дереве никак не взаимодействуют с родительским процессом и даже не «видят» его. В то же время процессам в основном дереве доступны все процессы дочернего дерева. Наглядно это показано на следующей схеме:

PID Namespace

Можно создавать несколько вложенных пространств имён PID: один процесс запускает дочерний процесс в новом пространстве имён PID, a тот в свою очередь порождает новый процесс в новом пространстве и т.п.

Один и тот же процесс может иметь несколько идентификаторов PID (отдельный идентификатор для отдельного пространства имён).

Для создания новых пространств имён PID используется системный вызов clone() c флагом CLONE_NEWPID. С помощью этого флага можно запускать новый процесс в новом пространстве имён и в новом дереве. Рассмотрим в качестве примере небольшую программу на языке C (здесь и далее примеры кода взяты отсюда и незначительно нами изменены):

Скомпилируем и запустим эту программу. По завершении её выполнения мы увидим следующий вывод:

Во время выполнения такой маленькой программы в системе произошло много интересных вещей. Функция clone() создала новый процесс, клонировав текущий, и начала его выполнение. При этом она отделила новый процесс от основного дерева и создала для него отдельное дерево процессов.

Попробуeм теперь изменить код программы и узнать родительский PID с точки зрения изолированного процесса:

Вывод изменённой программы будет выглядет так:

Строка «Родительский PID: 0» означает, что у рассматриваемого нами процесса родительского процесса нет. Внесём в программу ещё одно изменение и уберём флаг CLONE_NEWPID из вызова clone():

Системный вызов clone в этом случае сработал практически так же, как fork() и просто создал новый процесс. Между fork() и clone(), однако, есть существенное отличие, которое следует разобрать детально.

Fork() создаёт дочерний процесс, который представляет копию родительского. Родительский процесс копируется вместе со всем контекстом исполнения: выделенной памятью, открытыми файлами и т.п.

В отличие от fork() вызов clone() не просто создаёт копию, но позволяет разделять элементы контекста выполнения между дочерним и родительским процессами. В приведённом выше примере кода с функцией clone используется аргумент child_stack, который задаёт положение стека для дочернего процесса. Как только дочерний и родительский процессы могут разделять память, дочерний процесс не может выполняться в том же стеке, что и родительский. Поэтому родительский процесс должен установить пространство памяти для дочернего и передать указатель на него в вызове clone(). Ещё один аргумент, используемый с функцией clone() — это флаги, которые указывают, что именно нужно разделять между родительским и дочерним процессами. В приведённом нами примере использован флаг CLONE_NEWPID, который указывает, что дочерний процесс должен быть создан в новом пространстве имён PID. Примеры использования других флагов будут приведены ниже.

Итак, изоляцию на уровне процессов мы рассмотрели. Но это — всего лишь первый шаг. Запущенный в отдельном пространстве имён процесс все равно будет иметь доступ ко всем системным ресурсам. Если такой процесс будет слушать, например, 80-й порт, это этот порт будет заблокирован для всех остальных процессов. Избежать таких ситуаций помогают другие пространства имён.

NET: изоляция сетей

Благодаря пространству имён NET мы можем выделять для изолированных процессов собственные сeтевые интерфейсы. Даже loopback-интерфейс для каждого пространства имён будет отдельным.

Сетевые пространства имён можно создавать с помощью системного вызова clone() с флагом CLONE_NEWNET. Также это можно сделать с помощью iproute2:

Воспользуемся strace и посмотрим, что произошло в системе во время приведённой команды:

Обратим внимание: здесь для создания нового пространстве имён использован системный вызов unshare() , а не уже знакомый нам clone. Unshare() позволяет процессу или треду отделять части контекста исполнения, общие с другими процессами (или тредами).

Как можно помещать процессы в новое сетевое пространство имён?
Во-первых, процесс, создавший новое пространство имён, может порождать другие процессы, и каждый из этих процессов будет наследовать сетевое пространство имён родителя.

Во-вторых, в ядре имеется специальный системный вызов — setns() . С его помощью можно поместить вызывающий процесс или тред в нужное пространство имён. Для этого требуется файловый дескриптор, который на это пространство имён ссылается. Он хранится в файле /proc/<PID процесса>/ns/net. Открыв этот файл, мы можем передать файловый дескриптор функции setns().

Можно пойти и другим путём. При создании нового пространства имён с помощью команды ip создаётся файл в директории /var/run/netns/ (см. в выводе трассировки выше). Чтобы получить файловый дескриптор, достаточно просто открыть этот файл.

Сетевое пространство имён нельзя удалить при помощи какого-либо системного вызова. Оно будет существовать, пока его использует хотя бы один процесс.

MOUNT: изоляция файловой системы

Об изоляции на уровне файловой системы мы уже упоминали выше, когда разбирали системный вызов chroot (). Мы отметили, что системный вызов chroot() не обеспечивает надёжной изоляции. С помощью же пространств имён MOUNT можно создавать полностью независимые файловые системы, ассоциируемые с различными процессами:

MOUNT namespace

Для изоляции файловой системы используется системный вызов clone() c флагом CLONE_NEWNS:

Сначала дочерний процесс «видит» те же точки монтирования, что и родительский. Как только дочерний процесс перенесён в отдельное пространство имён, к нему можно примонтировать любую файловую системы, и это никак не затронет ни родительский процесс, ни другие пространства имён.

Другие пространства имён

Изолированный процесс также может быть помещён в другие пространства имён: UID, IPC и PTS. UID позволяет процессу получать привилегии root в пределах определённого пространства имён. С помощью пространства имён IPC можно изолировать ресурсы для коммуникации между процессами.
UTS используется для изоляции системных идентификаторов: имени узла (nodename) и имени домена (domainame), возвращаемых системным вызовом uname() . Рассмотрим ещё одну небольшую программу:

Вывод этой программы будет выглядеть так:

Как видим, функция child_fn() выводит имя узла, изменяет его, а затем выводит уже новое имя. Изменение происходит только внутри нового пространства имён.

Заключение

В этой статье мы в общих чертах рассмотрели, как работает механизм namespaces. Надеемся, она поможет вам лучше понять принципы работы контейнеров.
По традиции приводим ссылки на интересные дополнительные материалы:

Рассмотрение механизмов контейнеризации мы обязательно продолжим. В следующей публикации мы расскажем о механизме cgroups.

Читайте также: