Il existe plusieurs mécanismes proposés par le noyau Linux pour programmer un traitement à réaliser lorsqu’un périphérique externe déclenche une interruption pour nous notifier de l’occurrence d’un événement. Gestionnaire monolithique, tasklet, workqueue, threaded interrupt, chaque solution a des avantages et des inconvénients, qu’il est intéressant de connaître pour optimiser l’efficacité de nos traitements.
Gestion des interruptions
Récapitulons rapidement le principe des interruptions sous Linux. Il convient tout d’abord de préciser que du fait de la portabilité du noyau sur un grand nombre d’architectures, les mécanismes très bas-niveau sont totalement abstraits pour ce qui concerne la plupart du code kernel classique (dans un driver par exemple).
Lorsqu’un périphérique externe – disons un contrôleur d’entrées-sorties GPIO par exemple – désire notifier le processeur de l’occurrence d’une situation intéressante (par exemple le changement d’état d’une broche d’entrée), il envoie un signal sur une entrée du contrôleur d’interruption APIC (Advanced Programmable Interrupt Controler). De nos jours celui-ci est intégré directement dans le microprocesseur, mais dans les anciens PC par exemple, il existait sous forme de composant indépendant.
Le contrôleur d’interruption effectue une demande d’interruption IRQ (Interrupt Request) auprès du processeur principal. Ce dernier arrête le traitement en cours, sauvegarde son état (ses registres) dans la pile et interroge le contrôleur d’interruption pour connaître l’événement survenu. L’APIC lui indique la source du signal initial sous forme d’un numéro. Le processeur déroute alors son exécution sur une routine de traitement bas-niveau chargée de prendre en compte l’interruption et de réagir en conséquence.
Une fois cette routine de traitement terminée le processeur reprend le cours de ses opérations précédentes comme si de rien n’était.
![fig-01 - Déclenchement d'une interruption](https://www.blaess.fr/christophe/wp-content/uploads/2017/06/fig-01-Declenchement-d-une-interruption.png)
Fig-01 – Déclenchement d’une interruption
On peut voir les différentes interruptions gérées par le noyau Linux dans le pseudo-fichier /proc/interrupts
. Par exemple sur un Raspberry Pi 3, on observe les données suivantes.
# cat /proc/interrupts CPU0 CPU1 CPU2 CPU3 16: 0 0 0 0 bcm2836-timer 0 Edge arch_timer 17: 974 466 348 1505 bcm2836-timer 1 Edge arch_timer 23: 18 0 0 0 ARMCTRL-level 1 Edge 3f00b880.mailbox 24: 2 0 0 0 ARMCTRL-level 2 Edge VCHIQ doorbell 39: 1 0 0 0 ARMCTRL-level 41 Edge 46: 0 0 0 0 ARMCTRL-level 48 Edge bcm2708_fb dma 48: 267 0 0 0 ARMCTRL-level 50 Edge DMA IRQ 50: 0 0 0 0 ARMCTRL-level 52 Edge DMA IRQ 62: 8865 0 0 0 ARMCTRL-level 64 Edge dwc_otg, dwc_otg_pcd, dwc_otg_hcd:usb1 79: 0 0 0 0 ARMCTRL-level 81 Edge 3f200000.gpio:bank0 80: 0 0 0 0 ARMCTRL-level 82 Edge 3f200000.gpio:bank1 86: 4 0 0 0 ARMCTRL-level 88 Edge mmc0 87: 116 0 0 0 ARMCTRL-level 89 Edge uart-pl011 92: 456 0 0 0 ARMCTRL-level 94 Edge mmc1 FIQ: usb_fiq IPI0: 0 0 0 0 CPU wakeup interrupts IPI1: 0 0 0 0 Timer broadcast interrupts IPI2: 323 610 369 468 Rescheduling interrupts IPI3: 2 5 4 4 Function call interrupts IPI4: 2 2 0 0 Single function call interrupts IPI5: 0 0 0 0 CPU stop interrupts IPI6: 1 0 0 0 IRQ work interrupts IPI7: 0 0 0 0 completion interrupts Err: 0 #
La première colonne représente le numéro de l’interruption (sauf pour celles du bas de la liste, FIQ et IPIx qui représentent des interruptions internes au processeur). Les quatre colonnes suivantes indiquent le nombre d’occurrence de chaque interruption depuis le boot du système sur chacun des quatre cœurs de processeur. Les valeurs sont faibles ici, le Raspberry Pi 3 vient de démarrer. Les colonnes suivantes affichent le type de contrôleur, un numéro interne à celui-ci, et le driver concerné.
Les fonctions de traitement bas-niveau sont déjà écrites dans le noyau Linux et nous n’avons pas à y toucher. Elles ont pour rôle d’appeler des fonctions de plus haut-niveau que l’on nomme routines de service (Interrupt Service Routine ou ISR), et ce sont ces routines de service que nous pouvons écrire dans nos drivers. Une fonction bas-niveau a également pour rôle de désactiver dans l’APIC l’interruption qui l’a déclenchée avant d’appeler la routine de service et de réactiver l’interruption ensuite. Imaginons le déclenchement d’une hypothétique interruption 100 :
![Fig-02 - Handlers bas-niveau et ISR](https://www.blaess.fr/christophe/wp-content/uploads/2017/06/fig-02-Handlers-bas-niveau-et-ISR.png)
Fig-02 – Handlers bas-niveau et ISR
Lorsque, dans le reste de cet article je parlerai – avec un léger abus de langage – de gestionnaire (ou de handler) d’interruption, il s’agira toujours d’une routine de service invoquée par une fonction de plus bas-niveau. Autrement dit, les schémas à venir ne représenteront plus les handlers bas-niveau, mais uniquement les routines ISR.
![Fig-03 - Handler d'interruption](https://www.blaess.fr/christophe/wp-content/uploads/2017/06/fig-03-Handler-d-interruption.png)
Fig-03 – Handler d’interruption
Précisons dès à présent qu’une fonction bas-niveau peut appeler successivement plusieurs routines de service en réponse à la même interruption. Par exemple dans le fichier /proc/interrupts
ci-dessus, on voit que l’interruption 62 est traitée conjointement par trois routines de service appartenant aux drivers dwc_otg
, dwc_otg_pcd
, et dwc_otg_hcd
appelées successivement. C’est ce qu’on nomme une interruption partagée.
![Fig-04 - Interruption partagée](https://www.blaess.fr/christophe/wp-content/uploads/2017/06/fig-04-Interruption-partagee.png)
Fig-04 – Interruption partagée
Il est possible, lors de l’écriture d’un driver d’agir de différentes façons sur le traitement des interruptions.
On peut désactiver (on dit généralement « masquer« ) ou activer (« démasquer« ) le traitement d’une interruption. Si on masque une interruption, et qu’elle se produit effectivement, l’IRQ restera en attente au niveau de l’APIC, jusqu’à ce que le processeur démasque l’interruption. C’est à ce moment seulement qu’il recevra la demande en attente.
![Fig-05 - Masquage d'interruption](https://www.blaess.fr/christophe/wp-content/uploads/2017/06/fig-05-Masquage-d-interruption.png)
Fig-05 – Masquage d’interruption
Il faut bien comprendre qu’une seule instance de l’interruption sera délivrée au moment du déblocage, même si elle est survenue à plusieurs reprises pendant la période de masquage.
![Fig-06 - Occurrences d'interruption masquée](https://www.blaess.fr/christophe/wp-content/uploads/2017/06/fig-06-Occurences-d-interruption-masquee.png)
Fig-06 – Occurrences d’interruption masquée
Sur certains processeurs, il existe des interruptions non-masquables servant à la notification d’événements très urgents.
En outre certaines interruptions peuvent être plus prioritaires que d’autres. Du fait de la portabilité de Linux sur de nombreuses architectures, cette notion n’est pas prise en compte au niveau de l’API proposée aux drivers.
Un peu de pratique
Nous allons commencer quelques expériences avec les interruptions. J’ai choisi comme plate-forme d’illustration pour cet article le Raspberry Pi 3 en raison de sa grande disponibilité. Toutefois les exemples sont adaptables sur d’autres cartes. Durant des sessions de formation je les ai testés sur de nombreux autres systèmes (BeagleBoneBlack, Pandaboard, IGEPv2, i.MX6 Sabrelite, toute la gamme des Raspberry Pi, etc.) en adaptant les numéros de GPIO.
Dans notre premier exemple nous allons installer un petit gestionnaire d’interruption très simple qui inscrira lors de son déclenchement un message dans les traces du noyau. L’intérêt de travailler avec un Raspberry Pi est de pouvoir facilement gérer communications avec l’extérieur grâce aux GPIO, et de déclencher facilement des interruptions avec un simple morceau de fil électrique.
J’ai fait le choix, pour éviter de mettre en œuvre un environnement de cross-compilation qui compliquerait le propos de cet article, de faire toutes les compilations de modules du kernel directement sur le Raspberry Pi. La compilation d’un module prend deux à trois secondes sur un Raspberry Pi 3, ce qui est très raisonnable, même lorsque le nombre d’exemples est assez élevé. L’inconvénient, est que la compilation d’un module nécessite la présence des fichiers d’en-tête, du fichier de configuration et des Makefile du noyau cible. Or, les distributions Raspbian ne fournissent pas cet ensemble. Il existe bien un package linux-headers dans cette distribution mais il ne correspond pas du tout au noyau fourni.
Nous devons donc commencer par recompiler un noyau, opération très simple mais qui prend environ deux heures de compilation sur le Raspberry Pi 3. En partant d’une distribution Raspbian fraîchement installée, voici la suite de commandes à exécuter.
$ sudo apt update $ sudo apt install -y ncurses-dev bc $ git clone https://github.com/raspberrypi/linux --depth 1 $ cd linux/ $ make bcm2709_defconfig $ make -j 8 # C'est cette étape qui dure environ deux heures... $ sudo make scripts $ sudo make modules_install $ sudo mkdir /boot/old-dtbs $ sudo mv /boot/*dtb /boot/old-dtbs/ $ sudo make INSTALL_DTBS_PATH=/boot dtbs_install $ sudo cp /boot/kernel7.img /boot/old-kernel7.img $ sudo cp arch/arm/boot/zImage /boot/kernel7.img $ sudo reboot
Après redémarrage, on vérifie avec :
$ uname -a
que l’on se trouve bien sur notre noyau tout neuf.
Attention, le répertoire qui a servi pour la compilation sera référencé pendant la compilation des modules ultérieurs, et ne doit donc pas être déplacé, ni effacé. À la rigueur, on peut y faire un peu de ménage pour gagner de la place :
$ cd /lib/modules/$(uname -r)/build $ rm -rf Documentation $ find . -name '*.[coS]' | xargs rm -f
Ceci efface les fichiers sources C et assembleur ainsi que les fichiers objets. Il est important de conserver les fichiers d’en-tête .h
et les fichiers Makefile
. On gagne ainsi environ 700 Mo.
Handler d’interruption monolithique
Le cas le plus simple – et le plus courant – est celui du handler monolithique. Lorsque l’interruption se produit, le handler invoqué fait tout le travail attendu puis se termine et le processeur reprend son activité initiale.
Un handler d’interruption s’écrit en respectant un prototype bien défini (il existe d’ailleurs un typedef irq_handler_t
pour représenter ce prototype) :
irqreturn_t irq_handler(int irq_num, void *irq_id);
Lorsque le handler est appelé, il recevra automatiquement deux arguments : le numéro de l’interruption qui l’a déclenché (utile lorsque le même handler gère plusieurs interruptions différentes), et un identifiant représenté par un pointeur générique. Nous fournirons cet identifiant lors de l’installation du handler. En général il s’agit d’un pointeur sur une structure de données personnalisées, contenant des informations propres à notre instance de driver.
Ce pointeur joue également un second rôle, notamment dans le cas d’une interruption partagée entre plusieurs périphériques : on fournit le même pointeur lors du retrait du driver afin d’indiquer quel handler doit être désinstallé.
Le handler doit également renvoyer une valeur de type irqreturn_t
. Il s’agit d’un type énuméré pouvant prendre les valeurs IRQ_NONE
ou IRQ_HANDLED
(ainsi que la valeur IRQ_WAKE_THREAD
que nous verrons dans le prochain article). En principe le handler doit interroger le matériel qu’il gère afin de savoir si l’interruption lui était bien destinée. Dans l’affirmative il renverra IRQ_HANDLED
. Sinon, (il est probable que l’interruption soit partagée entre plusieurs drivers) il renverra IRQ_NONE
.
Pour installer et désinstaller le handler, on utilise les routines suivantes :
int request_irq(unsigned int irq_num, irq_handler_t irq_handler, unsigned long flags, const char *name, void *irq_dev); void free_irq(unsigned int irq_num, void *irq_dev);
Il existe divers flags pour l’installation d’un handler, on les trouve dans le fichier <linux/interrupt.h>
. En voici quelques-uns utilisés régulièrement :
IRQF_TRIGGER_RISING
: déclenchement sur le front montant d’un signal logique.IRQF_TRIGGER_FALLING
: déclenchement sur le front descendant d’un signal logique.IRQF_TRIGGER_HIGH
: déclenchement tant qu’un signal logique est au niveau haut.IRQF_TRIGGER_LOW
: déclenchement tant qu’un signal logique est au niveau bas.IRQF_SHARED
: le handler accepte le partage de l’interruption avec d’autres handlers.IRQF_NO_THREAD
: l’interruption ne peut pas être threadée (nous en reparlerons ultérieurement) même avec le patch PREEMPT_RT.
Implémentation
Nous allons programmer un premier module simple, qui installera un handler pour une entrée GPIO. Lorsqu’un front montant se présentera sur cette broche, le handler enverra un message dans les traces du noyau. Le numéro de l’interruption associé à une borne GPIO donnée est obtenu avec la fonction gpio_to_irq()
.
J’ai choisi arbitrairement la broche 16 (GPIO 23, comme on peut le voir sur ce schéma du connecteur P1 du Raspberry Pi).
Téléchargeons et compilons les exemples de cet article :
$ git clone https://github.com/cpb-/Article-2017-06-06 $ cd Article-2017-06-06 $ make
Voici le listing du premier exemple :
/// \file test-irq-01.c /// /// \brief Exemples de l'article "[KERNEL] Interruptions et tasklets" (https://www.blaess.fr/christophe/2017/06/05) /// /// \author Christophe Blaess 2017 (https://www.blaess.fr/christophe) /// /// \license GPL. #include <linux/gpio.h> #include <linux/interrupt.h> #include <linux/module.h> #define IRQ_TEST_GPIO_IN 23 static irqreturn_t irq_test_handler(int irq, void * ident) { printk(KERN_INFO "%s: %s()\n", THIS_MODULE->name, __FUNCTION__); return IRQ_HANDLED; } static int __init irq_test_init (void) { int err; if ((err = gpio_request(IRQ_TEST_GPIO_IN,THIS_MODULE->name)) != 0) return err; if ((err = gpio_direction_input(IRQ_TEST_GPIO_IN)) != 0) { gpio_free(IRQ_TEST_GPIO_IN); return err; } if ((err = request_irq(gpio_to_irq(IRQ_TEST_GPIO_IN), irq_test_handler, IRQF_SHARED | IRQF_TRIGGER_RISING, THIS_MODULE->name, THIS_MODULE->name)) != 0) { gpio_free(IRQ_TEST_GPIO_IN); return err; } return 0; } static void __exit irq_test_exit (void) { free_irq(gpio_to_irq(IRQ_TEST_GPIO_IN), THIS_MODULE->name); gpio_free(IRQ_TEST_GPIO_IN); } module_init(irq_test_init); module_exit(irq_test_exit); MODULE_DESCRIPTION("Simple monolithic interrupt handler"); MODULE_AUTHOR("Christophe Blaess <Christophe.Blaess@Logilin.fr>"); MODULE_LICENSE("GPL");
Chargeons le module, et vérifions que le handler est bien installé :
$ sudo insmod test-irq-01.ko $ cat /proc/interrupts CPU0 CPU1 CPU2 CPU3 [...] 189: 0 0 0 0 pinctrl-bcm2835 23 Edge test_irq_01 [...]
On peut alors faire un contact entre la broche 16 (notre GPIO) et la broche 1 (le +3.3V) avec un petit fil par exemple :
$ cat /proc/interrupts CPU0 CPU1 CPU2 CPU3 [...] 189: 54 0 0 0 pinctrl-bcm2835 23 Edge test_irq_01 [...]
Cinquante quatre interruptions ! Et oui, ce n’est pas surprenant, un petit contact sec entre deux fils produit de nombreux rebonds très brefs. Vérifions les traces de notre handler :
$ dmesg [...] [ 177.947441] test_irq_01: irq_test_handler() [ 177.947541] test_irq_01: irq_test_handler() [ 177.947615] test_irq_01: irq_test_handler() [ 177.947675] test_irq_01: irq_test_handler() [ 177.947686] test_irq_01: irq_test_handler() [ 177.947696] test_irq_01: irq_test_handler() [ 177.947706] test_irq_01: irq_test_handler() [ 177.947728] test_irq_01: irq_test_handler() [ 177.947738] test_irq_01: irq_test_handler() [ 177.947747] test_irq_01: irq_test_handler() [...] [ 178.107233] test_irq_01: irq_test_handler() [ 178.107290] test_irq_01: irq_test_handler() [ 178.107322] test_irq_01: irq_test_handler() [ 178.107357] test_irq_01: irq_test_handler() [ 178.107376] test_irq_01: irq_test_handler() $ sudo rmmod test_irq_01
Grâce à l’horodatage du printk()
, nous voyons que les petits rebonds sont séparés par quelques dizaines de microsecondes seulement.
Différer un traitement
Nous avons parfaitement réussi à installer un handler simple qui se déclenche à chaque occurrence de l’interruption et s’exécute immédiatement et entièrement. Il est important de se souvenir que pendant toute l’exécution d’un handler, l’interruption qui l’a déclenché est masquée (sur tous les cœurs dans le cas d’un processeur multicœur).
Supposons à présent, que nous ayons un travail un peu plus conséquent à réaliser dans le handler, et que nous souhaitions par ailleurs horodater précisément l’occurrence de l’interruption. On pourrait très bien imaginer un transfert de données à effectuer, une vérification de checksum, voire un déchiffrement d’information encodées.
Pour simuler ceci, j’ai simplement ajouté dans le handler une attente active d’une milliseconde avec udelay()
, après le printk()
qui nous sert d’horodatage :
/// \file test-irq-02.c [...] #include <linux/delay.h> #define IRQ_TEST_GPIO_IN 23 static irqreturn_t irq_test_handler(int irq, void * ident) { printk(KERN_INFO "%s: %s()\n", THIS_MODULE->name, __FUNCTION__); udelay(1000); return IRQ_HANDLED; } [...]
Après chargement du module je réitère la même expérience en faisant un bref contact entre l’entrée GPIO et le +3.3V. Voici les traces des messages du kernel :
$ cat /proc/interrupts CPU0 CPU1 CPU2 CPU3 [...] 189: 57 0 0 0 pinctrl-bcm2835 23 Edge test_irq_01 [...] $ dmesg [...] [ 1297.485853] test_irq_02: irq_test_handler() [ 1297.486866] test_irq_02: irq_test_handler() [ 1297.487876] test_irq_02: irq_test_handler()
Cette fois nous n’avons que trois interruptions prises en compte, toutes séparées d’une milliseconde. Pour simplifier notre propos, je n’en ai représentées que deux sur le schéma suivant, mais le raisonnement est tout aussi valable. Nous savons que des rebonds se produisent et qu’une cinquantaine d’IRQ seront envoyées par l’APIC, toutes les vingt microsecondes environ. Après avoir horodaté son déclenchement, par un printk()
, le handler monolithique effectue un travail consommant du temps CPU, une boucle active dans udelay()
, pendant une milliseconde.
![Fig-07 - Rafales d'interruptions monolithiques](https://www.blaess.fr/christophe/wp-content/uploads/2017/06/fig-07-Rafales-d-interruptions-monolithiques.png)
Fig-07 – Rafales d’interruptions monolithiques
Sur le schéma ci-dessus, j’ai numéroté entre parenthèses les occurrences des interruptions. Nous voyons que la première est correctement traitée, avec un retard (on parle généralement de latence) de quelques microsecondes parfaitement justifié. Lorsque la deuxième arrive, l’interruption 189 étant masquée dans l’APIC, elle reste en attente et sera délivrée plusieurs centaines de microsecondes plus tard, une fois que le premier handler sera terminé. Nous avons donc un problème d’horodatage pour cette deuxième occurrence. La situation est pire pour la troisième interruption et les suivantes, puisque le masquage ne conservant qu’une seule occurrence d’IRQ, elles seront tout simplement perdues.
Il est clair que si une interruption périodique se produit toutes les vingt microsecondes dont le handler dure une milliseconde, le système n’est pas viable. Ce qui m’intéresse, c’est le cas où nous avons quelques déclenchements occasionnellement très rapprochés, mais que cela se produit suffisamment rarement pour permettre au système de fonctionner normalement le reste du temps. Et avec un handler monolithique, le résultat n’est pas satisfaisant : seule la première occurrence est correctement traitée, la seconde est mal horodatée, et les suivantes sont perdues !
Top-half et bottom-half
Une autre approche est possible, qui consiste à distinguer les opérations devant être exécutées immédiatement au déclenchement de l’IRQ, de celles qui peuvent être réalisées une fois que l’interruption aura été démasquée dans l’APIC. Les premières sont regroupées dans la partie supérieure (top half) du traitement, et les secondes dans la partie inférieure (bottom half).
Il existe plusieurs supports pour implémenter top half et bottom half. Dans cet article nous observerons les tasklets, dans le suivant nous verrons les workqueues et les threaded interrupts.
Tasklets
Contrairement à ce que leur nom – particulièrement mal choisi – laisse entendre, les tasklets ne sont pas des petites tâches. Il s’agit simplement d’un mécanisme permettant à un handler d’interruption de programmer l’exécution d’une fonction après avoir démasqué l’IRQ qui l’a déclenché. La fonction qui s’exécute doit être de type :
void tasklet_function(unsigned long arg);
On déclare une tasklet avec :
DECLARE_TASKLET(tasklet_name, tasklet_function, tasklet_arg)
La tasklet ainsi déclarée est implémentée par une structure tasklet_struct
. Puis on peut programmer son exécution avec :
void tasklet_schedule(struct tasklet_struct *tasklet_name);
Lorsque le handler invoque tasklet_schedule()
, nous avons plusieurs garanties :
- La fonction de la tasklet sera exécutée (par abus de langage, on dit « la tasklet sera exécutée ») ultérieurement, le plus tôt possible après la fin du handler, sans passer par l’ordonnanceur (contrairement à ce que schedule dans
tasklet_schedule()
laisse entendre). - Si la tasklet est déjà programmée, mais n’a pas encore débuté, une seule instance sera exécutée.
- Si la tasklet est déjà en cours d’exécution, une seconde instance sera exécutée après la fin de la première, sur le même cœur de CPU que celle-ci.
- Si la tasklet n’est pas programmée ni en cours d’exécution, elle sera exécutée sur le même cœur de CPU que le handler qui invoque
tasklet_schedule()
.
Attention, il est important au retrait du module de s’assurer qu’il n’y a pas de tasklet en cours ou en attente d’exécution (sur un autre cœur). Pour cela il faut appeler :
void tasklet_kill(struct tasklet_struct * tasklet_name);
Voyons comment se déroule le traitement d’une occurrence unique de l’interruption :
![Fig-08 - Une interruption, une tasklet](https://www.blaess.fr/christophe/wp-content/uploads/2017/06/fig-08-Une-interruption-une-tasklet.png)
Fig-08 – Une interruption, une tasklet
J’ai abrégé sur le schéma ci-dessus T.H.
pour Top Half (le code qui est exécuté directement dans le handler d’interruption), et B.H.
pour Bottom Half, le code ultérieurement exécuté dans la tasklet. Pas de surprise, le traitement est identique à celui d’un handler monolithique, avec un temps d’exécution très légèrement plus long (non représenté sur ce schéma) dû au mécanisme de programmation et d’invocation de la tasklet.
Supposons maintenant que deux interruptions très rapprochées se déclenchent. La Bottom Half de la première est interrompue par la Top Half de la seconde, et la deuxième Bottom Half s’exécutera à la suite.
![Fig-09 - Deux interruptions, deux tasklets](https://www.blaess.fr/christophe/wp-content/uploads/2017/06/fig-09-Deux-interruptions-deux-tasklets.png)
Fig-09 – Deux interruptions, deux tasklets
L’avantage par rapport au handler monolithique, c’est que la seconde interruption est correctement horodatée. Intéressons-nous maintenant au cas de trois interruptions rapprochées :
![Fig-10 - Trois interruptions, deux tasklets](https://www.blaess.fr/christophe/wp-content/uploads/2017/06/fig-10-Trois-interruptions-deux-tasklets.png)
Fig-10 – Trois interruptions, deux tasklets
À nouveau les trois interruptions sont correctement horodatées. Mais cette fois, le troisième tasklet_schedule()
n’a pas d’effet car la seconde tasklet n’a pas encore démarré. C’est ce que confirme l’expérience suivante :
/// \file test-irq-03.c [...] static void irq_test_tasklet_function(unsigned long); static DECLARE_TASKLET(irq_test_tasklet, irq_test_tasklet_function, 0); static irqreturn_t irq_test_handler(int irq, void * ident) { printk(KERN_INFO "%s: %s()\n", THIS_MODULE->name, __FUNCTION__); tasklet_schedule(&irq_test_tasklet); return IRQ_HANDLED; } static void irq_test_tasklet_function(unsigned long unused) { printk(KERN_INFO "%s: %s()\n", THIS_MODULE->,name, __FUNCTION__); udelay(1000); } [...]
Lorsqu’on fait à nouveau un bref contact entre la broche 16 et la broche 1, on observe :
$ dmesg [...] [130129.690714] test_irq_03: irq_test_handler() [130129.690734] test_irq_03: irq_test_handler() [130129.690744] test_irq_03: irq_test_handler() [130129.690776] test_irq_03: irq_test_tasklet_function() [130129.690826] test_irq_03: irq_test_handler() [130129.690857] test_irq_03: irq_test_handler() [130129.690868] test_irq_03: irq_test_handler() [130129.690877] test_irq_03: irq_test_handler() [130129.690895] test_irq_03: irq_test_handler() [130129.691814] test_irq_03: irq_test_tasklet_function() [...]
Mais alors, la situation n’est pas meilleure qu’avec un driver monolithique ! Somme nous condamnés à perdre la troisième interruption et les suivantes jusqu’au déclenchement de la tasklet ? Non. La situation n’est pas tout à fait la même. Car dans la Top Half, nous avons brièvement le contrôle, et pouvons par exemple incrémenter un compteur d’interruptions. La Bottom Half bouclera autant de fois qu’il y a d’interruptions reçues, et décrémentera le compteur. Voici un exemple d’implémentation :
/// \file test-irq-04.c [...] #include <linux/atomic.h> #include <linux/delay.h> #include <linux/gpio.h> #include <linux/interrupt.h> #include <linux/module.h> [...] static atomic_t irq_test_counter = ATOMIC_INIT(0); static irqreturn_t irq_test_handler(int irq, void * ident) { printk(KERN_INFO "%s: %s()\n", THIS_MODULE->name, __FUNCTION__); atomic_inc(&irq_test_counter); tasklet_schedule(&irq_test_tasklet); return IRQ_HANDLED; } static void irq_test_tasklet_function(unsigned long unused) { printk(KERN_INFO "%s: %s()\n", THIS_MODULE->name, __FUNCTION__); do { printk(KERN_INFO "%s:%s() loop\n", THIS_MODULE->name, __FUNCTION__); udelay(1000); } while (atomic_dec_return(&irq_test_counter) > 0); } [...]
Heureuse surprise, dès mon premier test, j’ai eu trois interruptions rapprochées et le résultat est concluant :
[...] [171984.092518] test_irq_04: irq_test_handler() [171984.092569] test_irq_04: irq_test_handler() [171984.092581] test_irq_04: irq_test_handler() [171984.092595] test_irq_04: irq_test_tasklet_function() [171984.092606] test_irq_04:irq_test_tasklet_function() loop [171984.093617] test_irq_04:irq_test_tasklet_function() loop [171984.094627] test_irq_04:irq_test_tasklet_function() loop [...]
Tasklet et contexte d’appel-système
Nous avons vu que, programmée depuis un contexte d’interruption, une tasklet est exécutée immédiatement sans repasser par l’ordonnanceur. Ceci est facile à vérifier, en modifiant légèrement le code de la tasklet ainsi :
/// \file test-irq-05.c [..] static irqreturn_t irq_test_handler(int irq, void * ident) { tasklet_schedule(&irq_test_tasklet); return IRQ_HANDLED; } static void irq_test_tasklet_function(unsigned long unused) { printk(KERN_INFO "%s: current pid=%d comm=%s\n", THIS_MODULE->name, current->pid, current->comm); }
Cette tasklet affiche le PID et le nom du processus current
(celui actuellement ordonnancé sur le cœur de CPU où elle s’exécute). Nous voyons bien qu’il n’y a pas de tâche spécifique. Suivant les itérations différents processus où threads kernel sont actifs :
[198874.401602] test_irq_05: current pid=0 comm=swapper/0 [...] [198874.402142] test_irq_05: current pid=463 comm=rs:main Q:Reg [...] [198874.402669] test_irq_05: current pid=90 comm=jbd2/mmcblk0p2- [...] [198874.415554] test_irq_05: current pid=0 comm=swapper/0 [...] [198874.421986] test_irq_05: current pid=3 comm=ksoftirqd/0 [...] [198874.422530] test_irq_05: current pid=462 comm=in:imklog [...] [198874.423017] test_irq_05: current pid=463 comm=rs:main Q:Reg [...] [198874.423882] test_irq_05: current pid=0 comm=swapper/0 [...]
Rien ne nous empêche néanmoins de programmer une tasklet depuis un contexte d’appel-système. Voici par exemple un module qui implémente un mini-driver proposant un unique appel-système write()
qui ne fait qu’appeler notre tasklet :
/// \file test-irq-06.c [...] #include <linux/delay.h> #include <linux/interrupt.h> #include <linux/miscdevice.h> #include <linux/module.h> #include <linux/sched.h> static void irq_test_tasklet_function(unsigned long); static DECLARE_TASKLET(irq_test_tasklet, irq_test_tasklet_function, 0); static void irq_test_tasklet_function(unsigned long unused) { printk(KERN_INFO "%s: current pid=%d comm=%s\n", THIS_MODULE->name, current->pid, current->comm); } static ssize_t irq_test_write(struct file *filp, const char *buffer, size_t length, loff_t *offset) { tasklet_schedule(&irq_test_tasklet); return length; } static struct file_operations irq_test_fops = { .owner = THIS_MODULE, .write = irq_test_write, }; static struct miscdevice irq_test_misc = { .minor = MISC_DYNAMIC_MINOR, .name = THIS_MODULE->name, .fops = &irq_test_fops, }; static int __init irq_test_init (void) { return misc_register(&irq_test_misc); } static void __exit irq_test_exit (void) { misc_deregister(&irq_test_misc); } [...]
Pour déclencher l’appel-système, il nous suffit de faire une écriture avec un echo
redirigé depuis la ligne de commande du shell.
$ sudo insmod test-irq-06.ko $ echo 1 > /dev/test_irq_06 -bash: /dev/test_irq_06: Permission denied pi@raspberrypi:~/article-2017-06-06$ sudo -s root@raspberrypi:/home/pi/article-2017-06-06# echo 1 > /dev/test_irq_06 root@raspberrypi:/home/pi/article-2017-06-06# echo 1 > /dev/test_irq_06 root@raspberrypi:/home/pi/article-2017-06-06# echo 1 > /dev/test_irq_06 root@raspberrypi:/home/pi/article-2017-06-06# dmesg [...] [202648.772668] test_irq_06: current pid=15 comm=ksoftirqd/1 [202666.062771] test_irq_06: current pid=3 comm=ksoftirqd/0 [202668.352760] test_irq_06: current pid=3 comm=ksoftirqd/0
Lors de la programmation d’une tasklet depuis un contexte d’appel système, c’est donc un thread du kernel qui l’exécutera. Ces threads, nommés ksoftirqd sont parfaitement visibles dans la liste des tâches du système. Notons par ailleurs que ce n’est pas toujours le même thread, le noyau dispose d’un ensemble de ksoftirqd, et en sélectionne dynamiquement un disponible.
# ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.4 22780 3816 ? Ss Jun02 0:07 /sbin/init splash root 2 0.0 0.0 0 0 ? S Jun02 0:00 [kthreadd] root 3 0.0 0.0 0 0 ? S Jun02 0:05 [ksoftirqd/0] root 5 0.0 0.0 0 0 ? S< Jun02 0:00 [kworker/0:0H] root 7 0.0 0.0 0 0 ? S Jun02 0:17 [rcu_preempt] root 8 0.0 0.0 0 0 ? S Jun02 0:00 [rcu_sched] root 9 0.0 0.0 0 0 ? S Jun02 0:00 [rcu_bh] root 10 0.0 0.0 0 0 ? S Jun02 0:00 [migration/0] root 11 0.0 0.0 0 0 ? S< Jun02 0:00 [lru-add-drain] root 12 0.0 0.0 0 0 ? S Jun02 0:00 [cpuhp/0] root 13 0.0 0.0 0 0 ? S Jun02 0:00 [cpuhp/1] root 14 0.0 0.0 0 0 ? S Jun02 0:00 [migration/1] root 15 0.0 0.0 0 0 ? S Jun02 0:00 [ksoftirqd/1] root 17 0.0 0.0 0 0 ? S< Jun02 0:00 [kworker/1:0H] root 18 0.0 0.0 0 0 ? S Jun02 0:00 [cpuhp/2]
Conclusion
Nous avons vu le principe des tasklets pour réaliser un traitement différé depuis un handler d'interruption. Il existe d'autres mécanismes que nous verrons dans le prochain article. L'intérêt de la tasklet est d'être exécutée immédiatement, plus prioritairement que toutes les tâches ordonnancées du système. Ceci est toutefois différent si elle est programmée depuis un contexte d'appel système, puisqu'alors c'est un thread du kernel qui l'exécute.
Nous reviendrons sur cette exécution portée par un thread dans le prochain article, car cela peut nous réserver des surprises dans un contexte d'application temps réel...
Article très sympa.
J’ai deja pu voir dans des drivers similaires l’utilisation des kthread + semaphore (wait_event/wake_up) a la place des tasket. Ca serai bien d’en parlé
C’est quoi l’avantage/inconvénient ou la raison de cet usage plutot que les tasklets ?
C’est prévu pour le prochain article. L’intérêt du thread personnalisé, c’est qu’on maîtrise sa priorité (temps réel). Ça sera utile pour remplacer les tasklets sur un noyau patché PREEMPT_RT.
Salut,
J’adore tes articles d’une très grandes qualités.
Cependant, je n’arrive pas à comprendre un concept que tu expliques dès le début:
Lorsque tu parles de handler bas-niveau, quel est leurs rôles, sauvegarder le contexte (avant l’éxécution du handler haut-niveau) et ensuite recharger le contexte (à la fin du handler bs-niveau) ou autres (masquer/démasquer l’interruption).
Je n’ai vu que les interruptions dans un contexte microcontrôleur sans OS (style STM32 – Nucleo), il y a t-il des étapes en plus du au système d’exploitation ?
Merci de votre réponse.
Merci, génial.
J’avais déjà tester les ITs high level, mais trop lent pour une précision de l’ordre de 10-5s
Je test ca dés que j’ai un moment.
Bonjour,
J’ai lu cet article avec plaisir. Ferez-vous un prochain article sur les autres mécanismes?
Bien cordialement.
Bonjour,
J’ai commencé à rédiger un article sur les interruptions threadées mais… je n’ai pas trouvé le temps de le terminer. Je vais essayer de la faire prochainement quand même !
Merci pour votre réponse.
Dans l’attente de vous lire, bien cordialement.