Premiers pas avec Xenomai

| David Chabal | dchabal |

Xenomai est une solution « temps réel dur » libre adossée à Linux.

Comme il n'est pas forcément aisé de comprendre directement la pertinence des solutions apportées par Xenomai, ce tutoriel décrit les limitations de Linux dans le cadre d'applications temps réel contraintes et introduit progressivement Xenomai.

Ce document décrit, entre autres, l'installation et la configuration des noyaux Linux et Xenomai, le mécanisme de co-scheduling, ainsi que quelques exemples de réalisations.

Commentez

- Titre : Premiers pas avec Xenomai

- Auteurs : David Chabal, dchabal

- Parution : 12 février 2012

- Licence : Copyright ® 2012 David Chabal. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Licence

- II. Introduction

- II-A. Que contient ce document ?

- II-B. À qui s'adresse ce tutoriel ? Quels sont les prérequis ?

- II-C. Comment est organisé ce document ?

- II-D. Quelles sont les limites de ce document ?

- III. Quelques (petits) rappels

- III-A. Terminologie

- III-B. Les processus et les threads

- III-C. La latence

- III-D. La préemption

- III-E. Les modes user et kernel

- III-F. L'accumulation des latences dans la gestion d'une interruption matérielle

- III-G. Le noyau Linux et ses évolutions

- III-H. Les mécanismes de synchronisation

- III-I. Comment gérer la priorité des processus ?

- III-J. Comment gérer le temps ?

- III-J-1. Le modèle « tout périodique »

- III-J-2. Le modèle tickless

- III-K. Inversion et héritage de priorité

- IV. Linux et le(s) temps réel

- IV-A. Les avantages et les inconvénients de Linux pour les applications temps réel

- IV-B. Comment faire du temps réel avec Linux : les approches possibles

- IV-B-1. Aucune préemption forcée

- IV-B-2. Une première possibilité : l'option Voluntary Kernel Preemption

- IV-B-3. Pour du temps réel « mou » : Preemptible Kernel (Low-Latency Desktop)

- IV-B-4. Le temps réel avec le patch PREEMPT_RT

- IV-B-4-a. Drivers

- IV-B-4-b. Limitations actuelles et évolutions

- IV-B-5. Le temps réel dur avec les co-noyaux

- V. Qu'est-ce que Xenomai ?

- V-A. Origines

- V-B. Xenomai, comment ça marche ?

- V-B-1. Le routage des interruptions avec ADEOS

- V-B-2. Coexistence Xenomai/Linux et partage des ressources : le co-scheduling

- V-B-3. La gestion des priorités dans les modes primary et secondary

- V-B-4. ADEOS ou comment modifier Linux proprement

- V-B-5. Interrupt shield : sus aux interruptions !

- V-C. Les architectures supportées

- V-D. Les modes d'exécution

- V-E. L'API native et les skins

- V-F. Solutions concurrentes du marché

- VI. Installation et configuration

- VI-A. Configuration utilisée pour la rédaction de ce document

- VI-B. Récupération des sources

- VI-C. Procédure d'installation, patch des sources du noyau

- VI-D. Configuration de Xenomai dans le noyau Linux

- VI-D-1. Lancement de l'utilitaire de configuration du noyau

- VI-D-2. Le choix des options

- VI-D-3. Amélioration de la latence de Linux (pour le secondary mode)

- VI-D-4. Fonctionnalités ou options Linux potentiellement problématiques ou incompatibles

- VI-E. Compilation et installation du nouveau noyau

- VI-F. Installation des bibliothèques Xenomai

- VI-G. Démarrage sur le noyau incluant Xenomai

- VI-H. Xenomai et le noyau Linux sont-ils correctement installés ?

- VI-H-1. Première mesure de la latence

- VI-H-2. Le fichier TROUBLESHOOTING

- VI-H-3. Traquer les latences avec l'option I-pipe debugging

- VI-I. Wiki d'I-Pipe Tracer

- VI-J. Traquer les interruptions parasites avec powertop

- VII. Premiers pas

- VII-A. Un grand classique : « Hello world! »

- VII-B. La compilation

- VII-C. L'exécution

- VII-D. Que se passe-t-il ?

- VIII. Exemples d'utilisation

- VIII-A. Avant d'utiliser les fonctions Xenomai : les environnements d'appels

- VIII-B. Les tâches

- VIII-B-1. Les paramètres de lancement d'une tâche

- VIII-B-2. Tâches périodiques

- VIII-B-3. Ce qui donne les traces suivantes :

- VIII-B-4. Transformation d'un processus Linux en tâche Xenomai

- VIII-B-5. Affectation tâche/processeur (affinity )

- VIII-B-6. Contrôle du scheduling d'une tâche : la fonction rt_task_set_mode

