Linux Embedded

Le blog des technologies libres et embarquées

Une introduction à UDEV

Un système Linux moderne n'a plus grand chose à voir avec ce que l'on utilisait au début des systèmes Unix. Les façons d'utiliser nos machines ont considérablement évolué et ces évolutions ont à leur tour forcé l'infrastructure Unix à changer. Après une introduction à systemd dans un article précédent, nous allons nous intéresser à une autre brique de base des systèmes Linux trop peu connue : udev.

Qu'est-ce qu'un device node?

udev est le daemon chargé de gérer les device-nodes. Nous allons commencer par nous intéresser à ceux-ci pour comprendre le problème qu'udev résout.

Un aspect important de la philosophie Unix est dans cette phrase : "Tout est fichier". Un système Unix va présenter certains outils informatiques (en particulier les périphériques matériels) sous forme de pseudo-fichiers. Ils sont visibles dans le système de fichiers et peuvent être manipulés via les outils habituels : cat, grep, open(), read(), write() etc... Cette philosophie est très pratique et est en partie responsable du succès des systèmes Unix.

Les systèmes Linux ont donc des fichiers pour permettre l'accès aux périphériques. Ceux-ci sont traditionnellement stockés dans /dev et sont appelés device nodes. Un device node est un point d'entrée vers le noyau caractérisé par un type (bloc ou char) et deux nombres: le major et le minor. Ce triplé définit de façon unique quel périphérique matériel est accédé via ce fichier. Le majeur permet au noyau de savoir quel driver doit gérer le périphérique et le mineur permet au driver de savoir quel périphérique parmi ceux qu'il gère est utilisé.

Comment savoir quel paire majeur/mineur correspond à quel périphérique? C'est une question compliquée dont la réponse a beaucoup évolué depuis les débuts de Linux.

Un peu d'histoire

Le script MAKEDEV

Historiquement, la réponse au problème des choix de numéro majeur et mineur était simple : Ces numéros sont codés en dur dans le noyau et le noyau fournit un script shell (appelé MAKEDEV) qui utilise la commande mknode pour créer et maintenir tous les périphériques dans /dev. L'association est faite par les mainteneurs du kernel qui codent en dur ces chiffres dans le driver et dans MAKEDEV.

Cette approche avait de nombreux problèmes :

  • Il est nécessaire de maintenir à jour le script MAKEDEV,
  • Tous les périphériques sont créés par MAKEDEV indépendamment de leur existence. Il y avait des entrés dans /dev pour tous les périphériques pouvant exister, pas juste ceux existants sur votre machine et le total dépassait les 18 000 (!!) lorsque ce système a été abandonné,
  • Cette approche ne peut pas gérer le hotplug et l'arrivée de l'USB rendait le modèle du hardware statique très limitant,
  • L'espace des numéros majeurs et mineurs possibles (un octet chacun à l'époque) commençait à devenir étroit. En particulier pour les disques, chacun étant un majeur, les mineurs étant utilisés pour numéroter les partitions,
  • L'ordre de détection des périphériques pouvant varier, il est impossible de savoir de façon fiable quel périphérique correspond à quel nœud. Cela fonctionnait pour l'IDE mais très mal pour les disques SCSI et pas du tout pour l'USB,
  • Cette solution est peu souple. Tout changement local demande à maintenir un patch sur MAKEDEV.

L'invention de devfs

Ces problèmes, et en particulier la généralisation de l'USB, ont forcé les développeurs du noyau à trouver une autre approche. Un système de fichiers spécifique (similaire à /proc) a été créé pour l'occasion : devfs.

Devfs est un système de fichiers contenant des device nodes, mais contrairement à l'approche traditionnelle, les nœuds sont créés par les drivers des périphériques lorsque les périphériques sont détectés. Le driver est particulièrement bien placé pour savoir quels périphériques sont présents et donc pour savoir quel est le nom attendu par l'utilisateur.

