Le namespace cgroup ne sera pas le dernier de la lignée

Magazine
Marque
GNU/Linux Magazine
Numéro
258
Mois de parution
juillet 2022
Spécialité(s)


Résumé

Voici le dernier opus de cette longue série d’articles consacrée aux namespaces de Linux. Il nous reste à décrire le namespace cgroup, mais aussi les nouveaux namespaces en préparation pour les prochaines moutures de Linux. Nous finirons avec la gestion de la remontée des informations de plantage.


Body

Le namespace cgroup (cgroup_ns) est certes le dernier-né, mais nous verrons que Linux nous réserve des namespaces additionnels dans ses prochaines moutures, tant les besoins en la matière sont importants pour la virtualisation. Nous achèverons cette série d’articles avec la gestion de la remontée des informations de plantage qui souffre de quelques manques, mais pour lesquels des palliatifs existent.

Le code de cet article est disponible sur https://github.com/Rachid-Koucha/linux_ns.git.

1. Le namespace cgroup

Le cgroup_ns limite la vue sur l’arborescence des cgroups [1] en faisant du niveau où le processus se trouve, suite à un appel à unshare() ou clone() avec le drapeau CLONE_NEWCGROUP, la racine des cgroups pour ce processus.

Une entrée est dédiée à ce namespace dans le manuel en ligne : man 7 cgroup_namespaces.

L’intérêt majeur de ce namespace est de limiter la vue à un sous-répertoire dans les hiérarchies des cgroups. Listons les hiérarchies des cgroups du shell courant dans un terminal :

$ cat /proc/$$/cgroup
12:perf_event:/
11:cpuset:/
10:cpu,cpuacct:/
9:devices:/user.slice
8:blkio:/
7:hugetlb:/
6:pids:/user.slice/user-1000.slice/user@1000.service
[...]

La racine est dans le répertoire /sys/fs/cgroup. Créons un sous-répertoire nommé par exemple « sub » dans le contrôleur cpuset et redéfinissons l’ensemble des CPU qu’il autorise en le passant de 0-3 dans le cgroup racine (le PC utilisé ici a quatre cœurs) à 0-1 :

# cat /sys/fs/cgroup/cpuset/cpuset.cpus
0-3
# mkdir /sys/fs/cgroup/cpuset/sub
# echo 0 > /sys/fs/cgroup/cpuset/sub/cpuset.mems
# echo 0-1 > /sys/fs/cgroup/cpuset/sub/cpuset.cpus

Migrons le shell d’un super utilisateur d’un autre terminal dans ce nouveau cgroup (p. ex. 13437). Et affichons les chemins de ses cgroups pour vérifier qu’il est bien dans le répertoire /sys/fs/cgroup/cpuset/sub :

# echo $$
13437
# echo 13437 > /sys/fs/cgroup/cpuset/sub/cgroup.procs
# cat /sys/fs/cgroup/cpuset/sub/cgroup.procs
13437
# cat /proc/13437/cgroup
12:perf_event:/
11:cpuset:/sub
10:cpu,cpuacct:/
9:devices:/user.slice
8:blkio:/
7:hugetlb:/
6:pids:/user.slice/user-1000.slice/user@1000.service
5:rdma:/
[...]

Dans le terminal du shell 13437, exécutons (au sens littéral, car ici on écrase le shell courant avec la built-in exec !) la commande unshare avec l’option -C pour entrer dans un nouveau cgroup_ns. Cela a pour conséquence de remplacer le shell courant par le programme unshare. Ce dernier appelle unshare(CLONE_NEWCGROUP) afin d’entrer dans un nouveau cgroup_ns et se fait remplacer par un nouveau shell. Ainsi, l’identifiant de processus ne change jamais, comme indiqué dans la figure 1.

figure 01 unshare cg-s

Fig. 1 : Exécution dans un nouveau cgroup_ns.

Affichons de nouveau les chemins de ses cgroups :

# echo $$
13437
# exec unshare -C
# echo $$
13437
# cat /proc/$$/cgroup
12:perf_event:/
11:cpuset:/
10:cpu,cpuacct:/
9:devices:/
8:blkio:/
7:hugetlb:/
6:pids:/
5:rdma:/
[...]

On constate que le shell 13437 considère /sys/fs/cgroup/cpuset/sub comme la racine du sous-système cpuset dans le nouveau cgroup_ns. Si nous affichons la liste des chemins des cgroups d’un processus qui n’est pas dans son cgroup_ns (ici, 13217) :

# cat /proc/13217/cgroup
12:perf_event:/
11:cpuset:/..
10:cpu,cpuacct:/
9:devices:/
8:blkio:/
7:hugetlb:/
6:pids:/
5:rdma:/
[...]

Le /.. affiché montre que le processus en question n’est pas dans le cgroup_ns courant, mais situé au-dessus de la racine du contrôleur cpuset du cgroup_ns courant.

Vu d’un autre terminal (c.-à-d. du cgroup_ns initial), la liste des chemins des cgroups continue bien entendu à indiquer que le processus 13437 est dans le sous-répertoire sub :

# cat /proc/13437/cgroup
12:perf_event:/
11:cpuset:/sub
10:cpu,cpuacct:/
9:devices:/user.slice
8:blkio:/
7:hugetlb:/
[...]

Cependant pour parfaire l’isolation du processus 13437, il ne faut pas que son fichier mountinfo montre qu’il n’est pas à la racine :

# echo $$
13437
# cat /proc/$$/mountinfo | grep cpuset
47 32 0:42 /.. /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:25 - cgroup cgroup rw,cpuset,clone_children

De plus, bien que son fichier /proc/$$/cgroup montre / pour le sous-système cpuset, rien ne l’empêche de migrer vers le cgroup père :

# echo $$ > /sys/fs/cgroup/cpuset/cgroup.procs
# cat /sys/fs/cgroup/cpuset/cgroup.procs | grep $$
13437
# cat /sys/fs/cgroup/cpuset/sub/cgroup.procs
#

