Introduction

Un conteneur est un environnement d’exécution isolé. Il comprend le système d’exploitation, les outils systèmes et les librairies. Cette isolation est garantie par des fonctionnalités du noyau Linux : namespaces et cgroups. En effet, cela donne l’impression qu’il s’agit d’une machine virtuelle mais ce n’est pas tout à fait cela. Voyons cela en détails.

Conteneur vs virtualisation

La virtualisation permet d’exécuter plusieurs systèmes d’exploitation en parallèle sur une même machine physique en émulant le matériel. Chaque  machine virtuelle (VM) possède son propre système d’exploitation sur lequel les applications qu’elle supporte sont exécutées.

Les conteneurs partagent le même noyau du système d’exploitation de la machine hôte. Il n’est donc plus nécessaire d’avoir un hyperviseur pour gérer les machines virtuelles.

Les namespaces

Les namespaces sont une fonctionnalité du kernel Linux à partir de la version 2.6.24. Un namespace est une couche d’abstraction des ressources système qui fait que les processus de ce namespace ont leur propre instance isolée de ces ressources. Prenons l’exemple de la ressource système PID (Process ID), si on crée un namespace X de type PID et qu’on exécute un processus dans ce namespace, ce dernier aura l’id 1.

Process tree

Créons un namepace de type PID à l’aide de la commande unshare et exécutons la commande bash dans un nouveau namespace

# unshare --fork --pid --mount-proc bash
  • unshare : exécute un programme (dans note exemple c’est le bash) dans un nouveau namespace.
  • fork : permet d’exécuter le programme dans un sous-process de celui qui exécute le unshare.
  • pid : le type de namespace à créer.
  • mount-proc : permet de monter le fichier système /proc. En fait, indirectement, un nouveau namepsace de type mount est créé. Sinon, l’ecriture dans /proc du système peut impacter les programmes existants.

Visualisons les processus qui tournent sur ce nouveau namespace avec la commande : ps -ef (-e : affichage de tous les processus, -f : affichage détaillée) :

# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 15:32 pts/1    00:00:00 bash
root        14     1  0 15:32 pts/1    00:00:00 ps -ef

Comme vous pouvez le constater, seulement deux processus sont visibles le bash et le ps.
Toujours dans le nouveau namesapce, exécutons un autre programme :

# sleep 30 &

Dans un autre terminal, vérifions maintenant le réel PID :

# ps -ef --sort=start_time
UID        PID  PPID  C STIME TTY          TIME CMD
root     32703 31065  0 15:32 pts/1    00:00:00 unshare --fork --pid --mount-proc bash
root     32704 32703  0 15:32 pts/1    00:00:00 bash
root     3700  32704  0 15:34 pts/1    00:00:00 sleep 30

On a deux vues du même processus. Le process 32704, dans le système hôte, est le process 1 dans le nouveau namespace créé.

Linux supporte 6 types de namespaces : PID, Network, Mount, IPC, User, UTS.

  • Un namespace PID permet d’avoir un nouvel arbre de processus. Le premier processus aura le PID 1 et devient le processus d’init de ce namespace.
  • Un namespace newtork permet d’avoir un nouvel environnement réseau. Ceci inclut : les adresses IP, les interfaces, les ports, les tables de routages. Un processus peut utiliser n’importe quel port sans soucis de conflits.
  • Un namespace mount permet de monter et de démonter un système de fichier sans affecter le système de fichier de la machine.
  • Un namepace IPC (Inter process communication) permet d’avoir un espace mémoire paratgé uniquement à l’intérieur du namespace.
  • Un namespace user permet d’avoir un utilisateur root à l’intérieur de ce namespace qui réellement n’est pas root sur le système hôte.
  • Un namespace UTS permet d’avoir un hostname différent de la machine.

Les cgroups

Si les namespaces sont faits pour l’isolation, les cgroups sont fait pour définir les limites. Les cgroups sont une fonctionnalité Linux qui permet de limiter l’utilisation de certaines ressources comme la mémoire, le CPU et les I/O disque par un ensemble de processus.
Cgroups est l’abréviation de Control GROUPS. Cette fonctionnalité a été initialement développée par Google puis intégrée dans le noyau Linux à partir de la version 2.6.24. L’objectif est d’avoir un contrôle plus précis sur l’allocation, la priorisation, la gestion et la surveillance des ressources système. Ainsi, l’utilisation des ressources partagées peut être répartie de manière appropriée entre les processus, ce qui protège la machine  et améliore l’efficacité des programmes.

Créons un cgroup appelé cgexample pour limiter l’utilisation de la mémoire à l’aide de la commande cgcreate :

