9.11. Branchements « à chaud » : hotplug
Le sous-système hotplug du noyau permet de charger les pilotes des périphériques et de créer les fichiers de périphériques correspondants (avec l'aide d'udevd
). Avec le matériel moderne et la virtualisation, quasiment tous les périphériques peuvent être connectés à chaud, depuis les classiques USB/PCMCIA/IEEE 1394 et les disques durs SATA jusqu'au processeur et à la mémoire eux-mêmes.
Le noyau dispose d'une base de données associant à chaque identifiant de périphérique le pilote requis. Cette base de données est employée au démarrage de l'ordinateur pour charger tous les pilotes des périphériques détectés sur les différents bus mentionnés, mais aussi lors de l'insertion à chaud d'un périphérique supplémentaire. Une fois le pilote chargé, un message est envoyé à udevd
afin que celui-ci puisse créer l'entrée correspondante dans /dev/
.
9.11.2. La problématique du nommage
Avant l'introduction des branchements à chaud, il était simple de donner un nom fixe à un périphérique. On se basait simplement sur le positionnement des périphériques dans leur bus respectif. Mais si les périphériques apparaissent et disparaissent sur le bus, ce n'est plus possible. L'exemple typique est l'emploi d'un appareil photo numérique et d'une clé USB : tous les deux apparaissent comme des disques, le premier branché pourra être /dev/sdb
et le second /dev/sdc
(avec /dev/sda
représentant le disque dur). Le nom du périphérique n'est donc pas fixe, il dépend de l'ordre dans lequel ils ont été connectés.
En outre, de plus en plus de pilotes emploient des numéros majeur/mineur dynamiques, ce qui fait qu'il est impossible d'avoir une entrée statique pour le périphérique, puisque ces caractéristiques essentielles peuvent varier après un redémarrage de l'ordinateur.
C'est pour résoudre ces problématiques qu'udev a été créé.
9.11.3. Fonctionnement de udev
Lorsqu'udev est informé par le noyau de l'apparition d'un nouveau périphérique, il récupère de nombreuses informations sur le périphérique en question en consultant les entrées correspondantes dans /sys/
, en particulier celles qui permettent de l'identifier de manière unique (adresse MAC pour une carte réseau, numéro de série pour certains périphériques USB, etc.).
Armé de toutes ces informations, udev consulte l'ensemble de règles contenu dans /etc/udev/rules.d/
et /lib/udev/rules.d/
et décide à partir de cela du nom à attribuer au périphérique, des liens symboliques à créer (pour offrir des noms alternatifs), ainsi que des commandes à exécuter. Tous les fichiers sont consultés et les règles sont toutes évaluées séquentiellement (sauf quand un fichier fait appel à des constructions de type « GOTO »). Ainsi, il peut y avoir plusieurs règles qui correspondent à un événement donné.
La syntaxe des fichiers de règles est assez simple : chaque ligne contient des critères de sélection et des directives d'affectation. Les premiers permettent de sélectionner les événements sur lesquels il faudra réagir et les seconds définissent l'action à effectuer. Tous sont simplement séparés par des virgules et c'est l'opérateur qui désigne s'il s'agit d'un critère de sélection (pour les opérateurs de comparaison ==
ou !=
) ou d'une directive d'affectation (pour les opérateurs =
, +=
ou :=
).
Les opérateurs de comparaison s'emploient sur les variables suivantes :
KERNEL
: le nom que le noyau affecte au périphérique ;
ACTION
: l'action correspondant à l'événement (« add » pour l'ajout d'un périphérique, « remove » pour la suppression) ;
DEVPATH
: le chemin de l'entrée correspondant au périphérique dans /sys/
;
SUBSYSTEM
: le sous-système du noyau à l'origine de la demande (ils sont nombreux mais citons par exemple « usb », « ide », « net », « firmware », etc.) ;
ATTR{attribut}
: contenu du fichier attribut dans le répertoire /sys/$devpath/
du périphérique. C'est ici que l'on va trouver les adresses MAC et autres identifiants spécifiques à chaque bus ;
KERNELS
, SUBSYSTEMS
et ATTRS{attribut}
sont des variantes qui vont chercher à faire correspondre les différentes options sur un des périphériques parents du périphérique actuel ;
PROGRAM
: délègue le test au programme indiqué (vrai s'il renvoie 0, faux sinon). Le contenu de la sortie standard du programme est stocké afin de pouvoir l'utiliser dans le cadre du test RESULT
;
RESULT
: effectue des tests sur la sortie standard du dernier appel à PROGRAM
.
Les opérandes de droite peuvent employer certaines expressions de motifs pour correspondre à plusieurs valeurs en même temps. Ainsi, *
correspond à une chaîne quelconque (même vide), ?
correspond à un caractère quelconque et []
correspond à l'ensemble de caractères cités entre les crochets (l'ensemble inverse si le premier caractère est un point d'exclamation, et les intervalles de caractères sont possibles grâce à la notation a-z
).
En ce qui concerne les opérateurs d'affectation, =
affecte une valeur (et remplace la valeur actuelle) ; s'il s'agit d'une liste elle est vidée et ne contient plus que la valeur affectée. :=
fait de même mais empêche les modifications subséquentes de cette même variable. Quant à +=
, il ajoute une valeur dans une liste. Voici les variables qui peuvent être modifiées :
NAME
: le nom du fichier de périphérique à créer dans /dev/
. Seule la première affectation compte, les autres sont ignorées ;
SYMLINK
: la liste des noms symboliques qui pointeront sur le même périphérique ;
OWNER
, GROUP
et MODE
définissent l'utilisateur et le groupe propriétaire du périphérique ainsi que les permissions associées ;
RUN
: la liste des programmes à exécuter en réponse à cet événement.
Les valeurs affectées à ces variables peuvent employer un certain nombre de substitutions :
$kernel
ou %k
: équivalent de KERNEL
;
$number
ou %n
: le numéro d'ordre du périphérique, par exemple « 3 » pour sda3
;
$devpath
ou %p
: équivalent de DEVPATH
;
$attr{attribut}
ou %s{attribut}
: équivalent de ATTRS{attribut}
;
$major
ou %M
: le numéro majeur du périphérique ;
$minor
ou %m
: le numéro mineur du périphérique ;
$result
ou %c
: la chaîne renvoyée par le dernier programme invoqué par PROGRAM
;
enfin %%
et $$
pour les caractères pourcent et dollar respectivement.
Ces listes ne sont pas exhaustives (elles reprennent les paramètres les plus importants) mais la page de manuel udev(7) devrait l'être.
Prenons le cas d'une simple clé USB et essayons de lui affecter un nom fixe. Il faut d'abord trouver les éléments qui vont permettre de l'identifier de manière unique. Pour cela, on la branche et on exécute udevadm info -a -n /dev/sdc
(en remplaçant évidemment /dev/sdc par le nom réel affecté à la clé).
#
udevadm info -a -n /dev/sdc
[...]
looking at device '/devices/pci0000:00/0000:00:10.0/usb2/2-1/2-1:1.0/host4/target4:0:0/4:0:0:0/block/sdc':
KERNEL=="sdc"
SUBSYSTEM=="block"
DRIVER==""
ATTR{hidden}=="0"
ATTR{events}=="media_change"
ATTR{ro}=="0"
ATTR{discard_alignment}=="0"
ATTR{removable}=="1"
ATTR{events_async}==""
ATTR{alignment_offset}=="0"
ATTR{capability}=="51"
ATTR{events_poll_msecs}=="-1"
ATTR{stat}==" 130 0 6328 435 0 0 0 0 0 252 252 0 0 0 0"
ATTR{size}=="15100224"
ATTR{range}=="16"
ATTR{ext_range}=="256"
ATTR{inflight}==" 0 0"
[...]
looking at parent device '/devices/pci0000:00/0000:00:10.0/usb2/2-1/2-1:1.0/host4/target4:0:0/4:0:0:0':
[...]
ATTRS{max_sectors}=="240"
[...]
looking at parent device '/devices/pci0000:00/0000:00:10.0/usb2/2-1':
KERNELS=="2-1"
SUBSYSTEMS=="usb"
DRIVERS=="usb"
ATTRS{bDeviceProtocol}=="00"
ATTRS{bNumInterfaces}==" 1"
ATTRS{busnum}=="2"
ATTRS{quirks}=="0x0"
ATTRS{authorized}=="1"
ATTRS{ltm_capable}=="no"
ATTRS{speed}=="480"
ATTRS{product}=="TF10"
ATTRS{manufacturer}=="TDK LoR"
[...]
ATTRS{serial}=="07032998B60AB777"
[...]
Pour constituer une ligne de règle, on peut employer des tests sur les variables du périphérique ainsi que celles d'un seul des périphériques parents. L'exemple ci-dessus permet notamment de créer deux règles comme celles-ci :
KERNEL=="sd?", SUBSYSTEM=="block", ATTRS{serial}=="07032998B60AB777", SYMLINK+="usb_key/disk"
KERNEL=="sd?[0-9]", SUBSYSTEM=="block", ATTRS{serial}=="07032998B60AB777", SYMLINK+="usb_key/part%n"
Une fois ces règles placées dans un fichier, nommé par exemple /etc/udev/rules.d/010_local.rules
, il suffit de retirer puis réinsérer la clé USB. On peut alors constater que /dev/clef_usb/disk
représente le disque associé à la clé USB et que /dev/clef_usb/part1
est sa première partition.