Si devfs a beaucoup amélioré les choses en terme de gestion des nœuds (gestion dynamique, uniquement les périphériques existants), il restait un certain nombre de limitations qui se sont avérés impossibles à contourner:

  • Les numéros majeurs et mineurs étaient toujours codés en dur dans le noyau,
  • devfs n'était pas compatible avec LSB,
  • devfs était très difficile à paramétrer,
  • La politique de nommage des périphériques dans /dev était codée en dur dans le noyau,
  • Certaines conditions de courses dangereuses ne pouvaient pas être réparées sans changer l'API de devfs,
  • Il est impossible pour un programme d'être prévenu de l'arrivée d'un nouveau périphérique.

Tout ces points ont fait que, si devfs était clairement une amélioration, les développeurs du kernel se sont rendus compte que cette solution n'était pas suffisante et qu'il fallait passer à quelque chose d'autre.

Finalement udev

D'un certain point de vue, udev est un retour à la méthode MAKEDEV. Le noyau n'a pas de code spécifique à udev, et udev est un programme purement user-space relativement simple. Mais ce point de vue oublie un certain nombre d'évolutions du noyau qui sont apparues entre temps et sur lesquels s'appuie udev pour fonctionner.

  • Le noyau exporte un grand nombre d'informations sur ses périphériques dans /sys. Une source d'information extrêmement complète qui n'existait pas à l'époque de MAKEDEV. Cela permet de retrouver les majors et minors des périphériques (donc de ne pas les coder en dur) ainsi qu'un nom probable pour le fichier /dev correspondant.
  • Le noyau fournit un lien netlink permettant à n'importe quel programme user-space d'être prévenu de l'arrivée ou du départ de n'importe quel périphérique. L'interface netlink fournit (entre autre) la position du périphérique dans /sys, ce qui permet de récupérer toutes les informations disponibles sur le périphérique.

udev est donc un daemon relativement simple qui récupère les événements noyau, les compare à une base de règles venant de fichiers de configuration et crée les entrées udev correspondantes. Il peut également appeler des commandes externes pour obtenir des informations supplémentaires sur le périphérique (comme une base de données des identifiants USB ou PCI) ou pour permettre à d'autres programmes de réagir (en envoyant un message d-bus par exemple).

Les événements noyau en détail

