Linux Embedded

Le blog des technologies libres et embarquées

Comment exposer à l'espace utilisateur des interruptions avec Userspace I/O System

Dans le développement sur matériel spécifique, il est parfois ardu, ou non-souhaitable, de développer directement un driver dans le kernel. Dans cet article, nous allons voir comment permettre à des applications de l'espace utilisateurs d'interagir avec des composants matériels et d’en recevoir des interruptions. Un exemple de petit driver linux utilisant UIO et une application client sera expliqué.

Développement de driver kernel

Dans les sources du kernel linux, les drivers sont une partie assez conséquente. Ils permettent de mettre à disposition le matériel au système et à l’utilisateur.

Les drivers s’organisent en couches reflétant la réalité physique et logique du matériel à supporter. Par exemple, pour l’accès à une clef USB, il y a l'empilement USB HOST controller, USB Core, et USB Mass Storage. [1]

Couches de drivers pour un USB mass-storage

 

Pour chaque compilation de kernel, on sélectionne les drivers à embarquer en éditant le fichier .config, par exemple avec la commande make menuconfig  .

 

Dans le code des drivers, on trouve du code “vers le bas”  utilisé pour communiquer avec le composant matériel (comme lui envoyer des codes spécifiques pour lui faire faire ce que l’on veut).  Le code “vers le haut” permet d'exposer les capacités du matériel sous forme de fichiers "device nodes" dans le répertoire /dev, d'appels système (read, write, ioctl, etc.) ou de fichiers virtuels dans le sysfs monté sur /sys .

Faire des driver dans le userland

Dans la logique habituelle, la place d'un driver est dans le kernel. Ils y ont accès à des primitives et des priorités d'exécutions spécifiques. 

 

Il se peut toutefois que l’on veuille faire une partie ou tout un driver dans le userland :

 

  • Afin d'expérimenter sur un matériel spécifique pour lequel il n'y a pas (encore) de driver
  • Afin de faciliter la maintenance du driver car les compétence kernel sont rares.
  • Afin de ne pas publier le code source, ce qui est la raison la plus fréquente

En effet, la licence GPL v2 utilisé par le kernel impose la distribution du code source des drivers à l'utilisateur final.

Dans les exemples d'utilisation, on peut noter le cas du driver uio_prus. Ce driver développé par Texas Intrument permettait le contrôle les PRUs (Programmable Real-time Unit). Texas Instrument l'a finalement retiré avec l'arrivée des drivers remoteproc qui formalisent plus généralement l'accès aux autres processeurs (allumage, programmation, messagerie...) On peut retrouver le commit de suppression de uio_prus ici.

Userspace I/O

Documentation : https://www.kernel.org/doc/html/v6.10/driver-api/uio-howto.html

UIO est un type de driver qui va exposer à l’utilisateur un fichier /dev/uioX par device. 

 

On y accède comme suit :

 

  • Par l'appel système mmap qui permet l’accès à une grande plage d'adresses du périphérique “facilement”
  • Par l'appel système read pour lequel la lecture se débloque quand une interruption est émise par le périphérique
Couches entre le matériel, le kernel et UIO et l'utilisateur

UIO permet donc d’accéder à un périphérique avec un traitement minimal. Il reste cependant à coder l’accès au périphérique lui-même dans le module.

 

Expérience avec l’interruption clavier.

Je propose un exemple avec l'interruption clavier (IRQ numéro 1). En temps normal, cette interruption est uniquement accessible en espace kernel. Nous allons créer un module qui va rendre accessible cette interruption du côté utilisateur.

 

Le code auquel nous nous référons est accessible sur le dépôt https://github.com/Openwide-Ingenierie/uio_demo .

 

La configuration de la structure uio_info est la plus intéressante ici :

info->name = "uio_kbd_device";
info->version = "0.0.1";
info->irq = irq; // IRQ 1 is the keyboard IRQ on Intel architecture.
info->irq_flags = IRQF_SHARED;
info->handler = uio_handler;

La variable irq est initialisée à 1 pour écouter les interruptions clavier. Cette interruption est partagée.

 

La callback uio_handler doit rendre IRQ_HANDLED, et le flag IRQF_SHARED doit être mis pour pouvoir accéder à cette interruption, et permettre au reste du système de l’utiliser aussi. Attention au freeze sinon.

 

static irqreturn_t uio_handler(int irq, struct uio_info *dev_info)
{
  static unsigned int irq_count = 0;
  pr_info("UIO handler");
 
  irq_count++;
  memcpy(mem_area, &irq_count, sizeof(irq_count));
 
  return IRQ_HANDLED;
}

Dans le cadre de cet exercice, on compte le nombre d’interruptions reçues et on l’écrit dans le début de la plage mémoire exposée au userland.

 

La zone mémoire pour l'appel mmap est créée par un kzalloc puis est renseignée dans la structure de l’uio.

mem_area = kzalloc(PAGE_SIZE, GFP_KERNEL);
(...)
info->mem[0].name = "basic_mem_map";
info->mem[0].memtype = UIO_MEM_LOGICAL;
info->mem[0].addr = (phys_addr_t) mem_area;
info->mem[0].size = sizeof(mem_area);

 

Côté application userland, on vient ouvrir le device uio et  attendre l’interruption avec un appel système select :