# cgcreate -g memory:cgexample
  • cgcreate : crée un nouveau cgroup.
  • -g : définie les ressources à contrôler et le nom de cgroup.

Définissons la limite de mémoire que le processus ne doit pas dépasser :

# echo 4096 > /sys/fs/cgroup/memory/cgexample/memory.limit_in_bytes

A l’aide d’un script shell, exécutons un programme en boucle infinie :

#!/bin/sh
while [ true ]; do
 echo "just sleep"
 sleep 60
done

Maintenant, exécutons le script test.sh et attachons son process au contrôleur cgexample en insérant le PID du process dans le fichier cgroup.procs :

# /tmp/test.sh &
# echo 28872 > /sys/fs/cgroup/memory/cgexample/cgroup.procs

Ou bien, en une seule ligne avec la commande cgexec

# cgexec -g memory:cgexample /tmp/test.sh

Regardons maintenant les logs :

# tail -f /var/log/messages
Sep 3 10:36:02 zan kernel: [ 2329.717868] Call Trace:
Sep 3 10:36:02 zan kernel: [ 2329.717876] [] dump_stack+0x19/0x1b
Sep 3 10:36:02 zan kernel: [ 2329.717878] [] dump_header+0x90/0x229
Sep 3 10:36:02 zan kernel: [ 2329.717881] [] ? do_wp_page+0xfb/0x720
Sep 3 10:36:02 zan kernel: [ 2329.717884] [] ? find_lock_task_mm+0x56/0xc0
Sep 3 10:36:02 zan kernel: [ 2329.717887] [] ? try_get_mem_cgroup_from_mm+0x28/0x60
Sep 3 10:36:02 zan kernel: [ 2329.717888] [] oom_kill_process+0x254/0x3d0
Sep 3 10:36:02 zan kernel: [ 2329.717890] [] mem_cgroup_oom_synchronize+0x546/0x570
Sep 3 10:36:02 zan kernel: [ 2329.717892] [] ? mem_cgroup_charge_common+0xc0/0xc0
Sep 3 10:36:02 zan kernel: [ 2329.717893] [] pagefault_out_of_memory+0x14/0x90
Sep 3 10:36:02 zan kernel: [ 2329.717894] [] mm_fault_error+0x6a/0x157
Sep 3 10:36:02 zan kernel: [ 2329.717896] [] __do_page_fault+0x3c8/0x4f0
Sep 3 10:36:02 zan kernel: [ 2329.717898] [] do_page_fault+0x35/0x90
Sep 3 10:36:02 zan kernel: [ 2329.717899] [] page_fault+0x28/0x30
Sep 3 10:36:02 zan kernel: [ 2329.717920] Task in /cgexample killed as a result of limit of /cgexample
Sep 3 10:36:02 zan  kernel: [ 2329.717921] memory: usage 4kB, limit 4kB, failcnt 1207
Sep 3 10:36:02 zan kernel: [ 2329.717921] memory+swap: usage 4kB, limit 9007199254740988kB, failcnt 0
Sep 3 10:36:02 zan kernel: [ 2329.717922] kmem: usage 0kB, limit 9007199254740988kB, failcnt 0
Sep 3 10:36:02 zan kernel: [ 2329.717927] Memory cgroup stats for /cgexample: cache:0KB rss:4KB rss_huge:0KB mapped_file:0KB swap:0KB inactive_anon:4KB active_anon:0KB inactive_file:0KB active_file:0KB unevictable:0KB
Sep 3 10:36:02 zan kernel: [ 2329.717928] [ pid ] uid tgid total_vm rss nr_ptes swapents oom_score_adj name
Sep 3 10:36:02 zan kernel: [ 2329.718008] [28872] 0 28872 28294 304 13 39 0 test.sh
Sep 3 10:36:02 zan kernel: [ 2329.718010] Memory cgroup out of memory: Kill process 28872 (test.sh) score 0 or sacrifice child
Sep 3 10:36:02 zan kernel: [ 2329.718011] Killed process 28872 (test.sh) total-vm:113176kB, anon-rss:28kB, file-rss:1188kB, shmem-rss:0kB

Une fois notre programme a atteint la limite de 4KB de mémoire le processus est tué.

Conclusion

Nous avons vu un bref aperçu de deux fonctionnalités Linux : namespaces et cgroups. Vous pouvez écrire un script utilisant ces fonctionnalités, cela vous permettra d’isoler vos processus et de mieux allouer vos ressources. Ainsi, vous aurez construit vos propres conteneurs.

Finalement,  les gestionnaires de conteneurs comme Docker reposent principalement sur les fonctionnalités de base du noyau Linux, et nous simplifient leurs utilisations.