Semaine Athens Linux Device Drivers


Interruptions

Dans cette page, nous allons voir comment réagir aux interruptions.

Gestionnaires d’interruptions

Un gestionnaire d’interruption est la fonction, au sein du noyau, qui sera appelée quant une interruption survient. Les interruptions sont identifiées par un numéro (qui peut être partagé : plusieurs sources (périphériques) peuvent générer la même interruption) et les gestionnaires d’interruptions déclarent, au moment de leur enregistrement auprès du noyau, quel numéro d’interruption ils gèrent. Lorsque cette interruption se produit, le noyau appelle ainsi le gestionnaire approprié.

Enregistrement

La première étape pour pouvoir réagir à une interruption générée par notre périphérique consiste donc à enregistrer un gestionnaire d’interruption grâce à la fonction définie dans include/linux/interrupt.h :

int devm_request_irq(struct device *dev,
                     unsigned int irq,
                     irq_handler_t handler,
                     unsigned long irqflags,
                     const char *devname,
                     void *dev_id);

Ses arguments sont :

La fonction retourne 0 si tout s’est bien passé, une valeur négative en cas d’erreur.

Le gestionnaire d’interruption peut être supprimé à l’aide de la fonction :

void devm_free_irq(struct device *dev, unsigned int irq, void *dev_id);

Comme pour les autres fonctions devm_* l’appel à cette fonction n’est pas obligatoire, cela sera fait automatiquement quand le périphérique est déconnecté ou que le module est déchargé.

L’ancienne API (fonctions int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev) et void free_irq(unsigned int, void *)) reste utilisable.

Le gestionnaire lui-même

Le prototype du gestionnaire d’interruption est :

irqreturn_t foo_handler(int irq, void *dev_id);

Les arguments sont :

La fonction renvoie :

Plusieurs contraintes importantes s’imposent au gestionnaire d’interruption :

Les rôles classiques du gestionnaire d’interruption sont :

Dans la plupart des systèmes d’exploitation modernes, et c’est le cas de Linux, le traitement de l’interruption est découpé en deux parties :

Bottom half : Exécution différée

Comme le mécanisme de softirq n’est pas accessible directement par les pilotes, nous détaillerons uniquement le mécanisme de tasklet et de workqueue. Il peut être intéressant de noter que le mécanisme de tasklet est construit au dessus de celui de softirq.

Les tasklets on une latence d’exécution inférieure à celle des workqueue (elles s’exécuteront en général plus tôt après l’interruption les ayant déclenché).

Tasklet

Une tasklet est une fonction dont l’exécution peut être programmée par un gestionnaire d’interruption pour une exécution ultérieure.

Initialisation

Elle est représentée par la structure struct tasklet_struct (définie dans include/linux/interrupt.h).

Cette structure peut être initialisée dynamiquement par la fonction

void tasklet_init(struct tasklet_struct *t,
                  void (*func)(unsigned long),
                  unsigned long data);

ou statiquement à l’aide de la macro :

#define DECLARE_TASKLET(t, func, data)

func est la fonction qui sera appelée lorsque la tasklet sera exécutée et data un entier qui lui sera fourni en argument.

Programmation

L’exécution d’une tasklet peut être programmée à l’aide d’une des fonctions suivantes :

void tasklet_schedule(struct tasklet_struct *t);    /* Priorité normale */
void tasklet_hi_schedule(struct tasklet_struct *t); /* Haute priorité */

D’après la documentation du noyau :

Exécution

La fonction exécutée par la tasklet à pour prototype :

void foo_tasklet_func(unsigned long data);

Les tasklets sont exécutées alors que les interruptions sont activées. Elles peuvent donc se permettre de faire des traitements plus long quittes à être interrompues.

Néanmoins, elles s’exécutent toujours dans le contexte d’interruption et donc elles ne doivent pas bloquer ni être mises en sommeil (donc si elles ont besoin de mémoire, elles doivent utiliser le drapeau GFP_ATOMIC, elle ne peuvent pas utiliser de mutex, etc.).

Workqueue

Le mécanisme de workqueue permet de différer des opérations. Il est utilisable comme mécanisme pour implémenter le bottom half d’un gestionnaire d’interruption mais peut également être utilisé dans n’importe quel contexte où des opérations doivent être différées.