udev réagit donc à des événements envoyés par le noyau. Pour comprendre un peu plus ce qui se passe (et avant d'étudier la syntaxe des règles udev) nous allons étudier ces événements.

udev est capable d'afficher les événement noyau au fur et à mesure de leur arrivée. Pour cela, lancez la commande suivante :

udevadm monitor -k

puis branchez une souris USB. Vous devriez voir apparaître les lignes suivantes:

monitor will print the received events for:
KERNEL - the kernel uevent

KERNEL[117993.564897] add /devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.1 (usb)
KERNEL[117993.565452] add /devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.1/2-1.1:1.0 (usb)
KERNEL[117993.567809] add /devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.1/2-1.1:1.0/0003:15D9:0A4D.0009 (hid)
KERNEL[117993.568072] add /devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.1/2-1.1:1.0/0003:15D9:0A4D.0009/input/input28 (input)
KERNEL[117993.568443] add /devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.1/2-1.1:1.0/0003:15D9:0A4D.0009/input/input28/mouse0 (input)
KERNEL[117993.568713] add /devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.1/2-1.1:1.0/0003:15D9:0A4D.0009/input/input28/event1 (input)
KERNEL[117993.568763] add /devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.1/2-1.1:1.0/0003:15D9:0A4D.0009/hidraw/hidraw0 (hidraw)

Chaque ligne correspond à un événement envoyé par le noyau vers udev. On y voit apparaître :

  • Le périphérique USB lui-même,
  • L'unique interface sur ce périphérique,
  • le périphérique USB en tant que périphérique HID,
  • le périphérique en tant qu'input-device.

Le noyau est donc assez verbeux dans ses événements et chaque périphérique est vu plusieurs fois selon ses différentes fonctionnalités (une clé USB génère facilement une douzaine d'événements, plus si elle comporte un grand nombre de partitions).

Le noyau envoie un certain nombre de propriétés avec chaque événement. Nous pouvons demander à udev de nous afficher ces propriétés avec la commande suivante:

udevadm monitor -k -p

Le premier événement de branchement de notre souris affiche alors les propriétés suivantes:

KERNEL[118369.168889] add /devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.1 (usb)
ACTION=add
BUSNUM=002
DEVNAME=/dev/bus/usb/002/036
DEVNUM=036
DEVPATH=/devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.1
DEVTYPE=usb_device
MAJOR=189
MINOR=163
PRODUCT=15d9/a4d/100
SEQNUM=2678
SUBSYSTEM=usb
TYPE=0/0/0

Les propriétés dépendent du type exact de périphérique mais certaines propriétés sont toujours présentes:

  • ACTION : Le type d'événement à traiter,
  • MAJOR, MINOR : Les numéro majeur et mineur du périphérique concerné,
  • SEQNUM : un numéro croissant pour ordonner les événements,
  • SUBSYSTEM : Le sous-système noyau ayant causé l'événement,
  • DEVPATH : Le fichier dans /sys correspondant au périphérique.

Les règles udev

Lorsque udev reçoit un événement noyau, il utilise une base de règles et applique les règles qui correspondent à l'événement. Les règles viennent de trois répertoires différents (du plus au moins prioritaire):

  • /etc/udev/rules.d/*.rules (règles locales ajoutées par l'administrateur),
  • /run/udev/rules.d/*.rules (règles volatiles, généralement créées par d'autres règles),
  • /lib/udev/rules.d/*.rules (règles fourni par la distribution).

Les fichiers sont ordonnés selon leur nom (les noms sont donc préfixés par une priorité, comme pour les scripts d'init). Si deux fichiers ont le même nom, celui venant du répertoire le plus prioritaire est utilisé (on peut donc supplanter un fichier venant de la distribution avec notre propre version sans avoir à modifier le fichier d'origine).

Pour tester udev, créons un fichier /etc/udev/rules.d/99-test-linuxembedded.rules avec le contenu suivant:

ACTION=="add", RUN+="/bin/sh -c '/bin/echo %k : %p >> /root/events'"

Le daemon udev surveille ces fichiers de configuration via inotify, il est donc inutile de lui signaler les modifications.

Cette règle s'applique à la toute fin des règles udev (fichier nommé 99), il s'applique pour tout ajout de périphérique (ACTION=="add") et écrit dans un fichier le nom du périphérique suggéré par le noyau (%k) ainsi que le fichier sysfs correspondant (%p).

Les règles de filtrage

Il est possible de filtrer précisément les événements. La syntaxe est très simple. PROPRIETE==valeur ou PRORIETE!=valeur. Les propriétés possibles sont assez nombreuses et permettent une grande souplesse dans le filtrage. Outre les propriétés venant du noyau que nous avons vu précédemment, notons les propriétés suivantes :

  • ATTR{fichier} : permet de filtrer sur le contenu d'un fichier dans le répertoire sysfs correspondant à l'événement en cours. Les fichiers dans le répertoire sysfs correspondant à un périphérique contiennent généralement un seul nom correspondant à une propriété du périphérique. ATTR{idVendor}=="18d1" n'activera la règle que si le périphérique concerné a un fichier <repertoire sysfs>/idVendor contenant 18d1 c'est à dire un périphérique USB fabriqué par Google.
  • ATTRS{fichier} : permet de filtrer comme précédemment, mais non seulement sur les attributs sysfs du périphérique mais également sur ceux des périphériques parents (bus, plateforme etc...).
  • ENV{clé} : permet de filtrer sur une propriété ajoutée par une autre règle udev. Certaines règles udev fournies par la distribution ajoutent un grand nombre de propriétés intéressantes aux périphériques (comme les noms correspondants aux identifiants PCI) nous verrons comment ajouter ces propriétés plus bas.
  • PROGRAM : permet de filtrer sur la valeur de retour d'un programme. Il est également possible de filtrer sur la sortie standard du programme, reportez vous à la page de man de udev pour les détails.

 Les actions à effectuer

Lorsque nous sommes satisfaits de notre filtre, il est temps d'agir. udev permet principalement d'agir sur le contenu de /dev mais ses possibilités sont plus grandes que cela. Toutes les actions ont une syntaxe similaire à un positionnement de variable VARIABLE=valeur (notez le  et non ==) ou VARIABLE+=valeur. Certaines variables spéciales ont une signification particulière pour udev. En voici quelques unes :

  • SYMLINK : crée un lien symbolique vers le fichier /dev du pérphérique. Le  noyau fournit un premier fichier /dev dont le nom ne peut pas être changé. udev permet de créer des liens symboliques vers ces fichiers,
  • OWNER, GROUP, MODE, SECLABEL{module} : permet de préciser les droits d'accès et les propriétés SELinux du périphérique,
  • ATTR{fichier} : permet d'écrire une valeur dans un fichier du répertoire sysfs correspondant au périphérique,
  • ENV{clé} : permet de positionner un attribut interne à udev pour le périphérique en cours,
  • RUN : permet d'exécuter un programme. Le programme peut accéder à l'ensemble des propriétés udev sous forme de variables d'environnement. Le programme n'est exécuté qu'une fois toutes les règles analysées.

Notez également qu'un certain nombre de mots clés dans les arguments peuvent être étendus. Nous avons vu %k et %p dans notre exemple, mais la liste complète est dans la page de man de udev.

udevadm ou comment comprendre ce qui se passe.

udev est un programme simple mais qui permet une grande souplesse d'utilisation. La grande richesse de la base de règles fournies par les distributions rend pourtant la compréhension et l'analyse des événements compliquées. udev fournit un programme de mise au point, appelé udevadm, qui permet de facilement analyser les règles udev et ce qu'elles font.

Trouver les propriétés d'un périphérique

udevadm est capable de retrouver le répertoire sysfs correspondant à un périphérique dans /dev. Cette information est très utile pour examiner les propriétés filtrables.

udevadm info -x -q path -n <fichier dans /dev>

Plus directement, il est possible de récupérer toutes les propriétés connues d'udev pour un périphérique à partir de son entrée sysfs.

udevadm info <répertoire sysfs du périphérique>

Les propriétés sont préfixées par une lettre disant leur type:

  • E: une propriété purement udev. elles sont accessibles grâce à ENV{} et ne proviennent pas du noyau mais d'autres règles udev
  • N:  le nom du périphérique suggéré par le noyau
  • S: les liens symboliques créés vers le périphérique
  • P: le chemin vers l'entrée dans sysfs

Pour trouver les attributs sysfs d'un périphérique il suffit d'aller voir le contenu du répertoire correspondant. udevadm fournit une fonction permettant facilement de voir tous les attributs ainsi que ceux des périphériques parents.

udevadm info -a <repertoire sysfs>

Surveiller les événements

Nous avons déjà vu la commande udevadm monitor pour afficher les événements noyau. Comme nous l'avons vu, udev ajoute des propriétés aux événements noyau. udevadm permet de monitorer les deux types d'événements. Les événements noyau (au début du process udev, avec uniquement les propriétés venant du noyau) et les événements udev (à la fin de l'analyse des règles, avec toutes les propriétés ajoutées).

Pour voir ces deux types d'événements il suffit d'utiliser la commande suivante (l'argument -p permet de voir également les propriétés des événements):

udevadm monitor -p

Tester les actions

Je ne sais pas pour vous, mais je suis toujours un peu méfiant quand je développe des règles bas-niveau sur mon PC de bureau... udevadm permet de simuler le branchement d'un périphérique pour voir les règles appliquées sans qu'aucune action n'ait lieu.

udevadm test -a add <fichier sysfs>

Lorsque cette commande est appelée avec /class/net/eth0 toute l'information nécessaire à la compréhension des événements liés à l'ajout d'une carte réseau est affichée:

rules contain 786432 bytes tokens (65536 * 12 bytes), 36712 bytes strings
48117 strings (387000 bytes), 44279 de-duplicated (354127 bytes), 3839 trie nodes used
NAME 'eth0' /etc/udev/rules.d/70-persistent-net.rules:8
IMPORT builtin 'net_id' /lib/udev/rules.d/75-net-description.rules:6
IMPORT builtin 'hwdb' /lib/udev/rules.d/75-net-description.rules:12
IMPORT builtin 'path_id' /lib/udev/rules.d/80-net-setup-link.rules:5
IMPORT builtin 'net_setup_link' /lib/udev/rules.d/80-net-setup-link.rules:11
Config file /lib/systemd/network/99-default.link applies to device eth0
RUN 'net.agent' /lib/udev/rules.d/80-networking.rules:1
RUN '/lib/systemd/systemd-sysctl --prefix=/proc/sys/net/ipv4/conf/$name --prefix=/proc/sys/net/ipv4/neigh/$name --prefix=/proc/sys/net/ipv6/conf/$name --prefix=/proc/sys/net/ipv6/neigh/$name' /lib/udev/rules.d/99-systemd.rules:61
RUN '/bin/sh -c '/bin/echo %k : %p >> /root/events'' /etc/udev/rules.d/99-test.rules:1
unable to create temporary db file '/run/udev/data/n2.tmp': Permission denied
ACTION=add
DEVPATH=/devices/pci0000:00/0000:00:1c.6/0000:0a:00.0/net/eth0
ID_BUS=pci
ID_MM_CANDIDATE=1
ID_MODEL_FROM_DATABASE=NetXtreme BCM5761 Gigabit Ethernet PCIe
ID_MODEL_ID=0x1681
ID_NET_DRIVER=tg3
ID_NET_NAME_MAC=enxd067e53cdad2
ID_NET_NAME_PATH=enp10s0
ID_OUI_FROM_DATABASE=Dell Inc
ID_PATH=pci-0000:0a:00.0
ID_PATH_TAG=pci-0000_0a_00_0
ID_PCI_CLASS_FROM_DATABASE=Network controller
ID_PCI_SUBCLASS_FROM_DATABASE=Ethernet controller
ID_VENDOR_FROM_DATABASE=Broadcom Corporation
ID_VENDOR_ID=0x14e4
IFINDEX=2
INTERFACE=eth0
SUBSYSTEM=net
SYSTEMD_ALIAS=/sys/subsystem/net/devices/eth0
TAGS=:systemd:
USEC_INITIALIZED=200820
run: 'net.agent'
run: '/lib/systemd/systemd-sysctl --prefix=/proc/sys/net/ipv4/conf/eth0 --prefix=/proc/sys/net/ipv4/neigh/eth0 --prefix=/proc/sys/net/ipv6/conf/eth0 --prefix=/proc/sys/net/ipv6/neigh/eth0'
run: '/bin/sh -c '/bin/echo eth0 : /devices/pci0000:00/0000:00:1c.6/0000:0a:00.0/net/eth0 >> /root/events''
unload module index
Unloaded link configuration context.

udev affiche l'ensemble des actions effectuées et les propriétés de l'objet ainsi que la liste des fichiers de règles ayant contribué à cet état. La mise au point est donc très simple.

 Un petit mot sur mdev.

Nous avons mentionné plus haut que udev était un programme comme les autres. udev n'a pas de relation privilégiée avec le noyau et il est possible d'écrire d'autres programmes remplissant le même rôle. mdev est une alternative légère à udev utilisée dans le monde de l'embarqué et fourni par busybox.

Notons que udev est déjà un programme très léger (le binaire fait moins de 300 ko). Le principal avantage de mdev réside dans son mode de fonctionnement. udev est un daemon résident qui surveille le noyau via netlink, mdev est un programme lancé par le noyau à chaque événement. mdev utilise des règles plus simples qui permettent de réduire encore plus l'empreinte mémoire mais qui offrent moins de souplesse.

En conclusion

udev est un de ces daemons simples dont les possibilités permettent de comprendre facilement un aspect complexe des systèmes Unix. udev est la solution d'un problème qui a du être repris plusieurs fois avant d'être complètement compris (comme souvent dans le monde Linux), mais qui est maintenant mature. Quelques liens pour en savoir plus:

Le fonctionnement du hotplug, vu du noyau : https://www.kernel.org/doc/pending/hotplug.txt

Le débat devfs VS udev, amusant à lire à posteriori... https://lwn.net/Articles/65197/

Un autre article sur l'écriture de règles udev http://www.reactivated.net/writing_udev_rules.html

Une introduction à libudev, pour accéder facilement aux événements et informations analysées par udev http://www.signal11.us/oss/udev/

La page de man d'udev, avec la syntaxe complète des règles : http://linux.die.net/man/8/udev

La page de man d'udevadm http://linux.die.net/man/8/udevadm

La documentation de mdev http://git.busybox.net/busybox/plain/docs/mdev.txt

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.