Les *cgroup*s
-------------

Les *cgroup*s (pour *Control Group*s) permettent de collecter des statistiques
sur des **groupes de processus** (voire même, des threads !) et de leur
attribuer des propriétés. Il est par exemple possible de leur imposer des
limites d'utilisation de ressources ou d'altérer leur comportement : quantité
de RAM, temps CPU, bande passante, ...

Apparue dès [Linux
2.6.24](https://kernelnewbies.org/Linux_2_6_24#Task_Control_Groups)
(en 2008 !), les *cgroup*s sont répartis en différents sous-systèmes
(*subsystem*), chacun étant responsable d'un type de ressources
spécifique :

- [`blkio` (`io` dans la v2)
  :](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v1/blkio-controller.html)
  limites et statistiques de bande passante sur les disques ;
- `cpu` : cycles CPU minimums garantis ;
- [`cpuacct` (inclus dans `cpu` dans la v2)
  :](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v1/cpuacct.html)
  statistiques du temps CPU utilisé ;
- [`cpuset`
  :](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v1/cpusets.html)
  associe des tâches à un/des CPU particuliers (par exemple pour dédier un cœur
  du CPU à un programme, qui ne pourra alors utiliser que ce CPU et pas les
  autres) ;
- [`devices`
  :](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v1/devices.html)
  règles de contrôle de création (`mknod`) et d'accès aux périphériques ;
- [`freezer`
  :](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v1/freezer-subsystem.html)
  pour suspendre et reprendre l'exécution d'un groupe de tâches ;
- [`hugetlb` :](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v1/hugetlb.html) statistiques et limitation de l'usage de la fonctionnalité `HugeTLB` (permettant d'obtenir des pages mémoires plus grandes que 4 kB) ;
- [`memory` :](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v1/memory.html) statistiques et limitation d'usage de la mémoire vive et de la *swap* ;
- [`net_cls` (v1 seulement) :](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v1/net_cls.html) applique un `classid` à tous les paquets émis par les tâches du *cgroup*, pour filtrage par le pare-feu en sortie ;
- [`net_prio` (v1 seulement) :](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v1/net_prio.html) surcharge la valeur de l'option de priorité `SO_PRIORITY`, ordonnant la file d'attente des paquets sortants ;
- [`pids` :](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v1/pids.html) statistiques et limitation du nombre de processus ;
- ...

Nous allons commencer par faire quelques tests avec le *cgroup* *freezer*, qui
permet d'interrompre l'exécution d'un groupe de processus, puis de la reprendre
lorsqu'on le décide.


### Montage du *freezer*

En fonction de la configuration de votre système, vous allez vous trouver dans
l'une de ces trois situations :

- Votre dossier `/sys/fs/cgroup` contient à la fois des fichiers `cgroup.*` et
  éventuellement des dossiers : vous avez une distribution moderne qui utilise
  la nouvelle version des `cgroup`s.

- Votre dossier `/sys/fs/cgroup` contient d'autres dossiers au nom des
  sous-systèmes que l'on a listés ci-dessus : il s'agit des `cgroup`s v1.

- Votre dossier `/sys/fs/cgroup` est vide ou inexistant, vous pouvez choisir
  d'utiliser la version de votre choix :

  Pour utiliser la v1 :

  <div lang="en-US">
  ```bash
  mkdir /sys/fs/cgroup/freezer/
  mount -t cgroup -o freezer none /sys/fs/cgroup/freezer/
  ```
  </div>


  Pour utiliser la v2 :

  <div lang="en-US">
  ```bash
  mkdir /sys/fs/cgroup/
  mount -t cgroup2 none /sys/fs/cgroup/
  ```
  </div>

Avant d'aller plus loin, notez que les exemples seront donnés pour les deux
versions des `cgroup`s à chaque fois.

::::: {.question}

#### Quelles sont les différences entre les deux versions des *cgroups* ? {-}

La principale différence entre les deux est la fusion des différents
sous-systèmes au sein d'une même arborescence. Dans la première version, chaque
sous-système disposait de sa propre arborescence et il fallait créer les
groupes et associer les tâches pour chaque sous-système. Avec la seconde
version, une seule création est nécessaire, quelque soit le nombre de
sous-systèmes que l'on souhaite utiliser.

:::::


### Création d'un nouveau groupe

Les *cgroup*s sont organisés autour d'une arborescence de groupe, où chaque
groupe est représenté par un dossier. Il peut bien évidemment y avoir des
sous-groupes, en créant des dossiers dans les dossiers existants, etc.\

La première étape dans l'utilisation d'un *cgroup* est donc de créer un groupe.

Pour ce faire, il suffit de créer un nouveau dossier dans un groupe existant,
par exemple la racine.

On commence par se rendre à la racine :

<div lang="en-US">
```bash
cd /sys/fs/cgroup/freezer/  # v1
cd /sys/fs/cgroup/          # v2
```
</div>

Puis on crée notre groupe :

