Dans cette série d'articles, nous examinerons attentivement l'un des principaux ingrédients du conteneur - les espaces de noms. Dans le processus, nous créerons un clone plus simple de la docker run
- notre propre programme qui prendra la commande (avec ses arguments, le cas échéant) à l'entrée et développera le conteneur pour son exécution, isolé du reste du système, similaire à la façon dont vous l'exécuteriez docker run
pour s'exécuter à partir d'une image .
Qu'est-ce que l'espace de noms?
L'espace de noms Linux est une abstraction des ressources du système d'exploitation. Nous pouvons considérer l'espace de noms comme une boîte. Cette boîte contient des ressources système qui dépendent du type de boîte (espace de noms). Il existe actuellement sept types d'espaces de noms: Cgroups, IPC, Network, Mount, PID, User, UTS.
Par exemple, l'espace de noms réseau comprend des ressources système liées au réseau telles que les interfaces réseau (par exemple wlan0
, eth0
), les tables de routage, etc., l'espace de noms Mount comprend des fichiers et des répertoires dans le système, le PID contient des ID de processus, etc. . Ainsi, deux instances de l'espace de noms Network A et B (correspondant à deux cases du même type dans notre analogie) peuvent contenir des ressources différentes - peut-être A contient wlan0
, tandis que B contient eth0
et une copie distincte de la table de routage.
Les espaces de noms ne sont pas une fonctionnalité ou une bibliothèque supplémentaire que vous devez installer, par exemple, à l'aide du gestionnaire de packages apt. Ils sont fournis par le noyau Linux lui-même et sont déjà une nécessité pour exécuter n'importe quel processus sur le système. À tout moment donné, tout processus P appartient à exactement une instance d'espace de noms de chaque type. Par conséquent, lorsqu'il doit dire «mettre à jour la table de routage dans le système», Linux lui montre une copie de la table de routage d'espace de noms à laquelle il appartient à ce moment.
À quoi ça sert?
Absolument pour rien ... bien sûr, je plaisantais. L'une des grandes propriétés des boîtes est que vous pouvez ajouter et supprimer des éléments de la boîte et cela n'affectera pas le contenu des autres boîtes. C'est la même idée avec les espaces de noms - le processus P peut «devenir fou» et exécuter sudo rm –rf /
, mais un autre processus Q appartenant à un autre espace de noms Mount ne sera pas affecté, car ils utilisent des copies séparées de ces fichiers.
Notez que la ressource contenue dans l'espace de noms n'est pas nécessairement une copie unique. Dans certains cas qui se sont produits intentionnellement ou en raison d'une faille de sécurité, deux ou plusieurs espaces de noms contiendront la même copie, par exemple, le même fichier. Ainsi, les modifications apportées à ce fichier dans un espace de noms de montage seront réellement visibles dans tous les autres espaces de noms de montage, qui s'y réfèrent également. Par conséquent, nous abandonnerons notre analogie de tiroir, car l'article ne peut pas être dans deux boîtes différentes en même temps.
La restriction est une préoccupation
Nous pouvons voir les espaces de noms auxquels appartient le processus! Typiquement pour Linux, ils apparaissent sous forme de fichiers dans le /proc/$pid/ns
de ce processus avec l'ID de processus $pid
:
$ ls -l /proc/$$/ns total 0 lrwxrwxrwx 1 iffy iffy 0 May 18 12:53 cgroup -> cgroup:[4026531835] lrwxrwxrwx 1 iffy iffy 0 May 18 12:53 ipc -> ipc:[4026531839] lrwxrwxrwx 1 iffy iffy 0 May 18 12:53 mnt -> mnt:[4026531840] lrwxrwxrwx 1 iffy iffy 0 May 18 12:53 net -> net:[4026531957] lrwxrwxrwx 1 iffy iffy 0 May 18 12:53 pid -> pid:[4026531836] lrwxrwxrwx 1 iffy iffy 0 May 18 12:53 user -> user:[4026531837] lrwxrwxrwx 1 iffy iffy 0 May 18 12:53 uts -> uts:[4026531838]
Vous pouvez ouvrir un autre terminal, exécuter la même commande et cela devrait vous donner le même résultat. En effet, comme nous l'avons mentionné précédemment, le processus doit appartenir à un certain espace de noms (espace de noms) et jusqu'à ce que nous spécifions explicitement lequel, Linux l'ajoute aux espaces de noms par défaut.
Impliquons-nous un peu. Dans le deuxième terminal, nous pouvons faire quelque chose comme ceci:
$ hostname iffy $ sudo unshare -u bash $ ls -l /proc/$$/ns lrwxrwxrwx 1 root root 0 May 18 13:04 cgroup -> cgroup:[4026531835] lrwxrwxrwx 1 root root 0 May 18 13:04 ipc -> ipc:[4026531839] lrwxrwxrwx 1 root root 0 May 18 13:04 mnt -> mnt:[4026531840] lrwxrwxrwx 1 root root 0 May 18 13:04 net -> net:[4026531957] lrwxrwxrwx 1 root root 0 May 18 13:04 pid -> pid:[4026531836] lrwxrwxrwx 1 root root 0 May 18 13:04 user -> user:[4026531837] lrwxrwxrwx 1 root root 0 May 18 13:04 uts -> uts:[4026532474] $ hostname iffy $ hostname coke $ hostname coke
La commande unshare
lance le programme (facultatif) dans le nouvel espace de noms. L'indicateur -u
dit d'exécuter bash
dans le nouvel espace de noms UTS. Notez que notre nouveau processus bash
pointe vers un autre fichier uts
, tandis que tous les autres restent les mêmes.
La création de nouveaux espaces de noms nécessite généralement un accès superutilisateur. unshare
, nous supposerons que le non- unshare
et notre implémentation sont effectués à l'aide de sudo
.
L'une des conséquences de ce que nous venons de faire est que nous pouvons maintenant changer le nom d'hôte du système à partir de notre nouveau processus bash et cela n'affectera aucun autre processus du système. Vous pouvez le vérifier en exécutant le hostname
d' hostname
dans le premier terminal et en voyant que le nom d'hôte n'y a pas changé.
Mais qu'est-ce qu'un conteneur, par exemple?
J'espère que vous avez maintenant une idée de ce que l'espace de noms peut faire. Vous pouvez supposer que les conteneurs sont essentiellement des processus ordinaires avec des espaces de noms différents des autres processus, et vous aurez raison. En fait, c'est un quota. Un conteneur sans quotas n'est pas tenu d'appartenir à un espace de noms unique de chaque type - il peut en partager certains.
Par exemple, lorsque vous tapez docker run --net=host redis
, tout ce que vous faites est de dire au docker de ne pas créer un nouvel espace de noms réseau pour le processus redis. Et, comme nous l'avons vu, Linux ajoutera ce processus en tant que participant dans l'espace de noms Network par défaut, comme tout autre processus normal. Ainsi, d'un point de vue réseau, le processus redis est exactement le même que tout le monde. Ce n'est pas seulement une option de configuration réseau, l' docker run
vous permet d'effectuer de telles modifications pour la plupart des espaces de noms existants. Cela soulève la question, qu'est-ce qu'un conteneur? Y a-t-il un conteneur qui utilise un processus qui utilise tout sauf un de l'espace de noms commun? ¯ \ _ (ツ) _ / ¯ Habituellement, les conteneurs contiennent le concept d' isolement obtenu grâce aux espaces de noms: moins il y a d'espaces de noms et de ressources que le processus partage avec d'autres, plus il est isolé et c'est tout ce qui compte vraiment.
L'isolement
Dans la suite de cet article, nous jetterons les bases de notre programme, que nous appellerons isolate
. isolate
prend la commande comme arguments et la démarre dans un nouveau processus, isolé du reste du système et limité par ses propres espaces de noms. Dans les articles suivants, nous examinerons l'ajout de la prise en charge des espaces de noms individuels pour la commande de processus qui démarre l' isolate
.
En fonction de l'application, nous nous concentrerons sur les espaces de noms Utilisateur, Montage, PID et Réseau. Le reste sera relativement trivial à implémenter après la fin (en fait, nous ajouterons le support UTS ici dans la mise en œuvre initiale du programme). Et la considération, par exemple, des Cgroups, dépasse le cadre de cette série (l'étude des cgroups, un autre composant des conteneurs utilisé pour contrôler la quantité de ressources qu'un processus peut utiliser).
Les espaces de noms peuvent s'avérer très rapides et il existe de nombreuses façons différentes que vous pouvez utiliser pour explorer chaque espace de noms, mais nous ne pouvons pas tous les sélectionner en même temps. Nous ne discuterons que des moyens pertinents pour le programme que nous développons. Chaque article commencera par quelques expériences dans la console sur l'espace de noms en question afin de comprendre les étapes requises pour configurer cet espace de noms. En conséquence, nous aurons déjà une idée de ce que nous voulons réaliser, puis l'implémentation correspondante en isolate
suivra.
Pour éviter la surcharge de code des publications, nous n'inclurons pas des choses comme les fonctions auxiliaires qui ne sont pas nécessaires pour comprendre la mise en œuvre. Vous pouvez trouver le code source complet ici sur Github .
Implémentation
Le code source de cet article peut être trouvé ici . Notre implémentation isolate
sera un programme simple qui lit une ligne avec une commande de stdin et clone un nouveau processus qui l'exécute avec les arguments spécifiés. Le processus cloné avec la commande s'exécutera dans son propre espace de noms UTS de la même manière que nous l'avons fait avec unshare
. Dans les prochains messages, nous verrons que les espaces de noms ne fonctionnent pas nécessairement (ou au moins fournissent l'isolement) de la boîte et nous devrons effectuer une configuration après les avoir créés (mais avant d'exécuter réellement la commande) afin que la commande s'exécute vraiment de manière isolée.
Cette combinaison d'espace de noms créer-configurer nécessitera une certaine interaction entre le processus d' isolate
principal et le processus enfant de la commande à exécuter. En conséquence, une partie du travail principal ici consistera à configurer le canal de connexion entre les deux processus - dans notre cas, nous utiliserons le canal Linux en raison de sa simplicité.
Nous devons faire trois choses:
- Créez un processus d'
isolate
base qui lit les données de stdin. - Clonez un nouveau processus qui exécutera la commande dans le nouvel espace de noms UTS.
- Configurez le canal afin que le processus d'exécution de la commande ne démarre son lancement qu'après avoir reçu un signal du processus principal indiquant que la configuration de l'espace de noms est terminée.
Voici le processus de base:
int main(int argc, char **argv) { struct params params; memset(¶ms, 0, sizeof(struct params)); parse_args(argc, argv, ¶ms);
Notez les clone_flags
que nous transmettons à notre appel de clone
. Vous voyez à quel point il est facile de créer un processus dans son propre espace de noms? Tout ce que nous devons faire est de définir un indicateur pour le type d'espace de noms (l'indicateur CLONE_NEWUTS
correspond à l'espace de noms UTS), et Linux s'occupera du reste.
Ensuite, le processus de commande attend un signal avant de démarrer:
static int cmd_exec(void *arg) {
Enfin, nous pouvons essayer d'exécuter ceci:
$ ./isolate sh ===========sh============ $ ls isolate isolate.c isolate.o Makefile $ hostname iffy $ hostname coke $ hostname coke
Maintenant, isolate
est un peu plus qu'un programme qui fait simplement équipe (nous avons un UTS qui fonctionne pour nous). Dans le prochain article, nous prendrons une autre étape en examinant les espaces de noms utilisateur et en faisant isolate
exécuter la commande dans son propre espace de noms utilisateur. Là, nous verrons que nous devons en fait travailler pour avoir un espace de noms utilisable dans lequel la commande peut être exécutée.