- VIII-C. Les sémaphores (à compteur et d'exclusion mutuelle)

- VIII-D. Les files de messages

- VIII-E. Les alarmes (watchdogs )

- VIII-F. Les pipes et la communication avec les applications Linux « normales »

- VIII-G. Nommage et rémanence des objets

- VIII-H. Aperçu de l'API native

- IX. Exécution des programmes Xenomai et sécurité

- IX-A. « Xenomai GID »

- IX-B. Activation du bit d'endossement

- IX-C. sudo

- X. Méthodologie de développement

- X-A. Prototypage de l'architecture dynamique

- X-B. Pratiques

- X-B-1. Les mauvaises...

- X-B-2. ... et les bonnes

- X-C. Aide à la mise au point

- X-C-1. Ajout de traces

- X-C-2. Détection des basculements « sauvages » en secondary mode

- X-C-3. Tests de bon fonctionnement bas niveau

- X-C-4. Utilisation du débogueur gdb

- XI. Étude de cas n° 1 : simulateur GPS

- XI-A. Fonctionnalités

- XI-B. Architecture dynamique

- XI-B-1. Schéma

- XI-B-2. Justification

- XI-B-2-a. Tâche TempsGPS

- XI-B-2-b. Tâche TxTM

- XI-B-2-c. Tâche RxTC

- XI-B-2-d. Tâche PerioTM

- XI-B-2-e. Tâche TraiteTC

- XI-B-2-f. Tâche bvl_GPS

- XI-B-2-g. Tâche JDB

- XI-B-2-h. Tâche Traces

- XI-B-2-i. Tâche main

- XI-B-2-j. Files de messages

- XII. Étude de cas n° 2 : driver RTDM Fiji-Oslink

- XII-A. Ressources utiles

- XII-B. Fonctionnalités

- XII-C. Anatomie d'un driver RTDM

- XII-C-1. Comment interfacer un driver RTDM : la structure rtdm_device

- XII-C-2. Où mémoriser l'état d'un périphérique ouvert : le contexte

- XII-C-3. Comment accéder à des zones de mémoire partagée avec les applications ?

- XII-C-4. Comment partager du code en fonction du contexte d'appel ?

- XII-D. Installation et désinstallation

- XIII. Drivers

- XIII-A. Communication série

- XIII-A-1. Matériels compatibles

- XIII-A-2. Récupération de la configuration matérielle de la carte

- XIII-A-3. Configuration du driver RTDM 16550A

- XIII-A-4. Configuration de la liaison avec l'ioctlRTSER_RTIOC_SET_CONFIG

- XIII-B. Comment écrire un driver PCI ?

- XIII-B-1. Documentation de référence

- XIII-B-2. Configuration vs initialisation

- XIII-B-3. L'écriture d'un driver étape par étape

- XIV. Métriques : mesures de performances et d'occupation mémoire

- XIV-A. Latence sur basculement de contexte

- XIV-B. Latence sur interruption

- XIV-C. Taux d'occupation CPU

- XIV-D. Taille de la pile

- XV. Simulation avec Xenosim

- XVI. Matériels : recommandations, constructeurs, compatibilités

- XVI-A. Opto-isolation des interfaces

- XVI-B. PC industriel

- XVI-C. Refroidissement

- XVI-D. Connectique (aka jurisprudence de la loi de Murphy)

- XVI-E. Drivers Linux

- XVII. Aspects juridiques, licences

- XVII-A. Notions d'utilisation normale et de travail dérivé

- XVII-B. Travail dérivé du noyau

- XVII-C. Cession d'un produit basé sur Xenomai

- XVII-D. Coût

- XVIII. Organisation du développement

- XVIII-A. Plusieurs développeurs sur le même poste

- XVIII-B. Portabilité des Makefile

- XIX. À l'aide !!!

- XIX-A. Les mailing-lists

- XIX-B. Comment rédiger un message ?

- XX. À propos de l'auteur

- XXI. Annexes

- XXI-A. Codes d'erreurs spécifiques Xenomai, causes possibles

- XXI-B. Timers : indexation avec une table de hâchage

- XXI-C. Exemple d'implémentation d'une tâche de traces (log.c)

- XXI-D. Révisions du document

I - Licence

Copyright © 2008-2012 - David Chabal

Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.2 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A copy of the license is included in the section entitled "GNU Free Documentation License".

II - Introduction

II-A - Que contient ce document ?

Xenomai est une solution « temps réel dur » libre adossée à Linux.

Comme il n'est pas forcément aisé de comprendre directement la pertinence des solutions apportées par Xenomai, ce tutoriel décrit les limitations de Linux dans le cadre d'applications temps réel contraintes et introduit progressivement Xenomai.

Ce document décrit, entre autres, l'installation et la configuration des noyaux Linux et Xenomai, le mécanisme de co-scheduling, ainsi que quelques exemples de réalisations.

II-B - À qui s'adresse ce tutoriel ? Quels sont les prérequis ?

Ce tutoriel s'adresse à des développeurs ayant déjà des connaissances en C, Unix et temps réel. La connaissance de Linux est bien entendu un plus, mais n'est pas indispensable au vu de la simplicité d'installation des distributions actuelles et de la présence de nombreux tutoriels sur le Net.

Ce tutoriel peut également être utilisé dans le cadre d'une autoformation au temps réel à moindre coût. En effet, dans la majorité des cas, les OS temps réel fonctionnent sur des plates-formes matérielles dédiées, l'avantage de Xenomai est de pouvoir tourner sur n'importe quel PC.

Certains chapitres concernant les aspects juridiques ou encore l'organisation du développement peuvent également intéresser les rédacteurs de propositions techniques et commerciales.

II-C - Comment est organisé ce document ?

Ce tutoriel est divisé en quatre parties.

Avant d'expliquer l'intérêt de Xenomai, il est nécessaire de bien comprendre les limitations de Linux. Ce sera l'objet de la première partie qui dressera un historique du noyau Linux et expliquera comment il a été sans cesse optimisé.

Ensuite, les mécanismes de virtualisation des interruptions et de scheduling, utilisés par Xenomai seront détaillés grâce à de nombreux schémas.

Après la théorie, la pratique. La troisième partie décrit l'installation et la configuration de Xenomai et du noyau Linux ; ainsi que deux cas d'utilisation : un simulateur GPS et un driver temps réel.

La fin du document aborde également la mesure de performances, qui va de pair avec tout système temps réel, ainsi que la mise au point d'applications.

Les points importants sont signalés au fil des pages dans des encadrés.

Étant donné qu'une partie de ce document est une compilation d'informations publiées sur Internet, l'auteur s'est efforcé de citer ses sources sous forme de liens.

II-D - Quelles sont les limites de ce document ?

Ce tutoriel est basé sur des réalisations dans le domaine des bancs de tests simulant des équipements spatiaux, c'est pourquoi il est loin d'être exhaustif. Cependant, l'environnement utilisé étant très courant puisque basé sur des PC équipés de cartes série, ce qui est décrit ici devrait être applicable dans de nombreux projets.

III - Quelques (petits) rappels

III-A - Terminologie

Si l'emploi des mots « temps réel » , « tâche »… vous rappelle des lointains souvenirs avec une salle de classe comme décor, ce paragraphe est pour vous ! Les pros passeront directement au suivant.

Appel système (system call) : depuis une application utilisateur, lorsque l'on appelle un service fourni par le noyau, on fait alors un appel système. Cet appel système bascule l'application en mode noyau. Sous Linux, le noyau propose entre 300 et 400 services, ce nombre dépend de l'architecture utilisée. Si vous avez un peu de temps, vous pouvez jeter un œil dans les sources du noyau Linux, la liste se trouve dans le fichier entry.S correspondant à votre architecture matérielle. Xenomai en propose une dizaine.

Dans la pratique, tout ceci est transparent pour le développeur puisque les appels se font à travers des bibliothèques « système », donc on ne voit jamais réellement les appels aux fonctions du noyau. Pour la fonction getpid par exemple, la bibliothèque système l'implémentant est la glibc; à l'intérieur de celle-ci se trouve un appel au service n° 20, sys_getpid, du noyau Linux(1).

En mode noyau, toutes les structures internes (tâches, structures abstraites derrière les descripteurs, registres matériels...) sont alors accessibles et donc modifiables.

Affinity (ou CPU binding) : restriction de l'exécution d'une tâche sur un ou plusieurs CPU. Cette affectation permet évidemment de dédier une ressource processeur, mais elle optimise également l'accès à la mémoire cache.

Jiffy : intervalle entre deux ticks d'horloge.

LKM : Linux Kernel Module, module du noyau Linux, chargeable et déchargeable dynamiquement. Ce code, généralement un driver matériel, est exécuté en mode noyau.

Mainline : version stable d'un logiciel, par opposition aux versions de développement.

Noyau Linux (kernel) : il remplit plusieurs tâches : l'ordonnancement des processus, la fourniture d'appels système aux applications utilisateur, la gestion des entrées/sorties matérielles.

Inutile d'approfondir plus cette description car le howto du noyau Linux, Wikipedia ou encore Understanding the Linux Kernel(2), pour ne citer qu'eux, vous apporteront des explications très détaillées.

Points de préemption, de descheduling : appels au scheduler afin qu'il élise éventuellement une autre tâche.

Quantum, timequantum, timeslice, slice : dans un ordonnancement coopératif ou préemptif avec round-robin, temps pendant lequel une tâche profitera du processeur.

Round-robin : lorsque deux tâches ont la même priorité, ce mécanisme donne alternativement la main à l'une et à l'autre. De manière plus générale, dans un système préemptif, si n tâches ont la même priorité, deux politiques sont possibles : soit la tâche élue s'exécute et passe la main à la suivante lorsqu'elle se termine, et ainsi de suite ; soit les tâches s'exécutent à tour de rôle chacune pendant un quantum de temps défini. D'un point de vue utilisateur, cette dernière solution a pour effet de « lisser » l'exécution des applications.

RTOS : Real Time Operating System, OS temps réel.

Vanilla kernel : noyau Linux « officiel » dont les sources se trouvent sur www.kernel.org, par opposition aux noyaux modifiés par les éditeurs (Redhat, Mandriva...).

III-B - Les processus et les threads

Quelle différence y a-t-il entre un processus et un thread sous Linux ?

Si vous posez la question à un développeur, il vous répondra probablement que cela n'a rien à voir en vous expliquant qu'un processus peut comporter plusieurs threads, que l'espace d'adressage mémoire est commun... et bien d'autres arguments tout à fait justes.

Si vous reposez la même question à l'architecte d'une application temps réel, il vous répondra qu'il n'y a pas de différence fondamentale car les deux sont traités de la même manière dans le scheduler. C'est le point de vue qui est également adopté dans ce document. Dans un contexte d'ordonnancement et de priorité, les termes tâche, processus et thread se confondent.

III-C - La latence

Le principal problème pour qu'un OS satisfasse à une utilisation temps réel est la latence. La latence est le délai entre le moment où un traitement est nécessaire et l'instant où il est effectif, c'est par exemple le temps qui sépare la génération d'une interruption de son traitement.

Dans un système temps réel, la latence est bornée afin de garantir le déterminisme temporel.
La valeur de latence retenue pour décrire un système d'exploitation temps réel représente toujours le cas le plus défavorable.
Plus la latence est faible, plus on considère le système comme étant performant.

Dans un OS temps réel, la latence est classiquement de l'ordre de quelques microsecondes.

III-D - La préemption

(3) Le remplacement involontaire du processus s'exécutant par un autre est appelé préemption. Le terme involontaire(4) signifie que le processus en cours ne s'est pas, par exemple, mis en sommeil ou en attente de réception de données.

La préemption a lieu dans deux cas :

III-E - Les modes user et kernel

Lors de son exécution, un processus fait des allers-retours entre le mode utilisateur, dans lequel se déroulent les algorithmes codés par les développeurs, et le mode noyau pour les appels systèmes (lecture d'une socket, consultation de la date système, etc.).

Dans cet exemple, P1 et P2 ont la même priorité et s'exécutent en round-robin :

Le problème du noyau Linux est que, par défaut, il n'y a pas de préemption à l'intérieur du noyau.
Pour faire du temps réel, cela n'est pas tolérable pour au moins deux raisons :
un processus de basse priorité en mode noyau ne peut pas être interrompu par un processus de haute priorité, on se situe alors dans un cas d'inversion de priorité ; le temps passé à l'intérieur du noyau peut être très long.

Nous verrons par la suite qu'il est possible de préempter les tâches à l'intérieur du mode noyau.

III-F - L'accumulation des latences dans la gestion d'une interruption matérielle

Rentrons maintenant un peu plus dans le détail et analysons comment est traitée une interruption(5). Dans cet exemple, le matériel génère une interruption (signalant des données disponibles, l'expiration d'un timer, etc.) et un processus doit prendre en compte cet événement.

La latence de l'interruption(6) est le temps entre la génération de l'interruption et l'appel du handler.

Elle est augmentée par :

- le masquage des interruptions à l'intérieur du noyau : le traitement est alors reporté au démasquage ;

- le partage d'une ligne d'interruption : tous les handlers installés sur cette interruption sont alors appelés (charge à eux, ensuite, de gérer l'absence de « cause réelle »), ces appels multiples sont forcément coûteux ;

- l'arrivée d'une interruption plus prioritaire : matériellement, si deux interruptions arrivent simultanément, l'arbitrage est fait par le contrôleur d'interruptions (APIC).

Généralement, les handlers d'interruption ne sont pas réentrants. Par conséquent, afin de prévenir toute autre interruption qui serait alors imbriquée, la ligne est temporairement masquée. Pour fixer les idées, voici un extrait simplifié d'un driver de carte Ethernet (linux-2.6.25/drivers/net/e100.c) :

static irqreturn_t e100_intr(int irq, void *dev_id)
{
...
/* Au début du handler, on considère le cas où la ligne d'interruption est partagée, « est-ce pour nous ? »: telle est l'utilité de ce test */
if(stat_ack == stat_ack_not_ours ||    /* Not our interrupt */
stat_ack == stat_ack_not_present)    /* Hardware is ejected */
return IRQ_NONE;

/* Acquittement de l'interruption */
iowrite8(stat_ack, &nic->csr->scb.stat_ack);
...
/* Traitement... */
if(...) {
...
__netif_rx_schedule(netdev, &nic->napi);
}

return IRQ_HANDLED;
}

Si des interruptions sont masquées pendant l'exécution du handler, cela signifie que toutes celles arrivant vont être en attente (pending), augmentant ainsi leur latence... Mais rassurez-vous, ce mécanisme est optimisé : l'idée consiste à découper le handler en deux :

- une première partie -courte- qui va masquer les interruptions, acquitter la ligne, effectuer un nombre d'opérations minimum et surtout autoriser le déroulement d'une seconde partie, mais pas tout de suite ! En fait, ce sera seulement après avoir démasqué les interruptions. L'avantage ? Plutôt que de faire un long traitement atomique, la séparation permet aux interruptions en attente d'être traitées plus rapidement ;

- dans la seconde portion du handler, le traitement continue mais avec des contraintes de temps (un peu) plus relâchées.

Dans la terminologie Linux, « top half » est le « vrai » handler et « bottom half » (ou encore soft IRQ) est celui appelé dans un second temps.

Ce n'est pas encore tout à fait terminé, puisqu'il faut trouver un moment pour exécuter notre bottom half. L'introduire dans le scheduler comme une tâche ? Possible, mais par défaut, ce n'est pas la solution qui a été retenue. Les choses vont alors se compliquer (très) légèrement : le noyau Linux possède en fait un second scheduler dédié aux bottomhalf qui prennent alors le nom de tasklet(7). Ce scheduler de tasklets est prioritaire sur le scheduler de tâches(8).

On entre ensuite dans le scheduler en lui-même, il trouvera la prochaine tâche courante parmi les n éligibles puis basculera les contextes. Ce temps correspond à la durée de scheduling.

Un dernier point reste à aborder... Indiquée en gris sur le schéma, la latence induite par les économies d'énergie est matérielle. Elle est un peu particulière car elle n'est pas réellement ajoutée juste après la génération de l'interruption mais elle correspond à une moindre performance du processeur. Afin d'économiser de l'énergie, celui-ci diminue sa vitesse lorsqu'il n'a rien à faire et met un certain temps à retrouver ses pleines capacités.

III-G - Le noyau Linux et ses évolutions

Ce paragraphe détaille les mécanismes du noyau introduisant des temps de latence, et comment celle-ci a diminué au fil des évolutions.

Le problème de base est de rendre les processus en mode noyau préemptibles. Concrètement, cela signifie que si un processus P1 fait un appel système et que, un autre, P2 - plus prioritaire - devient éligible, P1 suspendra son exécution, P2 deviendra alors le processus courant et pourra faire à son tour des appels système.

Conclusion : le noyau doit être réentrant, et ce n'est pas forcément simple à appliquer sur un code déjà existant de plusieurs centaines de milliers de lignes... Le développement réentrant implique la mise en œuvre d'exclusion mutuelle dès qu'une donnée est partagée.

L'idéal pour résoudre un problème étant de le partager avec d'autres, la réentrance du noyau fît son apparition avec le support multiprocesseur (SMP), rien à voir avec le temps réel donc.

Ne confondez pas la préemption et la réentrance.
La réentrance est nécessaire pour la préemption. Cependant, un noyau peut être réentrant sans être préemptible, cela signifie simplement qu'il ne comporte alors pas de points de descheduling.

Dans la version 2.2, il existait un verrou appelé BKL (Big Kernel Lock) qui protégeait les sections critiques du noyau. La portée de ce «  supersémaphore  » était très étendue, en effet on trouvait des appels à lock_kernel() et à unlock_kernel(), à des emplacements très variés du code qui allaient des sémaphores IPC au driver de souris USB. Une grande partie des appels système étaient donc, dans les faits, sérialisés.

Un exemple dans drivers/usb/mousedev.c du kernel 2.2.26 :

static int mousedev_release(struct inode * inode, struct file * file)
{
struct mousedev_list *list = file->private_data;
struct mousedev_list **listptr;

lock_kernel();
...
unlock_kernel();

return 0;
}

L'évolution de la 2.2 vers la 2.4 a supprimé la majorité des appels au BKL au profit de verrous beaucoup plus localisés.

Exemple comparatif : fs/open.c du noyau 2.2.6:

asmlinkage int sys_close(unsigned int fd)
{
int error;
struct file * filp;

lock_kernel();
...
unlock_kernel();
return error;
}

Et celui du noyau 2.4.36, le verrou est local au système de fichiers :

asmlinkage long sys_close(unsigned int fd)
{
struct file * filp;
struct files_struct *files = current->files;

write_lock(&files->file_lock);
...
write_unlock(&files->file_lock);
return -EBADF;
}

Ce n'est finalement qu'à partir de la version 2.6, que les choses commencent à devenir vraiment intéressantes... Les besoins multimédias étant passés par là, l'exécution d'applications temps réel « mou »(9) est possible grâce à la préemption en mode kernel, via l'ajout de points de descheduling dans les appels système. Comme nous le verrons par la suite, possible ne veut pas dire obligatoire car par défaut le noyau n'est toujours pas optimisé en latence(10).

Il faut cependant noter que cette préemption n'est que partielle puisqu'elle exclut les sections critiques et des handlers d'interruption. Parmi les améliorations importantes, on trouve également dans cette version un nouveau scheduler(11) qui choisit la tâche à exécuter parmi celles éligibles en un temps quasi constant, et ce, quel que soit le nombre de tâches(12).

III-H - Les mécanismes de synchronisation

Dans le noyau, il existe plusieurs implémentations des sections critiques. Le choix est guidé par les performances : temps d'exécution de la section (bref ou long), nombre et type d'accès (lecture ou écriture) à une variable partagée, donnée accessible par plusieurs CPU, etc.

Pour les nommer, on peut par exemple citer les spinlocks, les RCU, les sémaphores. Des explications détaillées sont disponibles à cette adresse :

http://linux-security.cn/ebooks/ulk3-html/0596005652/understandlk-CHP-5-SECT-2.html.

III-I - Comment gérer la priorité des processus ?

Lors du développement d'applications « classiques », cet aspect n'est que rarement abordé, d'où ce paragraphe de rappel.

La fonction sched_setscheduler() prend en paramètre la stratégie d'ordonnancement (policy). Vous vous demandez sûrement ce que vient faire cette stratégie alors que l'on parle de priorités. Explications.

Il y a quatre stratégies ou classes d'ordonnancement disponibles :

- SCHED_OTHER : la politique de temps partagé (time slicing) utilisée par défaut, la priorité statique associée est toujours 0. À l'intérieur de cette classe, on peut pénaliser ou gratifier un processus en modifiant son quantum de temps via la fonction nice(), c'est la priorité dynamique. Elle n'est pas abordée dans ce document, par priorité on sous-entendra toujours priorité statique ;

- SCHED_BATCH(13): basé sur SCHED_OTHER, sert aux processus longs et non interactifs (calculs par exemple). Un processus s'exécutant en SCHED_BATCH a un quantum plus long et fait donc moins de rescheduling ;

- SCHED_FIFO : pour les processus temps réel, les niveaux de priorité vont de 1 (la plus faible) à 99. Si un processus appliquant la politique SCHED_FIFO s'exécute, il ne peut être préempté que par un processus de priorité strictement supérieure. L'emploi du terme « temps réel » est un peu usurpé car les mécanismes de scheduling sont exactement les mêmes que pour les processus SCHED_OTHER, la seule chose qui change est la priorité ;

- SCHED_RR : basé sur SCHED_FIFO, avec l'utilisation du round-robin.

Afin de trouver le processus qui doit s'exécuter, l'ordonnanceur parcourt d'abord la liste des processus temps réel (SCHED_FIFO et SCHED_RR) et prend celui de plus haute priorité. Si cette liste est vide, il passe à la liste contenant les processus SCHED_OTHER et si elle également vide, il traite les processus SCHED_BATCH.

Par défaut, Linux possède 101 niveaux de priorité (14).
Il faut des privilèges superutilisateur pour migrer un processus vers les classes temps réel.
La compréhension de ces stratégies d'ordonnancement est importante car Xenomai les utilise sous l'intitulé secondary mode.

À noter qu'il existe également une fonction sched_setaffinity qui affecte un processus à un ou plusieurs CPU.

III-J - Comment gérer le temps ?

Du quantum aux timers de plusieurs secondes, comment Linux gère-t-il le temps ?

Commençons tout d'abord par répondre à deux questions basiques : qui a besoin du temps et quelles « horloges » utiliser ?

III-J-1 - Le modèle « tout périodique »

À la première question, nous avons quatre bonnes réponses(15) :

- L'horloge interne (jiffies) du noyau qu'il faut incrémenter. Rien de très compliqué ici, lors de l'initialisation, on programme un timer pour appeler un handler d'interruption à une fréquence f. Chaque appel incrémentera la variable jiffies ;

- Le ou les processus s'exécutant afin de mesurer l'occupation CPU (accounting) ;

- Les processus en temps partagé : une fois expiré le quantum de temps alloué à un processus, le scheduler en élit un autre ;

- Les actions différées : « timers », time-out...

Avec cette organisation, la gestion du temps est relativement simple puisqu'une seule horloge est nécessaire. Il suffit d'une interruption matérielle périodique et le tour est joué : à chaque tick d'horloge on incrémente les jiffies, on vérifie s'il existe des timers expirés, etc.

L'autre question était avec quel composant le faire : ce sera le travail des timers programmables.

Vous vous demandez quelle fréquence positionner ? Trop lent et les timers seront imprécis. Trop rapide et l'overhead induit pénalisera le système. Ces interrogations sont tout à fait justifiées. On peut même rajouter une remarque empreinte d'écologie : les économies d'énergie passent par une diminution de la fréquence du processeur lorsque le système n'est pas chargé (idle), or cet état n'est pas atteignable si on est sans cesse réveillé par une horloge.

La latence matérielle augmente lorsque les fonctionnalités d'économie d'énergie sont activées.

III-J-2 - Le modèle tickless

Cette dernière approche est très intéressante pour expliquer les dernières évolutions (2.6.25) du noyau. L'idée consiste à distinguer le système chargé (c'est-à-dire avec des processus éligibles) et idle.

Dans l'état idle, le comptage du temps consommé par le processus courant et le rafraîchissement du quantum n'existent pas. Quant à l'horloge interne, plutôt que la mettre périodiquement à jour, on la rafraîchit lorsque l'on en a besoin.

La liste des horloges disponibles se trouve dans le fichier /sys/devices/system/clocksource/clocksource0, par exemple sur un PC :

hpet acpi_pm jiffies tsc

Quant aux évènements différés, la solution passe par un timer programmé pour générer une interruption, ce mécanisme est alors nettement plus précis que le modèle « tout périodique ».

De ce modèle tickless découlent les timers haute précision (HRT Timers).

Les timers disponibles sont listés dans le fichier /proc/timer_list(16) :

...
Clock Event Device: hpet
...
Clock Event Device: lapic
...
Clock Event Device: lapic
...

Si le système est en charge, le mécanisme périodique présenté au début assurera le comptage du temps CPU et le time slicing. Sa fréquence est par défaut de l'ordre de la centaine de Hertz.

À noter que les horloges grand public, quelle que soit leur précision « annoncée » dérivent dans le temps et en fonction de la température.
Pour connaître leur précision « réelle », il faut alors se reporter à la datasheet du composant.

III-K - Inversion et héritage de priorité

Considérons trois tâches :

- Thi de haute priorité ;

- Tmed de priorité moyenne ;

- Tlow de faible priorité.

Ainsi qu'une ressource partagée protégée par le sémaphore S.

Le but d'un scheduler préemptif est que la tâche la plus prioritaire soit celle qui s'exécute.

Imaginons le cas suivant :

Tlow s'exécute, prend S et accède à la ressource partagée.

Thi arrive parmi la liste des tâches éligibles et logiquement s'exécute. Cette tâche veut également accéder à la ressource, mais elle se retrouve bloquée par le sémaphore S.

Maintenant, c'est au tour de Tmed d'entrer dans la liste des tâches éligibles : Thi est en attente de S, Tlow est moins prioritaire, c'est donc Tmed qui s'exécute.

Illustration : Inversion de priorité

On se trouve alors dans un cas d'inversion de priorité car ce n'est pas la tâche Tmed qui devrait s'exécuter mais Thi.

Substituons ce cas d'école par un système où Tmed est remplacé par n tâches de priorités comprises entre celles de Tlow et Thi, il devient alors très lourd de calculer le moment où Thi s'exécutera…

L'idéal serait que Tmed laisse Tlow s'exécuter afin de libérer S le plus rapidement possible.

Cet idéal existe et est implémenté sous le nom d'« héritage de priorité ». Concrètement, lorsque Thi va demander le sémaphore possédé par Tlow, l'héritage de priorité va aligner la priorité de Tlow sur celle de Thi, le rendant ainsi ininterruptible par des tâches de moindre priorité. Lorsque Tlow relâchera le sémaphore, Thi s'exécutera alors immédiatement.

Xenomai utilise également le principe d'héritage de priorité.

IV - Linux et le(s) temps réel

Xenomai est une des réponses à la question « Comment faire du temps réel dur avec Linux ? ».

IV-A - Les avantages et les inconvénients de Linux pour les applications temps réel

Comme il est assez hasardeux de généraliser, on considère ici un besoin de type contrôle/commande RT à travers des interfaces série (RS422/485 et consorts), c'est-à-dire quelque chose qui puisse être mis en œuvre grâce à du matériel standard ; financièrement l'utilisation de Xenomai est très intéressante.

Pour la partie logicielle, le calcul est simple puisqu'avec une distribution Linux gratuite et Xenomai, on arrive à un total de… 0 €. Quant au matériel, son coût tourne autour de quelques milliers d'euros au maximum.

D'autres solutions propriétaires(17) auraient ici plusieurs inconvénients (en plus du côté « propriétaire ») : le prix de la partie matérielle serait beaucoup plus élevé et le développement nécessiterait l'acquisition de licences.

Quelle que soit la technologie utilisée, il ne faut jamais négliger les coûts et les durées de formation ou de prise en main.
Pour Linux et Xenomai, l'autoformation reste cependant la pratique la plus répandue.
Pour la résolution de problèmes complexes, une expertise externe peut apporter un gain de temps intéressant.

Néanmoins, il faut garder à l'esprit que dans les applications industrielles, l'intégration matériel/logiciel est une étape critique et coûteuse. Les fournisseurs de solutions propriétaires ont déjà fait ce travail pour vous (mise au point de drivers, certification de matériel, intégration à l'environnement de développement).

Si vous employez du matériel spécifique, le support de la Communauté sera limité par la difficulté à reproduire vos problèmes (mise au point de drivers en particulier).

IV-B - Comment faire du temps réel avec Linux : les approches possibles

IV-B-1 - Aucune préemption forcée

Comme nous l'avons vu, par défaut, le code exécuté en mode noyau n'est pas préemptible. Dans cette configuration, les changements de contexte sont réduits : le CPU est disponible pour les calculs et le cache est efficace.

La latence généralement constatée (non garantie) est de l'ordre de la dizaine de millisecondes(18).

L'activation de la préemption à l'intérieur du noyau dégrade les performances globales du système ( overhead).

IV-B-2 - Une première possibilité : l'option Voluntary Kernel Preemption

Cette option insère des points de descheduling supplémentaires. Concrètement, les appels système relancent volontairement le scheduler dans leur déroulement.

Ces appels modifient peu le code car ils redéfinissent simplement la fonction de débogage might_sleep() déjà existante et la font appeler le scheduler.

Dans cet exemple, extrait du fichier kernel/cpu.c 2.6.25 :

void get_online_cpus(void)
{
might_sleep();
...
}

IV-B-3 - Pour du temps réel « mou » : Preemptible Kernel (Low-Latency Desktop)

La préemption volontaire n'est qu'un artifice comparé à cette option : ici tout le noyau (hors sections critiques) devient préemptible.

Dans le code, il est alors nécessaire d'identifier les sections critiques afin de désactiver la préemption à l'intérieur de celles-ci. Travail volumineux me direz-vous, mais pas titanesque. En effet, la redéfinition des mécanismes ajoutés par nos amis du SMP couvre une partie de nos besoins.

L'exemple ci-dessous (2.6.25.4/fs/fat/inode.c) montre l'innocuité de cette option sur une section critique protégée par un spinlock :

void fat_attach(struct inode *inode, loff_t i_pos)
{
struct super_block *sb = inode->i_sb;
struct msdos_sb_info *sbi = MSDOS_SB(sb);

spin_lock(&sbi->inode_hash_lock);
...
spin_unlock(&sbi->inode_hash_lock);
}

Le pseudocode spin_lock :

int spin_lock(spinlock_t *lock)
{
preempt_disable();
...
}

Et dans preempt_disable :

#ifdef CONFIG_PREEMPT         // Correspond à l'option low-latency desktop
...
#define preempt_enable() {réactivation de la préemption et rescheduling si nécessaire}
#else
#define preempt_enable() {CONFIG_PREEMPT_NONE : on ne fait rien du tout}
#endif

Les versions standard 2.6 peuvent être une solution, rendant ainsi Linux immédiatement opérationnel. La latence maximale est alors de l'ordre de la milliseconde.

IV-B-4 - Le temps réel avec le patch PREEMPT_RT

Ce patch, élaboré par Ingo Molnar, ainsi que sa documentation sont disponibles à l'adresse http://rt.wiki.kernel.org/index.php/Main_Page.

Le principe est de rendre « totalement » préemptible le code du noyau, la latence est ainsi abaissée à moins de 50 µs.

Afin de bien comprendre quelles sont les modifications apportées, il faut revenir sur ce qui n'était pas encore préemptible : les sections critiques (au nombre de 11 000 !) et les handlers d'interruption. Grâce à ce patch, ces sections le sont désormais et celles protégées par des spinlock ou des sémaphores supportent l'héritage de priorité.

Les développements d'applications RT reposent sur l'API POSIX classique.

IV-B-4-a - Drivers

Les drivers du vanilla kernel étant affectés par le patch, cela signifie qu'ils peuvent être directement utilisés dans un contexte temps réel. L'emploi de drivers extérieurs au vanilla kernel (pilotage d'une carte spécifique), nécessitera probablement une adaptation.

IV-B-4-b - Limitations actuelles et évolutions

Le patch atteint presque les 60 000 lignes(19), ce qui modifie par conséquent sensiblement le code du noyau. À l'heure actuelle, il n'est disponible que pour les architectures x86 et ARM.

La feuille de route prévoit une intégration progressive du patch à la mainline du vanilla kernel.

IV-B-5 - Le temps réel dur avec les co-noyaux

Une autre approche consiste à se reposer sur un micronoyau pour satisfaire les contraintes temps réel. Ce micronoyau gère alors le scheduling des tâches temps réel, les timers, les interruptions et la communication entre processus. Linux reste utilisé pour les services non temps réel qu'il procure (connectivité réseau, USB…) et devient une simple tâche du micronoyau.

Xenomai repose sur ce principe.

V - Qu'est-ce que Xenomai ?

V-A - Origines

L'histoire de Xenomai est un peu tumultueuse. L'explication de M. Franke établit une synthèse et un « arbre généalogique » très clairs auquel le lecteur pourra se reporter.

V-B - Xenomai, comment ça marche ?

Xenomai est un système d'exploitation temps réel qui a Linux pour tâche de fond. Linux est alors préempté comme une simple tâche. Le discours sur la préemption impossible des sections critiques, les handlers... est caduc avec Xenomai.

Les tâches gérées par Xenomai apportent ainsi une garantie d'exécution temps réel dur.

Qui dit deux systèmes d'exploitation, dit deux ordonnanceurs… et (au moins) deux problèmes : comment partager le matériel, et comment faire interagir les tâches Linux et Xenomai entre-elles.

Les explications qui suivent sont une vulgarisation de haut niveau, on pourra se reporter à l'article technique Life with ADEOS de Philippe Gerum pour plus de détails.

Si vous être pressés, lisez ce paragraphe ! Il est essentiel.

V-B-1 - Le routage des interruptions avec ADEOS

Comment alors faire cohabiter ces deux OS sur le même matériel ? Pour l'accès au CPU et à la mémoire, il n'y a pas de problème car Linux est une tâche. Mais si une interruption (timer, carte…) arrive, qui va la traiter ?

La solution consiste à intercaler une couche entre le matériel et les systèmes d'exploitation. Cette couche de virtualisation (ou hyperviseur, ou encore couche d'abstraction matérielle) s'appelle ADEOS.

ADEOS est un nanokernel(20) qui va capturer les évènements pour ensuite les dispatcher vers Xenomai ou Linux, mais pas dans n'importe quel ordre ! Le but étant tout de même d'avoir un OS temps réel, il est impératif que Xenomai soit le premier à les traiter. Si elles ne sont pas pour lui, elles sont alors « passées » à Linux.

Le lecteur attentif que vous êtes aura remarqué le changement de terminologie entre le début et la fin du paragraphe : on est passé d'interruptions à évènements. Cette généralisation sera expliquée par la suite.

Très concrètement, ADEOS(21) se présente sous la forme d'un patch du noyau Linux. Son arborescence de développement est distincte de Xenomai, ses évolutions sont assurées par Philippe Gerum.

V-B-2 - Coexistence Xenomai/Linux et partage des ressources : le co-scheduling

En théorie, la virtualisation peut faire cohabiter en toute indépendance plusieurs OS sur le même matériel. Or, que veut-on ? Réponse : faire du temps réel avec Linux afin de profiter de tout l'environnement non temps réel. Intuitivement, on sent bien qu'il faut faire un lien entre les deux. Oui, mais comment ? Par les ressources utilisées !

Pour l'instant, chacun est tranquillement chez soi : les tâches Xenomai dans leur scheduler, idem pour les tâches Linux. Imaginons, un instant qu'une impétueuse tâche Xenomai cherche à obtenir la date. La ressource « date » appartient au noyau Linux et on ne peut pas aller la chercher directement depuis Xenomai, il se peut en effet qu'elle soit en cours de mise à jour et donc invalide ! Il faut par conséquent passer par un appel système Linux. Un peu idiot me direz-vous, puisqu'on va retomber sur le problème de performance de Linux pour du temps réel. Remettons la polémique à plus tard et attardons-nous plutôt sur le fonctionnel.

Comment traiter les appels système Linux depuis les tâches Xenomai ? Les appels système regroupent les accès à des ressources partagées (terminal, couche réseau…) dont la concurrence d'accès est gérée par Linux. Or, Linux ne connaît pas les tâches Xenomai !!!

La solution consiste à déplacer les tâches d'un scheduler à l'autre en fonction des ressources utilisées. Une tâche créée via l'API Xenomai pourra s'exécuter sous Linux, ce mécanisme totalement transparent est appelé shadow threading.

Une tâche schedulée par Xenomai est dite en primary mode.
Une tâche Xenomai schedulée par Linux est dite en secondary mode (22).
Une tâche Linux schedulée par Xenomai est dite... ha !ha ! c'était un piège, ça n'existe pas.
Le passage d'un mode à l'autre est fonction des ressources accédées et donc des appels système Xenomai ou Linux.

Les termes primary mode et secondary mode sont hérités de la terminologie ADEOS qui appelle les OS reposant sur lui des domaines (domains). Comme nous l'avons vu un peu plus haut, ces domaines ont une priorité dans la réception des évènements. Dans notre cas, Xenomai les réceptionne en premier (primary domain) et Linux en second (secondary domain).

Une fois que la tâche Xenomai a fait son appel système Linux et qu'elle se trouve en secondary mode, elle y reste tant qu'elle ne fait pas d'appel système Xenomai.

Illustration : Migration interdomaines d'une tâche Xenomai

Linux devient une tâche de Xenomai. Mais quelle priorité lui affecter ? A priori, la plus faible possible (tâche idle pour Xenomai).

Comment alors considérer la priorité des tâches Xenomai passées dans le scheduler Linux ? Faible comme Linux ou bien constante ?

Dans Xenomai, la deuxième implémentation a été retenue (23).

La priorité d'une tâche Xenomai est constante, quel que soit le scheduler dans lequel elle se trouve.

Cette implémentation n'est possible qu'à une condition : que la priorité de la tâche Linux dans l'ordonnanceur Xenomai évolue dynamiquement, sinon on se retrouverait dans un cas d'inversion de priorité.

La priorité de la tâche Linux dans l'ordonnanceur Xenomai est déterminée de la manière suivante :

- 0 s'il n'y a aucune tâche en secondary mode (idle Xenomai) ;

- la priorité maximale des tâches en secondary mode.

Les schémas du paragraphe suivant permettent de mieux visualiser cette politique d'ordonnancement.

V-B-3 - La gestion des priorités dans les modes primary et secondary

Nous voici face à un nouveau problème : une fois dans l'ordonnanceur Linux, la tâche Xenomai est maintenant une tâche comme les autres et on revient au point de départ, à savoir, comment faire du temps réel avec Linux ?

Or, qu'avions-nous écrit plus haut ? Que les dérives temporelles étaient introduites par :

- les interruptions (handlersLinux) : nous avons maintenant ADEOS pour les masquer (Interrupt Shield) ;

- la préemption par des tâches de plus forte priorité : il suffit de s'exécuter dans la classe SCHED_FIFO(24) ;

- l'absence de préemption à l'intérieur du noyau : les options natives (« low latency desktop ») la résolvent partiellement.

Une tâche en secondary mode est du temps réel mou et son temps d'exécution est borné (Interrupt Shield).
Pour les tâches à contrainte temps réel dur, il faut absolument rester en primary mode (25).
Les tâches Xenomai ont 99 niveaux de priorité possibles, exactement comme la classe SCHED_FIFO/RR. Ceci est normal puisque les priorités ne changent pas d'un scheduler à l'autre, la correspondance est donc immédiate (26).

Les exemples ci-dessous détaillent les différents cas de figure.

Soient Ln les tâches schedulées par Linux et Xn celles par Xenomai. Chaque tâche a une priorité, notée PrioLx ou PrioX, selon l'ordonnanceur de départ. Cette distinction est de pure forme car toutes les priorités évoluent dans le même espace.

Les priorités Xenomai sont constantes à travers les schedulers :

Le noyau Linux est une tâche de fond dont la priorité évolue dynamiquement en fonction de la tâche Xenomai de plus haute priorité éligible en mode secondaire.

Comme vous pouvez le constater (et peut-être vous en étonner), il y a deux tâches élues dans le schéma précédent. On est pourtant bien sur un exemple simple, monoprocesseur. En fait, chaque tâche élue est considérée en fonction de son ordonnanceur et non pas du processeur.

Parmi un ensemble de tâches Xenomai éligibles, c'est toujours celle de plus haute priorité qui sera exécutée, quel que soit son mode !!!

Ci-dessus, X1 est la tâche courante bien qu'il existe d'autres tâches Xenomai en primary mode.

Les tâches Xenomai en mode secondaire sont en concurrence avec les autres tâches Linux de la classe SCHED_FIFO/RR.

Dans l'exemple suivant, le système possède une tâche L1 sans rapport avec Xenomai. Cette tâche a une priorité de 80.

L'exécution de Xenomai a basculé la tâche élue X1 en secondary mode, la concurrence d'accès au CPU va donc se jouer dans l'ordonnanceur Linux. Or, L1 va préempter tout le monde !

Si vous êtes dubitatif sur ce changement de priorité dynamique de la tâche Linux, voici un contre-exemple qui devrait vous convaincre. Si la tâche Linux gardait sa priorité -1, on serait alors dans un cas d'inversion de priorité :

Dans le scheduler Xenomai, la priorité de la tâche Linux est égale :

- à celle de la tâche de plus haute priorité en secondary mode;

- à -1 s'il n'y en a pas.

Bizarre ? Disons logique plutôt. On pourrait en fait s'attendre à ce que X2 prenne la main. Non, non, non et non ! Puisque nous serions alors dans un cas d'inversion de priorité.

Attention : une tâche Linux peut préempter une tâche Xenomai.

Il faut être attentif à deux choses pour les tâches en secondary mode afin d'éviter des comportements non souhaités :
- leurs priorités doivent être inférieures à celles en primary mode  ;
- leurs priorités doivent être supérieures aux tâches Linux s'exécutant dans la classe SCHED_FIFO (dépend de la distribution).

V-B-4 - ADEOS ou comment modifier Linux proprement

Les fameux « évènements » se précisent. Peuvent provoquer un rescheduling :

- les interruptions matérielles : timer pour les tâches périodiques, équipement prêt...

- les signaux logiciels : fin d'une tâche suite à une faute...

- les créations/destructions de tâche : fork, exit...

- les changements de caractéristiques d'une tâche : priorité, policy ;

- les deschedulings volontaires.

L'idée est d'avoir un comportement uniforme quels que soient le type d'évènements et le domaine d'origine. On réapplique ce que nous avions vu pour les interruptions matérielles : tous les évènements sont envoyés dans un tuyau et les domaines (OS) viennent y puiser les informations qui les intéressent.

Le patch ADEOS est « respectueux » du code initial du noyau Linux car il ne le modifie que très peu.

Petite parenthèse : lors du démarrage de la machine, le domaine Linux est créé avant le domaine Xenomai. Cette primauté lui confère le nom de root domain, terme que l'on va ensuite retrouver dans la liste des tâches Xenomai.

V-B-5 - Interrupt shield : sus aux interruptions !

(27)Devinette : nous sommes en secondary mode, nous maîtrisons les priorités des autres processus Linux, quelle est la seule chose qui peut augmenter notre temps d'exécution ?

Tic, tac...

Bonne réponse : les handlers d'interruption Linux (top-half) et les tasklets hors de contrôle du scheduler des tâches (bottom-half).

Afin de s'en prémunir, un mécanisme appelé Interrupt shield protège les tâches en secondary mode en filtrant les interruptions. Le temps d'exécution de la tâche est alors borné.

Mais alors pourquoi l'IS n'est-il pas activé par défaut lors de l'installation ? Simplement, car il y a un risque d'inversion de priorité.

Prenons l'exemple suivant, soit deux tâches T1 et T2 en secondary mode. T2 active l'InterruptShield. T1 a la main (normal car elle est prioritaire) puis se place en attente de données. Elle est alors deschedulée, T2 entre alors en jeu. Une interruption arrive. T1 est censée s'exécuter, or elle est schedulée par Linux, et celui-ci n'a pas encore été informé que l'interruption est arrivée (IS). T2 termine alors son traitement, l'interruption est relâchée, T1 est débloquée.

La solution serait alors que T2 soit protégée des interruptions Linux tout en laissant T1 s'exécuter si des données arrivent.

Illustration : Interrupt Shield et driver RTDM

Ceci revient à dire qu'il faut que l'interruption pour T1 ne soit pas traitée par Linux mais directement par Xenomai. Si ce handler est géré par Xenomai, le read doit l'être également.

Tout ceci nous amène naturellement à définir des drivers spécifiques à Xenomai (RTDM: Real Time Model Device), un exemple de driver est présenté au chapitre XII.

C'est également depuis l'API RTDM que des tâches Xenomai en mode noyau peuvent être lancées (primitive rtdm_task_init()).

V-C - Les architectures supportées

Xenomai fonctionne sur les architectures suivantes :

- ARM ;

- Blackfin (DSP d'Analog Devices) ;

- i386 ;

- ia64 (Intel Itanium) ;

- Power PC ;

- x86_64 (Intel : Pentium 4, Pentium D, Pentium Extreme Edition, Celeron D, Xeon, Pentium Dual-Core ;

- AMD : Athlon 64, Athlon 64 FX, Athlon 64 X2, Turion 64, Turion 64 X2, Opteron, Sempron).

Le portage de Xenomai sur une autre architecture revient dans les faits à porter ADEOS. ADEOS joue un rôle de couche d'abstraction matérielle.

V-D - Les modes d'exécution

Les tâches Xenomai « courantes »(28) s'exécutent en mode utilisateur. Le code Xenomai a ainsi l'énorme avantage de tourner sous debugger.

Attention à ne pas confondre primary mode avec kernel mode et secondary mode avec user mode :

- primary et secondary désignent les schedulers ;

- user et kernel désignent le mode d'exécution et donc les ressources accessibles.

V-E - L'API native et les skins

Xenomai fournit une API très complète avec les fonctionnalités temps réel suivantes :

- tâches avec 99 niveaux de priorité, round robin optionnel…

- files de messages ;

- allocation dynamique de mémoire spécifique RT ;

- sémaphores ;

- watchdogs ;

- timers ;

- mutexes ;

- …

Cette API, dite « native », est complétée par des skins. Ce sont des API d'autres RTOS (pSos, VxWorks…) qui encapsulent des appels natifs.

Les skins sont destinées au portage d'applications existantes ou à du prototypage.

Dans le cas des tâches VxWorks, il y a d'ailleurs un problème de translation vers le secondary mode. En effet, les priorités VxWorks vont de 1 (la plus forte) à 255 (la plus faible), ce qui est incompatible avec les 99 niveaux de la classe SCHED_FIFO/RR.

Deux adaptations sont donc réalisées : une inversion et surtout un nivellement. Deux tâches lancées via la skin VxWorks avec les priorités 150 et 220, auront en fait la même priorité en secondary mode.

Attention à la correspondance des priorités en utilisant la skin VxWorks.

V-F - Solutions concurrentes du marché

La principale solution du marché pour le temps réel dur est l'incontournable VxWorks de WindRiver qui présente l'avantage d'avoir une liste de Board Support Packages sur étagère impressionnante.

À noter également, le produit IntervalZero RTX qui permet de faire du temps réel dur tout en gardant la partie « utilisateur » de Windows, à la manière du couple Xenomai/Linux. Il semblerait que la diffusion de ce produit soit cependant assez confidentielle en France.

VI - Installation et configuration

VI-A - Configuration utilisée pour la rédaction de ce document

Sauf mention contraire, les exemples présentés dans ce document ont été développés et testés sur un PC portable Fujitsu-Siemens Esprimo D9500, Intel Core 2 Duo 2,2 GHz (T7500), 2 Go de RAM.

Les versions utilisées sont :

- Mandriva Free 2008.0 x86_64 ;

- vanilla kernel Linux 2.6.24 ;

- Xenomai 2.4.3 ;

- ADEOS 2.6.24-2.0-06.

VI-B - Récupération des sources

Quelle que soit la distribution utilisée, Xenomai se greffe toujours sur le vanilla kernel. En effet, les sources livrées avec les distributions étant modifiées, l'application du patch échouera.

Le code source « originel » du noyau se trouve sur www.kernel.org.

Quant aux sources de Xenomai, elles sont téléchargeables à l'adresse suivante : http://download.gna.org/xenomai/stable.

ADEOS est présent dans le package Xenomai. Cependant, il se peut que de nouvelles versions soient disponibles. C'est pourquoi vous devez vérifier à l'adresse http://download.gna.org/adeos/patches, si en fonction de la version du noyau Linux et de l'architecture, une version plus récente existe.

Attention à ne pas télécharger des versions de test. Leur nom contient rc (Release Candidate).

VI-C - Procédure d'installation, patch des sources du noyau

L'installation nécessite d'être root.

Les sources et une version plus récente d'ADEOS ont été récupérées et placées dans le répertoire /usr/src.

[root@localhost src]# pwd
/usr/src
[root@localhost src]# whoami
root
[root@localhost src]# ls
adeos-ipipe-2.6.24-x86-2.0-06.patch  linux-2.6.24.tar.bz2  xenomai-2.4.3.tar.bz2
[root@localhost src]# tar xjf linux-2.6.24.tar.bz2
[root@localhost src]# tar xjf xenomai-2.4.3.tar.bz2
[root@localhost src]# ls
adeos-ipipe-2.6.24-x86-2.0-06.patch  linux-2.6.24/  linux-2.6.24.tar.bz2  xenomai-2.4.3/  xenomai-2.4.3.tar.bz2
[root@localhost src]# xenomai-2.4.3/scripts/prepare-kernel.sh
Linux tree [default /lib/modules/2.6.25.4/source]: /usr/src/linux-2.6.24
Target architecture [default x86_64]:
Adeos patch [default /usr/src/xenomai-2.4.3/ksrc/arch/x86/patches/adeos-ipipe-2.6.24-x86-2.0-04.patch]: /usr/src/adeos-ipipe-2.6.24-x86-2.0-06.patch
patching file Makefile
patching file arch/x86/Kconfig
patching file arch/x86/Makefile_32
patching file arch/x86/boot/Makefile
patching file arch/x86/boot/compressed/Makefile_32
patching file arch/x86/kernel/Makefile_32
patching file arch/x86/kernel/Makefile_64
patching file arch/x86/kernel/apic_32.c
patching file arch/x86/kernel/apic_64.c
patching file arch/x86/kernel/cpu/mtrr/cyrix.c
patching file arch/x86/kernel/cpu/mtrr/generic.c
patching file arch/x86/kernel/entry_32.S
patching file arch/x86/kernel/entry_64.S
patching file arch/x86/kernel/genapic_flat_64.c
patching file arch/x86/kernel/head64.c
patching file arch/x86/kernel/i8253.c
patching file arch/x86/kernel/i8259_32.c
patching file arch/x86/kernel/i8259_64.c
patching file arch/x86/kernel/io_apic_32.c
patching file arch/x86/kernel/io_apic_64.c
patching file arch/x86/kernel/ipipe.c
patching file arch/x86/kernel/mcount_32.S
patching file arch/x86/kernel/mcount_64.S
patching file arch/x86/kernel/nmi_32.c
patching file arch/x86/kernel/nmi_64.c
patching file arch/x86/kernel/process_32.c
patching file arch/x86/kernel/process_64.c
patching file arch/x86/kernel/setup64.c
patching file arch/x86/kernel/smp_32.c
patching file arch/x86/kernel/smp_64.c
patching file arch/x86/kernel/smpboot_32.c
patching file arch/x86/kernel/smpboot_64.c
patching file arch/x86/kernel/time_32.c
patching file arch/x86/kernel/traps_32.c
patching file arch/x86/kernel/traps_64.c
patching file arch/x86/kernel/vm86_32.c
patching file arch/x86/kernel/vsyscall_64.c
patching file arch/x86/lib/mmx_32.c
patching file arch/x86/lib/thunk_64.S
patching file arch/x86/mach-visws/visws_apic.c
patching file arch/x86/mm/fault_32.c
patching file arch/x86/mm/fault_64.c
patching file drivers/pci/htirq.c
patching file drivers/serial/8250.c
patching file include/asm-x86/apic_32.h
patching file include/asm-x86/apic_64.h
patching file include/asm-x86/hw_irq_32.h
patching file include/asm-x86/hw_irq_64.h
patching file include/asm-x86/i8259.h
patching file include/asm-x86/io_apic_64.h
patching file include/asm-x86/ipi.h
patching file include/asm-x86/ipipe.h
patching file include/asm-x86/ipipe_32.h
patching file include/asm-x86/ipipe_64.h
patching file include/asm-x86/ipipe_base.h
patching file include/asm-x86/ipipe_base_32.h
patching file include/asm-x86/ipipe_base_64.h
patching file include/asm-x86/irqflags_32.h
patching file include/asm-x86/irqflags_64.h
patching file include/asm-x86/mmu_context_32.h
patching file include/asm-x86/mmu_context_64.h
patching file include/asm-x86/nmi_32.h
patching file include/asm-x86/nmi_64.h
patching file include/asm-x86/processor_64.h
patching file include/asm-x86/spinlock_32.h
patching file include/asm-x86/system_64.h
patching file include/asm-x86/unistd_64.h
patching file include/asm-x86/vsyscall.h
patching file include/linux/hardirq.h
patching file include/linux/ipipe.h
patching file include/linux/ipipe_base.h
patching file include/linux/ipipe_compat.h
patching file include/linux/ipipe_percpu.h
patching file include/linux/ipipe_tickdev.h
patching file include/linux/ipipe_trace.h
patching file include/linux/irq.h
patching file include/linux/kernel.h
patching file include/linux/linkage.h
patching file include/linux/mm.h
patching file include/linux/preempt.h
patching file include/linux/sched.h
patching file include/linux/spinlock.h
patching file include/linux/spinlock_types.h
patching file init/Kconfig
patching file init/main.c
patching file kernel/Makefile
patching file kernel/exit.c
patching file kernel/fork.c
patching file kernel/ipipe/Kconfig
patching file kernel/ipipe/Kconfig.debug
patching file kernel/ipipe/Makefile
patching file kernel/ipipe/core.c
patching file kernel/ipipe/tracer.c
patching file kernel/irq/chip.c
patching file kernel/lockdep.c
patching file kernel/power/disk.c
patching file kernel/power/swsusp.c
patching file kernel/printk.c
patching file kernel/sched.c
patching file kernel/signal.c
patching file kernel/spinlock.c
patching file kernel/time/tick-common.c
patching file lib/Kconfig.debug
patching file lib/bust_spinlocks.c
patching file lib/ioremap.c
patching file lib/smp_processor_id.c
patching file lib/spinlock_debug.c
patching file mm/memory.c
patching file mm/mlock.c
patching file mm/vmalloc.c
[root@localhost src]#

Il ne doit pas y avoir de message « Hunk... ».

On peut maintenant passer à la configuration proprement dite du noyau...

VI-D - Configuration de Xenomai dans le noyau Linux

VI-D-1 - Lancement de l'utilitaire de configuration du noyau

La configuration de Xenomai est réalisée à travers la même interface que celle du noyau Linux.

Tapez les commandes suivantes

[root@localhost src]# cd linux-2.6.24
[root@localhost linux-2.6.24]#  make menuconfig

Optimisez le noyau afin qu'il soit le plus proche possible de la configuration matérielle (29). Supprimez tout ce qui est en trop (SMP sur uniprocesseur) et traquez les parties non optimisées (choix du type de CPU...).
Vous risquez de faire de nombreuses compilations avant de trouver la bonne combinaison d'options. Supprimez les options et les drivers inutiles afin de gagner du temps !

VI-D-2 - Le choix des options

Xenomai se trouve dans le menu Real-time sub-system.

Ce paragraphe détaille les différentes options de ce menu(30).

Xenomai

Nucleus

Nucleus est le nom du noyau interne Xenomai. Ce « Nucleus » n'a pas de rapport avec celui de Mentor Graphics.

Pervasive real-time support in user-space

Active le support temps réel depuis le mode user (processus Linux).
Activé par défaut, sinon les fonctionnalités temps réel ne sont disponibles que depuis le noyau Linux.

Interrupt shield support

Active l'Interrupt Shield comme décrit § V.B.5.
Désactivé par défaut car diminue les performances, à activer uniquement si nécessaire.

Priority coupling support

Politique de co-scheduling décrite § V.B.2, la priorité des tâches prend le pas sur le mode. En cas de désactivation, les tâches en primarymode seront alors toujours prioritaires par rapport à celles en secondary.
Activé par défaut. Configurable par la suite tâche par tâche.

Optimize as pipeline head

Force Xenomai comme le premier à traiter les interruptions.
Activé par défaut (est prévu pour le cas - très théorique - où il y aurait d'autres noyaux en plus de Xenomai et Linux).

Number of pipe devices

Nombres de pipes utilisables pour la communication avec Linux.
À modifier si le nombre de devices /dev/rtpn n'est pas suffisant.

Number of registry slots

Nombre total d'objets temps réel instantiables à un instant. Par objet temps réel on désigne les tâches, les sémaphores...

Size of the system heap (Kb)

Zone de mémoire allouée pour le noyau Nucleus. Si certains appels système Xenomai renvoient ENOMEM, cette valeur devra être augmentée.

Size of the private stack pool (Kb)

Pile allouée aux tâches en mode noyau (pour driver RTDM et tâche Xenomai implémentée dans un module noyau).

Statistics collection

Collecte de statistiques dans le fichier.

Debug support

Nucleus Debugging support

Active le débogage Nucleus (ajoute des traces printk) : trace les spinlocks...
Désactivé par défaut, utilisé pour le développement de Xenomai.

Queue Debugging support

Active le débogage des files de messages internes et utilisées via les skins (ajoute des traces printk et des contrôles).
Désactivé par défaut.

Registry Debugging support

Fonctionnalité sans effet dans la version 2.4.3.

Timer Debugging support

Fonctionnalité sans effet dans la version 2.4.3.

Watchdog support

Détecte si une tâche Xenomai garde le processeur plus de [Watchdog timeout] secondes sans rescheduler. Si c'est le cas, la tâche est suspendue. Utilisée lors de la mise au point d'applications, cette option évite de redémarrer la machine si une tâche ne rend pas la main.

En cas de déclenchement, un message du type :
Xenomai: watchdog triggered -- killing runaway thread '...' sera accessible par dmesg.

Watchdog timeout

Valeur de ce timeout comprise entre 1 et 60 secondes.

Shared interrupts (31)

Le partage d'interruptions est à activer si Xenomai doit prendre en charge des périphériques partageant la même interruption (exemple : carte à quatre ports série). Ce type de configuration est cependant à éviter car il introduit des latences.

Timing

Enable periodic timing

Le temps peut être manipulé à travers deux « unités » lors de l'utilisation de l'API Xenomai  :

- soit en ticks si cette option est activée, comme sous VxWorks par exemple. La durée d'un tick est alors fixée lors de la compilation du noyau et éventuellement modifiable lors de l'exécution (rt_timer_set_mode dans l'API native par exemple) ;

- soit en nanosecondes.

Scheduling latency (ns)

Délai de retour à l'exécution d'une tâche après une interruption (timer notamment). Si ce paramètre vaut 0 (défaut), une valeur par défaut sera considérée.

Ce délai est utilisé en interne pour corriger la date d'échéance des timers.

Scalability

O(1) scheduler

Active le scheduler en O(1), c'est-à-dire que le temps mis pour choisir une tâche parmi les éligibles est constant, et ce quel que soit leur nombre. Cette option présente un intérêt si ce nombre est supérieur à 10.
L'implémentation de ce scheduler est réalisée par une liste chaînée par niveau de priorité.

Timer indexing method

Le nombre de timers à gérer influe sur les performances du scheduler Xenomai, c'est pourquoi plusieurs algorithmes sont proposés.
Détermine l'algorithme utilisé pour démarrer, arrêter, échoir les timers apériodiques (les périodiques sont toujours indexés selon la méthode linéaire). Les choix ci-dessous sont les structures de données utilisées par ces algorithmes. Si l'option « Enable periodic timing » est activée, ce sera forcément la structure « Timer wheel » qui sera utilisée.

Linear

Complexité en O(N), le temps dépend du nombre de timers utilisés, recommandé jusqu'à 10 timers. Choix par défaut.

Tree

Complexité en O(log N) reposant sur un arbre binaire.

Binary heap capacity

L'arbre binaire étant alloué de manière statique, ce paramètre fixe le nombre maximum de timers simultanés.

Hash

Complexité en O(1), c'est-à-dire indépendante du nombre de timers. L'algorithme utilisé est décrit dans le paragraphe XXI.B.

Timer wheel step

Le Timer wheel step est la durée d'un slot (entrée dans la structure de donnée circulaire contenant les timers non échus). Le nombre de slots est fixé à 64 et il n'est pas modifiable.

Machine

Enable FPU support

Réservé aux processeurs sans FPU, ce qui n'est pas le cas sur les PC. Il n'y a priori aucune raison de la désactiver.

NMI watchdog

Le NMI est utilisé dans Linux pour le débogage du noyau lorsque le système est gelé.

Enable NMI watchdog

Sert au débogage du noyau Nucleus.
Active un watchdog hardware via une interruption non masquable (NMI). Le watchdog est armé à chaque programmation du timer. Si le timer n'a pas expiré après [NMI watchdog latency threshold]ns, le watchdog claque en collectant des informations de débogage.

NMI watchdog latency threshold

Tolérance du watchdog à l'égard du timer.

SMI workaround

Le SMI est un système de gestion d'interruptions utilisé par certains composants (USB...). Les interruptions générées ne sont pas masquables et introduisent des temps de latence parasites pour Xenomai.

Disable SMI detection

Pour les systèmes non équipés du SMI.

Enable SMI workaround

Détecte les composants équipés du SMI et en désactive la partie SMI.

Globally disable SMI

Inhibe le SMI de tous les périphériques lorsque Xenomai a la main. Les options ci-dessous permettent une granularité plus fine.

Enable Intel-Specific USB2 SMI

Enable legacy USB2 SMI

Enable periodic SMI

Enable TCO SMI

Enable microcontroller SMI

Enable APM SMI

Enable legacy USB SMI

Enable ACPI BIOS SMI

Number of pre-allocated kernel stacks

Spécificité Intel Itanium 64.

Interfaces

Gestion des skins. Les options les plus triviales ne sont pas décrites.
La customisation très fine des skins et des fonctionnalités utilisées à l'intérieur de celles-ci permet de réduire l'empreinte mémoire.

Native API

Base period (us)

Dans le cas où l'option « Enable periodic timing » est activée, on configure ici l'équivalence 1 tick = x microsecondes.

Message pipes

Active le support des pipes, le nombre de pipes est configuré dans Number of pipe devices. Nécessaire pour l'appel des fonctions rt_pipe_* et donc la communication avec les tâches Linux « classiques ».

Bytes in buffer space

Taille du buffer par pipe.

Registry support

Counting semaphores

Nécessaire pour l'appel des fonctions rt_sem_*.

Events flags

Nécessaire pour l'appel des fonctions rt_event_*.

Mutexes

Nécessaire pour l'appel des fonctions rt_mutex_*.

Condition variables

Nécessaire pour l'appel des fonctions rt_cond_*.

Messages queues

Nécessaire pour l'appel des fonctions rt_queue_*.

Memory heaps

Active l'allocation dynamique de mémoire depuis les tâches Xenomai. Nécessaire pour l'appel des fonctions rt_heap_*.

Alarms

Active les watchdogs. Nécessaire pour l'appel des fonctions rt_alarm_*.

Message passing support

Active l'échange de messages entre tâches. Nécessaire pour l'appel des fonctions rt_task_send/receive/reply.

Interrupts

Nécessaire pour l'appel des fonctions rt_intr_*.

Debugging support

Dans les versions antérieures de Xenomai, les objets « orphelins » (files de messages...) persistaient après la fin de l'application. Ils devaient alors être explicitement détruits (de même qu'au début de l'application supportant ainsi les cas de terminaisons brutales). Dorénavant, les destructions d'objets sont automatiques, cette option produit un message lors de celles-ci.

POSIX API

Base period (us)

Shared Memory

Interrupts

Select syscall

Debugging support

pSOS+ emulator

uITRON API

VRTX emulator

RTAI emulator

Fifos

Semaphores

Shared Memory

Real-Time Driver Model

Active le support RTDM, à utiliser pour le support des cartes série, CAN et autres.

Base period (us)

Maximum number of RTDM file descriptors

Nombre maximum de descripteurs ouverts à un instant donné. La valeur par défaut apparaît largement suffisante pour la plupart des applications.

Select support for RTDM file descriptors

Support de la fonctionnalité équivalente au select.

RTDM debugging support

Drivers

Active les drivers fournis par défaut. Ils peuvent correspondre à des interfaces physiques ou virtuelles.

Serial drivers

16550A UART driver

Ce driver supporte les chipsets 8250 et 16550x.

Hardware access mode

Port-based I/O

Option correspondant à la plupart des configurations PC (où il y a un espace d'adressage distinct pour les périphériques).

Memory-mapped I/O

Pour les configurations autres que PC.

Any access mode

Les deux modes d'accès sont supportés.

Testing drivers

Drivers virtuels établissant des benchmarks.

Timer benchmarck driver

Kernel-only latency measurement module

IRQ benchmark driver

Context switch unit testing driver

CAN drivers

RT-Socket-CAN, CAN raw socket interface

Le noyau Nucleus et les drivers peuvent être soit compilés avec le noyau Linux, soit en tant que modules.

Les modules permettent de mettre au point les options sans redémarrer et ainsi de disposer d'un système plus flexible. Dans la suite du document la configuration utilisée repose sur Xenomai intégré au noyau.

VI-D-3 - Amélioration de la latence de Linux (pour le secondary mode)

Comme nous l'avons vu, l'option « Low-Latency Desktop » améliore la latence de Linux, ce qui est important pour les tâches Xenomai en secondary mode.

Pour cela, il faut sélectionner :

Processor type and features/Preemption Model/Low-Latency Desktop.

VI-D-4 - Fonctionnalités ou options Linux potentiellement problématiques ou incompatibles

Ce paragraphe rappelle(32) les options Linux à désactiver, pour quelle raison, et où les désactiver dans le noyau Linux. Le positionnement d'options incompatibles provoque des warnings lors de la configuration du noyau.

ACPI Support (processor) : [incompatible] gestion de l'énergie, la fréquence du processeur peut être diminuée ce qui introduit des latences lors du retour à la vitesse nominale.

Power management options/Power Management support/ACPI Support/Processor.

CPU frequency scaling :[incompatible] driver modulant la fréquence du processeur en fonction de sa charge. L'horloge TSC (compteur de cycles processeur utilisé pour les timers) peut être instable et le processeur peut mettre du temps à retrouver sa vitesse nominale.

Se trouve dans :

Power management options/CPU Frequency scaling.

PC speaker : [incompatible] risque de conflit avec le timer Xenomai pour les architectures utilisant un timer 8254

Device Drivers/Input device support/Miscellaneous devices/PC Speaker support.

Tout ce qui est graphique repose sur des timers, il peut être souhaitable de désactiver le serveur X.

VI-E - Compilation et installation du nouveau noyau

Tapez les commandes :

[root@localhost linux-2.6.24]#  make && make modules_install install

Le nouveau noyau s'appelle 2.6.24 par défaut, on peut lui donner un nom plus parlant (Linux_Xenomai...).

Pour accélérer la compilation : l'option -j<2 *nb CPU> de GNU Make parallélise les commandes.
Exemple : make -j4 pour un Intel Dual Core.

VI-F - Installation des bibliothèques Xenomai

Et enfin pour terminer, on installe les bibliothèques et les fichiers d'en-tête avant de rebooter la machine sur le nouveau noyau :

[root@localhost linux-2.6.24]# cd ../xenomai-2.4.3
[root@localhost xenomai-2.4.3]# ./configure && make && make install

VI-G - Démarrage sur le noyau incluant Xenomai

On jette un œil au log de démarrage pour s'assurer que tout va bien :

[dchabal@localhost ~]$ dmesg
I-pipe 2.0-06: pipeline enabled.
...
I-pipe: Domain Xenomai registered.
Xenomai: hal/x86_64 started.
Xenomai: real-time nucleus v2.4.3 (Back to Shalla-Bal) loaded.
Xenomai: SMI-enabled chipset found
Xenomai: SMI workaround enabled
Xenomai: starting native API services.
Xenomai: starting POSIX services.
Xenomai: starting RTDM services.
...

VI-H - Xenomai et le noyau Linux sont-ils correctement installés ?

Nous avons vu que certaines options étaient systématiques dans la dégradation des performances. Mais comme les configurations sont très différentes (version de noyau Linux ou Xenomai, matériel), à cette étape on ne peut pas encore affirmer qu'il ne reste pas des latency killers.

Cette étape est impérative car elle valide l'intégration de Linux et Xenomai avec le hardware.
En cas de problème, la mise au point peut être longue.
Les architectures de type PC sont délicates à évaluer car d'une machine à l'autre (même sous une dénomination commerciale similaire), il n'y a aucune garantie que les composants et les configurations (BIOS) soient identiques.

VI-H-1 - Première mesure de la latence

En tant que root, exécutez /usr/xenomai/bin/latency.

Ce programme mesure par défaut la latence d'une tâche périodique, c'est-à-dire l'écart entre le moment où elle devrait être active et celui où elle l'est réellement.

Ce test est exécuté dans des conditions longues (plusieurs minutes voire heures) et de stress, en parallèle nous lançons la commande :

[dchabal@localhost ~]$ dd if=/dev/zero of=/dev/null

Lors de la première exécution, nous avons au bout de quelques secondes :

[root@localhost bin]# /usr/xenomai/bin/latency
== Sampling period: 100 us
== Test mode: periodic user-mode task
== All results in microseconds
warming up...
RTT|  00:00:01  (periodic user-mode task, 100 us period, priority 99)
RTH|-----lat min|-----lat avg|-----lat max|-overrun|----lat best|---lat worst
RTD|       0.235|       0.565|       1.347|       0|       0.235|       1.347
RTD|       0.226|       0.568|       3.734|       0|       0.226|       3.734
RTD|       0.258|       0.565|       1.463|       0|       0.226|       3.734
RTD|       0.219|       0.566|       1.357|       0|       0.219|       3.734
RTD|       0.200|       0.567|       1.400|       0|       0.200|       3.734
RTD|       0.282|       0.567|       2.192|       0|       0.200|       3.734
RTD|       0.218|       0.565|       2.295|       0|       0.200|       3.734
RTD|       0.235|       0.567|       2.710|       0|       0.200|       3.734
RTD|       0.221|       0.568|       3.701|       0|       0.200|       3.734
RTD|       0.222|       0.632|       2.052|       1|       0.200|     198.581
RTD|       0.201|       0.625|       2.078|       1|       0.200|     198.581
RTD|       0.223|       0.627|       3.737|       1|       0.200|     198.581
RTD|       0.179|       0.627|       2.217|       1|       0.179|     198.581
RTD|       0.345|       0.624|       1.957|       1|       0.179|     198.581
RTD|       0.202|       0.625|       2.149|       1|       0.179|     198.581
---|------------|------------|------------|--------|-------------------------
RTS|       0.179|       0.593|     198.581|       1|    00:00:17/00:00:17

Le cas le plus défavorable (qui est toujours celui à considérer) donne une latence de 198 µs.

On considère un temps supérieur à 100 µs comme pathologique. Sur une architecture de type PC, on s'attend à trouver des valeurs comprises entre 10 et 50 µs(33).

La valeur obtenue n'est pas acceptable, le paragraphe suivant décrit la procédure d'investigation afin de l'expliquer et la diminuer.

VI-H-2 - Le fichier TROUBLESHOOTING

Le fichier TROUBLESHOOTING contient des problèmes fréquemment rencontrés.

Les grandes pistes à suivre sont :

- un problème d'accélération matérielle du serveur X, relancez le programme en mode console ;

- une mauvaise configuration du noyau :

- SMI non désactivé (vérifiez le journal de démarrage),

- ACPI autres que processor à désactiver (sur PC : si vous percevez une variation du régime du ventilateur lors du lancement du bench, l'ACPI est probablement impliqué),

- l'arrivée de NMI (cf. /proc/interrupts),

- l'option idle=poll à passer sur la ligne de boot Lilo ou Grub ;

- une mauvaise configuration directement au niveau du BIOS (Power management, SMI, ACPI encore et toujours, options Legacy*).

Si malgré cela, les mauvais résultats persistent, il faut investiguer avec I-pipe Tracer.

VI-H-3 - Traquer les latences avec l'option I-pipe debugging

Si vous vous sentez un peu seul face à la question « qu'est-ce qui retarde ma tâche devant s'exécuter ? », ce débogueur va vous aider. Cela passe par deux choses :

- l'instrumentation d'ADEOS, avec le traçage des interruptions et du temps passé dans leur traitement ;

- l'instrumentation du code sous test, qui demande explicitement le gel du contexte conduisant au cas le plus défavorable.

On retourne dans notre configuration du noyau :

[root@localhost src]# cd /usr/src/linux-2.6.24
[root@localhost linux-2.6.24]#  make menuconfig

Puis dans le menu Kernel hacking, on active I-pipe debugging et son sous-menu Latency tracking.

Les options sont alors les suivantes :

I-pipe debugging

L'activation du débogage introduit une latence supplémentaire ;

Check for illicit cross-domain calls ;

Latency tracing

À activer pour notre investigation sur les temps de latence. Cette option peut être activée soit ici, soit via la commande echo 1 > /proc/ipipe/trace/enable ;

Enable tracing on boot ;

Instrument fonction entries ;

Trace IRQ-off times ;

Depth of trace log ;

Use vmalloc'ed trace buffer ;

Enable panic back traces.

Le détail des options, les fichiers de configuration et les fichiers produits se trouvent dans le manuel en ligne :http://www.xenomai.org/index.php/I-pipe:Tracer.

On conserve les choix par défaut puis on recompile (d'ailleurs, aviez-vous bien allégé le noyau ?) et on reboote :

[root@localhost linux-2.6.24]#  make && make modules_install install

On active le plus de traces possible :

[root@localhost bin]# echo 1 > /proc/ipipe/trace/enable
[root@localhost bin]# echo 1 > /proc/ipipe/trace/verbose

On agrandit la fenêtre de capture des évènements.

[root@localhost bin]# echo 128 > /proc/ipipe/trace/back_trace_points

On relance notre benchmark,avec l'option -f en plus (toujours en chargeant le système) :

[root@localhost bin]# /usr/xenomai/bin/latency -f
...
RTH|-----lat min|-----lat avg|-----lat max|-overrun|----lat best|---lat worst
RTD|       1.898|       3.099|       6.090|       3|       1.829|     202.798
RTD|       1.989|       3.133|     131.559|       4|       1.829|     202.798
RTD|       1.951|       3.105|       6.104|       4|       1.829|     202.798
RTD|       2.008|       3.197|       6.504|       4|       1.829|     202.798
RTD|       2.024|       3.091|       7.040|       4|       1.829|     202.798
RTD|       1.970|       3.136|       7.937|       4|       1.829|     202.798
RTD|       1.902|       3.117|       8.278|       4|       1.829|     202.798
RTD|       1.867|       3.091|       5.965|       4|       1.829|     202.798
RTD|       2.060|       3.082|       5.823|       4|       1.829|     202.798
RTD|       2.011|       3.094|       8.030|       4|       1.829|     202.798
RTD|       1.927|       3.099|       5.772|       4|       1.829|     202.798
RTD|       2.074|       3.133|       6.432|       4|       1.829|     202.798
RTD|       2.060|       3.132|     195.732|       5|       1.829|     202.798
RTD|       2.779|       3.112|       8.222|       5|       1.829|     202.798
RTD|       2.508|       3.072|       7.934|       5|       1.829|     202.798
RTD|       2.456|       3.124|       7.784|       5|       1.829|     202.798
RTD|       2.480|       3.117|       9.660|       5|       1.829|     202.798
RTD|       2.303|       3.049|       5.657|       5|       1.829|     202.798
---|------------|------------|------------|--------|-------------------------
RTS|       1.829|       3.086|     202.798|       5|    00:06:15/00:06:15

Il est normal que les résultats soient empirés par l'instrumentation.

Le résultat de la capture se trouve dans le fichier /proc/ipipe/trace/frozen :

I-pipe frozen back-tracing service on 2.6.24/ipipe-2.0-06
------------------------------------------------------------
CPU: 0, Freeze: 567619631513 cycles, Trace Points: 100 (+10)
Calibrated minimum trace-point overhead: 0.110 us

+----- Hard IRQs ('|': locked)
|+---- <unused>
||+--- <unused>
|||+-- Xenomai
||||+- Linux ('*': domain stalled, '+': current, '#': current+stalled)
|||||                        +---------- Delay flag ('+': > 1 us, '!': > 10 us)
|||||                        |        +- NMI noise ('N')
|||||                        |        |
Type    User Val.   Time    Delay  Function (Parent)
:    +func                -221    0.105  do_sync_read+0x17 (vfs_read+0xed)
:    +func                -221    0.095  generic_file_aio_read+0x21 (do_sync_read+0xf2)
:    +func                -220    0.100  generic_segment_checks+0xe (generic_file_aio_read+0x4f)
:    +func                -220    0.105  do_generic_mapping_read+0x16 (generic_file_aio_read+0xf5)
:    +func                -220    0.095  cond_resched+0x9 (do_generic_mapping_read+0x79)
:    +func                -220    0.090  find_get_page+0x19 (do_generic_mapping_read+0x12b)
:    +func                -220    0.090  _read_lock_irq+0x9 (find_get_page+0x2f)
:    +func                -220    0.085  _read_lock_irqsave+0x16 (_read_lock_irq+0xe)
:    +func                -220    0.100  ipipe_check_context+0x19 (_read_lock_irqsave+0x25)
:    +func                -220    0.115  ipipe_check_context+0x19 (_read_lock_irqsave+0x41)
:    +func                -220    0.145  ipipe_check_context+0x19 (_read_lock_irqsave+0x71)
:    #func                -220    0.135  radix_tree_lookup+0x9 (find_get_page+0x3a)
:    #func                -219    0.105  _read_unlock_irq+0x9 (find_get_page+0x61)
:    #func                -219    0.100  __ipipe_unstall_root+0x9 (_read_unlock_irq+0x11)
:|   #begin   0x80000000  -219    0.095  __ipipe_unstall_root+0x5c (_read_unlock_irq+0x11)
:|   +end     0x80000000  -219    0.100  __ipipe_unstall_root+0x4b (_read_unlock_irq+0x11)
:    +func                -219    0.115  ipipe_check_context+0x19 (_read_unlock_irq+0x1d)
:    +func                -219    0.095  file_read_actor+0x21 (do_generic_mapping_read+0x2f2)
:    +func                -219    0.240  ipipe_check_context+0x19 (file_read_actor+0x125)
:    +func                -219    0.110  ipipe_check_context+0x19 (file_read_actor+0x192)
:    +func                -218    0.115  put_page+0x20 (do_generic_mapping_read+0x31d)
:    +func                -218    0.095  touch_atime+0xe (do_generic_mapping_read+0x386)
:    +func                -218    0.085  current_fs_time+0xe (touch_atime+0x98)
:    +func                -218    0.095  current_kernel_time+0xd (current_fs_time+0x16)
:    +func                -218    0.110  timespec_trunc+0xe (current_fs_time+0x27)
:    +func                -218    0.105  dnotify_parent+0x19 (vfs_read+0x12b)
:    +func                -218    0.105  _spin_lock+0xe (dnotify_parent+0x43)
:    +func                -218    0.120  ipipe_check_context+0x19 (_spin_lock+0x1d)
:    +func                -218    0.300  _spin_unlock+0x9 (dnotify_parent+0x83)
:|   +begin   0x00000081  -217    0.115  common_interrupt+0x6d (__ipipe_trace+0x1f7)
:|   +func                -217    0.170  __ipipe_handle_irq+0x21 (common_interrupt+0x75)
:|   +func                -217    0.155  __ipipe_set_irq_pending+0xa (__ipipe_handle_irq+0x22f)
:|   +func                -217    0.130  __ipipe_ack_irq+0x9 (__ipipe_handle_irq+0x24a)
:|   +func                -217    0.125  __ipipe_ack_fasteoi_irq+0x9 (__ipipe_ack_irq+0x20)
:|   +func                -217    0.120  ack_apic_level+0x9 (__ipipe_ack_fasteoi_irq+0x10)
:|   +func                -216    0.135  __mask_IO_APIC_irq+0xc (ack_apic_level+0x24)
:|   +func                -216+   1.534  io_apic_base+0x9 (__mask_IO_APIC_irq+0x6f)
:|   +func                -215    0.165  io_apic_base+0x9 (__mask_IO_APIC_irq+0x84)
:|   +func                -215+   1.499  io_apic_base+0x9 (__mask_IO_APIC_irq+0x91)
:|   +func                -213    0.155  __ipipe_walk_pipeline+0x10 (__ipipe_handle_irq+0x96)
:|   +func                -213    0.170  __ipipe_sync_stage+0x16 (__ipipe_walk_pipeline+0x118)
:|   #end     0x80000000  -213    0.165  __ipipe_sync_stage+0x31e (__ipipe_walk_pipeline+0x118)
:    #func                -213    0.140  __ipipe_get_irq_vector+0x9 (__ipipe_sync_stage+0x3a1)
:    #func                -213    0.140  do_IRQ+0x19 (__ipipe_sync_stage+0x3ee)
:    #func                -212    0.115  exit_idle+0x9 (do_IRQ+0x44)
:    #func                -212    0.095  irq_enter+0x9 (do_IRQ+0x49)
:    #func                -212    0.095  ipipe_check_context+0x19 (irq_enter+0x15)
:    #func                -212    0.125  idle_cpu+0x9 (irq_enter+0x35)
:    #func                -212    0.100  handle_fasteoi_irq+0x20 (do_IRQ+0x8a)
:    #func                -212    0.090  _spin_lock+0xe (handle_fasteoi_irq+0x3b)
:    #func                -212    0.125  ipipe_check_context+0x19 (_spin_lock+0x1d)
:    #func                -212    0.110  _spin_unlock+0x9 (handle_fasteoi_irq+0x8d)
:    #func                -212    0.110  ipipe_check_context+0x19 (_spin_unlock+0x1b)
:    #func                -211    0.105  handle_IRQ_event+0x10 (handle_fasteoi_irq+0x98)
:    #func                -211    0.145  usb_hcd_irq+0xe [usbcore] (handle_IRQ_event+0x39)
:    #func                -211+   1.288  uhci_irq+0x21 [uhci_hcd] (usb_hcd_irq+0x3d [usbcore])
:    #func                -210    0.095  ata_interrupt+0x16 (handle_IRQ_event+0x39)
:    #func                -210    0.095  _spin_lock_irqsave+0xc (ata_interrupt+0x23)
:    #func                -210    0.105  ipipe_check_context+0x19 (_spin_lock_irqsave+0x1b)
:    #func                -210    0.120  ipipe_check_context+0x19 (_spin_lock_irqsave+0x37)
:    #func                -209    0.220  ipipe_check_context+0x19 (_spin_lock_irqsave+0x67)
:    #func                -209    0.105  ata_bmdma_status+0x9 (ata_interrupt+0x1bf)
:    #func                -209+   1.062  ioread8+0x9 (ata_bmdma_status+0x19)
:    #func                -208    0.100  ata_bmdma_stop+0x16 (ata_interrupt+0x1de)
:    #func                -208+   1.057  ioread8+0x9 (ata_bmdma_stop+0x29)
:    #func                -207! 198.631  iowrite8+0x9 (ata_bmdma_stop+0x39)
:|   #begin   0x000000f7    -8    0.125  common_interrupt+0x6d (ata_bmdma_stop+0x3c)
:|   #func                  -8    0.140  __ipipe_handle_irq+0x21 (common_interrupt+0x75)
:|   #func                  -8    0.135  __ipipe_ack_apic+0x9 (__ipipe_handle_irq+0xe2)
:|   #func                  -8    0.135  __ipipe_dispatch_wired+0x20 (__ipipe_handle_irq+0x71)
:|  #*func                  -8    0.170  xnintr_clock_handler+0x12 (__ipipe_dispatch_wired+0x178)
:|  #*func                  -8    0.275  xntimer_tick_aperiodic+0x10 (xnintr_clock_handler+0x92)
:|  #*func                  -7    0.135  xnthread_periodic_handler+0x9 (xntimer_tick_aperiodic+0xb7)
:|  #*func                  -7    0.150  xnpod_resume_thread+0x16 (xnthread_periodic_handler+0x2f)
:|  #*[ 4445] samplin 99    -7    0.265  xnpod_resume_thread+0xd3 (xnthread_periodic_handler+0x2f)
:|  #*func                  -7    0.295  xntimer_next_local_shot+0x9 (xntimer_tick_aperiodic+0x184)
:|  #*func                  -6    0.265  xnpod_schedule+0x16 (xnintr_clock_handler+0x1a2)
:|  #*[ 4442] -<?>-   -1    -6    0.421  xnpod_schedule+0x1fb (xnintr_clock_handler+0x1a2)
:|  #*func                  -6    0.300  __phys_addr+0x9 (xnpod_schedule+0x550)
:|  #*func                  -5    0.340  __switch_to+0x16 (xnpod_schedule+0x5bf)
:|  #*[ 4445] samplin 99    -5    0.371  xnpod_schedule+0x613 (xnpod_suspend_thread+0x2e4)
:|  #*func                  -5    0.195  xntimer_get_overruns+0x9 (xnpod_wait_thread_period+0x127)
:|  #*func                  -5    0.140  __ipipe_restore_pipeline_head+0x16 (xnpod_wait_thread_period+0x17f)
:|  +*end     0x80000000    -4    0.315  __ipipe_restore_pipeline_head+0xc6 (xnpod_wait_thread_period+0x17f)
:|  +*begin   0x80000001    -4    0.215  __ipipe_dispatch_event+0x23f (__ipipe_syscall_root+0x62)
:|  +*end     0x80000001    -4    0.130  __ipipe_dispatch_event+0x203 (__ipipe_syscall_root+0x62)
:|  +*begin   0x80000000    -4    0.827  __ipipe_syscall_root+0xdb (__ipipe_syscall_root_thunk+0x35)
:   +*func                  -3    0.155  __ipipe_syscall_root+0xc (__ipipe_syscall_root_thunk+0x35)
:   +*func                  -3    0.110  __ipipe_dispatch_event+0x16 (__ipipe_syscall_root+0x62)
:|  +*begin   0x80000001    -3    0.115  __ipipe_dispatch_event+0x2a8 (__ipipe_syscall_root+0x62)
:|  +*end     0x80000001    -3    0.135  __ipipe_dispatch_event+0x24f (__ipipe_syscall_root+0x62)
:   +*func                  -2    0.396  hisyscall_event+0x21 (__ipipe_dispatch_event+0x12b)
:   +*func                  -2    0.315  xnshadow_sys_trace+0x16 (hisyscall_event+0x1bf)
:   +*func                  -2    0.220  ipipe_trace_frozen_reset+0x12 (xnshadow_sys_trace+0xe1)
:   +*func                  -1    0.195  __ipipe_global_path_lock+0x9 (ipipe_trace_frozen_reset+0x17)
:   +*func                  -1    0.110  __ipipe_spin_lock_irqsave+0xc (__ipipe_global_path_lock+0x15)
:|  +*begin   0x80000001    -1+   1.323  __ipipe_spin_lock_irqsave+0x8b (__ipipe_global_path_lock+0x15)
:|  #*func                   0    0.165  __ipipe_spin_unlock_irqcomplete+0xe (__ipipe_global_path_unlock+0x7b)
:|  +*end     0x80000001     0    0.165  __ipipe_spin_unlock_irqcomplete+0x64 (__ipipe_global_path_unlock+0x7b)
<   +*freeze  0x0002e643     0    0.185  xnshadow_sys_trace+0x49 (hisyscall_event+0x1bf)
|  +*begin   0x80000001     0    0.155  __ipipe_dispatch_event+0x23f (__ipipe_syscall_root+0x62)
|  +*end     0x80000001     0    0.150  __ipipe_dispatch_event+0x203 (__ipipe_syscall_root+0x62)
|  +*begin   0x80000000     0    0.255  __ipipe_syscall_root+0xdb (__ipipe_syscall_root_thunk+0x35)
+*func                   0    0.160  __ipipe_syscall_root+0xc (__ipipe_syscall_root_thunk+0x35)
+*func                   0    0.135  __ipipe_dispatch_event+0x16 (__ipipe_syscall_root+0x62)
|  +*begin   0x80000001     1    0.150  __ipipe_dispatch_event+0x2a8 (__ipipe_syscall_root+0x62)
|  +*end     0x80000001     1    0.130  __ipipe_dispatch_event+0x24f (__ipipe_syscall_root+0x62)
+*func                   1    0.180  hisyscall_event+0x21 (__ipipe_dispatch_event+0x12b)
+*func                   1    0.145  __rt_task_wait_period+0x19 (hisyscall_event+0x1bf)
+*func                   1    0.000N rt_task_wait_period+0x9 (__rt_task_wait_period+0x31)

Au premier abord, ce fichier n'est pas très facile à lire, voici quelques explications qui vous aideront à le décoder.

Tout d'abord, que cherche-t-on ? Des sections anormalement longues.

La colonne Time est le temps relatif de l'évènement par rapport à capture de la trace (appel à xntrace_user_freeze dans l'outil de benchmark). La colonne suivante, Delay, est la durée qu'a prise l'évènement. Pour nous aider un petit peu, I-Pipe Tracer nous indique avec + et !, les délais les plus longs.

[pid] :indique le scheduling d'un processus, son nom (samplin) et sa priorité (-1 pour les tâches schedulées par Linux).

common_interrupt indique l'arrivée d'une interruption, la ligne d'IRQ est 0x000000f7.

L'exploitation de ce fichier reste tout de même assez délicate car elle nécessite un très bonne connaissance de l'architecture matérielle utilisée.

Sur cette configuration, il s'agissait ici de l'absence du paramètre de noyau idle=poll lié à l'économie d'énergie, c'est une piste assez logique lorsque rien ne paraît dans la fonction justifier un long temps de traitement.

VI-I - Wiki d'I-Pipe Tracer

Toutes les clés pour exploiter le fichier de traces se trouvent à cette adresse : http://www.xenomai.org/index.php?title=I-pipe:Tracer.

VI-J - Traquer les interruptions parasites avec powertop

Moins il y a d'interruptions à traiter par ADEOS, meilleure sera la latence du système. L'idée consiste alors à distinguer les interruptions parasites de celles nécessaires, et de supprimer les premières.

Pas évident de faire ce tri me direz-vous. Eh bien non, bien au contraire, l'outil powertop, initialement destiné aux ordinateurs portables fait cela très simplement. Un bémol cependant, Powertop ne fonctionne que sur plate-forme Intel.

Auparavant, il faut activer l'option suivante lors de la configuration du noyau :

Kernel hacking/Kernel Debugging/Collect kernel timers statistics.

Après installation et lancement de powertop, il n'y a plus qu'à admirer le résultat :

Dans cette exécution, ce sont les interruptions de la carte réseau (eth1) qui sont les plus nombreuses.

En lançant powertop -d, on obtient également des conseils sur la configuration de la machine. Attention ceux-ci ne sont pas forcément à prendre au pied de la lettre car ils sont édictés afin de consommer moins d'énergie, prudence donc.

VII - Premiers pas

VII-A - Un grand classique : « Hello world! »

Dans l'exemple ci-dessous, nous allons créer une tâche Xenomai qui affiche un message. Cela n'a aucun intérêt d'un point de vue temps réel me direz-vous. C'est vrai, mais cela permet tout de même de valider la chaîne de compilation et le support de Xenomai dans le noyau à travers un exemple basique.

Voici le code source que nous allons compiler :

#include <stdio.h>
#include <sys/mman.h>
#include <native/task.h>

static void task(void *arg)
{
/* Here, we are in primary mode... */

printf("Hello world!\n");

/* Outch... printf accesses a Linux ressource so we are now
in secondary mode */
}

int main (void)
{
RT_TASK task_desc;

/* disable memory swap */
mlockall( MCL_CURRENT | MCL_FUTURE );

if (rt_task_spawn( &task_desc,  /* task descriptor */
"my task",   /* name */
0           /* 0 = default stack size */,
99          /* priority */,
T_JOINABLE, /* needed to call rt_task_join after */
&task,      /* entry point (function pointer) */
NULL        /* function argument */ )!=0)
{
printf("rt_task_spawn error\n");
return 1;
}

/* wait for task function termination */
rt_task_join(&task_desc);

return 0;
}

Le mécanisme de pagination abstrait l'adressage mémoire. Cela permet de disposer de plus de mémoire vive qu'en réalité, la différence virtuel/réel étant comblée par une mémoire plus lente (disque dur). Dans une application temps réel, il est inconcevable d'avoir une variation dans les temps d'accès mémoire, c'est pourquoi le swap doit impérativement être désactivé en appelant mlockall.

VII-B - La compilation

On va simplement chercher le répertoire contenant les en-têtes et la bibliothèque :

[dchabal@localhost tuto]$ gcc -I/usr/xenomai/include hello.c -L/usr/xenomai/lib -lnative -o hello

Par la suite nous verrons une méthode de compilation plus portable.

VII-C - L'exécution

[dchabal@localhost tuto]$ ./hello
./hello: error while loading shared libraries: libnative.so.1: cannot open shared object file: No such file or directory
[dchabal@localhost tuto]$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/xenomai/lib
[dchabal@localhost tuto]$ ./hello
Xenomai: binding failed: Operation not permitted.
[dchabal@localhost tuto]$

Qu'est-ce qui ne va pas ici ?

La première erreur vient du fait que la bibliothèque libnative.so.1 est chargée dynamiquement et que son chemin ne se trouve pas dans la variable d'environnement LD_LIBRARY_PATH.

On peut également ne pas toucher au LD_LIBRARY_PATH et modifier le fichier /etc/ld.so.conf puis lancer ldconfig (en tant que root), comme ceci:

[root@localhost tuto]# more /etc/ld.so.conf
include ld.so.conf.d/*.conf
/usr/X11R6/lib
/usr/lib/qt3/lib64
/usr/xenomai/lib
[root@localhost tuto]# ldconfig
[root@localhost tuto]#

La seconde erreur est qu'il faut avoir les droits root pour lancer les applications Xenomai, d'où le message d'opération non autorisée.

En tant que root, on a bien le résultat escompté :

[root@localhost tuto]# ./hello
Hello world!
[root@localhost tuto]#

Le temps d'accès à la mémoire doit être constant. Or, ce n'est pas garanti lorsque le swap disque est activé.
Il faut donc impérativement le désactiver avec la fonction mlockall.
L'inconvénient est que l'application aura moins de ressources mémoire.
Il faudra être particulièrement vigilant lors de l'appel aux fonctions de type malloc (secondary mode) en contrôlant le code de retour.

VII-D - Que se passe-t-il ?

Les commandes top, fuser... existaient sous Linux afin de surveiller l'état du système, comment fait-on maintenant pour surveiller nos nouvelles tâches ?

Les variables internes de Xenomai sont accessibles dans le répertoire /proc/xenomai(34). L'arborescence est alors la suivante :

affinity

CPU utilisés pour les tâches Xenomai. Exemple: 00000003, soit en binaire 0000.0011 pour le dernier octet, correspondant aux CPU0 et CPU1 sur un Intel Dual Core ;

apc

L'APC est le mécanisme qui optimise de la prise en compte des handlers d'interruption Linux en attente lorsque Linux reprend la main ;

faults

Liste les erreurs levées par processeur ;

hal

Version d'Adeos ;

heap

Size of the private stack pool (comme son nom ne l'indique pas) internes configurés dans Size of the system heap et Size of the private stack pool.

interfaces/

Nombre d'applications Xenomai en cours d'exécution, classées par skins ;

native ;

rtdm ;

sys ;

irq

Comptage du nombre d'interruptions utilisées par Xenomai ;

latency

Latence du scheduler (expliqué dans Scheduling latency) ;

registry/

Liste des objets temps réel gérés par Xenomai ;

rtdm/

Informations concernant les drivers temps réel ;

fildes

Liste des descripteurs (attention contrairement à Linux, les descripteurs commencent à 0) ;

named_devices

Liste des périphériques, rtserx par exemple pour les « ports série temps réel ». Alter ego du /dev Linux ;

open_fildes

Liste des descripteurs pointant un périphérique utilisé ;

protocol_devices

Liste des protocoles utilisés par les périphériques (CAN) ;

rttest0/ ;

information

Informations (version, type de drivers...) disponibles pour chaque périphérique présent dans le répertoire RTDM. rttest0 correspond ici à un driver virtuel dédié au test ;

rttest1/ ;

information ;

rttest2/ ;

information ;

sched

Liste les tâches dans le scheduler Xenomai (voir ci-dessous) ;

stat

Statistiques (utilisation CPU...) collectées si l'option du noyau est activée ;

timebases

États des timers des différentes skins ;

timer

État du timer, exemple :

status=on+watchdog:setup=0:clock=379818707114:timerdev=lapic:clockdev=tsc

status : le timer est actif et le watchdog de débogage est activé ;
setup : latence du timer en nanosecondes ;
clock :nombre de ticks émis par clockdev écoulés depuis la mise sous tension du système ;
timerdev : timer utilisé (lapic) ;
clockdev : horloge utilisée (time stamp counter : timer haute résolution implémenté dans le CPU) ;

timestat/

Indications sur la programmation des timers ;

version

Version de Xenomai.

L'état d'ADEOS est également consultable dans le répertoire /proc/ipipe comme nous l'avons vu précédemment.

Nous avions dit qu'une tâche en secondary mode était dans le scheduler Linux, il est temps maintenant de le vérifier. En insérant un appel à pause() après notre printf(), nous retrouvons bien notre tâche via un ps :

[dchabal@localhost xenomai]$ ps -eTo pid,ppid,ucmd,class,rtprio,nice --sort class
PID  PPID CMD             CLS RTPRIO  NI
1     0 init            TS       -   0
...
4059  3926 hello           TS       -   0
4059  3926 my task         FF      99   -
[dchabal@localhost xenomai]$

Nous avons donc bien notre tâche Linux principale (code du main) qui s'appelle hello et qui est dans la classe SCHED_OTHER (TS pour time share), et notre tâche Xenomai en secondary mode "my task" qui a conservé sa priorité 99. Elle est bien dans la classe SCHED_FIFO (FF, on aurait eu RR pour SCHED_RR).

Et si maintenant on jette un œil du côté du scheduler Xenomai :

[dchabal@localhost xenomai]$ cat /proc/xenomai/sched
CPU  PID    PRI      PERIOD     TIMEOUT    TIMEBASE  STAT       NAME
0  0       -1      0          0          master    R          ROOT/0
1  0       -1      0          0          master    R          ROOT/1
0  4059    99      0          0          master    X          my task
[dchabal@localhost xenomai]$

On retrouve encore notre tâche "my task", marquée d'un 'X', c'est-à-dire en secondary mode.

La tâche ROOT est Linux en mode idle (priorité symboliquement notée -1).

Le tableau ci-dessous(35) liste les statuts possibles :

Tableau 1: Statuts des tâches

S

Forcibly suspended.

w/W

Waiting for a resource, with or without timeout.

D

Delayed (without any other wait condition).

R

Runnable.

U

Unstarted or dormant.

X

Relaxed shadow.

H

Held thread.

b

Priority boost undergoing.

T

Ptraced and stopped.

l

Locks scheduler.

r

Undergoes round-robin.

s

Interrupt shield enabled.

t

Mode switches trapped.

o

Priority coupling off.

f

FPU enabled (for kernel threads).

VIII - Exemples d'utilisation

Ce chapitre traite d'exemples simples. Le but ici n'est pas de faire un tour exhaustif de l'API mais d'en décrire la philosophie.

Des cas d'utilisation de chaque fonctionnalité de l'API sont disponibles dans le répertoire après décompression xenomai-<version>/ksrc/skins/native/snippets.

VIII-A - Avant d'utiliser les fonctions Xenomai : les environnements d'appels

Dans la documentation de l'API Xenomai, chaque fonction est associée à un ou plusieurs environnements d'appel :

- Kernel module initialization/cleanup code : code d'initialisation/suppression des modules Linux (.ko) contenu à l'intérieur des macros module_init() et module_exit() ;

- Interrupt service routine : handler d'interruption ;

- Kernel-based task : tâche Xenomai lancée par rtdm_task_init() ;

- User-space task : tâche Xenomai ou Linux lancée explicitement par l'utilisateur.

En cas de mauvais contexte, les appels système vous répondront un sympathique -EPERM.

Contrôlez toujours le code de retour des appels système, qu'ils soient Xenomai ou Linux.

VIII-B - Les tâches

VIII-B-1 - Les paramètres de lancement d'une tâche

Les tâches peuvent être créées (rt_task_create) puis lancées (rt_task_start), ou bien les deux à la fois (rt_task_spawn).

On retrouve les paramètres classiques pour les tâches d'un OS temps réel (point d'entrée, priorité, round-robin...).

La taille de la pile par défaut qui est dite « raisonnable » (4 Ko sur x86). Si celle-ci n'est pas suffisante, on obtient immédiatement un segmentation fault :

[root@localhost tuto]# ./stack_too_small
Segmentation fault
[root@localhost tuto]#

Pour mode, affinity sélectionne le processeur recevant la tâche.

VIII-B-2 - Tâches périodiques

Les tâches périodiques sont créées comme les autres tâches, il n'y a pas de flags spécifiques mais seulement deux appels de fonction :

- rt_task_set_periodic afin d'indiquer à la tâche sa période ;

- rt_task_wait_period pour descheduler la tâche en attendant le prochain cycle.

L'exemple ci-dessous inclut la détection de l'overrun de la tâche périodique, c'est-à-dire le nombre de fois où le réveil de la tâche a échoué car elle était encore en cours d'exécution.

#include <stdio.h>
#include <assert.h>
#include <sys/mman.h>
#include <native/task.h>
#include <native/timer.h>

#define ONE_SECOND 1000000000

static void task(void *arg)
{
/* Print a message each second */
unsigned int cnt=0;
unsigned long int ov=0;
volatile unsigned int i=0;
unsigned long long int start = 0;
RT_TIMER_INFO timer_info;

if ( rt_task_set_periodic(
NULL,      /* the current task */
TM_NOW,    /* delay before release, TM_NOW = none */
ONE_SECOND /* we don't use a periodic timer, so this value is in nanosec */  )!= 0)
{
printf("rt_task_set_periodic error\n");
return;
}

(void)rt_timer_inquire(&timer_info);
start = timer_info.date/ONE_SECOND;

while(1)
{
/* Go in secondary mode */
(void)rt_timer_inquire(&timer_info);

printf("Hello world #%u, date: %llu s\n",cnt,timer_info.date/ONE_SECOND - start);
++cnt;

if (cnt==3)
/* Big computation for step 3 */
for ( i=0;i<1000000000;++i);

ov = 0;
/* Go back in primary mode because we do a Xenomai system call */
switch ( rt_task_wait_period( &ov ) )
{
case 0:
case -ETIMEDOUT:
if (ov)
printf("Overrun: %lu cycles lost\n",ov); // secondary mode
break;

default:
printf("rt_task_wait_period error\n"); // secondary mode
return;
}
}
}

int main (void)
{
RT_TASK task_desc;
int n;

mlockall( MCL_CURRENT | MCL_FUTURE );

if ((n=rt_task_spawn( &task_desc, "my task", 0, 99, T_JOINABLE, &task, NULL ))!=0)
{
printf("rt_task_spawn error %d\n",n);
return 1;
}
rt_task_join(&task_desc);
return 0;
}

VIII-B-3 - Ce qui donne les traces suivantes :

[root@localhost tuto]# ./periodic
Hello world #0, date: 0 s
Hello world #1, date: 1 s
Hello world #2, date: 2 s
Overrun: 2 cycles lost
Hello world #3, date: 5 s
Hello world #4, date: 6 s
Hello world #5, date: 7 s
Hello world #6, date: 8 s
Hello world #7, date: 9 s
Hello world #8, date: 10 s
Hello world #9, date: 11 s
Hello world #10, date: 12 s

La fonction rt_task_set_period mérite un peu d'attention. Son troisième argument est la période, son unité dépend du type de timer choisi lors de la compilation du noyau (cf. Enable periodic timing).

Dans cet exemple, le basculement en secondary mode n'est pas gênant pour afficher un message d'erreur.
En effet, on considère l'application comme « perdue » et on ne tient plus compte des contraintes temporelles.

VIII-B-4 - Transformation d'un processus Linux en tâche Xenomai

Prenons l'exemple suivant :

#include <stdio.h>
#include <sys/mman.h>
#include <native/task.h>
#include <native/sem.h>

static RT_SEM sem;

static void task(void *arg)
{
int n=0;
volatile int i = 0;

/* computation */
for (i=0;i<10000;++i);

if ((n=rt_sem_v(&sem))!=0)
printf("rt_sem_v error %d\n",n);
}

int main (void)
{
RT_TASK task_desc;
int n=0;

mlockall( MCL_CURRENT | MCL_FUTURE );

if ( rt_sem_create( &sem, "my sem", 0, S_FIFO ) != 0 )
{
printf("rt_sem_create error\n");
return 1;
}

if ((n=rt_task_spawn( &task_desc,"my task",0,99,0,&task,NULL))!=0)
{
printf("rt_task_spawn error %d\n",n);
return 1;
}

/* wait for "my task" termination (like rt_task_join) */
if ((n=rt_sem_p(&sem,TM_INFINITE))!=0)
{
printf("rt_sem_p error %d\n",n);
return 1;
}

return 0;
}

Notre application se termine lorsque le sémaphore sem est relâché. Or, lors de l'exécution, nous obtenons le message :

[root@localhost tuto]# ./error_context
rt_sem_p error -1
[root@localhost tuto]#

-1 correspond au code -EPERM(36). L'opération est interdite car on ne se trouve pas dans le bon contexte, il faudrait être ici dans une tâche Xenomai. Pour cela, on va utiliser la fonction rt_task_shadow.

...
if ((n=rt_task_spawn( &task_desc,"my task",0,99,0,&task,NULL))!=0)
{
printf("rt_task_spawn error %d\n",n);
return 1;
}

if ( rt_task_shadow( &task_main, "main", 99, 0 ) != 0 )
{
printf("rt_task_shadow error\n");
return 1;
}

/* wait for "my task" termination (like rt_task_join) */
if ((n=rt_sem_p(&sem,TM_INFINITE))!=0)
{
printf("rt_sem_p error %d\n",n);
return 1;
}
...

Utiliser rt_task_shadow pour :

- terminer des tâches Xenomai, en appelant depuis la nouvelle tâche des fonctions de destruction de sémaphores, files de messages...

- simplifier l'architecture dynamique en réutilisant la tâche « principale » main.

VIII-B-5 - Affectation tâche/processeur (affinity )

Cet exemple monte l'utilisation de l'option T_CPU(n) dans le lancement des tâches.

#include <stdio.h>
#include <sys/mman.h>
#include <native/task.h>

#define TEN_SECONDS             (10*1e9)

static void task(void *arg)
{
rt_task_sleep( TEN_SECONDS );
}

int main (void)
{
RT_TASK task_desc0,task_desc1;

/* disable memory swap */
mlockall( MCL_CURRENT | MCL_FUTURE );

if (rt_task_spawn( &task_desc0, /* task descriptor */
"my task 0", /* name */
0           /* 0 = default stack size */,
99          /* priority */,
T_JOINABLE | T_CPU(0), /* needed to call rt_task_join after */
&task,      /* entry point (function pointer) */
NULL        /* function argument */ )!=0)
{
printf("rt_task_spawn error\n");
return 1;
}

if (rt_task_spawn( &task_desc1, /* task descriptor */
"my task 1", /* name */
0           /* 0 = default stack size */,
99          /* priority */,
T_JOINABLE | T_CPU(1), /* needed to call rt_task_join after */
&task,      /* entry point (function pointer) */
NULL        /* function argument */ )!=0)
{
printf("rt_task_spawn error\n");
return 1;
}

/* wait for task function termination */
rt_task_join(&task_desc0);
rt_task_join(&task_desc1);

return 0;
}

On vérifie maintenant que nos tâches sont bien affectées à deux processeurs différents dans le scheduler :

[dchabal@localhost tuto]$ more /proc/xenomai/sched
CPU  PID    PRI      PERIOD     TIMEOUT    TIMEBASE  STAT       NAME
0  0       -1      0          0          master    R          ROOT/0
1  0       -1      0          0          master    R          ROOT/1
0  4820    99      0          7629058446 master    D          my task 0
1  4821    99      0          7629328952 master    D          my task 1

VIII-B-6 - Contrôle du scheduling d'une tâche : la fonction rt_task_set_mode

Encore une fonction intéressante, qui nous permet cette fois-ci de jouer un peu avec le scheduler.

rt_task_set_mode s'applique à la tâche courante. Parmi les paramètres, on trouve notamment :

- l'activation de l'Interrupt Shield ;

- la détection des passages « sauvages » en secondary mode (cf. § X.C.2) ;

- le passage explicite d'un mode à l'autre.

VIII-C - Les sémaphores (à compteur et d'exclusion mutuelle)

Cet exemple (semaphore.c) montre l'utilisation des deux types de sémaphores.

#include <stdio.h>
#include <sys/mman.h>
#include <native/task.h>
#include <native/sem.h>
#include <native/mutex.h>

static RT_SEM sem;
static RT_MUTEX mutex;
static int shared_variable;

static void task1(void *arg)
{
int n=0;

printf("Task 1 computation\n");

/* Release sem */
if ((n=rt_sem_v(&sem))!=0)
printf("rt_sem_v error %d\n",n);
}

static void task2(void *arg)
{
int n=0;
/* Wait for sem */
if ((n=rt_sem_p(&sem,TM_INFINITE))!=0)
printf("rt_sem_p error %d\n",n);

printf("Task 2 computation\n");
}

static void task(void *arg)
{
unsigned int runs=10;
char *taskname = (char*)arg;

while(runs--)
{
if ( rt_mutex_acquire( &mutex, TM_INFINITE ) != 0 )
printf("rt_mutex_acquire error\n");
++shared_variable;

printf("%s, shared_variable=%d\n",taskname,shared_variable);

/* wait forcing round-robin */
rt_task_sleep( 1 );

if ( rt_mutex_release( &mutex ) != 0 )
printf("rt_mutex_release error\n");
}
}

int main (void)
{
RT_TASK task_desc1,task_desc2,task_desc3,task_desc4;
int n=0;

mlockall( MCL_CURRENT | MCL_FUTURE ):

if ( rt_sem_create( &sem, "my sem", 1, S_FIFO ) != 0 )
{
printf("rt_sem_create error\n");
return 1;
}
if ( rt_mutex_create( &mutex, "my mutex" ) != 0 )
{
printf("rt_mutex_create error\n");
return 1;
}

if ((n=rt_task_spawn( &task_desc1,"my task 1",0,99,T_JOINABLE,&task1,NULL))!=0)
{
printf("rt_task_spawn error %d\n",n);
return 1;
}
if ((n=rt_task_spawn( &task_desc2,"my task 2",0,99,T_JOINABLE,&task2,NULL))!=0)
{
printf("rt_task_spawn error %d\n",n);
return 1;
}

if ((n=rt_task_spawn( &task_desc3,"my task 3",0,50,T_JOINABLE|T_RRB,&task,"my task 3"))!=0)
{
printf("rt_task_spawn error %d\n",n);
return 1;
}

if ((n=rt_task_spawn( &task_desc4,"my task 4",0,50,T_JOINABLE|T_RRB,&task,"my task 4"))!=0)
{
printf("rt_task_spawn error %d\n",n);
return 1;
}

(void)rt_task_join(&task_desc1);
(void)rt_task_join(&task_desc2);
(void)rt_task_join(&task_desc3);
(void)rt_task_join(&task_desc4);

return 0;
}

Pour la mise au point :
des fichiers se trouvant dans /proc/xenomai/registry indiquent l'état des sémaphores et les tâches en attente.

VIII-D - Les files de messages

Dans cet exemple, deux tâches communiquent via des messages de taille variable.

#include <stdio.h>
#include <sys/mman.h>
#include <native/task.h>
#include <native/queue.h>

#define MSG_QUEUE_NAME  "my msgqueue"

#define N_MSGS   15

static RT_QUEUE msgq;

typedef struct
{
char buf[32];
} my_message;

static void task1(void *arg)
{
unsigned int i=0;
int n=0;
my_message m;

for(i=0;i<N_MSGS+1;++i)
{
memset(m.buf,i,i+1);
n = rt_queue_write(&msgq,&m,i+1,Q_NORMAL);

if (n<0)
printf("rt_queue_write error %d\n",n);
}
}

static void task2(void *arg)
{
int n=0,i=1;
my_message m;

while(1)
{
memset(m.buf,0x00,sizeof(m));
n = rt_queue_read(&msgq,&m,sizeof(m),TM_NONBLOCK);

if (n!=i)
{
printf("rt_queue_read error %d\n",n);
break;
}
printf("%d bytes read, first byte: %x\n",n,m.buf[0]);
++i;
}
}

int main (void)
{
RT_TASK task_desc1,task_desc2;
int n=0;

mlockall( MCL_CURRENT | MCL_FUTURE );

if ( (n=rt_queue_create( &msgq, MSG_QUEUE_NAME, sizeof(my_message)*N_MSGS, N_MSGS, Q_FIFO )) != 0 )
{
printf("rt_queue_create error %d\n",n);
return 1;
}

/* High priority task (writer) */
if ((n=rt_task_spawn( &task_desc1,"my task 1",0,99,T_JOINABLE,&task1,NULL))!=0)
{
printf("rt_task_spawn error %d\n",n);
return 1;
}
/* Low priority task (reader) */
if ((n=rt_task_spawn( &task_desc2,"my task 2",0,50,T_JOINABLE,&task2,NULL))!=0)
{
printf("rt_task_spawn error %d\n",n);
return 1;
}

(void)rt_task_join(&task_desc1);
(void)rt_task_join(&task_desc2);

if (rt_queue_delete( &msgq )!=0)
printf("rt_queue_delete error\n");

return 0;
}

Le répertoire /proc/xenomai/registry contient des informations sur les objets utilisés, par exemple pour notre file (le programme a été mis en pause) :

[dchabal@localhost queues]$ more /proc/xenomai/registry/native/queues/my\ msgqueue
type=shared:poolsz=8192:usedmem=512:limit=15:mcount=15
[dchabal@localhost queues]$

mcount indique le nombre de messages stockés.

VIII-E - Les alarmes (watchdogs )

Contrairement à ce que l'on pourrait penser, ces alarmes n'appellent pas un handler lorsqu'elles expirent, mais débloquent une tâche en attente.

Cet exemple présente un calcul de longue durée qui arme/désarme une alarme. Dès que le désarmement n'est plus opéré, la tâche « my alarm task » affiche le nombre de déclenchements.

#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <native/task.h>
#include <native/alarm.h>

#define ONE_SECOND 1e9

static RT_ALARM my_alarm;

static RT_TASK task_desc1;

static void task1(void *arg)
{
register unsigned int i=0;
int n=0;

/* start my_alarm */
if ((n=rt_alarm_start(&my_alarm, 0, ONE_SECOND ))!=0)
printf("rt_alarm_start error %d\n",n);

/* big computation */
for(i=0;i<100000;++i);

/* re-arm alarm */
if (rt_alarm_stop(&my_alarm )!=0)
printf("rt_alarm_stop error\n");
if (rt_alarm_start(&my_alarm, 0, /* expiration time */ ONE_SECOND )!=0)
printf("rt_alarm_start error\n");

for(i=0;i<100000;++i);

/* re-arm alarm */
if (rt_alarm_stop(&my_alarm )!=0)
printf("rt_alarm_stop error\n");
if (rt_alarm_start(&my_alarm, 0, ONE_SECOND )!=0)
printf("rt_alarm_start error\n");

/* really big computation (alarm not rearmed) */
for(i=0;i<1000000000;++i);
for(i=0;i<1000000000;++i);
for(i=0;i<1000000000;++i);
}

static void task_alarm(void *arg)
{
int n=0;
RT_ALARM_INFO info;

while(1)
{
if ((n=rt_alarm_wait(&my_alarm))==0)
{
if ( rt_alarm_inquire(&my_alarm,&info)==0)
{
printf("my_alarm %s triggerred %lu times\n",info.name,info.expiries);
rt_task_delete(&task_desc1);
}
else
printf("rt_alarm_inquire\n");
return;
}
else
printf("rt_alarm_wait error %d\n",n);
}
}

int main (void)
{
RT_TASK task_desc_alarm,task_main;
int n=0;

if (mlockall( MCL_CURRENT | MCL_FUTURE )!=0)
return 1;

if ( (n=rt_alarm_create( &my_alarm,"computation" )) != 0 )
{
printf("rt_alarm_create error %d\n",n);
return 1;
}

/* rt_alarm_start needs to be called from a Xenomai task */
if ( rt_task_shadow(&task_main,"main",1,0)!=0)
{
printf("rt_task_shadow error\n");
return 1;
}

if ((n=rt_task_spawn( &task_desc_alarm,"my_alarm task",0,99,T_JOINABLE,&task_alarm,NULL))!=0)
{
printf("rt_task_spawn error %d\n",n);
return 1;
}

if ((n=rt_task_spawn( &task_desc1,"my task 1",0,99,T_JOINABLE,&task1,NULL))!=0)
{
printf("rt_task_spawn error %d\n",n);
return 1;
}

rt_task_join(&task_desc_alarm);
rt_task_join(&task_desc1);
return 0;
}

VIII-F - Les pipes et la communication avec les applications Linux « normales »

Vous pilotez des cartes et vous souhaitez afficher des résultats ou des états dans une IHM. Comment faire ? Développer une application monobloc regroupant dialogue temps réel avec l'équipement en primary mode et affichage en secondary mode ? Pas très élégant de mélanger une IHM avec une réactivité pour être humain et du temps réel...

Heureusement, dans Xenomai il y a tout et en particulier des objets appelés RT_PIPE qui permettent à deux applications Xenomai et Linux de dialoguer à travers un « pipe » (les guillemets sont là pour vous rappeler que ce n'est pas un pipe créé avec pipe() ou mkfifo()).

L'approche est semblable aux tubes nommés qui, je vous le rappelle, ont une existence dans l'arborescence « / ».

Passons maintenant à une petite séance de brainstorming :

- une tâche Xenomai en primary mode doit accéder à ce pipe sans basculer en secondary mode, cette ressource doit donc être managée par Xenomai ;

- un programme Linux doit également y accéder sans même savoir qu'il y a une tâche Xenomai à l'autre bout du pipe ; qui doit donc être managé par Linux.

Intéressant paradoxe, la solution l'est tout autant : le pipe a deux noms, un pour les applications « pur » Linux, un autre pour les Xenomai.

Un RT_PIPE a deux noms :

- /dev/rtp npour les applications Linux ;

- rtp npour les applications Xenomai.

Voici un exemple de code qui envoie une chaîne de caractères d'une application Xenomai vers une application Linux.

L'application Xenomai :

#include <stdio.h>
#include <sys/mman.h>
#include <native/task.h>
#include <native/pipe.h>

static RT_PIPE my_pipe;

static void task(void *arg)
{
char *s="Hello world!";
unsigned int sz=strlen(s)+1;

if ( rt_pipe_write( &my_pipe, s, sz, P_NORMAL) != sz )
{
printf("rt_pipe_write error\n");
}
}

int main (void)
{
RT_TASK task_desc;

/* disable memory swap */
if ( mlockall( MCL_CURRENT | MCL_FUTURE ) != 0 )
{
printf("mlockall error\n");
return 1;
}

if ( rt_pipe_create( &my_pipe, "rtp0", P_MINOR_AUTO, 0 ) != 0 )
{
printf("rt_pipe_create error\n");
return 1;
}

if (rt_task_spawn( &task_desc,  /* task descriptor */
"my task",   /* name */
0           /* 0 = default stack size */,
99          /* priority */,
T_JOINABLE, /* needed to call rt_task_join after */
&task,      /* entry point (function pointer) */
NULL        /* function argument */ )!=0)
{
printf("rt_task_spawn error\n");
return 1;
}

/* wait for task function termination */
rt_task_join(&task_desc);

return 0;
}

et l'application Linux :

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(void)
{
char buf[128];
int fd = open( "/dev/rtp0", O_RDWR );

if (fd < 0)
{
printf("open error (%d)\n",fd);
return 1;
}

if ( 0 < read(fd, buf, sizeof(buf)) )
{
printf("==> %s\n",buf);
}
}

Le résultat de l'exécution :

VIII-G - Nommage et rémanence des objets

Les tâches, les files de messages, les sémaphores, les alarmes(37)... partagent un espace de nommage commun (registry) dans le noyau Xenomai. Autrement dit, une file de messages et une tâche ne peuvent avoir le même nom.

Depuis Xenomai 2.4, la suppression des objets orphelins est automatique.

VIII-H - Aperçu de l'API native

Tableau 2: Aperçu de l'API native

Fonctionnalité

Nom dans l'API

Alarmes

rt_alarm_*

Variables de condition

rt_cond_*

Évènements

rt_event_

Allocation de mémoire RT

rt_heap_*

Gestion des interruptions

rt_intr_*

Mutexes

rt_mutex_*

Pipes

rt_pipe_*

Files de messages

rt_queue_*

Sémaphores

rt_sem_*

Tâches

rt_task_*

Timers

rt_timer_*

IX - Exécution des programmes Xenomai et sécurité

Jusqu'à présent, nous avons lancé tous nos programmes logués en tant que root.

Cette situation, qui est acceptable en développement, l'est beaucoup moins lorsque le produit est opérationnel puisqu'une mauvaise manipulation peut mettre en péril Linux.

Les trois solutions ci-dessous répondent à ce besoin de sécurisation.

IX-A - « Xenomai GID »

Passée en tant que paramètre du noyau Linux, cette option autorise un groupe d'utilisateurs à lancer les applications Xenomai. Exemple : xeno_nucleus.xenomai_gid=500.

IX-B - Activation du bit d'endossement

C'est une solution simple. Comme pour la commande passwd, nous allons activer le bit d'endossement.

Le fichier est exécuté en tant que dchabal :

[dchabal@localhost tuto]$ ./hello
Xenomai: binding failed: Operation not permitted.
[dchabal@localhost tuto]$

En tant que root :

[root@localhost tuto]# ls -l hello
-rwxr-xr-x 1 dchabal dchabal 7794 Sep  6 10:34 hello
[root@localhost tuto]# chown root:root hello
[root@localhost tuto]# ls -l hello
-rwxr-xr-x 1 root root 7794 Sep  6 10:34 hello
[root@localhost tuto]# chmod +s hello
[root@localhost tuto]# ls -l hello
-rwsr-sr-x 1 root root 7794 Sep  6 10:34 hello
[root@localhost tuto]#

Le bit d'endossement activé, on s'approprie les droits du propriétaire du fichier :

[dchabal@localhost tuto]$
[dchabal@localhost tuto]$ ./hello
Hello world!

[dchabal@localhost tuto]$

À noter que n'importe quel utilisateur du même groupe que dchabal pourra utiliser ce programme.

IX-C - sudo

Sudo (SUperuser DO) permet une gestion des accès plus fine et plus « sécuritaire » que le bit d'endossement ou le contrôle au niveau du groupe.

Ici, on autorise l'utilisateur dchabal à lancer le programme hello avec tous les arguments qu'il souhaite.

En tant que root, lancer visudo et ajouter cette ligne :

dchabal ALL=NOPASSWD: /home/dchabal/tuto/hello *

L'exécutable est alors lancé comme un paramètre de sudo :

[dchabal@localhost tuto]$ ./hello
Xenomai: binding failed: Operation not permitted.
[dchabal@localhost tuto]$ sudo ./hello
Hello world!

X - Méthodologie de développement

Les deux premiers paragraphes s'adressent aux personnes n'ayant pas développé de système RT (ceux qui les trouvent simplistes, je leur rappelle que ce document est destiné aux débutants).

X-A - Prototypage de l'architecture dynamique

L'architecture dynamique comprend les tâches, les structures qu'elles utilisent pour communiquer (files de messages, variables partagées...) et la manière dont elles sont activées (périodiquement, sémaphore...).

L'étape du prototypage valide cet ensemble sans s'embarrasser de l'aspect fonctionnel du système puisque l'application est réduite à une coquille vide ne contenant que des appels Xenomai. Dans le cycle en V, elle s'effectue en phase de conception.

On débute par l'identification des interfaces du logiciel : monde extérieur (réseau...), matériel (carte série...), autres applications Linux, etc.

Ensuite, on identifie les tâches et on les caractérise, par exemple :

- asynchrone (sur quel événement ? interruption, action utilisateur) ou synchrone (sur quel événement ? libération d'un sémaphore...) ;

- si synchrone, la fréquence d'activation ;

- la priorité et sa justification ;

- les variables partagées avec les autres tâches ;

- le mode d'exécution attendu : primary ou secondary (dépend de la contrainte temporelle et des interfaces) ;

- le point d'entrée dans l'architecture statique.

Par la suite, on s'attaque aux variables partagées, ou plus exactement à leur protection. Elles peuvent être protégées par les priorités (surtout si l'architecture est très synchrone, c'est-à-dire que l'on maîtrise totalement l'ordre d'activation des tâches) ou utiliser les mécanismes de Xenomai (mutexes, files de messages...).

Exemple : soient trois tâches :

- Periodic, priorité = 30, périodique ;

- Compute, priorité = 20, synchrone sur Periodic ;

- Display, priorité = 10, synchrone sur Periodic.

Compute et Display partagent une variable appelée buffer.

L'ordre d'activation est alors le suivant :

La variable buffer est alors protégée par conception grâce aux priorités.

Attention: la protection des variables par les priorités n'est valable que dans les systèmes monoCPU.

La tenue des contraintes temporelles est également vérifiée de manière préliminaire à cette étape. Des benchmarks sont réalisés pour les traitements « lourds », en tenant compte des hypothèses les plus défavorables.

La validation du prototype ne doit pas s'arrêter au cas nominal, mais doit également prendre en compte les cas d'erreur. C'est d'ailleurs eux qui compliquent l'application puisqu'il faut terminer proprement l'application alors que tous les objets RT n'ont pas été créés, que des tâches sont bloquées derrière des sémaphores qui ne seront jamais débloqués, etc.

X-B - Pratiques

X-B-1 - Les mauvaises...

Rappelez-vous, rt_task_create prend en paramètre la taille de la pile de la tâche. Contrairement aux tâches Linux, elle n'est pas extensible dynamiquement, l'utilisation de la récursivité est donc à éviter.

X-B-2 - ... et les bonnes

Initialisez les variables locales lors de la déclaration, cela dépend des compilateurs et des options, mais elles peuvent alors avoir une valeur indéterminée (dépend du contenu de la pile) et surtout totalement différente d'une exécution sur l'autre (non déterminisme).

Notez symboliquement les priorités (#define) et regroupez ces définitions (dans un même fichier par exemple), cela permet de comprendre plus facilement le logiciel sans forcément avoir la documentation à côté.

Pour les files de messages, il est toujours intéressant de savoir si on a perdu des messages et combien lors du stockage (cela peut aider à la mise au point en indiquant un dysfonctionnement ou une sous-évaluation du volume des messages). Si l'écriture dans une file échoue, incrémentez un compteur de messages et affichez-le en fin d'exécution.

X-C - Aide à la mise au point

X-C-1 - Ajout de traces

Bien que très pratique, le printf n'est pas toujours accessible (intérieur du noyau) ou impacte les performances temporelles (changement de mode).

Dans les applications hors du noyau, on peut utiliser le même mécanisme que VxWorks : une tâche dédiée à l'affichage des traces. Les tâches appellent la fonction log_write(38) qui écrit un message dans une file. Cette file est scrutée par la tâche log_task qui a la priorité la plus faible et qui ensuite effectue un printf.

Cette solution présente plusieurs avantages :

- on peut insérer des informations en plus du message brut comme le nom de la tâche et la date ;

- à la manière de printk, on peut associer un niveau de criticité du message que l'on filtrera lors de l'affichage au fur et à mesure du développement (syslog).

Parfois, l'ajout de traces est nécessaire à la validation. Dans la version de production, ces traces doivent seulement être inhibées, les performances temporelles sont ainsi identiques.

X-C-2 - Détection des basculements « sauvages » en secondary mode

Certains basculements sont prévisibles (printf, write, read...), d'autres beaucoup moins évidents à trouver (code tiers ou générés automatiquement par exemple).

La suspicion d'un passage en secondary mode peut être envisagée lorsque les performances temporelles attendues sont dégradées ou si le comportement du logiciel est inattendu.

La recherche de la fonction fautive se fait en deux temps :

- l'activation de l'émission du signal SIGXCPU lors du basculement ;

- la recherche de la fonction incriminée.

Un exemple se trouve dans le répertoire d'installation de Xenomai : xenomai-<version>/examples/native/sigxcpu.c (test réalisé avec Xenomai 2.3.3 sur Pentium III).

[root@localhost native]# ./sigxcpu
./sigxcpu(warn_upon_switch+0x1f)[0x80488e7]
[0xffffe420]
/lib/i686/libpthread.so.0[0xb7f51540]
/lib/i686/libc.so.6(__clone+0x5e)[0xb7ee355e]
./sigxcpu(warn_upon_switch+0x1f)[0x80488e7]
[0xffffe420]
/usr/xenomai/lib/libnative.so.0[0xb7f4a0c4]
/lib/i686/libpthread.so.0[0xb7f51540]
/lib/i686/libc.so.6(__clone+0x5e)[0xb7ee355e]
Switched to secondary mode
./sigxcpu(warn_upon_switch+0x1f)[0x80488e7]
[0xffffe420]
/lib/i686/libc.so.6[0xb7e77b05]
/lib/i686/libc.so.6(_IO_do_write+0x1f)[0xb7e77ddf]
/lib/i686/libc.so.6(_IO_file_overflow+0x128)[0xb7e78708]
/lib/i686/libc.so.6(__overflow+0x53)[0xb7e7aeb3]
/lib/i686/libc.so.6(puts+0x140)[0xb7e6f8c0]
./sigxcpu(task_body+0x42)[0x80488c6]
...

On bascule en secondary mode dans la fonction task_body, lors de l'appel de la fonction puts (appelée par printf).

X-C-3 - Tests de bon fonctionnement bas niveau

Il est intéressant dès le début du développement, de prévoir des applications légères de bas niveau afin de vérifier les interfaces matérielles ou encore le câblage. Elles pourront être lancées lorsque rien ne va plus ou encore pour valider le bon fonctionnement du matériel après un remontage.

X-C-4 - Utilisation du débogueur gdb

Comme nous l'avons déjà dit un peu plus haut, les tâches Xenomai s'exécutent en mode utilisateur. On peut donc tout naturellement utiliser gdb, au prix de la perte des performances temps réel bien entendu.

Le programme doit être compilé avec l'option -g de gcc.

XI - Étude de cas n° 1 : simulateur GPS

XI-A - Fonctionnalités

Le but de cette application est de simuler un GPS. Elle de type commande/contrôle (ou dans le jargon utilisé ici télécommande (TC)/télémétrie(TM)).

Ce simulateur est intégré dans un environnement de test qui émule d'autres équipements, ses interfaces sont les suivantes :

- une liaison série RS422 à 9600 bits/s, identique à l'équipement réel (carte PCI Sunix) ;

- trois sockets de communication dialoguant avec l'application principale de conduite de test : contrôle du dialogue et modification des états internes du GPS, enregistrement traces reçues/émises de l'équipement, évènements (journal de bord).

Xenomai a été choisi afin de respecter les temps de réponse de l'équipement et d'avoir une datation précise dans les mesures télémétriques.

La liaison série utilise le driver RTDM fourni avec Xenomai.

XI-B - Architecture dynamique

XI-B-1 - Schéma

XI-B-2 - Justification

XI-B-2-a - Tâche TempsGPS

Rôle : simule l'horloge de bord de l'équipement.

Priorité : 80.

Justification : émulation d'une horloge matérielle.

Type d'activation : asynchrone, périodique 1 Hz.

Variables globales accédées : TuGPS (protégés par sémaphore).

Mode : primary.

XI-B-2-b - Tâche TxTM

Rôle : émet sur le port série les TM générées.

Priorité : 70.

Justification : l'émission des TM est prioritaire sur la génération des TC.

Type d'activation : asynchrone, sur lecture de la file de messages TM_MsgQueue.

Variables globales accédées : TuUTC (protégée par sémaphore).

Mode : primary.

XI-B-2-c - Tâche RxTC

Rôle : réceptionne les TC sur le port série.

Priorité : 60

Justification : la réception des TC prime sur leur traitement.

Type d'activation : asynchrone sur réception sur le port série.

Variables globales accédées : Equipement (protégée par sémaphore).

Mode : primary.

XI-B-2-d - Tâche PerioTM

Rôle : génère les TM périodiques.

Priorité : 50 (Round-robin).

Justification : les deux tâches de production de TM ont la même priorité. La génération est moins prioritaire que le traitement des E/S.

Type d'activation : asynchrone, périodique 1 Hz.

Variables globales accédées : Equipement (protégée par sémaphore).

Message_Ionospheric (protégée par sémaphore).

Message_Ephemesis (protégée par sémaphore).

Message_PVT (protégée par sémaphore).

Message_Almanac (protégée par sémaphore).

ErrorReport (protégée par sémaphore).

TuGPS (protégée par sémaphore).

Mode : primary.

XI-B-2-e - Tâche TraiteTC

Rôle : génère les TM en fonction des TC reçues.

Priorité : 50 (Round-robin).

Justification : les deux tâches de production de TM ont la même priorité. La génération est moins prioritaire que le traitement des E/S.

Type d'activation : asynchrone sur lecture de la file de messages TC_MsgQueue.

Variables globales accédées : Equipement (protégée par sémaphore).

Message_Ionospheric (protégée par sémaphore).

Message_Ephemesis (protégée par sémaphore).

Message_PVT (protégée par sémaphore).

Message_Almanac (protégée par sémaphore).

ErrorReport (protégée par sémaphore).

Mode : primary.

XI-B-2-f - Tâche bvl_GPS

Rôle : communication avec la conduite de test (réception des directives).

Priorité : 20.

Justification : aucune contrainte temps réel. On considère que la terminaison de l'essai (réception d'une directive) est prioritaire sur l'émission des traces.

Type d'activation : asynchrone sur réception d'une directive sur le socket (ou déconnexion).

Variables globales accédées : TuUTC (protégée par sémaphore).

Trace (protégée par sémaphore).

Equipement (protégée par sémaphore).

Message_Ionospheric (protégée par sémaphore).

Mode : secondary.

XI-B-2-g - Tâche JDB

Rôle : enregistrement d'évènements (réception de directives, changements d'état internes...)

Priorité : 10 (Round-robin).

Justification : aucune contrainte temps réel.

Type d'activation : asynchrone sur lecture de la file de messages JDB_MsgQueue.

Variables globales accédées : aucune.

Mode : secondary.

Il n'y a a priori pas de raison de rendre prioritaire l'enregistrement des événements sur les traces, et vice-versa. C'est pourquoi ces deux tâches ont la même priorité et que le round-robin est activé.

XI-B-2-h - Tâche Traces

Rôle : enregistrement des traces.

Priorité : 10 (Round-robin).

Justification : aucune contrainte temps réel.

Type d'activation : asynchrone sur lecture de la file de messages Traces_MsgQueue.

Variables globales accédées : Trace (protégée par sémaphore).

Mode : secondary.

XI-B-2-i - Tâche main

Rôle : attendre la terminaison des tâches.

Priorité : 1.

Justification : déroulement sans impact sur les traitements.

Type d'activation : tâche de fond uniquement active au démarrage à l'arrêt.

Variables globales accédées : aucune.

Mode : secondary.

Généralement les tâches logicielles émulant un comportement matériel (timer, génération de signal...) sont les plus prioritaires de l'architecture.
Les tâches identifiées comme « lentes » sont souvent celles qui enregistrent ou affichent des traces.
Lors de l'affectation initiale des priorités, prévoyez large. Plutôt que 1, 2, 3, 4... préférez 10, 20, 30, 40... en cas d'évolution de la spécification (rajout d'une tâche ou changement de priorité), l'impact sera moins lourd.
Pour le type d'activation, on désigne dans ce document par synchrone une tâche dont l'exécution est déterminée par un évènement (sémaphore libéré toutes les secondes par exemple).
Vous devez toujours veiller à ce que les tâches se terminent proprement. Des problèmes de terminaison indiquent parfois des erreurs de codage.

Les variables globales partagées peuvent être protégées de deux manières :

- par des sémaphores d'exclusion mutuelle ;

- par les priorités, à condition que les tâches soient synchrones, c'est-à-dire que l'on maîtrise toujours leur date d'exécution.

XI-B-2-j - Files de messages

Les files de messages sont utilisées pour que des tâches de priorités différentes puissent communiquer.

Dans la mesure du possible, le dimensionnement de chaque file doit être justifié. Cependant, dans la pratique, on considère souvent une taille maximale qui ne sera jamais atteinte.

XII - Étude de cas n° 2 : driver RTDM Fiji-Oslink

XII-A - Ressources utiles

Le site de Captain Universe contient des exemples de drivers RTDM : http://www.captain.at/xenomai.php.

L'exemple discuté dans ce chapitre est intéressant car il est très simple (pas de gestion d'interruption).

XII-B - Fonctionnalités

La sonde Fiji se connecte sur un port parallèle, elle implémente le protocole bidirectionnel Oslink.

La réception des données se fait par polling car la ligne habituellement utilisée pour lever une interruption (ACK) est utilisée comme ligne d'entrée de données.

XII-C - Anatomie d'un driver RTDM

Un driver RTDM peut être divisé en deux parties : une partie fonctionnelle qui est identique dans un driver Linux « classique », et une partie interfaçage avec Xenomai. C'est ce second point qui est développé ici.

XII-C-1 - Comment interfacer un driver RTDM : la structure rtdm_device

Cette structure contient les informations propres au driver, comme par exemple :

- la version ;

- le nom du périphérique dans l'arborescence Xenomai ;

- la correspondance entre les opérations standard proposées par le driver et l'implémentation interne.

Le comportement des opérations open , close... des drivers RTDM est configurable selon le contexte d'appel (RT ou non).
Cette configuration utilise les champs *_rt ou *_nrt de la structure rtdm_driver.
Les fonctions RT et non RT peuvent être identiques. La discrimination peut être faîte à l'intérieur de la fonction grâce à rtdm_in_rt_context() qui renvoie un booléen .

Dans un driver, l'appel de fonctions depuis un mauvais contexte peut conduire au crash du système.

XII-C-2 - Où mémoriser l'état d'un périphérique ouvert : le contexte

Plutôt qu'utiliser des variables globales pour stocker l'état du périphérique (vitesse, état de la ligne, parité...), les drivers RTDM utilisent la notion de contexte. Ce contexte est accessible depuis toutes les fonctions du driver puisque c'est leur premier paramètre (struct rtdm_dev_context*).

Une partie de cette structure (le champ dev_private) est réservée au développeur, qui peut y placer les données spécifiques à l'équipement (vitesse, parité...).

XII-C-3 - Comment accéder à des zones de mémoire partagée avec les applications ?

Cela concerne les paramètres de type buffer lors des read, write et autres ioctl.

La zone de mémoire que l'on va trouver dans le driver peut être visible depuis le reste de l'application, des tâches peuvent alors potentiellement y accéder.

La solution consiste à sécuriser ces données en les copiant à l'intérieur du driver, les opérations rtdm_copy_from_user et rtdm_copy_to_user sont là pour ça.

Que cela soit en lecture ou en écriture, l'accès doit auparavant toujours être testé avec les fonctions rtdm_rw_user_ok() et rtdm_read_user_ok().

XII-C-4 - Comment partager du code en fonction du contexte d'appel ?

La fonction rtdm_in_rt_context renvoie un booléen(39) valant vrai si l'appel à la fonction est réalisé depuis une tâche Xenomai.

XII-D - Installation et désinstallation

Comme pour les drivers Linux classiques, on utilise les fonctions module_init et module_exit.

L'arborescence des drivers Xenomai est mise à jour dans ces fonctions par rtdm_dev_register et rtdm_dev_unregister.

XIII - Drivers

XIII-A - Communication série

XIII-A-1 - Matériels compatibles

La distribution de Xenomai inclut par défaut le driver RTDM de l'UART 16550A, écrit par Jan Kiszka. Il est compatible avec la grande majorité des composants du marché (8250, 16550n, 16650...).

Pour les autres chipsets, les constructeurs (Moxa, Sunix...) indiquent le plus souvent eux-mêmes cette compatibilité ascendante dans leurs datasheets.

Pour les cartes mixtes RS(EIA) 232/422/485, la configuration électrique se fait généralement au moyen de mini-switches disposés sur la carte. Pensez-y avant de l'installer dans le PC !

XIII-A-2 - Récupération de la configuration matérielle de la carte

Contrairement à Linux qui identifie le matériel au démarrage, il faut préciser au driver série RTDM comment sont configurés les ports série (IRQ et adresse de base).

Pour cela, la manière la plus simple est d'installer la carte série sous Linux avec éventuellement son driver et de récupérer sa configuration.

À ce propos, Linux supporte par défaut(40) quatre ports série, et généralement deux sont déjà présents sur la carte mère. En toute logique, si vous utilisez une carte multiport, il se peut que tous ne soient pas visibles. Il vous faut donc repositionner dans la configuration du noyau les paramètres :

Device Drivers/Character devices/Serial drivers/Maximum number of 8250/16550 serial ports ;

Device Drivers/Character devices/Serial drivers/Maximum number of 8250/16550 serial ports to register at runtime.

Puis recompiler le noyau.

Une fois tous les ports visibles, nous allons utiliser setserial, vous devez être root pour le lancer.

Dans l'exemple suivant, nous disposons d'un PC équipé de deux cartes Moxa 4 ports.

[root@localhost etc]# ls /dev/ttyMI*
/dev/ttyMI0   /dev/ttyMI15  /dev/ttyMI21  /dev/ttyMI28  /dev/ttyMI5
/dev/ttyMI1   /dev/ttyMI16  /dev/ttyMI22  /dev/ttyMI29  /dev/ttyMI6
/dev/ttyMI10  /dev/ttyMI17  /dev/ttyMI23  /dev/ttyMI3   /dev/ttyMI7
/dev/ttyMI11  /dev/ttyMI18  /dev/ttyMI24  /dev/ttyMI30  /dev/ttyMI8
/dev/ttyMI12  /dev/ttyMI19  /dev/ttyMI25  /dev/ttyMI31  /dev/ttyMI9
/dev/ttyMI13  /dev/ttyMI2   /dev/ttyMI26  /dev/ttyMI32
/dev/ttyMI14  /dev/ttyMI20  /dev/ttyMI27  /dev/ttyMI4

# Récupération de la configuration des ports grâce à setserial
[root@localhost ~]#setserial /dev/ttyMI0
/dev/ttyMI0, UART: 16550A, Port: 0x1000, IRQ: 10
[root@localhost ~]# setserial /dev/ttyMI1
/dev/ttyMI1, UART: 16550A, Port: 0x1008, IRQ: 10
[root@localhost ~]# setserial /dev/ttyMI2
/dev/ttyMI2, UART: 16550A, Port: 0x1010, IRQ: 10
[root@localhost ~]# setserial /dev/ttyMI3
/dev/ttyMI3, UART: 16550A, Port: 0x1018, IRQ: 10
[root@localhost ~]# setserial /dev/ttyMI8
/dev/ttyMI4, UART: 16550A, Port: 0x1040, IRQ: 11
...

XIII-A-3 - Configuration du driver RTDM 16550A

Dans notre exemple, le driver est compilé directement dans le noyau. Sa configuration est faite au démarrage du système.

Nous utilisons Lilo, pour passer des paramètres au noyau lors du démarrage, il faut donc modifier le fichier /etc/lilo.conf :

image=/boot/vmlinuz-2.6.24
label="2.6.24_Xenomai"
root=/dev/hda1
initrd=/boot/initrd-2.6.24.img
append="resume=/dev/hda5 xeno_16550A.io=0x1000,0x1008,0x1010,0x1018,0x1040,0x1048,0x1050,0x1058  xeno_16550A.irq=10,10,10,10,11,11,11,11 xeno_16550A.baud_base=921600,921600,921600,921600,921600,921600,921600,921600
vga=788"

L'information baud_base est la vitesse maximale de transmission de la carte, elle est disponible dans sa datasheet.

La ligne append dans lilo.conf est limitée à 255 caractères.

Après un redémarrage, on vérifie que nos ports sont bien installés :

# ls /proc/xenomai/rtdm/
fildes         open_fildes       rtser0/  rtser2/  rtser4/   rtser6/
named_devices  protocol_devices  rtser1/  rtser3/   rtser5/   rtser7/

Si le driver est compilé en tant que module noyau, les paramètres sont des options de la commande de chargement insmod. Ils sont alors débarrassés de leur préfixe xeno_16550A.

# insmod /lib/modules/`uname -r`/kernel/drivers/xenomai/serial/xeno_16550A.ko io=0x1000 irq=10 baudrate=921600

Les autres paramètres possibles sont le redimensionnement du buffer de transmission (tx_fifo, positionné à 16 octets par défaut(41)) et le numéro de départ des rtsern.

XIII-A-4 - Configuration de la liaison avec l'ioctlRTSER_RTIOC_SET_CONFIG

Cet ioctl utilise la structure rtser_config, dont les champs sont décrits ci-dessous :

config_mask

Les paramètres de la liaison série ne sont pas positionnables séparément puisqu'à chaque fois la structure rtser_config est passée en argument. Ce masque permet de choisir les champs mis à jour.

baud_rate

Vitesse de la liaison.

parity

Parité.

data_bits

Nombre de bits de données.

stop_bits

Nombre de bits de stop.

handshake

Handshaking.

fifo_depth

Seuil de réception des données.

rx_timeout

Temps d'attente des données lors de l'appel à rt_dev_read (en nanosecondes).
RTSER_TIMEOUT_INFINITE provoque une lecture bloquante.
RTSER_TIMEOUT_NONE sort immédiatement si aucune donnée n'a été reçue, dans ce cas rt_dev_read retourne -EAGAIN.

tx_timeout

Temps de transmission des données (en nanosecondes). Si dans le rt_dev_write, les données n'ont pas été transmises pendant ce laps de temps, alors -ETIMEDOUT est retourné.

event_timeout

Utilisé par l'ioctl RTSER_RTIOC_WAIT_EVENT. Détermine le timeout sur la réception de l'évènement.

timestamp_history

Utilisé par l'ioctl RTSER_RTIOC_WAIT_EVENT. Active la datation des données reçues.

event_mask

Utilisé par l'ioctl RTSER_RTIOC_WAIT_EVENT. Événement sur lequel devient passant cet ioctl : état de la ligne, réception de données, erreurs.

XIII-B - Comment écrire un driver PCI ?

Ce chapitre a pour but de présenter de façon simple et succincte comment interfacer des périphériques PCI sous Xenomai.

XIII-B-1 - Documentation de référence

Les documents suivants ont servi à la rédaction de ce chapitre :

Web : http://en.wikipedia.org/wiki/Conventional_PCI ;

Web : http://en.wikipedia.org/wiki/PCI_configuration_space ;

Livre : Essential Linux Device Drivers (ISBN-13: 978-0132396554) ;

Texte : <Linux Sources>/Documentation/PCI/pci.txt.

XIII-B-2 - Configuration vs initialisation

Au démarrage du système, sur x86, le BIOS se charge de configurer les périphériques PCI ; sur d'autres plateformes, c'est le noyau Linux qui se charge de ce travail.

Mais, quelle que soit la cible matérielle, lorsque vous arrivez pour initialiser votre driver Linux/Xenomai, le périphérique PCI est déjà « configuré », vous n'avez pas à le faire. On entend par périphérique PCI « configuré », le fait que sa plage d'adresse mémoire est déjà réservée, que son IRQ lui est attribué, etc. ; le périphérique n'est pas encore initialisé !

L'initialisation est en effet spécifique à chaque périphérique et c'est le rôle du driver. Un driver PCI Xenomai va utiliser Linux pour découvrir la configuration du périphérique, récupérer son IRQ et les plages d'adresses d'IO ou mémoire qu'il utilise. On ne va pas chercher à ce que Linux puisse communiquer avec ce périphérique via notre driver. On utilise Linux ici comme un moyen de « découvrir » le bus PCI, rien d'autre.

L'utilisation de l'API PCI Linux et des fonctions appropriées de manipulation des IO/mémoire permet de créer des drivers PCI multiplateformes.

XIII-B-3 - L'écriture d'un driver étape par étape

Un driver PCI Xenomai doit :

static struct pci_device_id peak_pci_tbl[] = {
{PEAK_PCI_VENDOR_ID, PEAK_PCI_DEVICE_ID, PCI_ANY_ID, PCI_ANY_ID, 0, 0, 0},
{ }
};
MODULE_DEVICE_TABLE (pci, peak_pci_tbl);

Comme pour la majorité des « couches driver » Linux, il convient également d'enregistrer les callbacks qui seront appelées pour piloter notre périphérique PCI. C'est par exemple le cas sur tty, carte réseau, USB, etc. Ici, on se concentre uniquement sur un driver PCI et Xenomai.

Par conséquent, on ne va utiliser que le strict minimum fourni par Linux : les fonctions de « probe » et de « remove » appelées par Linux quand il découvre un périphérique PCI ou à l'arrêt du système. Les autres callbacks ne nous concernent pas et on ne souhaite pas de toute façon faire d'accès direct au matériel via Linux.

Concrètement, dans le code cela donne :

static struct pci_driver rtcan_peak_pci_driver = {
.name     = RTCAN_DRV_NAME,
.id_table = peak_pci_tbl,
.probe    = peak_pci_init_one,
.remove   = __devexit_p(peak_pci_remove_one),
};

static int __init rtcan_peak_pci_init(void)
{
return pci_register_driver(&rtcan_peak_pci_driver);
}

pci_read_config_word(pdev, 0x2e, &sub_sys_id)

Un autre exemple tiré du livre Essential Linux Device Drivers(42), pour lire l'IRQ associé à la carte :

unsigned char irq ;
pci_read_config_byte(pdev, PCI_INTERRUPT_LINE, &irq);

Vous n'avez donc pas besoin de savoir parmi les 64 octets de la zone de configuration lequel indique l'IRQ attribuée par le BIOS, la macro PCI_INTERRUPT_LINE que l'on trouve dans include/linux/pci_regs.h suffit. Référez-vous à ce fichier pour plus d'informations sur les macros et valeurs récupérables dans la zone de configuration de votre carte PCI.

Chaque périphérique PCI possède jusqu'à six « Base Address Register » ou « BAR ». Qu'est-ce que c'est ?

De façon simple, parmi les 64 octets de configuration, 6*4 octets indiquent des zones mémoire que la carte PCI va utiliser. L'utilisation de ces six plages d'adresses est laissée à la discrétion du constructeur de la carte. Il convient de se reporter à la configuration de la carte pour savoir combien de ces « BAR » sont utilisés.

Parmi ces « BAR », certains font référence à des adresses d'IO, d'autres des adresses mémoire ; encore une fois c'est la documentation de la carte qui l'indique.

- soit en s'adressant au constructeur.

- soit en « rétrofitant » à partir des sources Linux.

Pour développer un driver PCI, il vous faut impérativement la documentation « développeur » de la carte. Il y a deux manières de l'obtenir.
L'idéal étant tout de même d'avoir les deux (la théorie et la pratique qui fonctionne).
Cependant, certains constructeurs ne jouent pas forcément le jeu et ne fournissent qu'un module Linux (fichier .ko) avec leur carte. Ceci doit donc faire l'objet d'une attention particulière lors de la sélection du matériel.
À noter également que certains fabricants vendent séparément le code de leurs drivers Linux ou les fournissent gratuitement contre la signature d'un accord de confidentialité (43) (TEWS Technologies par exemple).

Linux fournit dans son API PCI les fonctions pour connaître si un « BAR » est défini et vers quelle plage d'adressage il renvoie. Avec ces fonctions Linux, il est ainsi possible de récupérer lors de la phase d'initialisation du driver les plages d'adresses qui vont nous intéresser et ainsi pouvoir y lire/écrire pour piloter notre matériel. Vous pouvez vous reporter aux fonctions pci_resource_[start|len|end|flags] qui prennent en premier argument le « descripteur PCI » passé par Linux et en second argument le numéro de BAR.

Exemple :

unsigned long addr;
addr = pci_resource_start(pdev, 0);

Note : le bus PCI 32 bits manipule des adresses de 32 bits, il est donc normal d'utiliser des « unsigned long » au lieu de « pointeurs » qui eux ont une taille qui varie suivant le bus d'adresse du processeur. Les « long » sont définis par la norme ANSI C sur 32 bits et ne varient pas suivant les compilateurs/plateformes. Le code est ainsi portable.

L'accès en lecture/écriture vers le périphérique se fait soit dans les IO (méthode d'accès PIO) soit directement en zone mappée en mémoire (méthode d'accès MMIO). C'est la documentation du périphérique qui donne ces informations. Au niveau de la programmation du driver, la seule chose qui change est l'API qui doit être utilisée.

·     io_base = pci_ressouce_start(my_pci_dev, 0) ; è On obtient ainsi l'adresse de base de BAR0

·     request_region(io_base, 1024, “my_driver_xenomai_pci”); è On annonce à Linux qu'on va utiliser 1024 octets à partir de l'adresse de base de BAR0. Ici, on suppose que la documentation du périphérique nous a donné cette information. Cette fonction sert surtout à vérifier qu'un autre driver n'accède pas déjà à cette zone. Auquel cas il y a conflit (chevauchement) et il convient de retourner un code d'erreur.

·     data = inb(io_base + 5) ; è On accède au 6e octet de la zone IO BAR0. On pourrait y écrire de la même manière : outb(0x01, iobase+5) è On écrit 0x01 dans le 6e octet de la zone IO BAR0.

·     io_base = pci_ressouce_start(my_pci_dev, 0); è On obtient ainsi l'adresse de base de BAR0.

·     io_longueur = pci_resource_length(my_pci_dev, 0); è On obtient la longueur annoncée de la zone mémoire BAR0. Ici, l'information a directement été trouvée dans la configuration PCI. On n'a pas recours à la documentation du périphérique.

·     request_mem_region(io_base, io_longueur, "my_driver_xenomai_pci"); è On annonce  Linux qu'on va utiliser la zone mémoire démarrant à l'adresse de base « io_base » et de longueur « io_longueur » octets. Cette fonction sert surtout à vérifier qu'un autre driver n'accède pas déjà à cette zone. Auquel cas il y a conflit et il convient de retourner un code d'erreur.

Note : pci_request_region est une fonction fournie par Linux qui permet de faire les trois étapes ci-dessus à la fois.

·    L'adresse mémoire « noyau » que vous pourrez accéder dans votre driver n'a pas la même valeur que celle trouvée par BAR0 et c'est tout à fait normal. En effet, le noyau utilise des adresses virtuelles, et c'est la MMU qui fait la translation « adresse noyau à adresse bus ». Pour obtenir l'équivalent « noyau » de notre adresse « bus » il faut utiliser la fonction ioremap().

Exemple :
adresse_noyau = ioremap(io_base, io_longueur) ;

Parfois, ces zones mappées en mémoire et qui correspondent au périphérique PCI, ne supportent pas d'être mises en cache par le processeur. En effet, lors d'un accès au bus mémoire (méthode MMIO), le processeur peut mettre dans son cache interne ces données pour en accélérer l'obtention lors d'un prochain accès. Mais cette « mémoire » est contrôlée directement par le périphérique et peut changer de valeur spontanément, lorsque le périphérique la met à jour. Ainsi, le cache du processeur peut contenir une valeur obsolète qui serait retournée lors du prochain read[b|l|..].

ioremap_nocache() est la fonction identique à ioremap() décrite ci-dessus, mais garantit que l'adresse noyau retournée ne sera pas mise en cache par le CPU.

Note 1 : la fonction pci_resource_flag() donne accès à ce type d'information pour savoir si la zone « BAR » peut être mise en cache ou non par le processeur.

Note 2 : voir lib/iomap.c pour l'utilisation de la fonction pci_iomap() qui regroupe en une seule opération celles décrites jusqu'ici.

·    data = readb(adresse_noyau+5)  è On accède au 6e octet de la zone mémoire BAR0. On pourrait écrire de la même manière : writeb(0x01, adresse_noyau +5) è On écrit 0x01 dans le 6e octet de la zone mémoire BAR0.

Astuce : concernant les « réservations » d'IO ou mémoire faites par le driver, vous pouvez les observer à travers /proc/ioports et /proc/iomem.

Voici l'exemple du driver CAN SJA1000 fourni avec Xenomai : rtan_peak_pci.c. Les fonctions mentionnées ci-dessus y sont surlignées. Ce driver succinct permet de retrouver les étapes qu'il faut pour mettre en œuvre un driver Xenomai PCI. Dans ce driver, la méthode d'accès est MMIO.

Ce code ne contient que la partie PCI « visible », le pilotage de la carte fait appel à d'autres fichiers source.

/*
* Copyright (C) 2006 Wolfgang Grandegger <wg@grandegger.com>
*
* Derived from the PCAN project file driver/src/pcan_pci.c:
*
* Copyright (C) 2001-2006  PEAK System-Technik GmbH
*
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/

#include <linux/module.h>
#include <linux/ioport.h>
#include <linux/delay.h>
#include <linux/pci.h>
#include <asm/io.h>

#include <rtdm/rtdm_driver.h>

/* CAN device profile */
#include <rtdm/rtcan.h>
#include <rtcan_dev.h>
#include <rtcan_raw.h>
#include <rtcan_sja1000.h>
#include <rtcan_sja1000_regs.h>

#define RTCAN_DEV_NAME    "rtcan%d"
#define RTCAN_DRV_NAME    "PEAK-PCI-CAN"

static char *peak_pci_board_name = "PEAK-PCI";

MODULE_AUTHOR("Wolfgang Grandegger <wg@grandegger.com>");
MODULE_DESCRIPTION("RTCAN board driver for PEAK-PCI cards");
MODULE_SUPPORTED_DEVICE("PEAK-PCI card CAN controller");
MODULE_LICENSE("GPL");

struct rtcan_peak_pci
{
struct pci_dev *pci_dev;
struct rtcan_device *slave_dev;
int channel;
volatile void __iomem *base_addr;
volatile void __iomem *conf_addr;
};

#define PEAK_PCI_CAN_SYS_CLOCK (16000000 / 2)

#define PELICAN_SINGLE  (SJA_CDR_CAN_MODE | SJA_CDR_CBP | 0x07 | SJA_CDR_CLK_OFF)
#define PELICAN_MASTER  (SJA_CDR_CAN_MODE | SJA_CDR_CBP | 0x07            )
#define PELICAN_DEFAULT (SJA_CDR_CAN_MODE                                 )

#define CHANNEL_SINGLE 0 /* this is a single channel device */
#define CHANNEL_MASTER 1 /* multi channel device, this device is master */
#define CHANNEL_SLAVE  2 /* multi channel device, this is slave */

// important PITA registers
#define PITA_ICR         0x00        // interrupt control register
#define PITA_GPIOICR     0x18        // general purpose IO interface control register
#define PITA_MISC        0x1C        // miscellanoes register

#define PEAK_PCI_VENDOR_ID   0x001C  // the PCI device and vendor IDs
#define PEAK_PCI_DEVICE_ID   0x0001

#define PCI_CONFIG_PORT_SIZE 0x1000  // size of the config io-memory
#define PCI_PORT_SIZE        0x0400  // size of a channel io-memory

static struct pci_device_id peak_pci_tbl[] = {
{PEAK_PCI_VENDOR_ID, PEAK_PCI_DEVICE_ID, PCI_ANY_ID, PCI_ANY_ID, 0, 0, 0},
{ }
};
MODULE_DEVICE_TABLE (pci, peak_pci_tbl);

static u8 rtcan_peak_pci_read_reg(struct rtcan_device *dev, int port)
{
struct rtcan_peak_pci *board = (struct rtcan_peak_pci *)dev->board_priv;
return readb(board->base_addr + ((unsigned long)port << 2));
}

static void rtcan_peak_pci_write_reg(struct rtcan_device *dev, int port, u8 data)
{
struct rtcan_peak_pci *board = (struct rtcan_peak_pci *)dev->board_priv;
writeb(data, board->base_addr + ((unsigned long)port << 2));
}

static void rtcan_peak_pci_irq_ack(struct rtcan_device *dev)
{
struct rtcan_peak_pci *board = (struct rtcan_peak_pci *)dev->board_priv;
u16 pita_icr_low;

/* Select and clear in Pita stored interrupt */
pita_icr_low = readw(board->conf_addr + PITA_ICR);
if (board->channel == CHANNEL_SLAVE) {
if (pita_icr_low & 0x0001)
writew(0x0001, board->conf_addr + PITA_ICR);
}
else {
if (pita_icr_low & 0x0002)
writew(0x0002, board->conf_addr + PITA_ICR);
}
}

static void rtcan_peak_pci_del_chan(struct rtcan_device *dev,
int init_step)
{
struct rtcan_peak_pci *board;
u16 pita_icr_high;

if (!dev)
return;

board = (struct rtcan_peak_pci *)dev->board_priv;

switch (init_step) {
case 0:             /* Full cleanup */
printk("Removing %s %s device %s\n",
peak_pci_board_name, dev->ctrl_name, dev->name);
rtcan_sja1000_unregister(dev);
case 5:
pita_icr_high = readw(board->conf_addr + PITA_ICR + 2);
if (board->channel == CHANNEL_SLAVE) {
pita_icr_high &= ~0x0001;
} else {
pita_icr_high &= ~0x0002;
}
writew(pita_icr_high, board->conf_addr + PITA_ICR + 2);
case 4:
iounmap((void *)board->base_addr);
case 3:
if (board->channel != CHANNEL_SLAVE)
iounmap((void *)board->conf_addr);
case 2:
rtcan_dev_free(dev);
case 1:
break;
}

}

static int rtcan_peak_pci_add_chan(struct pci_dev *pdev, int channel,
struct rtcan_device **master_dev)
{
struct rtcan_device *dev;
struct rtcan_sja1000 *chip;
struct rtcan_peak_pci *board;
u16 pita_icr_high;
unsigned long addr;
int ret, init_step = 1;

dev = rtcan_dev_alloc(sizeof(struct rtcan_sja1000),
sizeof(struct rtcan_peak_pci));
if (dev == NULL)
return -ENOMEM;
init_step = 2;

chip = (struct rtcan_sja1000 *)dev->priv;
board = (struct rtcan_peak_pci *)dev->board_priv;

board->pci_dev = pdev;
board->channel = channel;

if (channel != CHANNEL_SLAVE) {

addr = pci_resource_start(pdev, 0);
board->conf_addr = ioremap(addr, PCI_CONFIG_PORT_SIZE);
if (board->conf_addr == 0) {
ret = -ENODEV;
goto failure;
}
init_step = 3;

/* Set GPIO control register */
writew(0x0005, board->conf_addr + PITA_GPIOICR + 2);

if (channel == CHANNEL_MASTER)
writeb(0x00, board->conf_addr + PITA_GPIOICR); /* enable both */
else
writeb(0x04, board->conf_addr + PITA_GPIOICR); /* enable single */

writeb(0x05, board->conf_addr + PITA_MISC + 3);  /* toggle reset */
mdelay(5);
writeb(0x04, board->conf_addr + PITA_MISC + 3);  /* leave parport mux mode */
} else {
struct rtcan_peak_pci *master_board =
(struct rtcan_peak_pci *)(*master_dev)->board_priv;
master_board->slave_dev = dev;
board->conf_addr = master_board->conf_addr;
}

addr = pci_resource_start(pdev, 1);
if (channel == CHANNEL_SLAVE)
addr += 0x400;

board->base_addr = ioremap(addr, PCI_PORT_SIZE);
if (board->base_addr == 0) {
ret = -ENODEV;
goto failure;
}
init_step = 4;

dev->board_name = peak_pci_board_name;

chip->read_reg = rtcan_peak_pci_read_reg;
chip->write_reg = rtcan_peak_pci_write_reg;
chip->irq_ack = rtcan_peak_pci_irq_ack;

/* Clock frequency in Hz */
dev->can_sys_clock = PEAK_PCI_CAN_SYS_CLOCK;

/* Output control register */
chip->ocr = SJA_OCR_MODE_NORMAL | SJA_OCR_TX0_PUSHPULL;

/* Clock divider register */
if (channel == CHANNEL_MASTER)
chip->cdr = PELICAN_MASTER;
else
chip->cdr = PELICAN_SINGLE;

strncpy(dev->name, RTCAN_DEV_NAME, IFNAMSIZ);

/* Register and setup interrupt handling */
chip->irq_flags = RTDM_IRQTYPE_SHARED;
chip->irq_num = pdev->irq;
pita_icr_high = readw(board->conf_addr + PITA_ICR + 2);
if (channel == CHANNEL_SLAVE) {
pita_icr_high |= 0x0001;
} else {
pita_icr_high |= 0x0002;
}
writew(pita_icr_high, board->conf_addr + PITA_ICR + 2);
init_step = 5;

printk("%s: base_addr=%p conf_addr=%p irq=%d\n", RTCAN_DRV_NAME,
board->base_addr, board->conf_addr, chip->irq_num);

/* Register SJA1000 device */
ret = rtcan_sja1000_register(dev);
if (ret) {
printk(KERN_ERR
"ERROR %d while trying to register SJA1000 device!\n", ret);
goto failure;
}

if (channel != CHANNEL_SLAVE)
*master_dev = dev;

return 0;

failure:
rtcan_peak_pci_del_chan(dev, init_step);
return ret;
}

static int __devinit peak_pci_init_one (struct pci_dev *pdev,
const struct pci_device_id *ent)
{
int ret;
u16 sub_sys_id;
struct rtcan_device *master_dev = NULL;

printk("%s: initializing device %04x:%04x\n",
RTCAN_DRV_NAME,  pdev->vendor, pdev->device);

if ((ret = pci_enable_device (pdev)))
goto failure;

if ((ret = pci_request_regions(pdev, RTCAN_DRV_NAME)))
goto failure;

if ((ret = pci_read_config_word(pdev, 0x2e, &sub_sys_id)))
goto failure_cleanup;

/* Enable memory space */
if ((ret = pci_write_config_word(pdev, 0x04, 2)))
goto failure_cleanup;

if ((ret = pci_write_config_word(pdev, 0x44, 0)))
goto failure_cleanup;

if (sub_sys_id > 3) {
if ((ret = rtcan_peak_pci_add_chan(pdev, CHANNEL_MASTER,
&master_dev)))
goto failure_cleanup;
if ((ret = rtcan_peak_pci_add_chan(pdev, CHANNEL_SLAVE,
&master_dev)))
goto failure_cleanup;
} else {
if ((ret = rtcan_peak_pci_add_chan(pdev, CHANNEL_SINGLE,
&master_dev)))
goto failure_cleanup;
}

pci_set_drvdata(pdev, master_dev);
return 0;

failure_cleanup:
if (master_dev)
rtcan_peak_pci_del_chan(master_dev, 0);

pci_release_regions(pdev);

failure:
return ret;

}

static void __devexit peak_pci_remove_one (struct pci_dev *pdev)
{
struct rtcan_device *dev = pci_get_drvdata(pdev);
struct rtcan_peak_pci *board = (struct rtcan_peak_pci *)dev->board_priv;

if (board->slave_dev)
rtcan_peak_pci_del_chan(board->slave_dev, 0);
rtcan_peak_pci_del_chan(dev, 0);

pci_release_regions(pdev);
pci_disable_device(pdev);
pci_set_drvdata(pdev, NULL);
}

static struct pci_driver rtcan_peak_pci_driver = {
.name       = RTCAN_DRV_NAME,
.id_table   = peak_pci_tbl,
.probe            = peak_pci_init_one,
.remove           = __devexit_p(peak_pci_remove_one),
};

static int __init rtcan_peak_pci_init(void)
{
return pci_register_driver(&rtcan_peak_pci_driver);
}

static void __exit rtcan_peak_pci_exit(void)
{
pci_unregister_driver(&rtcan_peak_pci_driver);
}

module_init(rtcan_peak_pci_init);
module_exit(rtcan_peak_pci_exit);

XIV - Métriques : mesures de performances et d'occupation mémoire

XIV-A - Latence sur basculement de contexte

L'utilitaire /usr/xenomai/bin/switchtest permet de mesurer plusieurs basculements de tâches :

- kernel mode/kernel mode,

- primary mode/primary mode,

- secondary mode/secondary mode,

- primary mode/secondary mode alternativement.

Petit rappel au passage : les tâches en mode kernel sont lancées grâce à la fonction rtdm_task_start(), leur priorité évolue dans le même espace que les tâches Xenomai.

XIV-B - Latence sur interruption

La mesure de la latence sur interruption nécessite 2 machines. La connexion est réalisée via liaison série.

Une machine hôte Linux exécute /usr/xenomai/bin/irqbench et la machine cible Xenomai /usr/xenomai/bin/irqloop.

XIV-C - Taux d'occupation CPU

Les statistiques doivent être activées (). L'occupation CPU par CPU est contenue dans le fichier /proc/xenomai/stat .

XIV-D - Taille de la pile

La taille courante de la pile n'étant pas accessible depuis la structure RT_TASK_INFO (cf. l'exemple pour VxWorks : http://www.xs4all.nl/~borkhuis/vxworks/vxw_pt5.html#5.9How can I detect a stack overflow ?), cette mesure n'est donc pas réalisable.

Il est à noter que la récursivité est généralement bannie des programmes critiques.

XV - Simulation avec Xenosim

Xenosim est un simulateur pour applications Xenomai, il disponible à l'adresse suivante : http://download.gna.org/xenomai/stable/simulator.

Cependant, étant donné qu'il utilise gcc 2.95.3 et que celui-ci ne supporte pas les architectures x86_64, ce chapitre ne fournit pas d'exemple d'installation et d'utilisation.

XVI - Matériels : recommandations, constructeurs, compatibilités

XVI-A - Opto-isolation des interfaces

Certains constructeurs proposent des cartes dites « opto-isolées ». En entrée ou en sortie de la carte, la liaison électrique est brièvement remplacée par une liaison optique. Cette liaison met en jeu une LED et un phototransistor, physiquement noyés dans un boîtier de la taille d'un circuit intégré.

Il est recommandé d'utiliser ce type de carte afin de préserver tant le PC et sa carte que l'appareil connecté des mauvais câblages, boucles de masse, fausses manipulations, foudre, etc.

XVI-B - PC industriel

L'emploi d'un PC industriel conditionné dans un boîtier 19'' rackable dans une baie apporte un gain de place et de manutention intéressant dans un environnement de type laboratoire. En exploitation, la suppression du clavier, de la souris et de l'écran, avec une prise de contrôle à distance est également une possibilité pour réduire le nombre de câbles.

À noter que certains modèles disposent des sorties de cartes en face avant, ce qui rend plus aisé la connectique.

XVI-C - Refroidissement

Logiquement, si la fonctionnalité d'économie d'énergie du processeur est désactivée, il va tout le temps chauffer. Soyez attentif à la qualité de la ventilation (robustesse et silence) afin d'éviter les messages de type « thermal error ».

XVI-D - Connectique (aka jurisprudence de la loi de Murphy)

Par expérience, entre les manipulations sans ménagement, les faux contacts, les soudures qui cassent, les Tx/Rx croisés qui ne le sont pas… , il est préférable de passer par un câbleur dès qu'il s'agit d'un raccordement spécifique.

XVI-E - Drivers Linux

Si le matériel doit être adressé en primary mode, il faut s'assurer de la disponibilité de son driver Linux afin d'avoir une base pour effectuer le portage vers un driver RTDM.

XVII - Aspects juridiques, licences

Le code bas niveau de Xenomai est sous licence GPL, les modules de plus haut niveau sont sous licence LGPL (Lesser GPL).

XVII-A - Notions d'utilisation normale et de travail dérivé

La GPL distingue deux cas d'utilisation des produits qu'elle licencie :

- l'utilisation normale, par exemple un programme utilisateur qui appelle les services du noyau Linux (qui est sous GPL) ;

- le travail dérivé qui sous-entend que le programme développé ne peut se passer du produit GPL (bibliothèque graphique par exemple).

Dans le premier cas, pas de problème, le programme reste propriété de l'utilisateur qui n'a aucune obligation particulière. Dans le second cas de figure, la GPL dit que tout travail dérivé d'un produit GPL passe lui-même sous cette licence, ce qui implique de diffuser les sources en même temps que le produit final.

D'un point de vue de la confidentialité industrielle, la question des licences des produits employés est très importante.
Le conseil de juristes en propriété intellectuelle permet de lever ces risques.

Afin d'être complet, citons également la LGPL, qui est une GPL « relâchée » ne rendant pas obligatoire la diffusion des sources du travail dérivé (exemple : la bibliothèque graphique GTK).

Les modules du noyau (LKM) ne relèvent pas de la GPL mais sont une utilisation normale du noyau.

XVII-B - Travail dérivé du noyau

Le développement d'une application Xenomai constitue t-il un travail dérivé du noyau Linux ?

Après ce qui vient d'être dit, étant donné que l'on va bien au-delà d'une simple utilisation du noyau puisqu'on patche ses sources durant l'installation, la question se pose...

Étant donné la position très particulière de Xenomai vis-à-vis du noyau Linux, une exception est prévue dans la licence :

Extrait du fichier include/COPYING :

As a special exception to the following license, the Xenomai
project gives permission for additional uses of the header files
contained in this directory.
The exception is that, if you include these header files unmodified to
produce application programs executing in user-space that use Xenomai services by normal Xenomai system calls, this does not by itself cause the resulting executable to be covered by the GNU General Public License. This is merely considered normal use of the Xenomai system, and does not fall under the heading of "derived work".
This exception does not however invalidate any other reasons why the executable file might be covered by the GNU General Public License. Inany case, this exception never applies when the application code is built as a static or dynamically loadable portion of the Linux kernel.
This exception applies only to the code released by the Xenomai project under the name Xenomai and bearing this exception notice.
If you copy code from other sources into a copy of Xenomai, the exception does not apply to the code that you add in this way.

Le développement d'une application Xenomai est considéré comme étant une utilisation normale.
Il n'y a donc pas d'obligation de diffuser les sources du travail effectué.

XVII-C - Cession d'un produit basé sur Xenomai

La diffusion du produit n'est pas restreinte au client direct, il peut à son tour le céder à un tiers.

XVII-D - Coût

Xenomai est totalement gratuit. Il n'y a ni coût d'acquisition de l'environnement de développement, ni coût de distribution des produits (royalties).

XVIII - Organisation du développement

XVIII-A - Plusieurs développeurs sur le même poste

Si on considère le cas d'un développement sur PC, l'idéal est que chaque développeur ait sa propre machine pour au moins deux raisons :

- en cours de développement, l'application peut crasher tout le système, d'autres personnes pouvant alors perdre leur travail ;

- lorsque l'on utilise une cible spécificique (et en général onéreuse), la phase de test est problématique car un seul développeur peut valider à la fois. Sur PC, le faible coût du matériel lève cette limitation.

Si le développement implique plusieurs machines (PC), il faut être attentif à leur configuration matérielle, elle doit être identique.

XVIII-B - Portabilité des Makefile

Au lieu de placer des chemins en dur dans le Makefile pour pointer les répertoires include et les bibliothèques, il est recommandé d'utiliser la commande xeno-config.

Parmi les options intéressantes de xeno-config, on trouve :

--xeno-cflags

Flags à positionner lors de la compilation

--xeno-ldflags

Flags à positionner lors de l'édition de liens

--library-dir

Emplacement des bibliothèques Xenomai

--verbose

Affiche toutes les options disponibles

XIX - À l'aide !!!

XIX-A - Les mailing-lists

La page d'accueil des mailling-lists se trouve à cette adresse https://gna.org/mail/?group=xenomai.

En tant qu'utilisateur, vos problèmes concernent la liste xenomai-help :

https://mail.gna.org/listinfo/xenomai-help.

Les messages doivent être rédigés en anglais, et n'oubliez pas : soyez polis ! Ayez à l'esprit que bonjour et merci n'ont jamais tué personne.

XIX-B - Comment rédiger un message ?

Afin d'aider ceux qui vont vous lire et consacrer un peu de leur temps à vous aider, voici quelques points à ne pas oublier :

- indiquez toujours vos versions de Linux, Xenomai et Adeos dans vos messages ;

- utilisez les dernières versions du noyau Linux, de Xenomai et d'Adeos, les problèmes que vous rencontrez ont peut-être été corrigés dans les nouvelles versions. La Communauté ne maintient pas les versions obsolètes ;

- postez des codes com-pi-la-bles. Circonscrivez vos bugs à quelques lignes, cela facilitera la tâche de tous pour les reproduire.

XX - À propos de l'auteur

Ingénieur en logiciel embarqué, après avoir développé des logiciels de vol satellite, je suis ensuite passé à la réalisation de moyens de tests avec les technologies VxWorks et National Instruments. J'utilise les logiciels libres depuis de nombreuses années et, depuis 2006, Xenomai pour la réalisation de simulateurs d'équipements spatiaux.

Vous pouvez me contacter à cette adresse : david.chabal@yahoo.fr .

XXI - Annexes

XXI-A - Codes d'erreurs spécifiques Xenomai, causes possibles

Tableau 3: Codes d'erreurs

EPERM

1

Mauvais environnement d'appel.

ENOENT

2

Rare. /dev/rtheap non accessible.

ESRCH

3

EINTR

4

EIO

5

Erreur d'entrée/sortie bas niveau (driver).

ENXIO

6

EBADF

9

Mauvais descripteur de fichier. Exemple: rt_dev_read avec comme argument un descripteur fermé.

EWOULDBLOCK

11

L'appel à la fonction aurait été bloquant si TM_NONBLOCK n'avait pas été passé en paramètre.
Ne correspond pas forcément à un cas d'erreur.

ENOMEM

12

Zone allouée par Nucleus () insuffisante.

EFAULT

14

EBUSY

16

EEXIST

17

Objet existant déjà, vérifiable dans le répertoire /proc/xenomai/registery.
Objet non supprimé (versions antérieures à la 2.4).

ENODEV

19

Pour les timers, indique que le noyau n'inclut pas le mode périodique.

EINVAL

22

EDEADLK

35

Rare. rt_task_join récursif.

ENOSYS

38

EIDRM

43

EPIPE

86

ENOBUFS

105

ETIMEDOUT

110

Time-out expiré.

XXI-B - Timers : indexation avec une table de hâchage

Cette méthode, choisie lors de la configuration du noyau, permet de choisir le timer échu en O(1). Ce paragraphe décrit l'algorithme utilisé et ses limitations.

La seule manière d'obtenir un tel résultat est d'utiliser un tableau dont les indices représentent la date d'échéance du timer. Problème : il faudrait des ressources mémoire infinies ! Xenomai utilise une implémentation basée sur un tableau circulaire de timers (timing wheel). L'exemple ci-dessous décrit comment sont rangés trois timers.

Soit le tableau circulaire callwheel comprenant callwheelsize éléments. Les éléments sont de type liste de timers.

Un timer expirant à la date t sera rangé dans une liste du tableau de la manière suivante :
indice = t % callwheelsize
soit la case (backet) :
callwheel[t % callwheelsize]

On ajoute également au timer une information supplémentaire : c_time.
c_time est initialisé à la valeur :
c_time = (t-now)/callwheelsize
now étant la date courante.

À chaque interruption timer, on incrémente l'index. On décrémente le champ c_time de tous les timers de la liste pointée par callwheel[index]. Si des timers ont leur champ qui est inférieur ou égal à 0, alors ils sont expirés.

Dans l'exemple ci-dessous :

Exemple de timer circulaire (version simplifiée à 16 éléments)

À now=1 et on stocke trois timers expirant aux dates : 3, 89 et 137.

Ci-dessous, la résolution de la roue : le paramètre Timer wheel step configurable dans le noyau :

Timer wheel step

Les deux derniers timers sont dans le même backet, ils seront donc parcourus lorsque l'index sera sur eux. C'est pourquoi, il est recommandé qu'il n'y ait pas plusieurs timers dans le même slot, on risque de retomber dans une complexité en O(N).
En interne, Nucleus utilise la notion de prochain timer expiré. Cette recherche implique le parcours de tous les slots jusqu'à ce qu'un timer soit rencontré. Dans le pire des cas, aucun timer n'est présent. C'est pourquoi il est recommandé d'avoir un timer de la période d'un slot (Timer wheel step) afin d'avoir un temps de recherche correct.

XXI-C - Exemple d'implémentation d'une tâche de traces (log.c)

Cet exemple de code implémente l'affichage de messages sans basculer en secondary mode.

Les types utilisés :

typedef enum
{
E_DEBUG,
E_INFO,
E_WARNING,
E_ERREUR,
E_FATAL,
N_LEVELS
} e_log_level;

typedef struct
{
char                   message[MESSAGE_SIZE];
e_log_level            criticity;
RTIME                 timestamp;
char                   task_name[XNOBJECT_NAME_LEN];
} log_message_t;

Les variables globales :

RT_QUEUE log_MsgQueue;

unsigned int n_lost_messages = 0;

const char *criticity_label[N_LEVELS] = { "DEBUG  ", "INFO   ", "WARNING", "ERREUR  ", "FATAL  "};

e_log_level filter = E_INFO;

La fonction d'initialisation :

boolean log_init(void)
{
if (rt_queue_create (&log_MsgQueue, "log_MsgQueue", sizeof (log_message_t) * N_MESSAGES, N_MESSAGES, Q_FIFO) != 0)
return FALSE;
return TRUE;
}

La fonction d'écriture :

void log_write (e_log_level n, const char *m)
{
log_message_t lm;
RT_TASK_INFO  task_info;
RT_TIMER_INFO timer_info;

memset (&task_info, 0x00, sizeof (task_info));
if ( rt_task_inquire (NULL /* current task */ , &task_info) != 0 )
assert(0); /* Should not happend */

strncpy (lm.message, m, MESSAGE_SIZE);
strncpy (lm.task_name, task_info.name, XNOBJECT_NAME_LEN);
lm.criticity = n;

/* Always returns 0: */
(void)rt_timer_inquire( &timer_info );
lm.timestamp = timer_info.date;

if (rt_queue_write (&log_MsgQueue, (const void *) &lm, sizeof (lm), Q_NORMAL) < 0)
++n_lost_messages;
}

Quelques remarques :

- ce code est « primary mode safe », il ne provoquera pas de basculement en secondary mode ;

- la copie des chaînes de caractères est sécurisée, cela fait partie des bonnes pratiques afin de construire une application robuste ;

- le niveau de criticité est ignoré, par défaut tout est stocké. Cela signifie qu'une tâche importante du point de vue temporel, mise au point avec des traces E_DEBUG, inhibées par la suite, aura exactement les mêmes performances. Il est essentiel que les fonctions de validation ne modifient pas le comportement du logiciel si elles ne sont pas utilisées ;

- les messages non stockés sont comptabilisés.

La tâche de lecture :

void log_task (void *arg /* ignored */)
{
log_message_t lm;

while (flag /* externally set */ )
{
memset (&lm, 0x00, sizeof (lm));

if (rt_queue_read (&log_MsgQueue, (void *) &lm, sizeof (lm), TM_INFINITE) != sizeof (lm))
return; /* normal termination by queue destruction */

if ( lm.criticity <= filter )
{
printf ("[%s] %llu (ns) - %s - %s\n",
criticity_label[lm.criticity], lm.timestamp, lm.task_name, lm.message);
}

if (lm.niveau == E_FATAL)
exit (EXIT_FAILURE);
}
}

Le format de sortie est délimité afin de faciliter les post-traitements (awk, perl...).

XXI-D - Révisions du document

Date

Version

Nature de la modification

Auteur

01/01/2008

1

Version initiale

D.Chabal

(1)Le tout en image à l'adresse suivante : http://www-128.ibm.com/developerworks/linux/library/l-system-calls.

(2)Understanding the Linux Kernel, Daniel P. Bovet, Marco Cesati, éditions O'Reilly.

(3)http://www.informit.com/articles/article.aspx?p=101760

(4)A contrario, la demande explicite de préemption par un processus s'appelle le yielding.

(5)Ces schémas et explications sont extraits de l'excellente présentation Real Time in Embedded Linux Systems de Thomas Petazzoni et Michael Opdenacker, disponible sur free-electrons.com. J'invite le lecteur à consulter les autres présentations disponibles sur ce site.

(6)http://tldp.org/LDP/tlk/dd/interrupts.html, http://www.xml.com/ldd/chapter/book/ch09.html.

(7)http://lwn.net/Articles/239633.

(8)Un garde-fou existe tout de même en cas d'emballement des tasklets, il est implémenté par le démon ksoftirqd.

(9)Bien que ce ne soit pas le propos de ce document, ce type de temps réel satisfait les applications audio et vidéo.

(10)Soit dans le vanilia kernel, CONFIG_PREEMPT=n.

(11)Scheduler « CFS » : http://en.wikipedia.org/wiki/Completely_Fair_Scheduler.

(12)En O(log N) pour les puristes... Pour ceux choqués par le terme « quasi constant », je les invite à faire le calcul avec un nombre de tâches réaliste (entre 50 et 150) dans leur tableur favori.

(13)Non POSIX, apparu dans le noyau 2.6.16, http://lwn.net/Articles/96494.

(14)1499 pour le temps réel, 1 pour SCHED_OTHER, 1 pour SCHED_BATCH.

(15)Ce paragraphe est largement inspiré de cette page http://www.lesswatts.org/documentation/silicon-power-mgmnt.

(16)Dans cet exemple : HPET est un timer haute précision et lapic est un contrôleur local d'interruptions (en deux exemplaires puisqu'ici le PC est un biprocesseur).

(17)Comme par exemple les produits proposés par un célèbre fabricant texan.

(18)http://www.rdrop.com/users/paulmck/RCU/RCUrealtime.2005.04.20c.pdf.

(19)NDA : ma culture aéronautique et spatiale en matière de test, fait que je m'interroge sur la méthode de validation avec une couverture à 100 % de ce type de modifications.

(20)nanokernel est le terme consacré dans la documentation, le terme d'« hyperviseur » peut également correspondre.

(21)http://home.gna.org/adeos

(22)Enfonçons encore un peu plus le clou : dans tous les cas le terme « tâche en secondary mode » désigne implicitement une tâche Xenomai.

(23)La première option est implémentée par RTAI/LXRT.

(24)Ou SCHED_RR, tout dépend des options.

(25)Ce point sera plus amplement détaillé dans le chapitre Méthodologie de développement.

(26)Sauf dans la skin VxWorks, cf. §5.E.

(27)Voir également http://rtg.informatik.tu-chemnitz.de/docs/da-sa-txt/sa-franm.pdf.

(28)Par opposition aux tâches Xenomai en mode noyau, manipulables depuis la skin RTDM.

(29)http://www.xenomai.org/index.php/FAQs.

(30)Rappel : le processeur utilisé est un Dual Core, sur une autre architecture ces options peuvent légèrement varier.

(31)http://en.wikipedia.org/wiki/Interrupt.

(32)Ces options se trouvent dans le fichier TROUBLESHOOTING du package d'installation.

(33)Temps constatés par l'auteur sur d'autres PC et des posts sur la mailing list.

(34)Je rappelle à ceux qui ne sont pas familiers de Linux que tout n'est que répertoire et fichier dans cet OS.

(35)Extrait de include/nucleus/thread.h.

(36)Le tableau de correspondance est disponible dans l'annexe.

(37)Rappel : le nombre d'objets est positionné lors de la compilation du noyau dans le menu.

(38)cf. annexes.

(39)Que les puristes du C me pardonnent, je rêve des fois que les booléens existent pour de vrai...

(40)Noyau 2.6.24.

(41)Version 1.4.1.

(42)Éditions Prentice Hall Open Source Software Development, ISBN-13: 978-0132396554.

(43)En anglais NDA : Non Disclosure Agreement.