<div lang="en-US">
```bash
mkdir virli
ls virli/
```
</div>

Nous avons maintenant un nouveau groupe de processus `virli` dans le *cgroup*
*Freezer*. Comme il s'agit d'une hiérarchie, le groupe `virli` hérite des
propriétés de son (ses) père(s).


### Sélection de contrôleur (v2 seulement)

Du fait de l'unification de tous les sous-systèmes, si vous utilisez la seconde
version, vous allez devoir activer le ou les contrôleurs dont vous avez besoin
(tandis que dans la première version, on se rendait dans l'arborescence du
sous-système que l'on voulait).

Pour activer le contrôleur *memory*, nous utilisons la commande suivante à la
racine :

<div lang="en-US">
```bash
echo "+memory" > cgroup.subtree_control
```
</div>

::::: {.warning}
Si vous obtenez l'erreur `No such file or directory`, c'est sans doute que vous
avez les `cgroup`s v1 activé quelque part. Vous devriez plutôt utiliser la
première version, le fait qu'elle soit active empêche l'utilisation de la v2 en
parallèle.
:::::

On peut voir les contrôleurs actifs en consultant le fichier
`virli/cgroup.controllers`.

Le contrôleur *freezer* est généralement activé par défaut, il n'y a pas besoin
de l'activer.


### Rattachement de processus

Pour le moment, ce nouveau groupe ne contient aucun processus, comme le montre
le fichier `cgroup.procs` de notre groupe. Ce fichier contient la liste des
processus rattachés à notre *cgroup*.

Ouvrons un nouveau terminal (c'est lui que l'on va geler), et récupérons son
PID : `echo $$`.

Pour ajouter une tâche à ce groupe, cela se passe de cette manière :

<div lang="en-US">
```bash
echo $PID > /sys/fs/cgroup/{,freezer/}virli/cgroup.procs
```
</div>

Il faut ici remplacer `$PID` par le PID du shell que l'on a relevé juste avant.

::::: {.question}

#### Ne devrait-on pas utiliser `>>` pour ajouter le processus au fichier ? {-}

Malgré l'utilisation de la redirection `>` (et non `>>`), il s'agit bel et bien
d'un ajout au fichier et non d'un écrasement. Il faut garder en tête que le
système de fichier est entièrement simulé et que certains comportements sont
adaptés.

:::::

En validant cette commande, nous avons déplacé le processus dans ce groupe, il
n'est alors plus dans aucun autre groupe (pour ce *cgroup*, il ne bouge pas
dans les autres *cgroup*s de la v1).

::::: {.question}

#### Où sont placés les nouveaux processus ? {-}

Les nouveaux processus héritent des groupes de leur père.

Si vous lancez un `top` dans votre nouveau terminal, son PID sera présent dans
le fichier `cgroup.procs`.

:::::


### Consultation de l'état

En affichant le contenu du dossier `virli`, nous pouvions constater que
celui-ci contenait déjà un certain nombre de fichiers. Certains d'entre eux
sont en lecture seule et permettent de lire des statistiques instantanées sur
le groupe ; tandis que d'autres sont des propriétés que nous pouvons modifier.

Nous pouvons consulter l'état de gel du groupe en affichant le contenu du
fichier :

<div lang="en-US">
```bash
42sh$ cat /sys/fs/cgroup/freezer/virli/freezer.state      # v1
42sh$ cat /sys/fs/cgroup/virli/cgroup.freeze              # v2
```
</div>

Pour plus d'informations sur les différents fichiers présents dans ce *cgroup*,
consultez
[la documentation associée](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v1/freezer-subsystem.html) :\
<https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v1/freezer-subsystem.html>


### Changement d'état

Faisons exécuter à notre interpréteur une commande pour voir effectivement
l'exécution s'arrêter. Si vous manquez d'inspiration, utilisez :

<div lang="en-US">
```bash
for i in $(seq 9999); do echo -n $i; sleep .1; echo -n " - "; sleep .1; done
```
</div>

Maintenant, nous allons donner l'ordre au noyau de ne plus allouer de temps de
calcul à notre shell et ses fils :

<div lang="en-US">
```bash
echo FROZEN > /sys/fs/cgroup/freezer/virli/freezer.state  # v1
echo 1 > /sys/fs/cgroup/virli/cgroup.freeze               # v2
```
</div>

À cet instant, vous devriez voir votre compteur s'arrêter. Pour reprendre
l'exécution :

<div lang="en-US">
```bash
echo THAWED > /sys/fs/cgroup/freezer/virli/freezer.state  # v1
echo 0 > /sys/fs/cgroup/virli/cgroup.freeze               # v2
```
</div>


::::: {.exercice}

### Script de monitoring {- #script-monitoring}

À nous maintenant de concevoir un script de collecte de statistiques issues des
*cgroup*s, similaire à `telegraf`.

Dans un premier temps, commençons par afficher dans la console, la quantité de
mémoire utilisée par le groupe monitoré.