Il est donc conseillé de coupler un cgroup_ns à un mount_ns pour remonter les sous-systèmes au niveau de la racine. Remettons le processus 13437 dans son cgroup, puis faisons-le entrer dans un nouveau mount_ns (option -m de la commande unshare) dans lequel on va effectuer l’opération de remontage de l’arborescence cpuset :

# echo $$ > /sys/fs/cgroup/cpuset/sub/cgroup.procs
# cat /sys/fs/cgroup/cpuset/sub/cgroup.procs
13437
# exec unshare -m# mount –make-rslave /
# umount /sys/fs/cgroup/cpuset
# mount -t cgroup -o cpuset cpuset /sys/fs/cgroup/cpuset
# cat /proc/$$/mountinfo | grep cpuset
515 503 0:42 / /sys/fs/cgroup/cpuset rw,relatime - cgroup cpuset rw,cpuset,clone_children
# cat /proc/$$/cgroup
12:perf_event:/
11:cpuset:/
10:cpu,cpuacct:/
9:devices:/[...]

L’isolation est maintenant complète, car le shell 13437 ne pourra plus se positionner dans les niveaux père du sous-système cpuset : il ne peut plus les voir.

Un gestionnaire de conteneur comme LXC réalise cette opération pour tous les sous-systèmes afin de faire croire à l’image qu’il fait tourner qu’elle se situe à la racine de l’arborescence des cgroups (cf. encadré « Les cgroups dans LXC »).

Les cgroups dans LXC

Le nom de la sous-arborescence LXC dans les cgroups est décidé par défaut à la configuration de la génération du package avec l’option --with-cgroup-pattern passée au script « configure » ou dans la configuration générale de LXC avec le paramètre lxc.cgroup.pattern. Dans LXC 2.1.1, la valeur par défaut est le schéma lxc/%n%n désigne le nom du conteneur. Dans LXC 3.0.4, c’est lxc.payload/%n.

Par la suite, lxc-start utilise ce schéma afin de créer les répertoires /sys/fs/cgroup/<sous-système>/<schéma> dans l’arborescence des cgroups. Puis il crée le premier processus du conteneur (init) dans ses nouveaux namespaces (pid, network…) sauf pour le cgroup_ns qui reste celui de l’hôte. Puis lxc-start migre le processus dans les cgroups créés en écrivant son PID dans les fichiers /sys/fs/cgroup/<sous-système>/<schéma>/cgroup.procs. Enfin, avec une synchronisation père/fils, lxc-start indique au processus du conteneur d’appeler unshare(CLONE_NEWCGROUP). Cela a pour conséquence de faire de /sys/fs/cgroup/<sous-système>/<schéma>, la racine des cgroups pour le conteneur, car le processus init a été migré à cet emplacement par son père. Vu du côté hôte, le processus du conteneur est bien localisé dans le répertoire dédié dans les cgroups (ici, nous sommes avec LXC 2.1.1) :

# ./lxc-start2 bbox
# lxc-ls -f        
NAME    STATE   AUTOSTART GROUPS IPV4       IPV6
bbox    RUNNING 0         -      10.0.3.203 -
# ./lxc-pid bbox
10522
# cat /proc/10522/cgroup
12:cpuset:/lxc/bbox
11:rdma:/lxc/bbox
[...]
2:cpu,cpuacct:/lxc/bbox
1:name=systemd:/lxc/bbox
0::/lxc/bbox

2. Namespaces additionnels

Au cours des années, le nombre de namespaces s’est accru. Alors qu’on semble avoir fait le tour du sujet, de nouvelles demandes se font sentir pour pousser toujours plus loin l’isolation des ressources du système. Cependant, on arrive à certaines limites. Notamment, le nombre de bits disponibles dans l’entier 32-bits passé à la fonction clone() ou unshare() pour définir de nouveaux drapeaux de namespaces. Il suffit de regarder les macro-définitions dans <linux/sched.h> pour voir qu’ils sont tous utilisés :

/*
* cloning flags:
*/
#define CSIGNAL        0x000000ff    /* signal mask to be sent at exit */
#define CLONE_VM       0x00000100    /* set if VM shared between processes */
#define CLONE_FS       0x00000200    /* set if fs info shared between processes */
#define CLONE_FILES    0x00000400    /* set if open files shared between processes */
#define CLONE_SIGHAND  0x00000800    /* set if signal handlers and blocked signals shared */
#define CLONE_PIDFD    0x00001000    /* set if a pidfd should be placed in parent */
#define CLONE_PTRACE   0x00002000    /* set if we want to let tracing continue on the child too */
[...]
#define CLONE_NEWNS    0x00020000    /* New mount namespace group */
[...]
#define CLONE_NEWCGROUP   0x02000000    /* New cgroup namespace */
#define CLONE_NEWUTS      0x04000000    /* New utsname namespace */
#define CLONE_NEWIPC      0x08000000    /* New ipc namespace */
#define CLONE_NEWUSER     0x10000000    /* New user namespace */
#define CLONE_NEWPID      0x20000000    /* New pid namespace */
#define CLONE_NEWNET      0x40000000    /* New network namespace */
#define CLONE_IO             0x80000000    /* Clone io context */

Des solutions existent cependant : créer une nouvelle version de clone(), fusionner des namespaces, ajouter des ressources à isoler dans les namespaces existants, inventer des services dédiés à la création de chaque type de namespace...

2.1 Le namespace time

Le but est de permettre aux conteneurs d’avoir leur propre heure système afin par exemple de faciliter les migrations (« checkpoint/restore ») d’un système à l’autre. En septembre 2018, une première proposition de modification du noyau a été faite dans ce sens [2]. D’après [3], ce nouveau namespace apparaîtra dans la version 5.6 du noyau Linux. Il fonctionnera de manière similaire au pid_ns dans le sens où l’association au namespace sera faite par les processus fils du processus créateur.

