/etc/rc en action. Pfff, c'est pourri ton truc, y'a même pas des [OK] verts fluo :(
1. Un peu d'histoire
Branchons la DeLorean sur 1980 et voyons à quoi ressemblait un /etc/rc dans 3BSD :
echo '' >/dev/console
date >/dev/console
echo entering rc >/dev/console
HOME=/
export HOME
echo clearing mtab >/dev/console
cp /dev/null /etc/mtab
echo mounting /usr on /dev/rp0g >/dev/console
/etc/mount /dev/rp0g /usr
echo preserving Ex temps and clearing /tmp >/dev/console
cd /tmp
(/usr/lib/ex3.2preserve -a; rm -f *)
cd /
echo starting update >/dev/console
/etc/update&
: echo clearing lock and starting printer >/dev/console
: rm -f /usr/lpd/lock
echo starting cron >/dev/console
/etc/cron&
: echo starting accounting >/dev/console
: /etc/accton /usr/adm/acct
: echo starting net daemon >/dev/console
: sh /usr/net/bin/start &
chmod 666 /etc/motd
echo leaving rc >/dev/console
*tousse* *tousse* ah ça oui, ça sent un peu le renfermé et on distingue mal les caractères à travers toutes ces toiles d'araignées. Mais tout de même, depuis ce script, 33 ans nous observent ! Et de confirmer que la voie était toute tracée en lisant le code source de init(8) d'alors, appelé init.vm et situé dans /etc :
[...]
char shell[] = "/bin/sh";
char getty[] = "/etc/getty.vm";
char minus[] = "-";
char runc[] = "/etc/rc";
[...]
runcom()
{
register pid, f;
pid = fork();
if(pid == 0) {
open("/", 0);
dup(0);
dup(0);
execl(shell, shell, runc, (char *)0);
exit(0);
}
while(wait((int *)0) != pid)
[...]
Nous y sommes, les prémices de rcNG.
En 1998, NetBSD apporte sa première pierre dans la rénovation des scripts de démarrage en introduisant le fichier /etc/rc.conf dans la version 1.3 de l'OS. Ce fichier ouvrait la voie à une plus fine configuration des services à démarrer, les conditions étant malgré tout réalisées dans /etc/rc, /etc/netstart et /etc/rc.lkm.
C'est en décembre 2000, 20 ans plus tard tout de même, que Luke Mewburn (oui, celui-là même à qui nous « devons » le drapeau orange...) révolutionne le design historiquement monolithique de rc hérité de 4.4BSD pour intégrer une petite révolution dans NetBSD 1.5 : rc.d(8)[3]. Cette nouvelle implémentation propose :
- Une mécanique d'ordre de démarrage des scripts indépendante des noms de fichiers (au contraire des SXXservice de l'init des UNIX System V).
- La possibilité d'intégrer des scripts tiers, issus par exemple de paquets.
- La manipulation de scripts autonomes pour gérer les services indépendamment.
- L'utilisation intensive du fichier /etc/rc.conf pour contrôler le comportement des services démarrés.
- Promouvoir la portabilité et la réutilisation du code (une vieille habitude chez NetBSD).
- Éviter les inutiles runlevels .
La nouvelle infrastructure est un véritable succès, simple mais souple, comme à son habitude, le Projet réalise une évolution dans la continuité et la philosophie propre aux UNIces BSD. Peu après cette release, ce sera au tour de FreeBSD d'intégrer ce nouveau dispositif à partir de sa version 5.0 en janvier 2003, sous le nom de rcNG[4].
2. Oil of Codaz
Les concepts et origines étant maintenant acquis, rentrons un peu plus dans le détail. Notre noyau s'en est remis à init(8) après avoir mis en place les briques essentielles au bon fonctionnement d'un système BSD UNIX multitâches. Nous articulerons notre explication sur des morceaux de code issus de NetBSD 6.0 :
Issu de sys/kern/init_main.c :
/*
* Create process 1 (init(8)). We do this now, as Unix has
* historically had init be process 1, and changing this would
* probably upset a lot of people.
*
* Note that process 1 won't immediately exec init(8), but will
* wait for us to inform it that the root file system has been
* mounted.
*/
if (fork1(l, 0, SIGCHLD, NULL, 0, start_init, NULL, NULL, &initproc))
panic("fork init");
init(8), dans le cas d'un démarrage en mode multi-utilisateurs, va invoquer notre fameux /etc/rc en argument de sh :
Issu de src/sbin/init/pathnames.h :
#define _PATH_RUNCOM "/etc/rc"
Issu de src/sbin/init/init.c :
switch ((pid = fork())) {
case 0:
(void)sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = SIG_IGN;
(void)sigaction(SIGTSTP, &sa, NULL);
(void)sigaction(SIGHUP, &sa, NULL);
setctty(_PATH_CONSOLE);
argv[0] = "sh";
argv[1] = _PATH_RUNCOM;
argv[2] = (runcom_mode == AUTOBOOT ? "autoboot" : 0);
argv[3] = 0;
La séquence d'initialisation va maintenant prendre la main ; en premier lieu, rc va interpréter le fichier /etc/rc.subr, qui regroupe les fonctions communes à la manipulation de scripts de démarrage. Comme exposé par Luke, la nouvelle infrastructure rc.d se doit d'être portable et de partager tout le code qui peut l'être, ainsi, tout script de contrôle de service doit s'assurer que rc.subr est bien chargé afin de ne pas réecrire inutilement du code factorisé et utilisé par l'ensemble de la chaîne.
Une fois les fonctions chargées, rc interprétera le fichier /etc/rc.conf qui indiquera la liste des services à démarrer ainsi que leurs éventuels paramètres. Cette vérification est réalisée par la fonction checkyesno, par exemple :
if ! checkyesno rc_configured; then
echo "/etc/rc.conf is not configured. Multiuser boot aborted."
exit 1
fi
Simple, efficace, élégant. Notez que toutes les opérations, succès comme échecs, sont enregistrées dans le fichier de log /var/run/rc.log, ce qui peut s'avérer fort pratique si vous avez activé la variable rc_silent apparue avec NetBSD 6.0 qui ordonne à rc de ne pas afficher de statut sur la sortie standard.
Les services de base bénéficient tous d'une configuration par défaut située dans /etc/defaults/rc.conf, ces variables sont bien entendu écrasées par les valeurs placées dans le fichier /etc/rc.conf, il est défendu de modifier des variables directement dans le fichier /etc/defaults/rc.conf !
Voici un exemple de rc.conf sur une machine de bureau, également utilisée pour mener quelques tests :
# $NetBSD: rc.conf,v 1.96 2000/10/14 17:01:29 wiz Exp $
#
# see rc.conf(5) for more information.
#
# Use program=YES to enable program, NO to disable it. program_flags are
# passed to the program on the command line.
#
# Load the defaults in from /etc/defaults/rc.conf (if it's readable).
# These can be overridden below.
#
if [ -r /etc/defaults/rc.conf ]; then
. /etc/defaults/rc.conf
fi
# If this is not set to YES, the system will drop into single-user mode.
#
rc_configured=YES
# Add local overrides below
#
wscons=YES
sshd=YES
pf=YES
powerd=YES
postfix=YES
dovecot=YES
dbus=YES
hal=YES
rpcbind=YES
famd=YES
avahidaemon=YES
xdm=YES
cupsd=YES
rpcbind=yes
nfs_client=yes
lockd=yes
statd=yes
nginx=YES
php_fpm=YES
mysqld=YES
estd=YES
estd_flags="-a -P -m 1500"
ntpdate=YES
ntpd=YES
smbd=YES
nmbd=YES
Bien évidemment, l'ordre des services dans cette configuration n'a aucune importance, car c'est l'exécutable rcorder(8) qui va avoir la charge d'ordonner le démarrage des services en analysant leurs interdépendances ; rcorder réalise cette tâche en se basant sur des mots-clés que nous trouvons dans les scripts de démarrage. Par exemple :
# PROVIDE: ppp
# REQUIRE: mountcritremote syslogd
# BEFORE: SERVERS
Dans ce cas, le rc-script fournit ppp et nécessite mountcritremote (montage des systèmes de fichiers possiblement distants) et syslog (démon syslogd lancé).
3. Anatomie d'un rc-script
À quel point l'infrastructure rcNG est-elle puissante, factorisée et facile à mettre en œuvre ? Jugez plutôt :
$ cat /etc/rc.d/apmd # apmd est un démon de monitoring de la gestion de l'énergie
#!/bin/sh
#
# $NetBSD: apmd,v 1.6 2004/08/13 18:08:03 mycroft Exp $
#
# PROVIDE: apmd
# REQUIRE: DAEMON
# BEFORE: LOGIN
$_rc_subr_loaded . /etc/rc.subr
name="apmd"
rcvar=$name
command="/usr/sbin/${name}"
load_rc_config $name
run_rc_command "$1"
Shocking, isn't it? Avec ces seules six lignes de shell, rc.d est capable des actions suivantes :
Usage: /etc/rc.d/apmd [fast|force|one](start stop restart rcvar status poll)
Mais quelle est cette sorcellerie ! Revoyons la scène au ralenti :
$_rc_subr_loaded . /etc/rc.subr
- On s'assure que les fonctions issues de rc.subr sont bien chargées, $_rc_subr_loaded vaut : lorsque c'est le cas, inhibant ainsi l'interprétation à suivre.
name="apmd"
- $name est la variable principale dans la mécanique rcNG, il s'agit du nom du script/service.
rcvar=$name
- La variable utilisée pour contrôler le script, elle peut avoir un nom différent de $name, mais c'est évidemment peu souhaitable pour des raisons de clarté
command="/usr/sbin/${name}"
- La commande effective, celle qui sera exécutée par le script
load_rc_config $name
- Chargement de la configuration liée au service dans /etc/rc.conf ou /etc/rc.conf.d/$name. Précisons que ces fichiers de configuration sont sourcés, il peut donc s'avérer pratique dans certains cas d'ajouter des commandes spécifiques à un service dans /etc/rc.conf.d/$name, comme une redéfinition de ulimit pour un service très gourmand en file descriptors
run_rc_command "$1"
- Exécution de la commande rc.d souhaitée.
Arrêtons-nous un instant sur cette dernière fonction, véritable centre de contrôle du service. La fonction run_rc_command comprend les actions standards start, stop, reload, restart, status, poll et rcvar par défaut, chacune de ces actions pouvant être surchargée d'un préfixe fast, force et one. Par exemple :
# /etc/rc.d/apmd onestart
aura pour effet de démarrer une fois le service même si ce dernier n'est pas explicitement activé dans le fichier /etc/rc.conf. Mais notre fonction est capable de gérer bien plus de situations, en effet, sans plus de programmation, run_rc_command sait tirer profit des variables suivantes :
- command_args, arguments optionnels à passer à la commande à exécuter ;
- command_interpreter, un nom d'interpréteur, typiquement lorsque le service est en réalité un script et que le PID à considérer est celui de l'interpréteur (python, perl , php...) ;
- extra_commands, la liste des commandes supplémentaires acceptées par le rc-script ;
- pidfile, rc.subr utilisera un fichier de PID pour vérifier l'existence du processus ;
- procname, si on souhaite donner un nom de processus à vérifier différent de $command ;
- ${name}_chroot, répertoire dans lequel on souhaite chroot'er le service ;
- ${name}_chdir, répertoire dans lequel on souhaite se déplacer avant d'exécuter $command ;
- ${name}_flags, des arguments supplémentaires à passer $command ;
- ${name}_env, variables d'environnement supplémentaires nécessaires à l'exécution de $command ;
- ${name}_nice, niveau de priorité (nice level) avec lequel démarrer $command ;
- ${name}_user, utilisateur avec lequel démarrer $command ;
- ${name}_group, groupe avec lequel démarrer $command ;
- ${name}_groups, groupes supplémentaires ;
- ${rc_arg}_cmd, surcharge le comportement par défaut pour la commande passée en argument au script, par exemple start_cmd=“start_service” appellera la fonction (à définir) start_service au lieu de simplement invoquer $commmand ;
- ${rc_arg}_precmd, commande à invoquer avant l'action passée en argument au script ;
- ${rc_arg}_postcmd, commande à invoquer après l'action passée en argument au script ;
- required_dirs, répertoires dont il faudra vérifier l'existence avant de démarrer le service ;
- required_files, fichiers dont il faudra vérifier l'existence avant de démarrer le service ;
- required_vars, variables dont il faudra vérifier l'existence avant de démarrer le service.
Ces variables sont toutes utilisables dans le rc-script et les variables ${name}_* sont en général définies dans /etc/rc.conf ; par exemple, pour signifier au démon named de chrooter dans /var/chroot/named avant de démarrer :
$ grep ^named /etc/rc.conf
named=YES
named_chrootdir="/var/chroot/named"
Autre exemple plus complet, l'initialisation de variables particulières dans le script de démarrage du démon mysqld :
# [...]
name="mysqld"
rcvar=${name}
command="/usr/pkg/bin/mysqld_safe"
procname="/usr/pkg/sbin/${name}"
: ${mysqld_user:=mysql}
: ${mysqld_group:=mysql}
: ${mysqld_datadir:=/var/mysql}
extra_commands="initdb"
initdb_cmd="mysqld_initdb"
start_precmd="mysqld_precmd"
start_cmd="mysqld_start"
# [...]
Ici, si on n'a pas spécifié d'utilisateur, groupe et répertoire de travail dans le fichier /etc/rc.conf, alors on renseigne mysqld_user, mysqld_group et mysqd_datadir par des valeurs par défaut.
On ajoute au script une fonction supplémentaire, initdb, réalisée par la fonction mysqld_initdb, on demande explicitement d'exécuter la fonction mysqld_precmd avant le démarrage du service et on écrase la fonction de démarrage avec la fonction mysqld_start.
À l'issue de ce chapitre, et munis de ces seuls exemples, vous disposez de toutes les connaissances nécessaires et suffisantes à créer à peu près n'importe quel type de script rcNG.
4. Portabiliquoi ?
Au contraire des bloatware [1] dont je vous entretenais au début de cet article, nous avons pu constater que l'infrastructure rc.d se veut hautement portable ; n'utilisant aucune spécificité de son système d'exploitation d'origine, ce mécanisme de démarrage peut s'intégrer dans n'importe quel OS disposant d'un shell « standard » [5]. L'unification de ce dispositif de démarrage nous assure que les services de base comme les logiciels tierce partie bénéficient du même séquencement et permet en outre de s'appuyer sur les très nombreux exemples disponibles dans le système initial.
L'argumentaire en vogue prétend que faire dépendre le démarrage de son système d'exploitation d'une cascade de scripts shell induit un temps de démarrage abominablement long ; mon poste de travail sous NetBSD 6.0[6] démarre en approximativement 30 secondes, depuis le bootloader jusqu'à xdm, je ne suis pas exactement friand des concours de ce type, mais le ratio flexibilité/temps d’exécution me semble parfaitement honorable.
Liens
[1] http://www.freedesktop.org/wiki/Software/systemd
[2] http://upstart.ubuntu.com/
[3] http://static.usenix.org/event/usenix01/freenix01/full_papers/mewburn/mewburn_html/
[4] http://www.freebsd.org/releases/5.0R/relnotes.html
[5] http://pubs.opengroup.org/onlinepubs/009695399/utilities/sh.html
[6] http://imil.net/wp/2012/08/22/6-month-later/