Глава 3. Пространства имён

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

В наши дни контейнеры фактически выступают механизмом предоставления облачного программного обеспечения. Они предоставляют быстрое время раскрутки и обладают меньшими накладными расходами по сравнению с виртульными машинами. Позади таких функциональных возможностей имеются определённые очень специфические причины.

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

Контейнеры Linux создаются из трёх основных примитивов ядра Linux:

  • Пространств имён Linux

  • cgroups

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

Некое пространство имён это логическая изоляция внутри самого ядра Linux. Пространство имён контролирует видимость внутри своего ядра. Всё такке управление определяется на уровне самого процесса. Это означает, что некое пространство имён контролирует то, какие именно ресурсы внутри своего ядра можно наблюдать. Представляйте себе ядро Linux как охранника, защищающего такие ресурсы как память ОС, привилегированные инструкции ОС, диски, а также прочие ресурсы, доступ к которым может иметься только из самого ядра. Запускаемые из пространства пользователя приложения способны получать доступ к таким ресурсам исключительно через некие ловушки (прерывания), причём в этом случае само ядро получает такое управление и исполняет данные инструкции от имени соответствующего приложения пространства пользователя. Например, какое- то желающее получить доступ к некому файлу на диске приложение будет обязано делегировать такой вызов в своё ядро через некий системный вызов (который внутренним образом перехватывается в само ядро) в своё ядро Linux и котррый впоследствии исполняет данный запрос от имени соответствующего приложения.

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

Та техника, за счёт которой достигаются подобные песочницы посредством особых структур данных в соответствующем ядре Linux, носит название пространства имён (namespace).

Типы пространств имён

В этом разделе мы поясним различные имеющиеся внутри ядра Linux пространства имён и обсудим как они реализуются внутри самого ядра.

UTS

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

PID

Все процессы внутри этого пространства имён PID обладают различными деревьями процессов. Они имеют некий процесс init с PID 1. Однако, на уровне установленной структуры данных, такой процесс относится к одному глобальному дереву процессов, который видим только с уровня самого хоста. Такие инструменты как ps или непосредственное применение файловой системы /proc изнутри соответствующего пространства имён перечислят лишь те процессы и относящиеся к ним ресурсы, которые относятся к соответствующему дереву процессов внутри рассматриваемого пространства имён.

Mount

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

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


struct vfsmount {
      struct list_head mnt_hash;
      struct vfsmount *mnt_parent;    /* fs в которой мы выполняем монтирование */
      struct dentry *mnt_mountpoint;  /* dentry точки монтирования */
      struct dentry *mnt_root;        /* корень монтируемого дерева*/
      struct super_block *mnt_sb;     /* указатель на суперблок */
      struct list_head mnt_mounts;    /* список потомков,
                                         закреплённых здесь */
      struct list_head mnt_child;     /* и проходящих через их
                                         mnt_child */
      atomic_t mnt_count;
      int mnt_flags;
      char *mnt_devname;              /* название устройства, т.е.
                                         /dev/dsk/hda1 */
      struct list_head mnt_list;
};
 	   

При каждом осуществлении операции монтирования создаётся некая структура vfsmount и заполняются dentry этой точки монтирования, а также dentry самого дерева монтирования. Некая dentry представляет собой структуру данных, которая устанавливает соответствие значения inode его файловому имени.

Помимо монтирования имеется некая привязка монтирования, которая позволяет каталогу (вместо устройства) быть смонтированным в точке монтирования. Такой процесс привязки монтирования имеет результатом создание структуры vfsmount, которая указывает на значение dentry этого каталога.

Контейнеры работают по принципу привязки монтирований. Поэтому, когда для некого контейнера создаётся том, в действительности это некая привязка монтирования какого- то каталога внутри его хоста к точке монтирования внутри файловой системы такого контейнера. Так как это монтирование происходит внутри надлежащего пространства имён, необходимая структура vfsmount пребывает в сфере пространства имён этого монтирования. Это означает, что создавая некую привязку монтирования каталога мы способны выставлять некий том внутри того пространства имйн,которое содержит этот контейнер.

Network

Сетевое пространство имён предоставляет контейнеру некий набор сетевых подсистем. Это подразумевает, что соответствующий процесс внутри этого сетевого пространства имён будет видеть различные сетевые интерфейсы, маршруты и ip таблицы. Оно отделяет сетевые ресурсы своего контейнера от сетевых ресурсов его хоста. Мы изучим его более подробно при рассмотрении примера потока пакетов между двумя контейнерами из одного и того же пространства имён в одном и том же хосте, а также между контейнерами из различных пространств имён в одном и том же хосте.

IPC

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

Cgroup