Sur GitHub (par exemple https://github.com/torvalds/linux), on peut d’ores et déjà voir les modifications associées dans la configuration du noyau. Le paramètre CONFIG_TIME_NS a été ajouté dans init/Kconfig :

config TIME_NS
  bool "TIME namespace"
  depends on GENERIC_VDSO_TIME_NS
  default y
  help
    In this namespace boottime and monotonic clocks can be set. The time will keep going with the
    same pace.

Le fichier d’en-tête include/uapi/linux/sched.h contient la définition du drapeau associé au time_ns :

#define CLONE_NEWTIME 0x00000080 /* New time namespace */

On aura noté que le bit utilisé empiète sur la définition de CSIGNAL vue plus haut. Afin d’éviter les ambiguïtés, ce drapeau ne pourra être utilisé qu’avec le nouveau service clone3() présenté dans [4]. Ce dernier adopte une interface plus extensible que son aîné, car il reçoit en paramètres la structure clone_args et la taille de cette dernière. Si des champs sont ajoutés dans le futur (toujours à la fin !), le noyau saura s’adapter en fonction de la valeur du second paramètre. Voici cette structure dans le fichier d’en-tête include/uapi/linux/sched.h :

struct clone_args {
        __aligned_u64 flags;
        __aligned_u64 pidfd;
        __aligned_u64 child_tid;
        __aligned_u64 parent_tid;
        __aligned_u64 exit_signal;
        __aligned_u64 stack;
        __aligned_u64 stack_size;
        __aligned_u64 tls;
        __aligned_u64 set_tid;
        __aligned_u64 set_tid_size;
};

Dans les nouvelles moutures de la GLIBC, clone3() est défini comme suit :

long clone3(struct clone_args *cl_args, size_t size);

Le namespace est décrit par la structure time_namespace définie dans le fichier d’en-tête include/linux/time_namespace.h :

struct time_namespace {
    struct kref kref;
    struct user_namespace *user_ns;
    struct ucounts *ucounts;
    struct ns_common ns;
    struct timens_offsets offsets;
    struct page *vvar_page;
    /* If set prevents changing offsets after any task joined namespace. */
    bool frozen_offsets;
} __randomize_layout;

Le champ offsets structure timens_offsets contient les offsets à additionner à l’heure système pour obtenir une heure vue du namespace :

struct timens_offsets {
    struct timespec64 monotonic;
    struct timespec64 boottime;
};

Le champ vvar_page sert à optimiser les appels système (par exemple gettimeofday(), clock_gettime(), etc.) relatifs au temps, car ils sont fréquemment utilisés par les programmes en espace utilisateur [5].

La structure nsproxy, avec laquelle le descripteur de tâche task_struct garde des références sur les descripteurs des namespaces auxquelles une tâche est associée, se voit augmentée de deux pointeurs pour le nouveau namespace. Le pointeur time_ns référence le descripteur de namespace de la tâche, tandis que sur le même modèle de fonctionnement que les pid_ns, le pointeur time_ns_for_children référence le time_ns pour les processus fils :

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;
};

La liste des numéros d’inodes des namespaces initiaux se voit augmentée d’une entrée dans include/linux/proc_ns.h :

/*
* We always define these enumerators
*/
enum {
    PROC_ROOT_INO        = 1,
    PROC_IPC_INIT_INO    = 0xEFFFFFFFU,
    PROC_UTS_INIT_INO    = 0xEFFFFFFEU,
    PROC_USER_INIT_INO    = 0xEFFFFFFDU,
    PROC_PID_INIT_INO    = 0xEFFFFFFCU,
    PROC_CGROUP_INIT_INO    = 0xEFFFFFFBU,
    PROC_TIME_INIT_INO    = 0xEFFFFFFAU,
};

