Semaine Athens Linux Device Drivers


Tâches et ordonnancement

L'élément de base utilisé par l'ordonnanceur est une tâche qui est représentée par la structure struct task_struct. Une tâche correspond à un thread. Lorsqu'un processus est crée grâce à l'appel système fork, il dispose d'un thread principal et est donc représenté par une instance de la structure task_struct. Si ce processus crée un autre thread (par exemple en appelant la fonction pthread_create qui utilise l'appel système clone), ce nouveau thread (qui partage notamment le même espace d'adressage avec le premier) sera lui aussi représenté par sa propre instance de tast_struct.

Une tâche peut être dans l'un des états suivants :

Tâches et appels systèmes

Lorsqu'une tâche s'exécutant dans l'espace utilisateur est interrompue (à cause d'une interruption en provenance du matériel, d'une exception ou d'un appel système), le processeur bascule en espace noyau et du code du noyau s'exécute (le gestionnaire d'interruption ou l'appel système). Ce code provenant du noyau s'exécute dans le contexte de la tâche qui s'exécutait initialement (l'espace d'adressage virtuel est celui de la tâche, current pointe vers l'instance de la structure struct task_struct correspondant à la tâche, etc.).

À la fin du traitement dans l'espace noyau, la tâche initiale reprend son exécution dans l'espace utilisateur (sauf si elle a été mise en sommeil ou tuée, ou si une tâche plus prioritaire est ordonnancée à sa place).

Mise en sommeil en attendant un événement

Un pilote de périphérique peut avoir besoin d'attendre un événement (par exemple attendre qu'une donnée soit disponible sur le périphérique, qu'il soit prêt à transmettre, etc.). Pendant ce temps, il peut se mettre en sommeil (ainsi que la tâche correspondante) pour permettre à l'ordonnanceur d'exécuter d'autres tâches en attendant.

Création d'une liste d'attente

L'attente se fait à l'aide d'une liste wait_queue_head_t (définie dans include/linux/wait.h). Cette liste va contenir les tâches en attente d'un événement.

Elle peut être déclarée et initialisée statiquement :

DECLARE_WAIT_QUEUE_HEAD(queue);

ou dynamiquement :

struct foo_s {
    wait_queue_head_t queue;
}

void foo_init() {
    init_waitqueue_head(&foo->queue);
}

Mise en sommeil sur une liste d'attente

Pour mettre en sommeil la tâche courante, il faut appeler une des fonctions suivantes :

void wait_event(wait_queue_head_t wait_queue, bool condition)
int wait_event_interruptible(wait_queue_head_t wait_queue, bool condition)

Ces deux fonctions mettent la tâche courante en sommeil sur la liste wait_queue. La tâche sera réveillée (la fonction rendra la main) lorsque la condition booléenne condition sera évaluée à vrai et que la liste d'attente wait_queue sera explicitement réveillée (voir plus bas). Dans la première fonction, la tâche est mise en état TASK_UNINTERRUPTIBLE et donc l'attente ne pourra être interrompue (il devient impossible alors de tuer le processus). Dans la seconde, la tâche est mise dans l'état TASK_INTERRUPTIBLE et donc l'attente peut être interrompue par n'importe quel signal envoyé à la tâche. La fonction wait_event_interruptible retourne -ERESTARTSYS si l'attente a été interrompue par un signal ou 0 si l'attente s'est terminée normalement (condition vraie et liste d'attente réveillée).

Une variante de ces fonctions incluant un timeout existe :

int wait_event_timeout(wait_queue_head_t wait_queue, bool condition, timeout)
int wait_event_interruptible_timeout(wait_queue_head_t wait_queue, bool condition, timeout)

timeout est exprime en jiffies1. Elles renvoient 0 si timeout a expiré et la condition condition est toujours fausse, 1 si condition est vraie (à l'issue du timeout ou si la liste d'attente wait_queue est réveillée avant) et -ERESTARTSYS si l'attente a été interrompue par un signal (pour la version _interruptible).

Réveil

Pour réveiller les tâches en sommeil sur une liste d'attente, il suffit d'appeler la fonction :

void wake_up(wait_queue_head_t *q);

Toutes les tâches en attente sont alors réveillées et redeviennent actives (TASK_RUNNING). Lorsque l'ordonnanceur leur donne du temps pour s'exécuter, elles évaluent alors la condition d'attente et, si elle n'est pas vérifiée, sont remises en sommeil. Si la condition est vérifiée, la fonction wait_event* qui avait été appelée retourne.

L'appel à la fonction de réveil est typiquement fait dans un gestionnaire d'interruption pour réveiller la ou les tâches qui étaient en attente de cet événement.

Une variante existe :

void wake_up_interruptible(wait_queue_head_t *q)

Cette fonction ne réveille que les tâches qui ont été mises en attente via wait_event_interruptible*. Par convention, si vous utilisez wait_event_interruptible*, utilisez wake_up_interruptible (vous pouvez aussi utiliser wake_up).

Variantes : attente exclusive

Dans certaines situations, plusieurs tâches peuvent être en attente d'un événement mais une seule de ces tâches sera capable de traiter cet événement. Il est donc superflu de les réveiller toutes.

La fonction wait_event_interruptible place la tâche en attente non exclusive.

La fonction int wait_event_interruptible_exclusive(wait_queue_head_t wait_queue, bool condition) place quant à elle la tâche en attente exclusive.

Au niveau du réveil :


  1. Linux (comme de nombreux OS faisant du multitâche préemptif) se base sur un timer qui génère des interruptions régulièrement afin de pouvoir reprendre régulièrement la main et de décider s'il faut changer de tâche en cours d'exécution. Un jiffy représente une période de ce timer (en général entre 1 et 10 ms). Dans include/linux/jiffies.h est déclarée une variable unsigned long volatile jiffies qui compte le nombre de déclenchement de ce timer (ticks, i.e. le nombre de périodes d'horloge) depuis le démarrage du système. En ce qui concerne les timeouts, deux fonctions permettent de convertir un intervalle de temps en jiffies : msecs_to_jiffies et usecs_to_jiffies.


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