Contrairement aux tasklets, le travail effectué par une workqueue est exécuté dans le contexte d’un processus, c’est-à-dire avec les interruptions activées et la possibilité de bloquer et de se mettre en sommeil.

Les workqueues sont donc appropriées quand l’opération a différer est longue ou doit bloquer.

Une petite documentation est disponible dans Documentation/core-api/workqueue.rst.

Création

Une workqueue est représentée par la structure struct workqueue_struct définie dans include/linux/workqueue.h. Cette structure est allouée par l’une des deux fonctions :

struct workqueue_struct * create_workqueue(const char *name);
struct workqueue_struct * create_singlethread_workqueue(const char *name);

La première fonction crée un thread noyau par processeur. Les travaux soumis dans cette workqueue s’exécuteront dans l’un de ces thread. La seconde fonction ne crée qu’un seul thread ce qui peut être amplement suffisant.

Une workqueue peut être détruite à l’aide de la fonction :

void destroy_workqueue(struct workqueue_struct *wq);

Enfin, il existe une workqueue globale au noyau qui peut être utilisée plutôt que de créer des workqueues supplémentaires.

Programmation de travail

Déclaration du travail

Un travail à effectuer par une workqueue est représenté par la structure struct work_struct ou par la structure struct delayed_work pour un travail qui doit attendre l’expiration d’un délai avant d’être exécuté.

Ces structure peuvent être déclarées et initialisées statiquement grâce aux macros :

DECLARE_WORK(n, f);
DECLARE_DELAYED_WORK(n, f);

qui déclarent une structure struct work_struct ou struct delayed_work appelée n et dont le travail à effectuer consiste en la fonction f qui a pour prototype

void foo_work_func(struct work_struct *work);

Ces structures peuvent aussi être initialisées dynamiquement grâce aux macros :

struct work_struct work;

INIT_WORK(&work, f)
INIT_DELAYED_WORK(&work, f)

Programmation du travail

Un travail peut être programmé sur une workqueue à l’aide des fonctions :

bool queue_work(struct workqueue_struct *wq,
                struct work_struct *work);

bool queue_delayed_work(struct workqueue_struct *wq,
                        struct delayed_work *dwork,
                        unsigned long delay);

Ces fonctions programment l’exécution d’un travail (i.e. l’exécution de la fonction pointée par la structure work_struct). S’il est déjà programmé, elles renvoient false. Dans le cas de la seconde fonction, le travail ne débutera pas avant l’expiration du délai delay exprimé en jiffies.

Pour utiliser la workqueue globale au noyau il faut appeler les fonctions :

bool schedule_work(struct work_struct *work);
bool schedule_delayed_work(struct delayed_work *dwork, unsigned long delay)

