Linux Embedded

Le blog des technologies libres et embarquées

kernel, udev et systemd : la gestion du hotplug

La gestion des événements hardware est un domaine un peu mystérieux sous linux. Le noyau voit des événements, udev réagit, et il se passe des choses.

Cet article va essayer de démystifier cet aspect des systèmes linux en implémentant quelque chose de simple : changer automatiquement la luminosité de l'écran lorsque l'alimentation d'un ordinateur portable est branchée. Pour cela nous allons trouver l’événement noyau, le remonter via udev et systemd jusqu'à un script qui s'occupera de la mise à jour.

Udev et systemd : suivre le périphérique

Lorsque le noyau linux repère un événement intéressant, il doit prévenir un daemon en espace utilisateur. Pour faire cela, il envoie un message à travers un type de socket particulier : les sockets netlink.

Les sockets netlink ont toutes sortes d'utilisation dans le monde linux

  • détection d’événements hotplug
  • filtrage en espace utilisateur de paquets netfilter
  • réaction à des changements de paramètres réseau
  • et beaucoup d'autres.

Il n'est pas nécessaire de traiter nous même les événements netlink. Le daemon udev s'en charge pour nous.

Lorsqu'udev reçoit un événement noyau il utilise un grand nombre de règles pour réagir à cet événement. C'est lui qui est chargé, entre autre, de :

  • charger un module noyau pour du matériel qui en aurait besoin
  • créer des liens dans udev (le devnode lui-même est créé par le noyau)
  • paramétrer le matériel (type de clavier, sensibilité des touchpad et touchscreen)
  • appeler ifupdown (si c'est encore utilisé sur votre distribution)
  • prévenir d'autres daemons de l'arrivée du périphérique

Parmi les daemons qui surveillent les événements udev, l'un d'entre eux nous intéresse particulièrement : systemd. systemd est chargé de lancer des daemons lorsque du nouveau matériel apparaît. C'est ce mécanisme que nous allons utiliser pour modifier notre luminosité.

Surveiller les évenements kernel

La première question que nous devons résoudre concerne notre hardware : Quel événement est généré lorsque notre alimentation est branchée. Udev fournit un outil pour cela : udevadm. L'option monitor permet de surveiller les évenements reçu par le daemon.

Lorsqu'on lance cette commande, puis que l'on débranche notre alimentation, on obtient les traces suivantes :

root@pcrosen:/etc/systemd/system# udevadm monitor
 monitor will print the received events for:
 UDEV - the event which udev sends out after rule processing
 KERNEL - the kernel uevent

KERNEL[4487.028467] change /devices/LNXSYSTM:00/LNXSYBUS:00/ACPI0003:00/power_supply/ADP0 (power_supply)
 UDEV [4487.146286] change /devices/LNXSYSTM:00/LNXSYBUS:00/ACPI0003:00/power_supply/ADP0 (power_supply)
KERNEL[4487.149554] change /devices/LNXSYSTM:00/LNXSYBUS:00/PNP0C0A:00/power_supply/BAT0 (power_supply)
 UDEV [4487.152380] change /devices/LNXSYSTM:00/LNXSYBUS:00/PNP0C0A:00/power_supply/BAT0 (power_supply)

Nous avons deux types de traces :

  • les événements KERNEL qui correspondent à ceux que le noyau envoie à udev
  • les événements UDEV qui sont ceux générés par udev à partir de l’événement précédent

Notre action a affecté deux périphériques : ADP0 (l'alimentation, qui change d'état) et BAT0 (la batterie, qui commence à se décharger). Notons que l’événement généré est de type change. Il ne s'agit pas de l'ajout ou la suppression d'un périphérique mais bien d'un changement d'état du périphérique.

Maintenant que nous savons quel périphérique nous intéresse, voyons quels informations udev peut nous donner sur celui-ci.

Les attributs de notre périphérique

Systemd ne suit pas tous les périphériques du système. Seuls ceux qui sont explicitement marqués pour être suivi le sont. Le périphérique ADP0 n'est pas suivi. Nous allons ajouter la règle udev pour qu'il le soit.

Pour que udev marque un périphérique pour être suivi par systemd il faut lui associer un TAG systemd. Pour associer ce tag nous devons préciser à udev quel critère permet de choisir le périphérique qui nous intéresse. Nous pourrions nous baser sur le nom du périphérique mais celui-ci peut varier selon les PC. Nous allons trouver des critères un peu plus génériques.

Udev peut filtrer selon un très grand nombre de critères. Pour savoir quel critère utiliser, udev fournit la commande udevadm info -a qui liste toutes les propriétés filtrables d'un périphérique.

Pour connaître les critères utilisables sur notre alimentation, nous lançons donc la commande suivante:

root@pcrosen:/etc/udev/rules.d# udevadm info -a /sys/devices/LNXSYSTM\:00/LNXSYBUS\:00/ACPI0003\:00/power_supply/ADP0

Udevadm info starts with the device specified by the devpath and then
 walks up the chain of parent devices. It prints for every device
 found, all possible attributes in the udev rules key format.
 A rule to match, can be composed by the attributes of the device
 and the attributes from one single parent device.

looking at device '/devices/LNXSYSTM:00/LNXSYBUS:00/ACPI0003:00/power_supply/ADP0':
 KERNEL=="ADP0"
 SUBSYSTEM=="power_supply"
 DRIVER==""
 ATTR{online}=="1"
 ATTR{type}=="Mains"

looking at parent device '/devices/LNXSYSTM:00/LNXSYBUS:00/ACPI0003:00':
 KERNELS=="ACPI0003:00"
 SUBSYSTEMS=="acpi"
 DRIVERS=="ac"
 ATTRS{hid}=="ACPI0003"
 ATTRS{path}=="\_SB_.ADP0"

looking at parent device '/devices/LNXSYSTM:00/LNXSYBUS:00':
 KERNELS=="LNXSYBUS:00"
 SUBSYSTEMS=="acpi"
 DRIVERS==""
 ATTRS{hid}=="LNXSYBUS"
 ATTRS{path}=="\_SB_"

looking at parent device '/devices/LNXSYSTM:00':
 KERNELS=="LNXSYSTM:00"
 SUBSYSTEMS=="acpi"
 DRIVERS==""
 ATTRS{hid}=="LNXSYSTM"
 ATTRS{path}=="\"

udev peut filtrer sur des propriétés du périphérique, mais également sur des propriétés des périphériques parents. udev nous fournit donc toute l'arborescence des critères.

Nous devons filtrer les périphériques à faire surveiller par udev. Ici, nous avons deux critères qui nous intéressent

  • Notre périphérique a un attribut SUBSYSTEM=="power_supply"
  • Le noeud parent a un attribut DRIVERS=="ac"

Un périphérique qui répond à ces deux critères est une alimentation externe. C'est ce que nous cherchons.

Écrire une règle udev

Udev va chercher des règles dans plusieurs endroits. Ajoutons un fichier 99-backlight_update.rules dans /etc/udev/rules.d/ contenant la ligne suivante :

SUBSYSTEM=="power_supply" DRIVERS=="ac" TAG+="systemd" ENV{SYSTEMD_ALIAS}+="/sys/subsystem/power_supply/main_AC"

Notez que certains champs utilisent un '==' d'autre un '='. Les '==' sont utilisés pour définir une condition.

Notez également le 'S' à DRIVERS. Le 'S' ici sert à spécifier que la condition s'applique à au moins un des drivers parents.

  • SUBSYSTEM=="power_supply" : Le périphérique doit appartenir au sous-système power_supply
  • DRIVERS=="ac" : L'un des parents du périphérique doit apartenir au sous-système 'ac'
  • TAG+="systemd" : Marquer le périphérique pour être suivi par systemd
  • ENV{SYSTEMD_ALIAS}+="/sys/subsystem/power_supply/main_AC" : Indiquer à systemd que le périphérique doit avoir un alias.

Une fois le fichier créé, nous devons demander à udev de recharger sa configuration, faire redétecter le périphérique qui nous intéresse et vérifier que tout fonctionne.

 root@pcrosen:/etc/udev/rules.d# udevadm control -R
 root@pcrosen:/etc/udev/rules.d# udevadm trigger /sys/class/power_supply/ADP0
 root@pcrosen:/etc/udev/rules.d# udevadm info /sys/class/power_supply/ADP0
 P: /devices/LNXSYSTM:00/LNXSYBUS:00/ACPI0003:00/power_supply/ADP0
 E: DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/ACPI0003:00/power_supply/ADP0
 E: POWER_SUPPLY_NAME=ADP0
 E: POWER_SUPPLY_ONLINE=1
 E: SUBSYSTEM=power_supply
 E: SYSTEMD_ALIAS=/sys/subsystem/power_supply/main_AC
 E: TAGS=:systemd:
 E: USEC_INITIALIZED=3880380

Faire le script shell

L'étape suivante consiste à faire un petit script shell qui sera appelé sur les événements de notre périphérique et qui réagira à ceux-ci.

Pour savoir si nous sommes branchés, nous pourrions aller lire l'information dans le sysfs mais systemd fournit un utilitaire pour détecter cela: systemd-ac-power. En utilisant ce script nous pouvons facilement vérifier si nous sommes branchés et modifier la luminosité en conséquence.

Le script backlight_update.sh est appelé sur les événements de branchement et de débranchement. Il modifie la luminosité via un fichier particulier dans le sysfs:

#!/bin/sh

if /lib/systemd/systemd-ac-power ; then
 cat /sys/class/backlight/intel_backlight/max_brightness > /sys/class/backlight/intel_backlight/brightness
else
 echo 30 > /sys/class/backlight/intel_backlight/brightness
fi

Note : si vous n'avez pas systemd-ac-power, vous pouvez redemander l'état de la batterie via udevadm info. L'information POWER_SUPPLY_ONLINE vaut 1 si le cable d'alimentation est branchée, 0 sinon.

Où mettre notre fichier

Notre script est un exécutable, mais il n'est pas fourni par la distribution et il n'est pas censé être utilisé par un être humain, uniquement par systemd. Quel est l'endroit correct pour mettre ce script ?

  • Quasiment tout doit être dans /usr
  • Ce fichier ne provient pas de la distribution : /usr/local
  • Ce fichier n'est pas censé être utilisé par l'utilisateur. Il ne va pas dans /usr/local/bin ou /usr/local/sbin
  • Ce fichier est interne à notre application, d'autres applications ne sont pas censée l'utiliser. Il ne va pas dans /usr/local/share
  • Nous allons donc le mettre dans le répertoire de /usr/local/lib/backlight_update.

Faire le service correspondant

Vérifier que systemd surveille notre device

Comme nous l'avons dit précédement, systemd ne surveille que certains périphériques, pour savoir lesquels, il faut lancer la commande suivante

root@pcrosen:/etc/udev/rules.d# systemctl --type=device
 UNIT LOAD ACTIVE SUB DESCRIPTION
 sys-devices-LNXSYSTM:00-LNXSYBUS:00-ACPI0003:00-power_supply-ADP0.device loaded active plugged /sys/devices/LNXSYSTM:00/LNXSYBUS:00/ACPI0003:00/power_supply/ADP0
 sys-devices-LNXSYSTM:00-LNXSYBUS:00-PNP0A08:00-device:01-PNP0C09:00-ACPI0008:00-iio:device0.device loaded active plugged /sys/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A08:00/device:01/PNP0C09:00/ACPI0008:00/iio:devic
 sys-devices-pci0000:00-0000:00:02.0-drm-card0-card0\x2deDP\x2d1-intel_backlight.device loaded active plugged /sys/devices/pci0000:00/0000:00:02.0/drm/card0/card0-eDP-1/intel_backlight
 sys-devices-pci0000:00-0000:00:14.0-usb2-2\x2d6-2\x2d6:1.0-bluetooth-hci0.device loaded active plugged /sys/devices/pci0000:00/0000:00:14.0/usb2/2-6/2-6:1.0/bluetooth/hci0
 sys-devices-pci0000:00-0000:00:1c.2-0000:02:00.0-net-wlp2s0.device loaded active plugged Wireless 7260 (Dual Band Wireless-AC 7260)
 sys-devices-pci0000:00-0000:00:1f.2-ata3-host2-target2:0:0-2:0:0:0-block-sda-sda1.device loaded active plugged LITEONIT_LMT-512L9M-11_MSATA_512GB 1
 sys-devices-pci0000:00-0000:00:1f.2-ata3-host2-target2:0:0-2:0:0:0-block-sda-sda2.device loaded active plugged LITEONIT_LMT-512L9M-11_MSATA_512GB 2
 sys-devices-pci0000:00-0000:00:1f.2-ata3-host2-target2:0:0-2:0:0:0-block-sda-sda3.device loaded active plugged LITEONIT_LMT-512L9M-11_MSATA_512GB 3
 sys-devices-pci0000:00-0000:00:1f.2-ata3-host2-target2:0:0-2:0:0:0-block-sda.device loaded active plugged LITEONIT_LMT-512L9M-11_MSATA_512GB
 sys-devices-platform-dell\x2dlaptop-leds-dell::kbd_backlight.device loaded active plugged /sys/devices/platform/dell-laptop/leds/dell::kbd_backlight
 sys-devices-platform-serial8250-tty-ttyS0.device loaded active plugged /sys/devices/platform/serial8250/tty/ttyS0
 sys-devices-platform-serial8250-tty-ttyS1.device loaded active plugged /sys/devices/platform/serial8250/tty/ttyS1
 sys-devices-platform-serial8250-tty-ttyS2.device loaded active plugged /sys/devices/platform/serial8250/tty/ttyS2
 sys-devices-platform-serial8250-tty-ttyS3.device loaded active plugged /sys/devices/platform/serial8250/tty/ttyS3
 sys-devices-virtual-block-loop0.device loaded active plugged /sys/devices/virtual/block/loop0
 sys-devices-virtual-misc-rfkill.device loaded active plugged /sys/devices/virtual/misc/rfkill
 sys-subsystem-bluetooth-devices-hci0.device loaded active plugged /sys/subsystem/bluetooth/devices/hci0
 sys-subsystem-net-devices-wlp2s0.device loaded active plugged Wireless 7260 (Dual Band Wireless-AC 7260)
 sys-subsystem-power_supply-main_AC.device loaded active plugged /sys/subsystem/power_supply/main_AC

LOAD = Reflects whether the unit definition was properly loaded.
 ACTIVE = The high-level unit activation state, i.e. generalization of SUB.
 SUB = The low-level unit activation state, values depend on unit type.

19 loaded units listed. Pass --all to see loaded but inactive units, too.
 To show all installed unit files use 'systemctl list-unit-files'.

L'unité sys-subsystem-power_supply-main_AC.device est celle que nous souhaitons surveiller. Elle est apparue grâce à notre règle udev (plus précisément la partie SYSTEMD_ALIAS de notre règle). Commençons par générer des traces dans le journal, nous le connecterons à notre script plus tard.

Un service simple pour observer ce qui se passe

Lorsqu'un daemon écrit des traces sur stdout, systemd les envoie à journald. Notre service de surveillance, dans sa forme la plus simple, aura l'allure suivante

# /etc/systemd/system/backlight_update.service
 [Service]
 ExecStart=/bin/echo %n started

Une fois ce service écrit, nous pouvons le tester en surveillant journald sur un terminal

root@pcrosen:~# journalctl -fe -u backlight_update.service

Sur un autre terminal, nous demandons à systemd de recharger sa configuration, puis nous démarrons le service :

root@pcrosen:~# systemctl daemon-reload
 root@pcrosen:~# systemctl start backlight_update.service

Nous avons vu précédemment que le branchement causait un événement "change" sur udev. Ces événements deviennent des évenements "reload" pour systemd. Nous devons détecter ces évenements.

# /etc/systemd/system/backlight_update.service
 [Service]
 ExecStart=/bin/echo %n started
 ExecReload=/bin/echo %n reloaded
root@pcrosen:~# systemctl daemon-reload
root@pcrosen:~# systemctl start backlight_update.service
root@pcrosen:~# systemctl reload backlight_update.service

Systemd proteste car backlight_update n'est pas démarré. Notre service ne faisant rien d'autre que lancer la commande echo, il se termine immédiatement. Pour dire à systemd que notre service doit garder un pseudo-état "active" jusqu'à ce qu'il soit explicitement terminé, il faut utiliser la command RemainAfterExit

# /etc/systemd/system/backlight_update.service
 [Service]
 ExecStart=/bin/echo %n started
 ExecReload=/bin/echo %n reloaded
 RemainAfterExit=true
root@pcrosen:~# systemctl daemon-reload
root@pcrosen:~# systemctl start backlight_update.service
root@pcrosen:~# systemctl reload backlight_update.service

Nous avons un service qui trace les événements qu'il reçoit. Il ne nous reste plus qu'à le faire réagir aux événements "change" de notre alimentation.

L'instruction ReloadPropagatedFrom permet de propager l'événement reload depuis une autre unité. Dans notre cas, nous voulons propager vers notre service l'événement reload du .device.

# /etc/systemd/system/backlight_update.service
 [Unit]
 ReloadPropagatedFrom=sys-susbsystem-power_supply-main_AC.device

[Service]
 ExecStart=/bin/echo %n started
 ExecReload=/bin/echo %n reloaded
 RemainAfterExit=true
root@pcrosen:~# systemctl daemon-reload

Lorsque nous débranchons et rebranchons notre alimentation, nous voyons les traces apparaître dans le journal.

Le vrai service

Notre système de détection fonctionne. Il ne reste plus qu'à lui associer le vrai script.

[Service]
 ExecStart=/usr/local/lib/backlight_update/backlight_update.sh
 ExecReload=/usr/local/lib/backlight_update/backlight_update.sh
 RemainAfterExit=true

Nous devons encore expliquer à systemd que le service doit réagir à l’événement reload de l'alimentation, comme nous l'avons fait pour le service de trace.

# /etc/systemd/system/backlight_update.service
 [Unit]
 Description=Update luminosity on AC plug event
 ReloadPropagatedFrom=sys-susbsystem-power_supply-main_AC.device

[Service]
 ExecStart=/usr/local/lib/backlight_update/backlight_update.sh
 ExecReload=/usr/local/lib/backlight_update/backlight_update.sh
 RemainAfterExit=true

Il ne nous reste plus qu'à expliquer à systemd quand lancer notre script. Il serait tentant de simplement démarrer le script au boot, mais le vrai événement que nous recherchons est la détection de l'alimentation par le noyau. Utilisons donc l'apparition du device pour démarrer notre service. La version finale de notre service a donc l'alure suivante :

# /etc/systemd/system/backlight_update.service
 [Unit]
 Description=Update luminosity on AC plug event
 ReloadPropagatedFrom=sys-susbsystem-power_supply-main_AC.device

[Service]
 ExecStart=/usr/local/lib/backlight_update/backlight_update.sh
 ExecReload=/usr/local/lib/backlight_update/backlight_update.sh
 RemainAfterExit=true

[Install]
 WantedBy=sys-subsystem-power-supply-main_AC.device

Nous activons notre service :

root@pcrosen:~# systemctl enable backlight_update

Et nous avons terminé. Il ne reste plus qu'a redémarrer et vérifier que tout fonctionne...

Conclusion

Le hotplug linux peut sembler mysterieux mais, lorsque l'on ouvre la boite, il est assez simple de suivre l'enchaînement des événements

  • Le noyau détecte le périphérique
  • Le noyau prévient udev qui fait la mise en place initiale et prévient d'éventuels daemons plus complexes
  • Les daemons plus complexes réagissent.

Dans le cadre de cet article, le daemon plus complexe est systemd. Nous avons pu rapidement voir comment systemd peut suivre les états des périphériques et réagir à leur changement.

L'intégration du hotplug avec systemd est finalement assez simple et il est facile de mettre en place des systèmes complexes en intégrant cette logique dans le système de démarrage et de surveillance de systemd.

    • le 02 août 2018 à 15:40

      Très intéressant et exactement ce que je cherchais.
      Merci beaucoup.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.