// open uio0
uiofd = open("/dev/uio0", O_RDONLY);
(...)
//  five second timeout (reset each time)
tv.tv_sec = 5;
tv.tv_usec = 0;
// wait for interrupt
err = select(uiofd+1, &uiofd_set, NULL, NULL, &tv);
(...)
err = read(uiofd, buf, 4);
(...)

L'appel système select permet d'effectuer une attente avec un timeout. La lecture de 4 octets (taille d’un int) permet d'acquitter l’interruption. L’entier lu contient le nombre d’interruptions (fournis par uio)

 

Pour finir l’exercice, on vient lire les premiers octets de la mémoire mappée :

// read interrupt count from memory mapping
memcpy(&interrupt_count, mem, sizeof(interrupt_count));
printf("Read interrupt count : %d \n", interrupt_count);

Démonstration

Le test a été réalisé sur Linux Mint 21.2, noyau 5.1.5.0-118-generic . Le test nécessite l'installation des outils de développement C/C++ classiques ainsi que le paquet "linux-headers".

# Cloner le répo:
$ git clone https://github.com/Openwide-Ingenierie/uio_demo
$ cd uio_demo
# Installer les outils de compilation
$ sudo apt install build-essential
# Installer les headers linux
$ sudo apt install linux-headers-$(uname -r)
# Compiler le module
$ make
# Compiler l'application de test
$ make uio_app
# Dans un autre terminal, lancer dmesg
$ sudo dmesg -w
# Charger le module
$ sudo modprobe uio
$ sudo insmod uio_kbd.ko
[dmesg]
[ 7278.086406] Registered UIO handler for IRQ=1
[ 7278.175579] UIO handler
[ 7281.757641] UIO handler
...
# Les évenements claviers sont perçus par le module
# Lancer l'application de test
$ sudo ./uio_app
Read interrupt count : 86
Read interrupt count : 87
dRead interrupt count : 88
(... 10x when typing on keyboard)
# L'application récupère les évenements clavier du module par le device /dev/uio0
# Le nombre d'évenements est lu depuis le module via le mapping mémoire.
$ sudo ./uio_app
Read interrupt count : 74
(not typing on the keyboard)
Timeout, exiting
# Déchargement du module
$ sudo rmmod uio_kbd  
[dmesg]
[ 8670.908880] UIO release
[ 8670.908882] Un-Registered UIO handler for IRQ=1

note : l'application doit être lancée en sudo, car le device /dev/uio0 n'est accessible qu'à root. On pourrait automatiquement attribuer un groupe au device avec une règle udev, mais c'est un autre sujet.

Pour aller plus loin

lsuio 

L'outil lsuio est proposé pour inspecter les devices uio instanciés.
https://www.osadl.org/UIO-Archives.uio-archives.0.html

Avec notre exemple, voici ce que lsuio rend: 

$ sudo ./lsuio -m -v
uio0: name=uio_kbd_device, version=0.0.1, events=76
	map[0]: addr=0x3D85B000, size=4096, mmap test: OK
	(No device attributes)

On peut voir le nom du driver auquel uio0 est associé : uio_kbd_device. On retrouve aussi le mapping de la taille d'une page (4096 bytes dans notre cas). lsuio  liste aussi les attributs de la structure uio_info->dev_attrs, dans notre cas : rien :).

uio_pci_generic et uio_pdrv_genirq

Dans les outils pratiques, il y a aussi le module uio_pci_generic. Dans le cas où le matériel à utiliser est compatible PCI 2.3 ou PCI Express, il n'y a pas besoin d'écrire un module spécifique, mais "juste" d'instancier le module uio_pci_generic, et de le configurer par le sysfs. Il reste toujours à écrire le driver dans l'espace utilisateur. Un bon exemple est donnée dans la documentation : 
https://www.kernel.org/doc/html/v6.10/driver-api/uio-howto.html#generic-pci-uio-driver

Dans le cas où le matériel a utiliser nécessite la création d'un driver de type platform_device, il est aussi possible de l'exposer aux utilisateurs par uio. Cette configuration se fait par les champs .name et .ressource de la structure platform_device. Selon si l'on a besoin de l'interruption ou de mapping mémoire dynamique, les types uio_pdrv, uio_pdrv_genirq, et uio_dmem_genirq peuvent être utilisés. Plus de détails ici:
https://www.kernel.org/doc/html/v6.3/driver-api/uio-howto.html#using-uio-pdrv-for-platform-devices

Conclusion

Nous avons pu voir qu'en créant un "petit" module kernel, il était possible d'accéder à un device, puis de créer un driver plus étoffé côté userland. L'interface se fait via un ou plusieurs mmaps pour un accès en mémoire et un open pour l'accès a l'interruption. Libre alors au développeur de choisir son langage : C, Lua, Python, Rust, etc. afin de réaliser le traitement voulu.

Références

 

[1] Andrey Konovalov OffensiveCon 2019

https://docs.google.com/presentation/d/1z-giB9kom17Lk21YEjmceiNUVYeI6yIaG5_gZ3vKC-M/edit#slide=id.g4eef912d32_1_1100

 

[2] The Userspace I/O HOWTO - Documentation
https://www.kernel.org/doc/html/v6.10/driver-api/uio-howto.html

 

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.