Une variante xxx_on(int cpu, ... de toutes ces fonctions existe pour laquelle il est possible de choisir le processeur sur lequel le travail sera exécuté (cela permet certaines optimisations, notamment si le processeur en question a déjà certaines données dans son cache ou si le processeur à un accès plus rapide à certaines ressources matérielles).

Les fonctions :

void flush_workqueue(struct workqueue_struct *wq);
bool flush_work(struct work_struct *work);

bloquent jusqu’à ce qu’un travail soit terminé ou qu’il n’y ai plus de travaux à effectuer sur une workqueue.

Il est possible d’annuler un travail qui n’a pas encore débuté grâce aux fonctions :

bool cancel_work_sync(struct work_struct *work);
bool cancel_delayed_work(struct delayed_work *dwork);

Il est également possible de savoir si un travail est programmé grâce aux macros :

work_pending(work);
delayed_work_pending(w);

Threaded IRQ

Le dernier mécanisme disponible pour gérer les interruptions est le mécanisme de threaded IRQ.

Il permet de traiter une interruption dans un thread noyau. Les opérations de traitement de l’interruption peuvent alors être bloquantes. Cela peut être utile notamment dans le cas des périphériques pour lesquels l’échange des données est lent (connectés à un bus I2C par exemple) et pour lesquels le gestionnaire d’interruption doit communiquer avec eux.

La fonction devm_request_threaded_irq permet d’enregistrer un tel gestionnaire d’interruption (à utiliser à la place de devm_request_irq) :

int devm_request_threaded_irq(struct device *dev, unsigned int irq,
                              irq_handler_t handler, irq_handler_t thread_fn,
                              unsigned long irqflags, const char *devname,
                              void *dev_id);

Les paramètres sont les mêmes que ceux de la fonction devm_request_threaded_irq à l’exception de :

De nombreux périphériques dans drivers/input/touchscreen/ utilisent des threaded IRQ. N’hésitez pas à aller les regarder.

Travail à faire

  1. Faites un tableau récapitulant ce qu’à le droit de faire ou pas les fonctions utilisées dans chacun des cas : gestionnaire d’interruption classique, tasklet, workqueue, threaded IRQ, ainsi que la latence d’exécution des trois derniers mécanismes.

  2. Ajoutez la gestion des interruptions à votre pilote de l’accéléromètre :

    L’accéléromètre dispose d’une sortie d’interruption (en réalité deux mais une seule seulement est connectée au FPGA). Dans la configuration par défaut de l’accéléromètre, elle est active à l’état haut. Elle est connectée à la patte B22 du FPGA, qui est configurée pour correspondre au GPIO numéro 61 qui lui-même est connecté au contrôleur GPIO numéro 2 (aussi appelé portc dans le Device Tree qui vous est fourni) sur la ligne numéro 3.

    On peut décrire cette configuration dans l’entrée correspondant à l’accéléromètre dans le Device Tree sous la forme :

    interrupt-parent = <&portc>;
    interrupts = <3 1>;

    Le paramètre interrupt-parent indique à quel contrôleur d’interruption est connecté le signal d’interruption : ici au contrôleur GPIO portc qui sert également de contrôleur d’interruption.

    Le paramètre interrupts contient ici deux valeur. La première indique la ligne du contrôleur GPIO à laquelle le signal est connecté (ici la ligne 3). La seconde valeur indique quelle condition doit déclencher l’interruption (1 : front montant, 2 : front descendant, 4 : niveau logique haut, 8 : niveau logique bas).

    Ces deux paramètres du Device Tree sont décrits en détails dans Documentation/devicetree/bindings/interrupt-controller/interrupts.txt.

    1. Commencez par ajouter la configuration des interruptions dans le fichier dts, recompilez le dtb et installez-le (voir les instructions données précédemment dans le cours

    2. Dans la fonction d’initialisation de l’accéléromètre, configurez-le pour utiliser les interruptions. Dans un premier temps, nous utiliserons la FIFO en mode stream et l’interruption watermark.

      L’accéléromètre dispose d’une FIFO permettant de stocker 32 ensembles de résultats de mesure (voir page 20 de la documentation de l’accéléromètre). En mode stream (registre FIFO_CTL, voir page 26), la FIFO stocke les 32 dernières mesures effectuées. Si la FIFO est pleine, les mesures les plus anciennes sont supprimées et remplacées par les nouvelles mesures.

      L’interruption watermark indique qu’il y a plus de samples mesures actuellement stockées dans la FIFO. La valeur de samples est définie dans le registre FIFO_CTL. Cette interruption indique que la FIFO est en train de se remplir et qu’il devient nécessaire de la vider.

      Configurez samples à la valeur 20 (arbitraire, il faudrait réfléchir à une valeur plus appropriée en fonction du débit de mesure, de la vitesse de transmission entre l’accéléromètre et le processeur et de la latence de traitement de l’interruption).

      Il faut enfin activer l’interruption via le registre INT_ENABLE (voir page 25).

    3. Écrivez le traitement de l’interruption (top et bottom halves avec le mécanisme qui vous parait le plus approprié). Modifiez également le comportement de votre module, notamment au niveau de la lecture depuis l’espace utilisateur.

      Lors de l’interruption, récupérez toutes les données contenues dans la FIFO de l’accéléromètre et stockez-les dans un tampon interne à votre module. La taille maximale de ce tampon sera un paramètre de votre module et aura pour valeur par défaut 64.

      Une lecture depuis l’espace utilisateur doit renvoyer une ou plusieurs mesures en provenance du tampon. Elle doit bloquer si aucune mesure n’est actuellement disponible.

    4. Testez !


Retour au sommaire du cours


© Copyright 2017 Guillaume Duc. Le contenu de cette page est mis à disposition selon les termes de la Licence Creative Commons Attribution - Partage dans les Mêmes Conditions 4.0 International (à l'exception des exemples de code tirés du noyau Linux et qui sont distribués sous leurs licences d'origine).

Licence
Creative Commons