::::: {.code}

Vous pouvez utiliser un programme comme `memhog`[^memhog] pour remplir rapidement votre
mémoire.

[^memhog]: Ce programme fait partie du paquet `numactl`, mais vous trouverez une
    version modifiée plus adaptée à nos tests sur
    <https://nemunai.re/post/slow-memhog>.

:::::

<div lang="en-US">
```
42sh# ./monitor group_name memhog 500
~~~ 13595 ~~~ Current memory usage: 75194368
~~~ 13595 ~~~ Current memory usage: 150290432
~~~ 13595 ~~~ Current memory usage: 223690752
~~~ 13595 ~~~ Current memory usage: 296828928
~~~ 13595 ~~~ Current memory usage: 368001024
~~~ 13595 ~~~ Current memory usage: 438517760
~~~ 13595 ~~~ Current memory usage: 480329728
~~~ 13595 ~~~ Current memory usage: 155648
```
</div>

Le modèle de sortie standard de votre script `monitor` n'a pas d'importance, il
doit cependant être possible d'y trouver des statistiques intéressantes, dont
la quantité de mémoire utilisée. Ici nous affichons au début le PID du
processus, ce qui peut simplifier le débogage du script.\

Il s'utilise de la manière suivante :

<div lang="en-US">
```bash
./monitor group_name prog [args [...]]
```
</div>

Où :

- `group_name` correspond au nom du/des *cgroup*(s) à créer/rejoindre.
- `prog [args [...]]` est la commande que l'on souhaite monitorer, à exécuter
  dans le *cgroup*.

::::: {.warning}
Si vous n'avez pas le *cgroup* *memory*, il est possible qu'il ne soit pas
activé par défaut par votre système. Si vous êtes dans ce cas, essayez
d'ajouter `cgroup_enable=memory` à la ligne de commande de votre noyau.
:::::

Gardez ce script dans un coin, nous allons le compléter dans les sections
suivantes.

:::::

### Fixer des limites {#Fixer-des-limites}

Au-delà de la simple consultation, les *cgroup*s peuvent servir à limiter la
quantité de ressources mises à disposition à un groupe de processus.

Pour définir une limite, nous allons écrire la valeur dans le fichier
correspondant à une valeur limite, comme par exemple
`memory.max_usage_in_bytes` (v1) ou `memory.max` (v2), qui limite le nombre
d'octets que notre groupe de processus va pouvoir allouer au maximum :

<div lang="en-US">
```
# cgroup v1
42sh$ cat /sys/fs/cgroup/memory/virli/memory.max_usage_in_bytes
0
# 0 = Aucune limite
42sh$ echo 4M > /sys/fs/cgroup/memory/virli/memory.max_usage_in_bytes
# Maintenant, la limite est à 4MB, vérifions...
42sh$ cat /sys/fs/cgroup/memory/virli/memory.max_usage_in_bytes
4194304
```
</div>

<div lang="en-US">
```
# cgroup v2
42sh$ cat /sys/fs/cgroup/virli/memory.max
max
# max = Aucune limite
42sh$ echo 4M > /sys/fs/cgroup/virli/memory.max
# Maintenant, la limite est à 4MB, vérifions...
42sh$ cat /sys/fs/cgroup/virli/memory.max
4194304
```
</div>

Chaque *cgroup*s définit de nombreux indicateurs et possède de nombreux
limiteurs, n'hésitez pas à consulter la documentation associée à chaque
*cgroup*.

::::: {.exercice}

Mettez à jour votre script de monitoring pour prendre en compte les
limites que vous avez définies :

<div lang="en-US">
```
42sh# mkdir /sys/fs/cgroup...
42sh# echo 512M > /sys/fs/cgroup.../memory.max_usage_in_bytes
42sh# ./monitor group_name memhog 500
~~~ 13595 ~~~ Current memory usage: 75194368/550502400 (13%)
~~~ 13595 ~~~ Current memory usage: 150290432/550502400 (27%)
~~~ 13595 ~~~ Current memory usage: 223690752/550502400 (40%)
~~~ 13595 ~~~ Current memory usage: 296828928/550502400 (53%)
~~~ 13595 ~~~ Current memory usage: 368001024/550502400 (66%)
~~~ 13595 ~~~ Current memory usage: 438517760/550502400 (79%)
~~~ 13595 ~~~ Current memory usage: 480329728/550502400 (87%)
~~~ 13595 ~~~ Current memory usage: 155648/550502400 (0%)
```
</div>

:::::


### Pour aller plus loin  {-}

Pour tout connaître en détail, [la série d'articles de Neil Brown sur les
Control groups](https://lwn.net/Articles/604609/)[^lwncgroups] est excellente !
Plus [cet article sur la version 2](https://lwn.net/Articles/679786/)[^lwncgroupsv2].

[^lwncgroups]: <https://lwn.net/Articles/604609/>
[^lwncgroupsv2]: <https://lwn.net/Articles/679786/>