Это пространство имён ограничивает зону видимости значений cgroup файловой системы на значение той cgroup, к которой относится данный процесс. Без данного ограничения процесс имел бы возможность заглядывать в установленные глобальные cgroup через установленную иерархию /proc/self/cgroup. Это пространство имён действенно создаёт виртуальную среду самой по себе данной cgroup.

Помимо упомянутых здесь пространств имён, на момент написания данных строк в сообществе Linux обсуждается еще одно пространство имён с названием пространства имён time.

Time

Пространство имён time имеет два главных варианта применения:

  • Изменяет значение даты и времени внутри контейнера

  • Выравнивает значение часов для восстанавливаемого из некой контрольной точки контейнера

Имеющееся ядро предоставляет доступ к различным часам: CLOCK_REALTIME, CLOCK_MONOTONIC и CLOCK_BOOTTIME. Последние двое часов являются монотонными, но отправные точки для них не определяются чётко (в настоящее время это время запуска системы, однако POSIX утверждает "с неустановленного момента в прошлом") и различаются для каждой системы. Когда некий контейнер мигрирует из одного узла в другой, все значения часов восстанавливаются в их непротиворечивые состояния. Иначе говоря, они продолжают идти с того самого момента, когда они были сброшены.

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

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

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


/* Предварительное задание участников task_struct (сортированы в алфавитном порядке): */
struct ;audit_context;
struct ;backing_dev_info;
struct ;bio_list;
struct ;blk_plug;
struct ;capture_control;
struct ;cfs_rq;
struct ;fs_struct;
struct ;futex_pi_state;
struct ;io_context;
struct ;mempolicy;
struct ;nameidata;
struct ;nsproxy;
struct ;perf_event_context;
struct ;pid_namespace;
struct ;pipe_inode_info;
struct ;rcu_node;
struct ;reclaim_state;
struct ;robust_list_head;
struct ;root_domain;
struct ;rq;
struct ;sched_attr;
struct ;sched_param;
struct ;seq_file;
struct ;sighand_struct;
struct ;signal_struct;
struct ;task_delay_info;
struct ;task_group;
 	   

Структурой, удерживающей различные пространства имён, которые относятся к такой задаче упроцесса) выступает nsproxy.


struct nsproxy {
       atomic_t count;
       struct uts_namespace *uts_ns;
       struct ipc_namespace *ipc_ns;
       struct mnt_namespace *mnt_ns;
       struct pid_namespace *pid_ns_for_children;
       struct net           *net_ns;
       struct time_namespace *time_ns;
       struct time_namespace *time_ns_for_children;
       struct cgroup_namespace *cgroup_ns;
};
extern struct nsproxy init_nsproxy;
 	   

Приведённая nsproxy содержит восемь структур данных пространств имён. Одной пропущенной является пространство имён самого пользователя, которое является частью структуры данных cred в соответствующей task_struct.

Для помещения задач в конкретные пространства имён имеются три системных вызова. Это clone, unshare и setns. Вызовы clone и setns получают в результате создание объекта nsproxy с последующим добавлением особого пространства имён, необходимого для такой задачи.

Мы обсудим в этой главе сетевое пространство имён. Сетевое пространство имён представлено структурой net. Здесь показана часть этой структуры данных:


struct net {
       /* First cache line can be often dirtied.
        * Do not place read-mostly fields here.
        */
       refcount_t           passive;       /* Для решения когда это сетевое
                                           * пространство имён надлежит освободить.
                                           */

       refcount_t           count;        /* Для решения когда это сетевое
                                           *  пространство имён надлежит отключить.
                                           */
       spinlock_t           rules_mod_lock;

       unsigned int         dev_unreg_count;
 
       unsigned int         dev_base_seq;   /* защищено через rtnl_mutex */
       int                  ifindex;
       spinlock_t          nsid_lock;
       atomic_t            fnhe_genid;

       struct list_head    list;            /* список сетевых пространств имён */
       struct list_head    exit_list;       /* Для привязки вызова выхода из каждой сети
                                            * методы при уничтожении net (
                                            * pernet_ops_rwsem read locked),
                                            * или операций для отмены регистрации каждой сети
                                            * (pernet_ops_rwsem write locked).
                                            */
       struct llist_node   cleanup_list;   /* пространства имён в уничтожаемой строке */

#ifdef CONFIG_KEYS
       struct key_tag          *key_domain; /* Ключ домена тега операции */
#endif
       struct user_namespace   *user_ns;    /* Пространство имён пользователя владельца */
       struct ucounts           *ucounts;
       struct idr               netns_ids;

       struct ns_common    ns;

       struct list_head    dev_base_head;
       struct proc_dir_entry    *proc_net;
       struct proc_dir_entry    *proc_net_stat;

#ifdef CONFIG_SYSCTL
       struct ctl_table_set      sysctls;
#endif

       struct sock            *rtnl;        /* rtnetlink socket */
       struct sock            *genl_sock;

       struct uevent_sock     *uevent_sock;  /* uevent socket */