Le paquet util-linux est aussi prêt pour gérer ce nouveau namespace. Par exemple, dans le code source de la commande lsns (https://github.com/karelzak/util-linux/blob/master/sys-utils/lsns.c), les modifications ont été apportées en début mars 2020 pour prendre en compte le nouveau namespace :

static char *ns_names[] = {
[LSNS_ID_MNT] = "mnt",
[LSNS_ID_NET] = "net",
[LSNS_ID_PID] = "pid",
[LSNS_ID_UTS] = "uts",
[LSNS_ID_IPC] = "ipc",
[LSNS_ID_USER] = "user",
[LSNS_ID_CGROUP] = "cgroup",
[LSNS_ID_TIME] = "time"
};

2.2 Le namespace syslog

Il ne s’agit pas ici du service syslog() de la librairie C (man 3 syslog) qui dialogue avec le daemon (r)syslogd, mais de l’appel système syslog() (man 2 syslog) qui accède au journal de bord du noyau accessible via /dev/kmsg. Pour éviter les confusions entre le service en librairie et l’appel système, la fonction klogctl() de la librairie C enrobe ce dernier. Il s’agit donc ici des messages affichés par la fameuse fonction printk() et stockés dans une mémoire tampon circulaire du noyau. L’utilitaire dmesg du shell affiche son contenu. La figure 2 résume tout cela.

figure 02 journal-s

Fig. 2 : Le journal en espace noyau et utilisateur.

Jusqu’à maintenant, les affichages résultant des appels printk() du noyau sont visibles sur hôte et dans tous les conteneurs. Cela peut gêner l’administration des conteneurs pour lesquels les opérateurs n’ont pas forcément accès au système hôte. Cela peut aussi poser des problèmes de sécurité avec une fuite d’informations critiques de l’hôte vers les conteneurs ou d’un conteneur à l’autre.

Par exemple, un appel à dmesg dans un conteneur LXC busybox affiche le contenu du journal système commun au système hôte et à tous les conteneurs :

# lxc-console -n bbox -t 0
[...]
bbox# dmesg
[    0.000000] Linux version 5.3.0-40-generic (buildd@lcy01-amd64-026) (gcc version 9.2.1 20191008 (Ubuntu 9.2.1-9ubuntu2)) #32-Ubuntu SMP Fri Jan 31 20:24:34 UTC 2020 (Ubuntu 5.3.0-40.32-generic 5.3.18)
[    0.000000] Command line: BOOT_IMAGE=/boot/vmlinuz-5.3.0-40-generic root=UUID=556f4290-5880-4c5d-8725-94a4cf7c0690 ro quiet splash vt.handoff=7
[...]
[13878.824203] lxcbr0: port 1(vethJI7KGL) entered forwarding state
[13878.824313] IPv6: ADDRCONF(NETDEV_CHANGE): lxcbr0: link becomes ready

L’idée d’un syslog_ns est d’isoler les affichages de printk() de chaque conteneur. L’article [6] explique les problèmes liés à l’implémentation et explore notamment certaines solutions pour résoudre le cas des appels printk() hors contexte de tâche (p. ex. contexte d’interruption), alors que les namespaces normalement liés aux tâches sont retrouvés par le descripteur de la tâche en cours (c.-à-d. current→nsproxy comme indiqué dans nos articles [7] et [8] dédiés à l’implémentation dans le noyau). Jusqu’à aujourd’hui, aucune solution n’a été mise dans la branche principale de Linux.

Faute de mieux, la valeur 1 dans le fichier /proc/sys/kernel/dmesg_restrict est un pis-aller afin de limiter la vision du journal du noyau aux utilisateurs privilégiés. Si nous faisons l’essai sur hôte :

# echo 1 > /proc/sys/kernel/dmesg_restrict
# cat /proc/sys/kernel/dmesg_restrict
1

Un utilisateur non privilégié ne pourra plus voir le contenu du journal système :

$ id
uid=1000(rachid) gid=1000(rachid)…
$ dmesg
dmesg: read kernel buffer failed: Operation not permitted

Ce fichier étant commun au système hôte et aux conteneurs, rien ne change dans un conteneur LXC busybox privilégié, car il s’exécute dans le user_ns initial. Son utilisateur root est par conséquent le super utilisateur du système hôte :

bbox# cat /proc/sys/kernel/dmesg_restrict
1
bbox# dmesg | more
[    0.000000] Linux version 5.3.0-40-generic (buildd@lcy01-amd64-026) (gcc version 9.2.1 20191008 (Ubuntu 9.2.1-9ubuntu2)) #32-Ubuntu SMP Fri Jan 31 20:24:34 UTC 2020 (Ubuntu 5.3.0-40.32-generic 5.3.18)
[    0.000000] Command line: BOOT_IMAGE=/boot/vmlinuz-5.3.0-40-generic root=UUID=556f4290-5880-4c5d-8725-94a4cf7c0690 ro quiet splash vt.handoff=7
[...]

Mais dans un conteneur non privilégié (c.-à-d. avec son propre user_ns !) comme notre session shns3 maintes fois utilisée jusqu’à maintenant, même l’utilisateur privilégié sera restreint, car root est mappé sur un utilisateur non privilégié du système hôte (c.-à-d. UID/GID égaux à 1000) :

$ ./shns3 -u '0 1000 1' -u '1 100000 100' -g '0 1000 1' -g '1 100000 100' -d 1
DEBUG_1 (main#358): New namespace 'ipc'
DEBUG_1 (main#358): New namespace 'pid'
DEBUG_1 (main#358): New namespace 'net'
DEBUG_1 (main#358): New namespace 'user'
DEBUG_1 (main#358): New namespace 'uts'
DEBUG_1 (main#358): New namespace 'cgroup'
DEBUG_1 (main#358): New namespace 'mnt'
DEBUG_1 (main#409): Running 'newuidmap 10034 0 1000 1 1 100000 100'...
DEBUG_1 (main#424): Running 'newgidmap 10034 0 1000 1 1 100000 100'...
# id           
uid=0(root) gid=0(root) groups=0(root),65534(nogroup)
# cat /proc/sys/kernel/dmesg_restrict
1
# dmesg
dmesg: read kernel buffer failed: Operation not permitted

2.3 Le namespace device

Les périphériques sont liés au user_ns initial. Et pour les interfaces réseau, ils sont même liés au net_ns initial [9]. Même si certaines interfaces peuvent migrer d’un net_ns à l’autre. Lors de l’étude des net_ns, nous avons vu le concept de SR-IOV qui permet de partager un même périphérique entre plusieurs namespaces. Le champ d’application reste cependant limité à certains types de périphériques répondant à la norme PCI Express.

Des solutions ont été proposées afin de créer un namespace device [10] ou de virtualiser devtmpfs, le pseudo système de fichiers qui gère les périphériques, de sorte à pouvoir le monter dans différents user_ns [11]. Mais cela n’a visiblement pas été retenu.

C. Brauner (l’un des développeurs de LXC) propose une autre direction avec la virtualisation des périphériques [12] en utilisant le protocole netlink (cf. man 7 netlink) utilisé pour le dialogue entre l’espace noyau et l’espace utilisateur.

3. Durée de vie des namespaces

Le manuel man 7 namespaces indique qu’un namespace disparaît lorsqu’il n’a plus de processus associé, sauf sous certaines conditions :

  • le namespace est un namespace initial ;
  • la cible d’un lien symbolique est ouverte sur le namespace dans /proc/<pid>/ns ;
  • le namespace est hiérarchique (c.-à-d. pid_ns ou user_ns) et il a des namespaces fils actifs ;
  • le namespace est un user_ns et il est propriétaire de namespaces actifs ;
  • le namespace est un pid_ns et un processus référence le namespace via le lien symbolique /proc/<pid>/ns/pid_for_children ;
  • le namespace est un ipc_ns et un système de fichiers pour ses queues de message POSIX est monté ;
  • le namespace est un pid_ns et un système de fichiers de type proc le référence.

4. Gestion du plantage de processus

4.1 Nommage du fichier core

Par défaut, les informations générées lors du plantage d’un processus sont stockées dans un fichier nommé core. Il est possible de définir un autre nom respectant un schéma de nommage stocké dans /proc/sys/kernel/core_pattern (cf. man 5 core). Ce fichier est global au noyau : son contenu concerne tous les namespaces. En d'autres termes, il n’est pas virtualisé. Les spécificateurs suivants sont des raccourcis (placeholders) remplacés par les valeurs associées au moment du crash :

%% a single % character
%c core file size soft resource limit of crashing process
%d dump mode--same as value returned by prctl(2) PR_GET_DUMPABLE
%e executable filename (without path prefix)
%E pathname of executable, with slashes ('/') replaced by exclamation marks ('!')
%g real GID of dumped process
%h hostname (same as nodename returned by uname(2))
%i TID of thread that triggered core dump, as seen in the PID namespace in which the thread resides
%I TID of thread that triggered core dump, as seen in the initial PID namespace
%p PID of dumped process, as seen in the PID namespace in which the process resides
%P PID of dumped process, as seen in the initial PID namespace
%s number of signal causing dump
%t time of dump, expressed as seconds since the Epoch, 1970-01-01 00:00:00 +0000 (UTC)
%u real UID of dumped process

Le code source de Linux 5.3.0 qui gère la substitution des spécificateurs se trouve dans fs/coredump.c. C’est géré par une instruction switch dans la fonction interne format_corename() :

[...]
/* Double percent, output one percent */
case '%':
    err = cn_printf(cn, "%c", '%');
    break;
/* pid */
case 'p':
    pid_in_pattern = 1;
    err = cn_printf(cn, "%d",
              task_tgid_vnr(current));
    break;
/* global pid */
case 'P':
    err = cn_printf(cn, "%d",
              task_tgid_nr(current));
    break;
case 'i':
    err = cn_printf(cn, "%d",
              task_pid_vnr(current));
    break;
case 'I':
    err = cn_printf(cn, "%d",
              task_pid_nr(current));
    break;
/* uid */
case 'u':
    err = cn_printf(cn, "%u",
            from_kuid(&init_user_ns,
                 cred->uid));
    break;
/* gid */
case 'g':
    err = cn_printf(cn, "%u",
            from_kgid(&init_user_ns,
                 cred->gid));
    break;
case 'd':
    err = cn_printf(cn, "%d",
        __get_dumpable(cprm->mm_flags));
    break;
/* signal that caused the coredump */
case 's':
    err = cn_printf(cn, "%d",
            cprm->siginfo->si_signo);
    break;
/* UNIX time of coredump */
case 't': {
    time64_t time;
    time = ktime_get_real_seconds();
    err = cn_printf(cn, "%lld", time);
    break;
}
/* hostname */
case 'h':
    down_read(&uts_sem);
    err = cn_esc_printf(cn, "%s",
              utsname()->nodename);
    up_read(&uts_sem);
    break;
/* executable */
case 'e':
    err = cn_esc_printf(cn, "%s", current->comm);
    break;
case 'E':
    err = cn_print_exe_file(cn);
    break;
/* core limit size */
case 'c':
    err = cn_printf(cn, "%lu",
              rlimit(RLIMIT_CORE));
    break;
[...]

Les spécificateurs qui nous intéressent dans le contexte des namespaces sont :

  • %p et %P : identifiants du thread principal du processus respectivement dans le pid_ns courant et le pid_ns initial. On notera l’utilisation respective des « helpers » task_tgid_vnr() et task_tid_nr() vus lors de l’étude des sources du noyau [8] ;
  • %i et %I : identifiants du thread courant respectivement dans le pid_ns courant et le pid_ns initial. Si le processus est monothreadé, ce sont respectivement les mêmes valeurs que %p et %P. On notera l’utilisation respective des « helpers » task_pid_vnr() et task_pid_nr() vus lors de l’étude des sources du noyau [8] ;
  • %u et %g : identifiants d’utilisateur et groupe du processus dans le user_ns initial. D’où la référence à la structure init_user_ns vue lors de l’étude des structures de données dans le noyau [7] ;
  • %h : le manuel donne la définition laconique « hostname (same as nodename returned by uname(2)) ». Est-ce le nom de l'hôte dans l’uts_ns initial ou celui de l’uts_ns courant ? Le fait qu’il soit en minuscule, on subodore que c’est le second choix conformément à la méthodologie de nommage utilisée pour les identifiants de processus et thread. Voyons cela de plus près...

Le code source précédent montre que le spécificateur est substitué par utsname()->nodename. La fonction utsname() est définie dans include/linux/utsname.h comme suit :

static inline struct new_utsname *utsname(void)
{
    return &current->nsproxy->uts_ns->name;
}

C’est le résultat du déréférencement du champ nsproxy à partir du pointeur current. Ce dernier est le pointeur sur la tâche en cours d’exécution dans le noyau. Autrement dit, c’est la tâche en cours de plantage. Cela fait de current->nsproxy->uts_ns->name, donc du spécificateur %h, le nom d’hôte dans l’uts_ns de cette tâche, donc l’uts_ns courant.

Mettons en pratique un schéma de nommage du fichier core :

# echo "%e_%h_%g_%u_%p_%P_%i_%I.core" > /proc/sys/kernel/core_pattern
# cat /proc/sys/kernel/core_pattern
%e_%h_%g_%u_%p_%P_%i_%I.core

Introduisons le programme multi-th qui lance autant de threads que le nombre passé en argument. Le thread principal (main) ainsi que les secondaires (th_main) se mettent ensuite en attente sur pause() :

static pthread_t tid[256];
 
static void *th_main(void *param)
{
int i = (int)((pthread_t *)param - tid);
 
  printf("Thread %d is running...\n", i);
 
  pause();
 
  printf("Thread %d is terminating...\n", i);
 
  return NULL;
} // th_main
 
int main(int ac, char *av[])
{
[...]
  nth = atoi(av[1]);
[...]
  printf("Creating %d threads\n", nth);
 
  for (i = 0; i < nth; i ++)
  {
    p = (void *)&(tid[i]);
    rc = pthread_create(&(tid[i]), NULL, th_main, p);
[...]
  } // End for
 
  printf("Waiting...\n");
 
  pause();
 
  printf("Exiting...\n");
 
  return 0;
 
} // main

Lançons le programme shns3. Par défaut, il lance un shell dans de nouveaux namespaces pour chaque type. Configurons la taille des fichiers core si nécessaire ainsi que le système de fichiers /proc (pour avoir des identifiants de processus cohérents avec le pid_ns dans le résultat de la commande ps) et le nom de l’hôte.

$ ./shns3 -u '0 1000 1' -u '1 100000 100' -g '0 1000 1' -g '1 100000 100' -d 1 -s /bin/bash
DEBUG_1 (main#358): New namespace 'ipc'
DEBUG_1 (main#358): New namespace 'pid'
DEBUG_1 (main#358): New namespace 'net'
DEBUG_1 (main#358): New namespace 'user'
DEBUG_1 (main#358): New namespace 'uts'
DEBUG_1 (main#358): New namespace 'cgroup'
DEBUG_1 (main#358): New namespace 'mnt'
DEBUG_1 (main#409): Running 'newuidmap 18199 0 1000 1 1 100000 100'...
DEBUG_1 (main#424): Running 'newgidmap 18199 0 1000 1 1 100000 100'...
# ulimit -c
0
# ulimit -c unlimited
# ulimit -c
unlimited
# mount --make-rslave /
# mount -t proc proc /proc
# hostname
rachid-pc
# hostname new-pc
# hostname
new-pc

Faisons une digression sur une faiblesse de Bash. Au démarrage, ce dernier obtient le nom de l’hôte avec un appel à gethostname() et mémorise le résultat une fois pour toutes dans ses contextes. Si par la suite nous changeons le nom de l’hôte, il n’est pas pris en compte, car Bash continue à utiliser la valeur stockée dans ses contextes. Le spécificateur \h dans la variable PS1 ne change donc pas de valeur de nom d’hôte. Pour contourner ce problème, on réexécute Bash pour qu’il relance son initialisation (sans relire le fichier .bashrc avec l’option --norc au cas où ce dernier change PS1) afin que le nouveau nom de l’hôte soit pris en compte dans le prompt (variable PS1) :

# export PS1='\h# '
rachid-pc# exec /bin/bash --norc
new-pc#

Puis lançons le programme multi-th en arrière-plan :

new-pc# ps
  PID TTY          TIME CMD
    1 pts/1    00:00:00 sh
    5 pts/1    00:00:00 ps
new-pc# ./multi-th 3 &
new-pc# Creating 3 threads
Thread 0 is running...
Thread 1 is running...
Waiting...
Thread 2 is running...
new-pc# ps -eT
  PID SPID TTY          TIME CMD
    1     1 pts/1    00:00:00 sh
   28    28 pts/1    00:00:00 multi-th
   28    29 pts/1    00:00:00 multi-th
   28    30 pts/1    00:00:00 multi-th
   28    31 pts/1    00:00:00 multi-th
   32    32 pts/1    00:00:00 ps

Simulons un plantage de l’un des threads de multi-th en lui envoyant le signal SIGSEGV :

new-pc# kill -SEGV 30
[1] + Segmentation fault (core dumped) ./multi-th 3
new-pc# ps -eT
  PID SPID TTY          TIME CMD
    1     1 pts/1    00:00:00 sh
   35    35 pts/1    00:00:00 ps

Nous constatons l’apparition du fichier core nommé conformément au schéma stocké dans core_pattern :

new-pc# ls | grep .core
multi-th_new-pc_1000_1000_28_18647_30_18649.core

Les spécificateurs %e, %h, %g, %u, %p, %P, %i et %I ont respectivement été substitués par le nom de l’exécutable (multi-th), le nom de l'hôte (new-pc) dans l’uts_ns auquel le thread était associé, les identifiants d’utilisateur et de groupe du processus dans le user_ns initial (1000), l’identifiant de processus dans le pid_ns du processus (28), l’identifiant de processus dans le pid_ns initial (18647), l’identifiant de thread dans le pid_ns du processus (30) et l’identifiant de thread dans le pid_ns initial (18649).

4.2 Redirection du fichier core

Linux supporte une syntaxe alternative dans /proc/sys/kernel/core_pattern : si le premier caractère est « | » (le fameux tube du shell), alors le reste de la ligne est interprété comme un exécutable suivi de ses paramètres à déclencher lors du plantage d’un processus. Les informations liées au plantage (le fichier core) sont injectées sur son entrée standard. Les spécificateurs vus plus haut sont aussi utilisables sur la ligne de commande. Il est important de noter que le chemin du programme à exécuter est situé dans le mount_ns initial et c’est d’ailleurs toujours dans les namespaces initiaux qu’il est exécuté. Dans un contexte où des conteneurs sont en action, cela signifie que tout plantage de processus à l’intérieur ou à l’extérieur des conteneurs sera pris en charge par ce programme qui s’exécute dans le contexte de l’hôte ! D’où l’intérêt de la possibilité de passer les spécificateurs en paramètres du programme afin qu’il puisse identifier les namespaces, voire même le conteneur où le plantage a eu lieu.

Notre script shell getcore s’appuie tout simplement sur la commande dd pour stocker dans /tmp le contenu du core reçu sur son entrée standard. Le fichier de sortie est nommé de la même manière que dans notre exemple précédent avec les informations passées en paramètres :

# Create a core file when installed in /proc/sys/kernel/core_pattern
#
# The parameters are passed through the "%" specififiers:
#
# $1 = Name of the crashing executable
# $2 = Hostname in the uts_ns where the process crashed
# $3 = Group identifier of the executable in the initial user_ns
# $4 = User identifier of the executable in the initial user_ns
# $5 = Pid of the process in its pid_ns
# $6 = Pid of the process in the initial pid_ns
# $7 = Tid of the process in its pid_ns
# $8 = Tid of the process in the initial pid_ns
 
# Redirect the input core file into a file
dd > /tmp/$1_$2_$3_$4_$5_$6_$7_$8.core 2>/dev/null

Modifions core_pattern de sorte à invoquer ce script (avec un chemin absolu !). Ici, on transfère l’outil dans /tmp pour les besoins de l’article, mais dans un environnement professionnel, il devra de préférence se trouver dans un répertoire comme /sbin :

# cp getcore /tmp
# echo "|/tmp/getcore %e %h %g %u %p %P %i %I" > /proc/sys/kernel/core_pattern
# cat /proc/sys/kernel/core_pattern
# |/tmp/getcore %e %h %g %u %p %P %i %I

Relançons shns3 comme précédemment avec la configuration de la taille des fichiers core et le montage du système de fichiers /proc, le lancement de multi-th en arrière-plan et la terminaison de l’un de ses threads par le signal SIGSEGV. Le script est invoqué pour produire en sortie le fichier core conformément à nos attentes :

$ ./shns3 -u '0 1000 1' -u '1 100000 100' -g '0 1000 1' -g '1 100000 100' -d 1
[...]
# ./multi-th 4 &
# Creating 4 threads
Thread 0 is running...
Thread 1 is running...
Thread 2 is running...
Waiting...
Thread 3 is running...
# ps -eT
  PID SPID TTY          TIME CMD
    1     1 pts/0    00:00:00 sh
   21    21 pts/0    00:00:00 multi-th
   21    22 pts/0    00:00:00 multi-th
   21    23 pts/0    00:00:00 multi-th
   21    24 pts/0    00:00:00 multi-th
   21    25 pts/0    00:00:00 multi-th
   26    26 pts/0    00:00:00 ps
# kill -SEGV 24
[1] + Segmentation fault (core dumped) ./multi-th 4
# ls /tmp/*.core
/tmp/multi-th_new-pc_1000_1000_21_16972_24_16975.core

On peut même lancer une session gdb afin de vérifier que le fichier généré est bien au format correct :

# /usr/bin/gdb ./multi-th /tmp/multi-th_new-pc_1000_1000_21_16972_24_16975.core
GNU gdb (Ubuntu 8.3-0ubuntu1) 8.3
[...]
Core was generated by `./multi-th 4'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x00007fb85ec4aba2 in __libc_pause () at ../sysdeps/unix/sysv/linux/pause.c:29
[Current thread is 1 (Thread 0x7fb85da3f700 (LWP 24))]
(gdb) where
#0 0x00007fb85ec4aba2 in __libc_pause () at ../sysdeps/unix/sysv/linux/pause.c:29
#1 0x000055d1067f0360 in th_main (param=<optimized out>) at /home/rachid/GLMF/NAMESPACES/exemples/multi-th.c:15
#2 0x00007fb85ec3f669 in start_thread (arg=<optimized out>) at pthread_create.c:479
#3 0x00007fb85eb67323 in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95
(gdb)

4.3 Les fichiers core dans LXC

Le système de fichiers /proc et notamment sa sous-arborescence /proc/sys ainsi que /sys contiennent des informations sensibles ainsi que des paramètres de configuration du système hôte. Il faut donc éviter la fuite d’information et les risques de corruption par le truchement des conteneurs. La configuration de LXC prévoit différentes manières de monter /proc et /sys :

$ man lxc.container.conf
[...]
lxc.mount.auto
    specify which standard kernel file systems should
    be automatically mounted. This may dramatically
    simplify the configuration. The file systems are:
 
  o proc:mixed (or proc): mount /proc as read-write,
    but remount /proc/sys and /proc/sysrq-trigger read-only for security
    / container isolation purposes.
 
  o proc:rw: mount /proc as read-write
 
  o sys:mixed (or sys): mount /sys as read-only but
    with /sys/devices/virtual/net writable.
 
  o sys:ro: mount /sys as read-only for security / container
    isolation purposes.
 
  o sys:rw: mount /sys as read-write
[...]

Les conteneurs LXC busybox choisissent par défaut la valeur proc:mixed de sorte à monter /proc en lecture/écriture, mais avec /proc/sys en lecture seule :

# cat /var/lib/lxc/bbox/config
[...]
lxc.mount.auto = cgroup:mixed proc:mixed sys:mixed
[...]

Si on lance un conteneur, son fichier core_pattern a la valeur que nous avons positionnée côté hôte plus haut, car il est global au système et l’on ne peut pas le modifier :

# lxc-console -n bbox -t 0
[...]
bbox# echo foo > /proc/sys/kernel/core_pattern
/bin/sh: can't create /proc/sys/kernel/core_pattern: Read-only file system
bbox# cat /proc/sys/kernel/core_pattern
|/tmp/getcore %e %h %g %u %p %P %i %I

Configurons la taille du fichier core dans le conteneur et vérifions que getcore n’est pas dans le répertoire /tmp du rootfs du conteneur (nous rappelons que le conteneur a son propre système de fichiers situé dans /var/lib/lxc/<nom de conteneur>/rootfs dans lequel il fait un pivot_root()) :

bbox# ulimit -c
0
bbox# ulimit -c unlimited
bbox# ulimit -c
unlimited
bbox# ls -l /tmp
total 0

En simulant un plantage dans le processus syslogd du conteneur, nous constatons qu’il n’y a pas de fichier core dans son répertoire /tmp :

bbox# ps
PID   USER     COMMAND
    1 root     init
    4 root     /bin/syslogd
   14 root     /bin/udhcpc
   15 root     /bin/getty -L tty1 115200 vt100
   16 root     /bin/sh
   21 root     {ps} /bin/sh
bbox# kill -SEGV 4
bbox# ps
PID   USER     COMMAND
    1 root     init
   14 root     /bin/udhcpc
   15 root     /bin/getty -L tty1 115200 vt100
   16 root     /bin/sh
   22 root     {ps} /bin/sh
bbox# ls -l /tmp
total 0

Par contre, côté hôte, le fichier core associé au plantage est bien dans /tmp :

$ ls -l /tmp/*.core
-rw-rw-rw- 1 root root   364544 févr. 23 21:25 /tmp/syslogd_bbox_0_0_4_17265_4_17265.core

Nous avons donc bien vérifié que, quel que soit l’endroit du plantage d’un exécutable (en conteneur ou hors conteneur), le script mentionné dans core_pattern est lancé dans les namespaces initiaux (c.-à-d. côté hôte). Donc de ce point de vue, le script getcore va écrire dans le répertoire du mount_ns de l’hôte. Les spécificateurs, et notamment %h, sont d’un grand secours pour identifier le conteneur où le plantage a eu lieu.

Conclusion

Les namespaces sont une réponse au célèbre aphorisme de D. Wheeler [13] : « tous les problèmes dans le domaine de l’informatique peuvent être résolus par un nouveau niveau d’indirection » (ou d’abstraction).

Cette série d’articles agrémentée de nombreux exemples est une approche essentiellement pratique des namespaces. Le but n’était pas d’être exhaustif, mais de constituer un complément à la documentation disparate et parfois incomplète sur certains aspects. Nous avons apporté une vision sous des angles différents et notamment un aperçu de l’implémentation dans les utilitaires, le noyau et LXC. Nous pouvons même nous targuer d’avoir révélé des secrets d’implémentation jusqu’ici jalousement gardés par la communauté des développeurs du noyau Linux. Nous avons ainsi levé le voile sur ce qui constitue les fondations de la conteneurisation sous Linux.

Le concept de namespace va dans le sens d’une nouvelle approche de la construction des applications dans un monde qui demande de plus en plus aux machines en termes de puissance et de fonctionnalités. Nous passons d’un modèle monolithique à un modèle modulaire. Le maître mot en la matière est une interprétation au sens figuré de l’adage « diviser pour mieux régner » : diviser pour mieux partager les ressources matérielles, diviser pour découper les applications en microservices [14] afin de réduire les temps de développement, de faciliter la maintenance, d’améliorer la robustesse et la sécurité, de favoriser l’évolutivité pour une juste adaptation aux besoins et enfin, de faciliter la portabilité et le déploiement.

Alors que les namespaces permettent d’instancier les ressources du noyau, le lecteur pourra aussi se pencher sur les cgroups, l’autre pilier des conteneurs, dont l’objectif est de contrôler l’utilisation des ressources.

Références

[1] Les cgroups : https://fr.wikipedia.org/wiki/Cgroups

[2] Le namespace time : https://lwn.net/Articles/766089/

[3] Le namespace time dans le noyau :
https://www.phoronix.com/scan.php?page=news_item&px=Linux-Time-Namespace-Coming

[4] L’appel système clone3() : https://lwn.net/Articles/792628/

[5] Implementing virtual system calls : https://lwn.net/Articles/615809/

[6] Le namespace syslog : https://lwn.net/Articles/527342/

[7] R. KOUCHA, « Les structures de données des namespaces dans le noyau », GNU/Linux magazine n°243, décembre 2020 : https://connect.ed-diamond.com/GNU-Linux-Magazine/GLMF-243/les-structures-de-donnees-des-namespaces-dans-le-noyau

[8] R. KOUCHA, « Le fonctionnement des namespaces dans le noyau », GNU/Linux magazine n°245, février 2021 :
https://connect.ed-diamond.com/GNU-Linux-Magazine/GLMF-245/Le-fonctionnement-des-namespaces-dans-le-noyau

[9] R. KOUCHA, « Les namespaces network et pid », GNU/Linux magazine n°256, mars - avril 2022 :
https://connect.ed-diamond.com/gnu-linux-magazine/glmf-256/les-namespaces-network-et-pid

[10] Device namespaces : https://lwn.net/Articles/564854/

[11] Add support for devtmpfs in user namespaces : https://lwn.net/Articles/598782/

[12] Making the Kernel and Udev Namespace Aware : https://www.youtube.com/watch?v=ondREXbSa5E

[13] David Wheeler (computer scientist) : https://en.wikipedia.org/wiki/David_Wheeler_(computer_scientist)

[14] Les microservices : https://en.wikipedia.org/wiki/Microservices



Article rédigé par

Par le(s) même(s) auteur(s)

Exécution concurrente avec les coroutines

Magazine
Marque
GNU/Linux Magazine
Numéro
251
Mois de parution
septembre 2021
Spécialité(s)
Résumé

Concept datant des premières heures de l'informatique, puis laissé en désuétude au profit des threads, les coroutines suscitent un engouement depuis quelques années, notamment dans le domaine de l'embarqué et de l'Internet of Things (IoT). Certains langages les supportent nativement. Le langage C ne les propose pas, mais la librairie C recèle des services qui permettent de les mettre en œuvre.

Les derniers articles Premiums

Les derniers articles Premium

Bénéficiez de statistiques de fréquentations web légères et respectueuses avec Plausible Analytics

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Pour être visible sur le Web, un site est indispensable, cela va de soi. Mais il est impossible d’en évaluer le succès, ni celui de ses améliorations, sans établir de statistiques de fréquentation : combien de visiteurs ? Combien de pages consultées ? Quel temps passé ? Comment savoir si le nouveau design plaît réellement ? Autant de questions auxquelles Plausible se propose de répondre.

Quarkus : applications Java pour conteneurs

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Initié par Red Hat, il y a quelques années le projet Quarkus a pris son envol et en est désormais à sa troisième version majeure. Il propose un cadre d’exécution pour une application de Java radicalement différente, où son exécution ultra optimisée en fait un parfait candidat pour le déploiement sur des conteneurs tels que ceux de Docker ou Podman. Quarkus va même encore plus loin, en permettant de transformer l’application Java en un exécutable natif ! Voici une rapide introduction, par la pratique, à cet incroyable framework, qui nous offrira l’opportunité d’illustrer également sa facilité de prise en main.

Les listes de lecture

9 article(s) - ajoutée le 01/07/2020
Vous désirez apprendre le langage Python, mais ne savez pas trop par où commencer ? Cette liste de lecture vous permettra de faire vos premiers pas en découvrant l'écosystème de Python et en écrivant de petits scripts.
11 article(s) - ajoutée le 01/07/2020
La base de tout programme effectuant une tâche un tant soit peu complexe est un algorithme, une méthode permettant de manipuler des données pour obtenir un résultat attendu. Dans cette liste, vous pourrez découvrir quelques spécimens d'algorithmes.
10 article(s) - ajoutée le 01/07/2020
À quoi bon se targuer de posséder des pétaoctets de données si l'on est incapable d'analyser ces dernières ? Cette liste vous aidera à "faire parler" vos données.
Voir les 64 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous