Trente ans après l'introduction des premiers RTOS commerciaux, Linux est une option viable pour les applications temps réel sophistiquées. Mais l'univers temps réel des applications industrielles ne se résume pas à une plate-forme x86 gavée de processeurs dopés à la fréquence d'horloge, avec POSIX pour seul horizon. C'est pourquoi nous avons imaginé Xenomai 3.
1. Xenomai en bref
Xenomai est un projet logiciel libre fondé en 2001 [1], qui produit une infrastructure pour les applications temps réel sur Linux. Résolument tourné vers les systèmes embarqués, ses utilisateurs sont en majorité des professionnels actifs dans des domaines aussi variés que l'impression numérique, les automatismes et la vision industrielle, les télécommunications, l'imagerie médicale ou la robotique.
Après avoir maintenu pendant dix ans sa précédente architecture logicielle, le projet vient de publier Xenomai 3, devenu compatible avec les deux approches majeures qui sont suivies pour rendre Linux temps réel : le co-noyau et la préemption native.
Xenomai 3 est à la fois un outil de migration d'applications basées sur des RTOS traditionnels vers Linux, et un composant intégré qui offre des services POSIX temps réel à faible latence. Il est développé et maintenu sur de multiples architectures processeur 32 et 64 bit, dont ARM, PowerPC, Blackfin et x86.
2. Pourquoi Xenomai ?
À l'origine du projet, il y a une volonté d'ouvrir une voie de migration plus simple, pour les applications basées sur des RTOS traditionnels, vers Linux.
Parmi ces RTOS, on trouvera par exemple VxWorks, pSOS, VRTX, µC/OS, ThreadX et Nucleus, ou encore les implémentations des spécifications µITRON, et arinc653. Certains ne sont plus maintenus, d'autres sont encore présents sur le marché, mais ils sont présents dans une part considérable des systèmes embarqués existants. En revanche, la plupart proposent aux applications un écosystème d'outils et d'interfaces de programmation (API) qui leur sont spécifiques.
Les RTOS traditionnels sont nés autour d'un système minimal pour gérer des contraintes temps réel ; ils ont ensuite incorporé des fonctions généralistes, à mesure que les plates-formes matérielles devenaient plus performantes, et les applications plus sophistiquées. À l'inverse, Linux a d'abord proposé une gamme de services généralistes étendue avant d'introduire des fonctionnalités temps réel. La migration vers Linux est donc l'occasion de reprendre les fonctions non temps réel de l'application avec une palette d'outils et d'interfaces bien plus riche, qui ne doivent pas avoir d'impact sur les aspects temps réel du traitement.
Xenomai facilite la migration des applications issues des RTOS traditionnels vers Linux parce qu'il y propose les mêmes APIs système, ou permet à défaut de les émuler rapidement grâce à une boite à outils très souple. Même si les émulateurs distribués aujourd'hui par le projet ne couvrent encore qu'une partie des services présents dans les APIs originales, celle-ci concerne les fonctions qui constituent le cœur du RTOS, sur lesquelles s'appuient les algorithmes temps réel des applications. Les services auxiliaires dont elles ont également besoin trouvent presque toujours leur équivalent dans le riche catalogue des fonctions généralistes de Linux (piles et interfaces réseau diverses, interfaces graphiques, stockage, etc.), tandis que le logiciel système de couplage avec le matériel (BSP) correspond au code plate-forme du noyau adapté au SoC cible.
2.1 Pourquoi émuler les APIs originales ?
Il est toujours possible de se dire que la migration vers Linux devrait être un moment propice pour rebaser l'application sur l'API POSIX dans le même mouvement, mais la réalité des projets dans l'industrie est souvent plus complexe. Lorsqu'on envisage déjà de changer de plate-forme cible y compris d'architecture processeur, de périphériques, de système d'exploitation, de technologie temps réel et d'environnement de programmation, la conservation de l'API système originale peut contribuer à limiter le stress.
Il est commun d'hériter d'applications dont le cœur représente plusieurs centaines de milliers de lignes de code ; celui-ci dialogue avec l'environnement matériel et/ou fournit des services généraux au reste du logiciel. Par exemple, tel logiciel système d'un copieur numérique dépasse allègrement la barre des 350 000 lignes de code C++, et 500 000 lignes pour cette autre infrastructure dédiée aux télécommunications. Convertir un tel volume de code très dépendant des services du RTOS original, pour qu'il utilise une API avec des sémantiques différentes telle que POSIX, requiert un appétit solide, ainsi qu'un grand sens du dévouement.
De plus, la durée de vie de ces logiciels embarqués est longue. Elle excède le temps pendant lequel les ingénieurs qui connaissent leur fonctionnement en détail seront disponibles pour y intervenir de nouveau. Or, changer l'API qui connecte l'application au logiciel système, c'est d'abord faire table rase de tous les détails de comportement du RTOS original sur lesquels l'application compte. C'est généralement à ce moment précis que les ennuis commencent.
2.2 Faux-amis et vrais ennuis
Les ingénieurs les plus méfiants auront isolé tous les appels système spécifiques au RTOS cible dans une ou plusieurs bibliothèques de services dits « génériques », pour limiter la dépendance. Une bibliothèque de ce genre encapsulera les primitives utiles, par exemple les sémaphores, des tâches ou des files de messages, via une API pivot indépendante du RTOS hôte. Mais cette manœuvre ne règle souvent que le problème d'adaptation de la syntaxe entre deux services de RTOS équivalents, comme peuvent l'être taskSuspend() pour VxWorks et t_suspend() pour pSOS. En revanche, elle ne gomme pas les différences sémantiques entre des APIs qui l'on pourrait croire voisines. L'interface POSIX que Linux propose à ses applications réserve quelques surprises dans ce domaine, lorsqu'on la compare à nombre d'APIs de RTOS traditionnels.
Par exemple, les services taskSuspend() et taskResume() de VxWorks qui suspendent et réactivent respectivement une tâche particulière (au sens d'un thread utilisateur) n'ont pas de correspondance POSIX, directe ou indirecte. Dans un environnement Linux moderne (c'est-à-dire basé sur le couple glibc/NPTL), les signaux SIGSTOP et SIGCONT sont de faux-amis qui agissent sur toutes les tâches du processus auquel appartient le récepteur désigné, qui ne peut donc pas être individualisé. Envoyer SIGSTOP à une tâche revient donc à arrêter toutes les tâches du même processus, ce qui est rarement une bonne idée dans le contexte d'une application héritée d'un RTOS. Après tout, l'idée même de suspendre une tâche de manière inconditionnelle sur requête d'une autre est assez dangereuse en soi pour que POSIX ne fournisse aucune aide particulière pour l'implémenter - imaginons simplement l'état du système après qu'une tâche soit arbitrairement suspendue, alors qu'elle détient toujours un verrou sur une ressource partagée. Néanmoins, ces services sont largement utilisés par les applications cibles, qu'une émulation fidèle devra mettre à leur disposition.
Dans le même esprit, la plupart des RTOS traditionnels donnent le moyen à l'application de verrouiller l'ordonnanceur des tâches, ce qui empêche tout changement de contexte pendant une durée arbitraire. On obtient alors une sorte de verrou géant utilisé pour la sérialisation des tâches, qui agit globalement sur tout le système en désactivant la préemption pour la tâche courante. Qu'il s'agisse de taskLock() avec VxWorks, de sc_lock() avec VRTX ou de OSSchedLock() avec uC/OS, les RTOS donnent aux applications le moyen de désactiver leur fonction de base, qui consiste à gérer leur ordonnancement. Là encore, POSIX a ignoré ce genre de construction vaguement brutale dans sa spécification, afin de promouvoir une protection individualisée des accès pour chaque ressource partagée, notamment via les objets mutex.
Néanmoins, ces mécanismes - parmi d'autres tout aussi problématiques - sont régulièrement utilisés dans nombre d'applications héritées des environnements embarqués traditionnels, où ils y structurent profondément les algorithmes. En leur absence, changer des pans entiers de la logique interne du code au moment de sa transition vers POSIX peut s'avérer aussi subtil que de jouer au Mikado avec des moufles.
3. Assister Linux et ses applications
Xenomai 3 a deux fonctions : émuler fidèlement les APIs traditionnellement proposées par les RTOS, et fournir des garanties temps réel si le noyau Linux hôte ne peut le faire seul.
3.1 Factoriser les points communs entre RTOS
La plupart des RTOS traditionnels proposent des services qui sont tous basés sur un nombre limité de mécanismes communs, même si leurs APIs respectives pour y accéder sont très différentes. Parmi ces mécanismes, on trouve pêle-mêle l'ordonnanceur à priorité fixe FIFO préemptif, la sérialisation des accès, l'émission et la réception de données ou d'événements selon la priorité des tâches ou l'ordre d'arrivée de la requête, les réveils et notifications programmés ou encore l'allocation mémoire à temps constant. Ces RTOS partagent aussi des sémantiques communes, comme les états possibles d'une tâche – généralement READY, RUNNING, WAITING, SUSPENDED, INACTIVE - et les règles de transition entre eux.
C'est précisément ce travail de factorisation des points communs qui a été entrepris avec Xenomai. On peut alors proposer pour chacun d'eux l'implémentation d'un seul mécanisme, qui reproduit fidèlement le comportement attendu d'un RTOS traditionnel. Par exemple, la suspension/réactivation de tâche à la façon des RTOS est implémentée par un mécanisme que tout émulateur peut utiliser. La somme de tous ces services forme un RTOS abstrait, qui peut être habillé de différentes manières pour le présenter à l'application.
Une application répartie qui repose sur une API de RTOS orientée « message » pour uniformiser son accès aux ressources gérées sur des nœuds physiques différents du système, pose un problème de portage d'une autre envergure. La migration d'une telle application consiste parfois à consolider tous les nœuds au sein de machines virtuelles hébergées sur un seul système Linux, contrôlées par un hyperviseur temps réel. Dans ce cas, le choix de la virtualisation ouvre la possibilité d'exécuter le code binaire original (presque) verbatim au lieu d'émuler ses APIs, puisque l'architecture matérielle de départ peut être simulée.
Xenomai a donc été conçu comme un ensemble de briques pour construire un RTOS intégré à Linux, dont on pourrait changer l'API selon les besoins de l'application. Une sorte de RTOS caméléon, en somme, comme l'a un jour écrit Jan Kiszka [2]. Chaque brique gère un aspect particulier du système temps réel vu par les applications, et propose une interface bien définie pour accéder à ses services, que vont ensuite utiliser les émulateurs d'API que l'on veut proposer.
Fig. 1 : Sémantiques communes ou similaires entre RTOS.
Plusieurs émulateurs ont été construits avec ces briques, et sont fournis dans la distribution Xenomai standard, comme ceux des interfaces VxWorks et pSOS. Un des objectifs de Xenomai est de proposer une émulation fidèle du comportement de ces APIs vu de l'application, tel qu'il est décrit par l'éditeur du logiciel original dans sa documentation technique.
Fig. 2 : Factorisation des mécanismes RTOS.
3.2 Embarqué toujours, temps réel souvent
Aussi étrange que cela puisse paraître, les applications héritées des environnements RTOS traditionnels n'ont pas toutes de sévères contraintes de latence. Certaines peuvent s'accommoder de retards dans l'exécution de leurs tâches, tant qu'ils restent raisonnables et peu fréquents. Une autre nécessité pratique a longtemps imposé d'utiliser un RTOS pour des applications embarquées, indépendamment des aspects temps réel : contrairement à celles d'un système d'exploitation généraliste, les empreintes mémoire et processeur de ces systèmes étaient compatibles avec les capacités limitées des plates-formes cibles du moment.
Pour ce genre d'applications, les SoC actuels offrent des performances de calcul assez élevées pour que les latences moyennes soient satisfaisantes, sans avoir besoin d'un support temps réel spécifique pour obtenir des latences compatibles avec les contraintes, dans la mesure où des dépassements de délais seraient acceptables. L'un des principaux obstacles d'un portage vers Linux reste alors celui de l'API non-POSIX du système originel, et c'est l'une des solutions que Xenomai peut fournir avec ses émulateurs. En revanche, nombre d'applications héritées ont effectivement des contraintes temps réel au sens strict, c'est-à-dire qu'un retard dans une tâche particulière fausse le résultat de son calcul. Lorsque le calcul permet de contrôler un moteur pas-à-pas ou un laser, cela devient rapidement anxiogène.
On a pu l'entendre et le lire dans toutes les langues depuis des années : le noyau standard Linux, sans ajout de fonctionnalités temps réel spécifiques, ne donne aucune garantie de latences courtes et bornées aux applications. Encore aujourd'hui, une simple boucle périodique à 5 Khz, servie par un noyau Linux 3.18.20 sur une plate-forme ARM Phyflex/imx6 dual core (1.2Ghz), produit ce genre de courbe de latence pathologique après quelques minutes de fonctionnement :
Fig. 3 : Mesure des latences sur i.MX6.
La boucle de mesure est fournie par le programme cyclictest issu de la suite de tests de validation temps réel [4] du projet PREEMPT-RT, dont la tâche de mesure aura été verrouillée sur un seul cœur CPU, à la priorité SCHED_FIFO maximale (99). La charge ajoutée est constituée d'un test de stress issu du projet LTP [3] (stress_part.2), susceptible de travailler sur chaque cœur disponible. À moins que l'application puisse s'accommoder de pics de latence supérieurs à 3400% de la consigne comme dans cet exemple, elle devra compter sur une extension temps réel du noyau.
3.3 Garantir le comportement temps réel
Linux propose deux approches qui ont chacune été améliorées au cours du temps.
3.3.1 PREEMPT-RT
La préemption native consiste à réduire et borner la latence du noyau standard. L'implémentation de référence est PREEMPT-RT [5]. Cette technologie a progressé depuis la fin des années 90 [6], en affinant la granularité du code noyau avec des sections critiques plus courtes, par l'élimination des inversions de priorité, et grâce à l'ajout d'une riche instrumentation qui permet de mieux identifier les sources de latence (ftrace [7]). Graduellement, nombre d'améliorations produites par le projet PREEMPT-RT ont été intégrées au noyau standard. Timers haute-résolution, mutexes avec héritage de priorité, et gestion apériodique du temps système (nohz) notamment. De fait, Linux doit en grande partie à PREEMPT-RT son efficacité sur des architectures multicœurs, car identifier toutes les sections critiques qui doivent échapper à la préemption et les minimiser pour réduire la latence du système, ce sont également les conditions d'un multi-processing symétrique fiable et capable de monter en charge sur un grand nombre de processeurs.
3.3.2 Co-noyau
Au lieu de transformer le GPOS Linux en RTOS, l'approche co-noyau lui ajoute un sous-système temps réel indépendant. Linux et le co-noyau partagent l'espace superviseur de la machine, mais sont très faiblement couplés, et jamais lors du fonctionnement temps réel. L'organisation du travail est simple : seul le co-noyau prend en charge les aspects temps réel, Linux s'occupe du reste comme à l'habitude. L'application de la technique déjà ancienne du co-noyau à Linux commence dans la seconde moitié des années 90, avec le projet RTLinux [8].
Le composant Cobalt de Xenomai 3 est un co-noyau. Il vient s'appuyer sur une couche logicielle appelée Pipeline d'Interruptions ou I-pipe [9] issue du projet Adeos, qui lui donne une priorité absolue sur la prise en charge des interruptions matérielles. En parallèle, cette interface virtualise le masquage des interruptions pour Linux, de manière à réduire la latence pour le co-noyau tout en garantissant la cohérence du noyau standard. Le pipeline d'interruptions est une simple abstraction logicielle, qui définit l'ordre de prise en charge des interruptions entre deux étages possibles, respectivement de haute et basse priorité. Cobalt occupe l'étage haut, au plus près du point d'injection des interruptions dans le pipeline, tandis que la mécanique native du noyau Linux procrastine plus bas. Le pipeline maintient un contexte séparé pour chaque processeur disponible. La virtualisation du masquage côté Linux permet de ne pas bloquer l'entrée du pipeline à de nouvelles interruptions qui restent ainsi immédiatement transférables à Cobalt, même lorsque le code noyau hôte traverse de son côté une section critique.
Pratiquement, cela signifie que les services spin_lock_irq(), spin_lock_irqsave(), local_irq_save() et local_irq_disable() n'affectent plus le registre processeur contrôlant le masquage, mais seulement un registre virtuel géré par le pipeline pour autoriser ou différer l'exécution des routines d'interruptions. Cette technique fait penser à un contrôleur d'interruptions virtuel, simulant le masquage en fonction d'un état purement logiciel.
Fig. 4 : Pipeline d'interruptions.
Au cours du temps, le modèle de programmation a été simplifié pour les applications temps réel basées sur un co-noyau. En 1997, les applications RTLinux devaient résider en espace noyau Linux, au mieux dans un contexte très restreint de fonction callback exécutée avec une protection mémoire utilisateur temporaire. En 1999, RTAI [10] permettait aux applications d'accéder aux services temps réel depuis un processus utilisateur normal. En 2005, Xenomai 2 réduisait encore l'écart avec le modèle de programmation classique, en introduisant une véritable interface POSIX 1003.1c pour les applications en espace utilisateur, et un support GDB fiable pour leur mise au point.
3.3.3 Le pour et le contre
Pour résumer la situation, la préemption native a l'avantage du modèle de programmation inchangé par rapport à l'environnement applicatif habituel :
- la bibliothèque glibc non modifiée permet d'accéder aux services temps réel ;
- l'outillage classique est disponible pour l'analyse du système sans restriction ni adaptation nécessaire (valgrind, perf) ;
- la collection de pilotes de périphériques natifs est disponible pour une utilisation temps réel, dans la mesure où leur implémentation garantit déjà des latences bornées ;
- la montée en charge est possible jusqu'à concurrence d'un nombre élevé de processeurs disponibles, même si les effets de cache et la sérialisation entre processeurs vont finir par la limiter à un certain point.
En revanche, il existe aussi un prix à payer pour intégrer nativement et à grande échelle des mécanismes temps réel à Linux. D'une part, toutes les activités du système doivent s'adapter aux contraintes de latence, qu'elles soient temps réel ou non. Ainsi, la grande majorité des routines d'interruptions ne sont plus exécutées directement, mais doivent être déléguées à des tâches système (IRQ threading) pour limiter les temps de masquage, l'héritage de priorité est appliqué à toutes les tâches, la quasi-totalité des verrous interprocesseurs (spinlocks) deviennent dormants lors d'une contention d'accès. Tout ceci génère des changements de contexte supplémentaires entre tâches, et nécessairement plus de charge et d'effets de cache, y compris pour les applications qui n'ont pourtant aucun besoin temps réel.
Du point de vue de l'utilisateur, les activités temps réel et « ordinaires » partageant la même infrastructure noyau, il n'est pas facile d'optimiser la configuration du système pour améliorer les performances des secondes, sans risquer de pénaliser les premières.
Enfin, la conservation de la préemption native tout au long du cycle de vie du noyau Linux est difficile parce qu'elle dépend d'une infrastructure partagée avec une masse logicielle très largement majoritaire, pour laquelle servir le maximum de requêtes en un minimum de temps sera toujours plus important que de servir la bonne requête au bon moment. Le temps réel est une niche applicative, le nombre de contributeurs expérimentés dans ce domaine est restreint, et la grande majorité des contributeurs Linux ne se préoccupe pas des problèmes de latence, et encore moins de latence bornée. Toutes les conditions sont réunies pour que la conservation des caractéristiques temps réel demande un effort considérable d'adaptation et de validation sur le noyau standard, de version en version [11][12]. On peut rappeler qu'une nouvelle version du noyau Linux est publiée toutes les 8 à 10 semaines, qu'elle représente en moyenne une dizaine de milliers de jeux de modifications distincts, contribués par environ 1 500 développeurs, sur plusieurs centaines de milliers de lignes de code affectées, potentiellement dans tous les sous-systèmes [13].
L'approche co-noyau a les avantages liés à son découplage partiel du noyau natif hôte :
- L'implémentation d'un co-noyau spécialisé dans le traitement exclusif de tâches temps réel est comparativement plus simple que d'intégrer des mécanismes temps réel à un GPOS, tout comme son couplage au noyau hôte, car l'interface ne concerne qu'une partie limitée et bien identifiée du code Linux. Le portage du co-noyau Xenomai vers neuf architectures processeur différentes s'en est trouvé facilité.
- L'expérience de plus d'une décennie de maintenance du pipeline d'interruptions Xenomai a montré que peu de régressions dans le comportement temps réel du co-noyau ont été causées par les mises à jour du noyau standard.
- L'empreinte processeur de ces mécanismes sur les activités GPOS normales est faible puisque la gestion de l'ordonnancement temps réel est isolée et indépendante. Cette technique est donc bien adaptée aux plates-formes matérielles qui offrent des performances modestes.
- L'absence de contention avec les activités GPOS du noyau hôte pour le routage des interruptions et l'ordonnancement des tâches permet d'obtenir des latences maximales significativement plus basses que celles de la préemption native, notamment sur les plates-formes embarquées [14][15].
Mais, le co-noyau a également ses contraintes et limitations. La principale tient dans la nécessité de développer spécifiquement les pilotes des périphériques que l'on voudra exploiter en mode temps réel. Le co-noyau ne peut garantir de latences faibles que dans la mesure où seuls ses services sont utilisés, et non pas ceux de Linux, ce qui aliène les pilotes standards pour une utilisation temps réel.
On retrouve cette contrainte à différents niveaux de la pile logicielle : parce que le co-noyau et Linux fonctionnent en parallèle sans pouvoir se synchroniser sur le mode temps réel, l'outillage disponible avec Linux doit être adapté pour qu'une utilisation soit possible depuis le sous-système temps réel. Si Ftrace [11] l'a été pour Xenomai, d'autres manquent encore cruellement, comme Perf [16] et Valgrind [17].
3.3.4 Comment choisir entre Cobalt et PREEMPT-RT?
Si une évaluation de la latence maximale observée reste évidemment un critère central, d'autres critères méritent également d'être mentionnés. S'ils ne sont pas exhaustifs, ils représentent bien les contraintes de développement et de maintenance d'une application temps réel en environnement Linux :
- de quel type d'extension temps réel disposez-vous déjà sur votre SoC pour le noyau Linux cible ? Si aucune n'est disponible, et en l'absence de support pour une version noyau voisine qui rendrait éventuellement le portage plus simple, il est probable qu'une approche co-noyau serait plus facile à adapter et valider. Elle engage moins de modifications de code, sur un périmètre bien plus étroit du noyau hôte, ce qui rend - statistiquement au moins - les mises à jour plus faciles et moins exposées aux régressions.
- combien de processeurs seront utilisés en parallèle pour exécuter les traitements temps réel ? Le mécanisme de sérialisation interprocesseurs de Cobalt a l'avantage de la simplicité jusqu'à quatre processeurs temps réel, au-delà PREEMPT-RT aura de meilleures performances grâce à une granularité plus fine. Ce critère ne concerne pas le nombre de processeurs disponibles, mais uniquement ceux qui seront amenés à exécuter des tâches temps réel concurrentes. Par exemple, Cobalt aurait des performances correctes sur une machine équipée de seize processeurs dont seulement quatre seraient dédiés aux opérations temps réel.
- quelle est la marge de temps processeur disponible au-delà du traitement temps réel, réservé aux activités « ordinaires » de Linux ? Une approche co-noyau est préférable si cette marge est limitée par les performances de la plate-forme matérielle (processeur, bande passante mémoire, performance des caches, etc.), car structurellement, elle mobilise moins de ressources puisque seul le co-noyau prend en charge la partie critique de manière isolée. Cette marge sera très différente entre un SoC x86 haut de gamme, un SoC PowerPC 85xx ou Freescale iMX6, et une plate-forme Freescale MPC52xx ou Atmel AT91.
- pouvoir réutiliser des pilotes de périphériques déjà présents dans le noyau standard, mais cette fois dans un contexte temps réel, simplifierait-il le développement de votre application ? PREEMPT-RT est une solution d'autant plus séduisante que cette possibilité existe. À l'inverse, Cobalt ne permettrait pas de réutiliser le code natif de pilotes tel quel, mais imposerait leur portage au-dessus de son interface RTDM.
Quant au critère de latence maximale tolérée, il dépend bien sûr des contraintes temps réel de l'application : l'objectif n'est pas d'avoir la plus petite latence, mais une latence adaptée au problème posé. On notera cependant que depuis 2010, des études ont constamment montré que PREEMPT-RT ne pouvait toujours pas concurrencer l'approche co-noyau dans le domaine des latences les plus faibles [14][15][18].
3.3.5 Mythes et légendes
Les chapelles constituées autour de ces deux approches se sont très souvent écharpées sur leurs avantages revendiqués et les inconvénients perçus du camp d'en face, en fonction des objectifs marketing de chacun. Petit florilège des affirmations aventureuses :
« Contrairement à PREEMPT-RT, le modèle co-noyau impose de modifier l'application pour obtenir un comportement temps réel. »
De deux choses l'une : soit « modifier » signifie utiliser une autre API que POSIX, ce qui est faux dans le cas de Xenomai depuis plus de dix ans, soit l'on parle de la nécessité de distinguer les parties temps réel du reste du code applicatif pour ne pas risquer d'introduire des latences, et ce n'est pas spécifique au co-noyau. Penser que n'importe quel service Linux pourrait être invoqué indistinctement au milieu d'une boucle d'acquisition à haute fréquence sera toujours une erreur, que ce soit avec PREEMPT-RT ou un co-noyau.
« Contrairement à PREEMPT-RT, le co-noyau est peu ou pas affecté par une défaillance du noyau standard. »
Si par « défaillance » on entend plantage, c'est bien entendu une vue de l'esprit : après une faute processeur irrécupérable, le spectacle s'arrête pour tout le monde, car le co-noyau appartient au domaine superviseur géré par Linux. Si l'on parle d'un dysfonctionnement moins définitif impliquant un pilote ou un service particulier, c'est rarement pertinent dans la mesure où nombre d'applications du co-noyau profitent de l'association symbiotique d'un GPOS avec un RTOS sur une même machine, telle une IHM ou une liaison réseau pour communiquer avec une station de supervision distante, échangeant des données avec un contrôle en boucle fermée sur la machine cible. Bref, si la partie GPOS éternue, l'application va certainement tousser.
« Les interfaces de communication entre un co-noyau et Linux sont horribles, atroces. »
Voire l'inverse. Soyons raisonnables et réglons nos montres, le temps des FIFO RTLinux [19] comme seul moyen de communication entre tâches temps réel et le contexte Linux « ordinaire » est révolu depuis une décennie. Les tâches Xenomai peuvent communiquer à faible latence avec des tâches standard par un protocole dédié de l'interface socket(2) (IPCPROTO_XDDP) côté temps réel, mais également se synchroniser directement avec des tâches Xenomai spéciales qui feront office de proxy avec le contexte Linux non temps réel (ordonnancement SCHED_WEAK). Une tâche Xenomai peut même quitter temporairement le domaine temps réel si nécessaire pour exécuter des services Linux, de manière complètement transparente. De fait, le principe d'interface « sans couture » avec le monde non temps réel a été intégré dès le design de Xenomai 2.
« La préemption native ne donne pas de limites claires de la partie de l'API POSIX utilisable en contexte temps réel. » [20]
Sous-entendu : il serait impossible de reconnaître les services qui ont une latence maximale bornée, parmi les centaines de routines disponibles avec la glibc, puisque la préemption native ne fournit pas de bibliothèque de services temps réel spécialisée. En toute bonne foi, on devrait y arriver en s'en tenant aux services normalisés par POSIX.1b, « Real-time extensions » (IEEE Std 1003.1b-1993) et POSIX.1c « Threads extensions » (IEEE Std 1003.1c-1995), qui ont été regroupés dans la norme IEEE Std 1003.13-1998, même si les niveaux de latence peuvent être variables dans cet espace. Pour le reste, il n'est pas interdit de faire appel au bon sens, comme l'utilisation d'un co-noyau l'exigerait également.
4. Pourquoi une nouvelle architecture Xenomai ?
Le projet Xenomai a toujours eu une place à part dans l'écosystème, paradoxale pour certains, qui consiste à développer et maintenir depuis treize ans le pipeline d'interruptions que plusieurs co-noyaux utilisent sous différentes formes pour s'interfacer à Linux [21], tout en rendant ses émulateurs de RTOS utilisables dans un contexte PREEMPT-RT, avec sa version 3.
Pour comprendre, il faut en revenir au premier objectif du projet : Xenomai s'intéresse d'abord aux applications, à la facilitation de leur portage depuis les RTOS traditionnels vers Linux. De ce point de vue, le co-noyau de Xenomai n'est qu'un produit dérivé de l'objectif original : sans garantie de latence bornée, pas d'émulation correcte des APIs lorsque l'application a des contraintes temps réel. Lorsque PREEMPT-RT n'est pas disponible pour le noyau cible, ou montre des latences trop élevées pour satisfaire l'application, Xenomai 3 propose le co-noyau Cobalt pour obtenir les garanties temps réel requises.
La nouvelle architecture reflète ce besoin d'indépendance vis-à-vis de la technologie temps réel, en gommant toute dépendance directe de ses émulateurs sur cette dernière.
4.1 Cobalt et Mercury
Xenomai 3 peut cibler l'un des deux cœurs temps réel disponibles, choisi lors de la compilation du système : Cobalt pour le co-noyau, ou Mercury pour les services Linux natifs (avec ou sans PREEMPT-RT). Seul Cobalt nécessite un support système spécifique, intégré au noyau hôte. Il consiste en un co-noyau du même nom, qui intègre une interface POSIX à destination des applications en espace utilisateur, et une interface pour le développement de pilotes temps réel (RTDM). Pour borner le temps de réaction aux événements externes, Cobalt les intercepte grâce au pipeline d'interruptions, et contrôle l'activité temps réel en continu.
Fig. 5 : Architecture Xenomai co-noyau (Cobalt).
Cobalt est une évolution significative du composant noyau de Xenomai 2, sur plusieurs aspects :
- le transfert des émulateurs de RTOS vers le contexte utilisateur. Contrairement à Xenomai 2 qui implémente toutes les APIs et émulateurs dans l'espace noyau, Cobalt propose une interface temps réel unique de type POSIX avec quelques extensions spécifiques à usage interne, qui sera au cœur de l'émulation optimisée de RTOS. Outre la fin de la dépendance directe sur une technologie temps réel particulière, cette architecture a l'avantage de réduire le nombre d'appels système vers le (co-)noyau, tout en profitant de la communication directe de mémoire à mémoire entre tâches sans passer par le contexte superviseur, qu'elles appartiennent au même processus ou non (memory mapping).
- la refonte, l'optimisation et l'extension de l'implémentation POSIX dans le co-noyau. Xenomai 3 propose plus de services POSIX temps réel (107) en comparaison de Xenomai 2 (89), notamment issus du standard 1003.1b avec la gestion des timers et des signaux (en mode synchrone). Il introduit également un équivalent temps réel pour certaines fonctions spécifiques à Linux, comme l'interface timerfd. Contrairement à Xenomai 2 qui met toutes les APIs sur un même plan, POSIX est l'API cœur de Xenomai 3, qui permet de construire toutes les autres. De ce point de vue, Xenomai 3 réduit le coût de migration des applications déjà basées sur Linux, entre les technologies co-noyau et préemption native.
- la prise en compte des ABIs mixtes, qui permet d'exécuter des applications Xenomai 32bit sur un couple noyau+co-noyau 64bit.
- l'intégration de la nouvelle spécification RTDM, pour le développement de pilotes temps réel conformes au modèle de programmation des pilotes natifs. L'API RTDM a été clarifiée, pour supprimer les particularismes de l'approche co-noyau. Un objectif à moyen terme du projet Xenomai est de fournir une implémentation native de RTDM conforme à la nouvelle spécification, qui permettra alors de migrer librement les pilotes temps réel entre les environnements Cobalt et Mercury.
- l'intégration et l'amélioration du composant RTNet [22], la pile de protocole réseau basée sur une approche TDMA.
- le travail de fond sur l'interface Analogy [23] pour la gestion de cartes d'acquisition, qui crée les conditions pour un accroissement futur du nombre de cartes supportées.
Xenomai 3 ne contient aucun composant noyau pour Mercury, car dans ce mode, on part du principe que les capacités natives de Linux sont compatibles avec les contraintes temps réel de l'application, après y avoir éventuellement ajouté PREEMPT-RT. Mercury sera utile aux applications qui reposent sur des interfaces temps réel non-POSIX, elles-mêmes fondées sur les services d'émulation de RTOS de Xenomai.
Fig. 6 : Architecture Xenomai native (Mercury).
4.2 Copperplate
Les services d'émulation des RTOS ont été déplacés du contexte noyau de Xenomai 2 vers l'espace utilisateur. Ils font partie d'une bibliothèque de services nommée Copperplate, qui assure également la médiation entre les APIs émulées et les services temps réel fournis par le noyau. Lorsque le système intègre le composant Cobalt, Copperplate utilise les services POSIX de ce co-noyau pour implémenter les briques d'émulation. Dans le cas contraire, Copperplate repose totalement sur la glibc et donc les services POSIX natifs.
Fig. 7 : Interface de médiation Copperplate.
Copperplate est donc un pivot qui procure des briques logicielles pour l'émulation de RTOS à ses APIs clientes, tout en masquant la technologie temps réel - native ou co-noyau - utilisée pour y parvenir. Les émulateurs basés sur Copperplate fonctionnent à l'identique dans les deux environnements, avec la même implémentation.
4.2.1 La factorisation par l'exemple
Copperplate offre des abstractions intermédiaires, situées entre les mécanismes cœur fournis par Cobalt ou l'interface POSIX Linux (threads, les mutexes, variables condition, timers, etc.), et les services des APIs émulées que vont utiliser les applications. Cinq abstractions couvrent à elles seules la majeure partie des besoins pour l'émulation :
- un objet qui représente une tâche/thread temps réel (threadobj), pris en charge par l'un des ordonnanceurs disponibles, le plus souvent à priorité fixe FIFO et préemptif, ou round-robin. Il peut attendre et consommer des ressources ou les produire, être suspendu ou réveillé inconditionnellement, ou simplement s'endormir jusqu'à une échéance définie (un nombre de ticks de l'horloge système simulée du RTOS, une heure relative ou absolue de l'horloge système réelle).
- un objet de synchronisation à double entrée (syncobj), que les tâches vont utiliser d'une part pour produire une ressource au sens large, d'autre part pour la consommer. L'objet gère la sérialisation pour accéder à cette ressource, la mise en attente du producteur lorsqu'il n'est plus possible d'augmenter la taille de la ressource disponible, et bien sûr la mise en attente du consommateur lorsque cette même ressource est indisponible.
- une abstraction qui implémente un allocateur de blocs mémoire de tailles arbitraires (heapobj). L'algorithme utilisé pour l'allocation et la libération des blocs est compatible avec les besoins temps réel des applications, et s'adapte aux transferts de mémoire inter- comme intra-processus, selon la configuration choisie.
- un objet qui représente une horloge temps réel haute résolution (nanoseconde), ou basée sur des ticks périodiques de granularité plus large (clockobj).
- un objet timer que l'on programme pour une seule échéance ou pour un déclenchement périodique (timerobj), et qui exécute une routine spécifique le moment venu.
Le fragment de code qui suit illustre dans les grandes lignes l'émulation de l'appel système q_send() de pSOS, grâce à l'interface Copperplate. q_send() alimente une file de messages partagée avec d'autres tâches. Le cœur de cette opération tient en cinq appels Copperplate distincts, pour transférer le message immédiatement à la tâche réceptrice si elle est prête à le recevoir, ou mettre le message en attente dans la file si nécessaire. Il repose sur le constat qu'une file de messages n'est finalement qu'une ressource que des tâches peuvent venir consommer ou alimenter, couplée à une zone mémoire pour stocker les messages en attente ; on a donc une combinaison des abstractions threadobj, syncobj et heapobj de Copperplate.
struct psos_queue_wait *wait;
struct threadobj *thobj;
struct msgholder *msg;
u_long maxbytes;
thobj = syncobj_peek_grant(&q→sobj); /* Anyone waiting on the consumer side? */
if (thobj && threadobj_local_p(thobj)) {
/* Fast path: direct copy to the receiver's buffer. */
wait = threadobj_get_wait(thobj);
maxbytes = wait->size;
if (bytes > maxbytes)
bytes = maxbytes;
if (bytes > 0)
memcpy(__mptr(wait->ptr), buffer, bytes);
wait->size = bytes;
goto done;
}
if ((q->flags & Q_LIMIT) && q->msgcount >= q->maxmsg)
return ERR_QFULL;
/* Pull some message space from the system-defined heapobj */
msg = xnmalloc(bytes + sizeof(*msg));
if (msg == NULL)
return ERR_NOMGB;
q->msgcount++;
msg->size = bytes;
if (bytes > 0)
memcpy(msg + 1, buffer, bytes);
if (flags & Q_JAMMED)
list_prepend(&msg->link, &q->msg_list);
else
list_append(&msg->link, &q->msg_list);
if (thobj) {
/*
* We could not copy the message directly to the
* remote buffer, tell the thread to pull it from the
* pool.
*/
wait = threadobj_get_wait(thobj);
wait->size = -1UL;
}
done:
if (thobj) /* Wake up the consumer heading the wait queue. */
syncobj_grant_to(&q->sobj, thobj);
return SUCCESS;
On peut appliquer la même logique pour émuler les services de la bibliothèque msgQLib de VxWorks (msgQReceive(), msgQSend()), ou les services équivalents de VRTX (sc_qpend(), sc_qpost()), de FreeRTOS (xQueueReceive(), xQueueSend()), ou des spécifications µITRON (snd_dtq(), rcv_drq()) et arinc653 (SEND_BUFFER(), RECEIVE_BUFFER()).
4.2.2 L'illusion de la différence
Cette même logique de factorisation est applicable à la majorité des services proposés par les RTOS traditionnels. Ils implémentent les mêmes mécanismes fondamentaux, qu'ils exposent aux applications dans un délire créatif de conventions et de syntaxes d'appel différentes. Considérons neuf APIs de RTOS traditionnels que l'on retrouve dans les applications héritées du monde embarqué temps réel, et intéressons-nous à quatre fonctionnalités communes qu'elles proposent à leurs tâches :
- la file de messages de taille fixe ou variable qui est un IPC inter-tâches, avec ou sans gestion de priorité des messages, avec ou sans attente bloquante sur réception et/ou émission.
- le pool mémoire, pour l'allocation de blocs de taille fixe ou variable, avec ou sans attente sur contention mémoire.
- la synchronisation sur événements, c'est-à-dire la possibilité d'émettre et de recevoir des notifications ou des données particulières, de façon bloquante ou non. L'information associée à l'événement est basique, comme une valeur binaire dont chaque bit représenterait un état ou un statut, ou un pointeur mémoire arbitraire à se transmettre de tâche en tâche.
Les tâches qui communiquent grâce à ces trois premières fonctions peuvent appartenir ou non au même espace mémoire (processus) avec Xenomai. Il est par exemple possible d'allouer des blocs issus d'une zone de mémoire partagée entre plusieurs processi de manière transparente, grâce à l'abstraction Copperplate du pool mémoire.
- les timers et autres alarmes, qui déclenchent une action programmée, comme l'exécution d'une routine ou le réveil d'une tâche, lorsqu'ils arrivent à échéance.
Ces APIs sont celles de VxWorks, pSOS, VRTXsa, Chorus/micro, ARINC653, FreeRTOS, eCOS,ThreadX et µITRON. Le tableau suivant associe les fonctionnalités de ces APIs aux abstractions Copperplate qui peuvent être utilisées pour les émuler. Certaines fonctionnalités sont directement transposables vers une seule abstraction, d'autres nécessitent la combinaison de plusieurs d'entre elles, mais dans tous les cas, on constate que les mécanismes sont simples à modéliser.
Les mécanismes comme les mutexes, les sémaphores et les variables condition ne sont pas repris dans ce tableau, car ils sont presque directement transposables vers l'API POSIX. Le cas des sémaphores est cependant particulier, dans la mesure où une émulation correcte exige de pouvoir contrôler l'ordre d'attente des tâches selon un mode FIFO sur demande de l'application, ce que l'implémentation POSIX de Linux ne permet pas (ordre de réveil par priorité des tâches uniquement). Dans ce dernier cas, on aura alors recours à l'émulation via une abstraction syncobj, couplée à une simple variable entière qui représentera la valeur du sémaphore.
Features |
queue, mailbox, blackboard |
memory heap |
event group |
timer, watchdog |
VxWorks msgQLib |
syncobj + heapobj |
|
|
|
pSOS queues |
syncobj + heapobj |
|
|
|
VRTX queues |
syncobj + heapobj |
|
|
|
Chorus/micro message spaces |
syncobj + heapobj |
|
|
|
ARINC653 queuing ports, buffers |
syncobj + heapobj |
|
|
|
FreeRTOS queues |
syncobj + heapobj |
|
|
|
eCOS mailboxes |
syncobj + heapobj |
|
|
|
ThreadX queues |
syncobj + heapobj |
|
|
|
µITRON |
syncobj + ring buffer |
|
|
|
VxWorks partitions |
|
heapobj |
|
|
pSOS partitions, regions |
|
heapobj, syncobj + heapobj |
|
|
VRTX partitions, heaps |
|
heapobj, syncobj + heapobj |
|
|
Chorus/micro message pools |
|
syncobj + heapobj |
|
|
eCOSmemory pools |
|
syncobj + heapobj |
|
|
ThreadX block and byte pools |
|
syncobj + heapobj |
|
|
µITRON memory pools |
|
syncobj + heapobj |
|
|
pSOS task events, timed events |
|
|
syncobj + bitmask, syncobj + timerobj + bitmask |
|
VRTX event flag groups |
|
|
syncobj + bitmask |
|
Chorus/micro event sets |
|
|
syncobj + bitmask |
|
eCOS flags |
|
|
syncobj + bitmask |
|
ThreadX event flags |
|
|
syncobj + bitmask |
|
µITRON flags |
|
|
syncobj + bitmask |
|
VxWorks wdLib |
|
|
|
timerobj |
pSOS alarms |
|
|
|
timerobj |
Chorus/micro timers |
|
|
|
timerobj + syncobj |
eCOS counters |
|
|
|
timerobj + counter |
ThreadX timers |
|
|
|
timerobj |
µITRON cyclic timers, alarms |
|
|
|
timerobj, timerobj |
Abstraction Copperplate des fonctions de RTOS.
Comme le suggère ce tableau, une bonne partie des services proposés par ces RTOS tourne autour de quelques abstractions bien définies, auxquelles l'émulateur viendra ajouter des particularités de syntaxe ou de comportement. Néanmoins, le mécanisme central qu'elles partagent restera le même.
4.3 Mettre en oeuvre Xenomai 3
Xenomai 3 en mode Cobalt utilise le même mécanisme d'installation que Xenomai 2, divisé en trois étapes décrites dans l'article de Patrice Kadionik [24], c'est-à-dire :
- la préparation de l'arbre source du noyau Linux cible pour y incorporer le code du co-noyau ;
- la configuration et la génération du noyau cible selon la procédure habituelle ;
- la configuration et la génération des bibliothèques et des exécutables Xenomai pour l'espace utilisateur.
Le site du projet propose une description détaillée de ce processus [24] pour Xenomai 3, ainsi qu'une aide à la migration [25] d'applications développées avec Xenomai 2.
L'installation en mode Mercury consistera uniquement dans les deux dernières étapes, puisque le co-noyau est absent de la configuration. Cobalt et Mercury partagent les mêmes APIs de RTOS traditionnels, l'interface POSIX temps réel dans la limite des fonctionnalités implémentées par Cobalt, et la méthode de génération des applications.
Conclusion
Parce que la cohabitation d'activités temps réel dans un contexte de système d'exploitation généraliste consiste par principe à trouver un bon compromis entre besoins divergents, Linux ne propose pas d'approche technique unique qui serait adaptable à toutes les situations, mais plusieurs options possibles, en fonction de critères variés. En parallèle, la question de la disponibilité des APIs temps réel traditionnellement utilisées dans le monde industriel depuis trois décennies se pose, lorsqu'un projet de migration applicative vers Linux est entrepris.
Xenomai 3 propose une architecture temps réel hybride fondée sur une alternative transparente entre le co-noyau et la préemption native, qui offre un socle commun aux applications temps réel. Dans le même temps, ses émulateurs de RTOS traditionnels simplifient la migration grâce à leur reproduction fidèle de la syntaxe des APIs originales, mais également des détails de leur comportement.
Les objectifs à court terme du projet sont d'une part un portage de Cobalt vers l'architecture arm64 qui sera officiellement intégrée à la version 3.1, d'autre part l'amélioration de la pile RTNet [21]. À moyen terme, l'implémentation native (indépendante du co-noyau) de l'interface RTDM [26] sera réactualisée, avec pour objectif de rendre les pilotes temps réel développés pour Cobalt également disponibles dans un environnement PREEMPT-RT.
À plus long terme, le développement de Xenomai 3 passera par l'implémentation de nouveaux émulateurs de RTOS, car l'ADN du projet s'y trouve. En parallèle, la maintenance du co-noyau Cobalt sur les architectures existantes et son portage vers de nouvelles continueront.
Liens et références
[1] http://xenomai.org/about-xenomai/
[2] http://xenomai.org/documentation/slides/Xenomai-OSMB-2007-01.pdf
[3] https://github.com/linux-test-project/ltp/wiki
[4] https://rt.wiki.kernel.org/index.php/Cyclictest#Documentation
[5] http://www.osadl.org/Realtime-Linux.projects-realtime-linux.0.html
[6] http://kpreempt.sourceforge.net/
[7] http://www.linuxembedded.fr/2011/03/introduction-a-ftrace/
[8] http://www.windriver.com/products/product-overviews/realtime-core-for-linux_overview.pdf
[9] https://lwn.net/Articles/140374/
[10] http://rtai.org
[11] https://lwn.net/Articles/572740/
[12] https://lwn.net/Articles/617140/
[13] http://kernelnewbies.org/DevelopmentStatistics
[14] https://www.osadl.org/fileadmin/dam/rtlws/12/Brown.pdf
[15] http://www.at91.com/linux4sam/bin/view/Linux4SAM/RealTime
[16] https://perf.wiki.kernel.org/index.php/Tutorial
[17] http://valgrind.org
[18] http://www.hindawi.com/journals/mpe/2014/945850/
[19] http://www.tldp.org/HOWTO/RTLinux-HOWTO-6.html
[20] www.yodaiken.com/papers/preempt.pdf
[21] https://lwn.net/Articles/1261/
[23] http://xenomai.org/analogy-general-presentation/
[24] « Temps Réel dur avec Xenomai sur processeur ARM AT91 » par Patrice Kadionik, Open Silicium n°15
[25] http://xenomai.org/installing-xenomai-3-x/
[26] http://xenomai.org/migrating-from-xenomai-2-x-to-3-x/
[27] http://git.xenomai.org/rtdm-native.git