
Gustave Caillebotte - Périssoires sur l’Yerres (1877), huile sur toile, 103 × 155 cm, Milwaukee (USA), Milwaukee Art Museum.
Nous avons vu dans le précédent article quelques différences entre SysV init et systemd, ainsi que le mécanisme d’initialisation de l’espace utilisateur par le kernel.
Dans cet article, je vais aborder plus en détail le fonctionnement de systemd.
Les Managers
L’objet manager est central, il contient les valeurs par défaut des différentes propriétés du système et est responsable de la gestion des units qui lui sont attachées. C’est avec lui que les “utilisateurs” vont communiquer.
Il est chargé de propager les actions appelées job (par exemple : start, stop, reload…) vers les units et d’interagir avec les éléments tiers du système (watchdog, inotify, D-bus, cgroups…).
Il en existe sous 2 formes ayant chacune des portées différentes :
La première (celle par défaut) appelée “system” est unique par machine et est normalement lancée par le processus d’init avec les droits root.
On peut vérifier que systemd est bien l’init manager du système de la façon suivante :
| |
Ou plus simplement utiliser cette commande :
| |
La seconde est “user”, il est unique par utilisateur et hérite des mêmes droits que ce dernier. Ce type d’instance s’avère particulièrement utile pour limiter les droits des services qui lui sont associés (rootless).
Pour connaître l’état d’une instance utilisateur on peut utiliser la commande suivante :
| |
Bien spécifier l’argument --user devant toutes les commandes pour interagir avec l’instance de l’utilisateur.
La gestion des Units
On l’a vu les managers sont responsables de la gestion des units.
Une unit chargée par un manager elle est unique si son nom est unique (pour les templates, les alias, les fragments, le nom est résolu au runtime).
Les units files sont lues au premier “référencement” (par exemple lors de leur activation, ou bien via de liens de dépendances…), transformées en unit et mise en cache dans la mémoire du manager qui leur est associé.
Elles sont stockées à l’intérieur d’une HashMap sous la forme nom/valeur (d’où leur unicité).
Pour cette raison, il faut forcer le ramasse-miette (GC) ce qui a pour effet de sérialiser sur disque l’état des units actives, vider le cache du manager et recharger la configuration des units via leurs unit file (ce n’est pas néanmoins pas toujours nécessaire, mais ça évite des erreurs).
Pour forcer le ramasse miette on utilise la commande suivante :
| |
Arborescence et précédence
La définition des unit files répondent à une précédence très précise utilisée afin de faciliter l’administration du système et sa mise à jour.
Par ordre d’importance croissante (non exhaustif) en mode “–system” :
/lib/systemd/system/*: installés par le gestionnaire de paquets (il est recommandé d’éviter de les modifier sous peine de pertes lors des mises à jour du paquet)/usr/local/lib/systemd/system/*: gérés par l’administrateur/run/systemd/system/*: générées au runtime/etc/systemd/system/*: gérés par l’administrateur
Ainsi une unit file définie dans le répertoire /etc/systemd/system/* sera prioritaire sur celle définie dans /lib/systemd/system/*
En mode “–user” la hiérarchie est un peu différente :
/lib/systemd/user/*: installés par le gestionnaire de paquets (ne doivent pas être modifiés sous peine de pertes lors des mises à jour)/usr/local/lib/systemd/user/*: gérés par l’administrateur/run/systemd/user/*: générées au runtime/etc/systemd/user/*: gérés par l’administrateur~/.config/systemd/user/*: géré par l’utilisateur
Ça peut paraître compliqué aux premiers abords, mais limite le périmètre des différents intervenants sur une machine (utilisateurs, administrateurs, packageurs…) pour qu’ils ne se marchent sur les pieds.
De plus des outils spécifiques sont fournis (on le verra plus loin) pour aider à la compréhension de ce mécanisme.
Fragment Configuration
Les fragments configuration sont des fichiers qui permettent de modifier les propriétés d’une ou d’un groupe de units sans avoir à la/les redéfinir entièrement. Ces fichiers répondent aux mêmes ordres de précédence que nous venons d’aborder plus haut.
Pour surcharger, il est possible de définir des fichiers .conf qui seront lus par ordre alphabétique dans les formats suivants :
- Par type de unit sous la forme
<unit_type>.d/*.conf. Ainsi un fragment dans un répertoireservice.d/*.confs’appliquera à tous les*.service - Une unit spécifique sous la forme
<unit_name>.d/*.conf - Ou grâce à une pseudo hiérarchie et l’utilisation du signe ‘-’ dans le nom des units. Ainsi un fragment de configuration dans le répertoire
foo-.d/*.confva surcharger les configurations des unitsfoo-bar.service,foo-foo.serviceetfoo-bar-foo.service…
Voyons maintenant comment appliquer cela et les outils fournis par systemd pour nous assister dans la mise en place.
Si l’on crée le fichier /etc/systemd/system/service1.service de la façon suivante :
| |
Puis le fragment de configuration dans le fichier suivant /etc/systemd/system/service1.service.d/00-override.conf :
| |
Puis le fichier suivant /etc/systemd/system/service.d/00-override.conf :
| |
On obtient en utilisant la commande systemctl cat le détail de la configuration de la unit ainsi que ses différents fragments :
| |
Et produit le résultat suivant :
| |
Il est intéressant de noter que les surcharges sont additives. Pour passer outre les précédentes définitions, il faut d’abord déclarer la propriété à vide avant de la surcharger avec la valeur attendue.
| |
Les Units
Les units sont les éléments de bases de systemd. Il existe 11 types différents de unit. Ils correspondent chacun à un type d’actions particulières.
Le type d’une unit est déterminé par le suffix de son unit file :
- *.service : Qui permet de définir un groupe de processus. C’est l’objet qu’on est le plus souvent amené à utiliser.
- *.socket : Pour faire de l’activation de services par sockets (IPC, network, unix socket…).
- *.device : Pour prendre en compte l’activation par périphériques via udev (dont le hot-plug).
- *.mount : Pour gérer le montage de partitions.
- *.automount : Qui permet l’activation des units .mount automatique lors du premier accès.
- *.swap : Configuration d’un point de montage swap
- *.target : Sont des point de synchronisation d’un ensemble de units (comme les run-levels de SysVinit), mais également des points de déclenchements.
- *.path : Activation par observation de chemins
- *.timer : Déclencheur périodique (à la manière de cron)
- *.scope : Configuration d’un ensemble de processus externes
- *.slice : Qui permet de regrouper un ensemble de processus au sein d’un cgroup commun.
Une unit est décrite par une unit file qui est lue au premier référencement pour être transformée en unit.
L’objet units implémente les opérations de bases génériques, une spécialisation est faite via la structure UnitVtable qui va référencer les méthodes spécifiques à chaque type.
Voici un exemple non exhaustif des méthodes de UnitVtable :
| |
On retrouve logiquement dans l’objet Unit l’ensemble des sous-types, qui permettra de propager les actions génériques vers leurs implementations.
| |
Un unit a un cycle de vie, et donc un état. Les états de base sont les suivants :
- Active
- Reloading
- Inactive
- Failed
- Activating
- Deactivating
- Maintenance
Cette liste d’état peut être, elle aussi, enrichie pour répondre au cycle de vie spécifique d’un type d’unit.
Par exemple pour une unit de type service on trouve cette liste d’états qui font tous la correspondance avec les états de base.
| |
Job et Transaction
Tout changement d’état d’une unit se fait au travers d’un Job. Il existe plusieurs types de job :
- Start / Verify
- Stop
- Reload
- Restart
- Nop
On trouve également d’autres types de Job qui sont la combinaison des types précédents : par exemple “try-restart” se transformera à l’exécution en “start” ou “nop” si la unit n’est pas activée ou en cours de rechargement.
Prenons l’exemple de la unit file suivante :
| |
La propriété Wants= a pour effet de créer une dépendance entre cette unit et la target unit “multi-user.target”.
NB : La plupart des relations peuvent être notées dans un sens ou dans l’autre (Before=/ After=, Require=/RequiredBy=…)
On pourrait donc réaliser cette même dépendance à l’aide de la propriété WantedBy= dans la target et cela aurait le même effet.
Cette dépendance va avoir pour effet de propager l’activation de la target “multi-user.target” aux unit ayant une relation de type Wants= avec elle.
Lors de cette activation une transaction qui va contenir le job d’activation de la target en elle-même plus autant de job que de dépendances.
Ainsi dans le cas d’une relation avec une unit de type Wants= la transaction pourra être mise en succès même si ce job est en échec.
Ce qui n’aurait pas été le cas avec une relation plus “forte” (par exemple Require=/RequiredBy=).
En pratique ces relations sont spécifiées par une énumération de propriétés beaucoup plus fines les UnitDependencyAtom ou “atoms” (qui ne sont rien d’autre qu’un bitmask).
Ainsi la propriété Wants= est composée de la sorte :
| |
Cela lui permet de réagir aux propriétés suivantes qui peuvent être utilisées par différents objets (transaction, units…). Sans rentrer dans les détails on retrouve l’attribut “UNIT_ATOM_PULL_IN_START_IGNORED” le comportement décrit plus haut.
Il implémente un mécanisme de transaction connu sous le nom de Job qui s’assure de la transition d’une unit d’un état A vers un état B (par exemple start / stop / restart…).
Conclusion
Vous l’avez peut-être remarqué, mais systemd est conçu comme un système orienté objet. On retrouve d’ailleurs beaucoup de principes de la POO (polymorphisme, sous-typage, redéfinition…) dans les différents points que j’ai abordés
Voilà ainsi va se conclure cet article. Dans le prochain, nous aborderons l’architecture de systemd.