       struct hlist_head      *dev_name_head;
       struct hlist_head      *dev_index_head;
       struct raw_notifier_head     netdev_chain;
 	   

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


struct netns_ipv4 {
#ifdef CONFIG_SYSCTL
      struct ctl_table_header    *forw_hdr;
      struct ctl_table_header    *frags_hdr;
      struct ctl_table_header    *ipv4_hdr;
      struct ctl_table_header    *route_hdr;
      struct ctl_table_header    *xfrm4_hdr;
#endif
      struct ipv4_devconf        *devconf_all;
      struct ipv4_devconf        *devconf_dflt;
      struct ip_ra_chain __rcu *ra_chain;
      struct mutex         ra_mutex;
#ifdef CONFIG_IP_MULTIPLE_TABLES
      struct fib_rules_ops  *rules_ops;
      bool                       fib_has_custom_rules;
      unsigned int               fib_rules_require_fldissect;
      struct fib_table __rcu    *fib_main;
      struct fib_table __rcu    *fib_default;
#endif
      bool                  fib_has_custom_local_routes;
#ifdef CONFIG_IP_ROUTE_CLASSID
      Int                   fib_num_tclassid_users;
#endif

      struct hlist_head    *fib_table_hash;
      bool                  fib_offload_disabled;
      struct sock          *fibnl;

      struct sock  * __percpu    *icmp_sk;
      struct sock         *mc_autojoin_sk;

      struct inet_peer_base      *peers;
      struct sock  * __percpu    *tcp_sk;
      struct fqdir         *fqdir;
#ifdef CONFIG_NETFILTER
      struct xt_table      *iptable_filter;
      struct xt_table      *iptable_mangle;
      struct xt_table      *iptable_raw;
      struct xt_table      *arptable_filter;
#ifdef CONFIG_SECURITY
      struct xt_table      *iptable_security;
#endif
      struct xt_table      *nat_table;
#endif

      int sysctl_icmp_echo_ignore_all;
      int sysctl_icmp_echo_ignore_broadcasts;
      int sysctl_icmp_ignore_bogus_error_responses;
      int sysctl_icmp_ratelimit;
      int sysctl_icmp_ratemask;
      int sysctl_icmp_errors_use_inbound_ifaddr;

      struct local_ports ip_local_ports;

      int sysctl_tcp_ecn;
      int sysctl_tcp_ecn_fallback;

      int sysctl_ip_default_ttl;
      int sysctl_ip_no_pmtu_disc;
      int sysctl_ip_fwd_use_pmtu;
      int sysctl_ip_fwd_update_priority;
      int sysctl_ip_nonlocal_bind;
      int sysctl_ip_autobind_reuse;
      /* Следует ли нам разрушать выводимые пакеты при изменении устройства маршрута? */
      int sysctl_ip_dynaddr;
 	   

Именно таким образом все необходимые ip таблицы и правила маршрутизации помещаются в сферу действия соответствующего сетевого пространства имён.

Другими относящимися здесь к делу структурами данных выступают net_device (именно таким образом само ядро представляет соответствующую карту/ устройство) и sock (представляемая ядром структура данных сокета). Эти две структуры позволяют самому устройству входить в сферу действия сетевого пространства имён, а также и соответствующему сокету быть представленным в этом пространстве имён. Обе эти структуры могут быть в один и тот же момент времени быть частью только одного пространства имён. При помощи утилиты iproute2 мы способны перемещать такое устройство в другие пространства имён.

Вот некоторые команды пространства пользователя для обработки имеющихся сетевых пространств имён:

  • Ip netns add testns: Добавляет сетевое пространство имён

  • Ip netns del testns: Удаляет обозначенное пространство имён

  • Ip netns exec testns sh: Исполняет оболочку внутри указанного testns пространства имён

Добавление устройства в Пространство имён

Прежде всего создадим пару устройств veth (эти устройства могут применяться для соединения двух пространств имён):


ip link add veth0 type veth peer name veth1
		

Затем добавим один конец созданной пары veth в сетевое пространство имён testns:


ip link set veth1 netns testns
		

Другой конец (veth0) пребывает в пространстве имён самого хоста и потому весь отправляемый в veth0 обмен завершается в veth1 из пространства имён testns.

Допустим мы запускаем некий HTTP сервер в пространстве имён testns, что означает, что сокет ожидания относится к сфере действий пространства имён testns, как это пояснялось ранее в нашей структуре данных sock. Поэтому подлежащий доставке в соответствующие IP и порт приложения внутри пространства имён testns пакет TCP будет доставлен в необходимый сокет, относящийся к сфере действий данного пространства имён.

Именно таким образом имеющееся ядро выполняет виртуализацию самой операционной системы и различных подсистем, таких как сетевая среда, IPC, монтирования и тому подобное.

Выводы

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