![]() ![]() ![]() | QNX Système temps réel : Le micro-noyau |
Le microkernel de QNX dirige :
Le microkernel de QNX supporte 3 type d'IPC (communication inter process) : messages, proxies et signaux
Dans QNX un message est un packet d'ocets qui transmis d'une manière synchrone entre deux process. QNX n'attache aucune importance au contenu du message. Le contenu du message à une importance pour l'éméteur et pour le receveur mais pour personne d'autre.
Pour communiquer directement avec un autre, un process utilise ces fonctions C :
La transmission de messages ne permet pas seulement aux processus de s'echanger des données, mais fournit aussi une synchronisation au niveau de leur eecution (j(envois un message j'attend de recevoir une réponse, je renvois ....). Regardons lillustration ci-dessus encore une fois. Une fois que le process A génère un Send() il se bloque jusqu'à la réceptiond'une réponse à son envois. Cela permet de s'assurer que le travail que doit effectuer le process B sera complet lorsque le process A s'eécutera à nouveau. De plus lorsque le process B renvoit un Receive() il peut continuer a travailler jusqu'à la reception d'un autre message.
Quand un process ne peut continuer a s'executer -parcequ'il doit attendre un message- on dit de lui qu'il est bloqué. Le tableau suivant résume les étant bloquant auquels peuvent être sujet les process.
Si un process a généré un : | le process est : |
Send() et qu'il n'a pas recu de réponse | SEND-blocked |
Send() qu'il a recu un accusé de reception mais qu'il n'a pas eu de réponse | Reply-blocked |
Receive() et qu'il n'a pas encore recu de message | Receive-blocked |
Regardons maintenant plus en details les fonctions Send(), Receive(), and Reply(). Nous garderons notre exemple avec le process A et le process B.
Le process A envois un message par l'intermédiaired'un send par l'intermédiaire d'un appelle de la fonction :
Send( pid, smsg, rmsg, smsg_len, rmsg_len );
Le process B peut recevoir un Send() du process A grâce à la fonction Recive dont voici le prototype :
pid = Receive( 0, msg, msg_len );
Apres la reception d'un message du process A, le process B devra renvoyer au process A une reponse en utilisant la fonction Reply() :
Reply( pid, reply, reply_len );
L'exemple d'échange de messages que nous avons étudié illustre la manière la plus répandue de ce type d'IPC dans lequel un processus serveur est normalement dans l'état RECEIVE-blocked en attendant une requête d'un client afin de la
traiter. Cet exemple montre un échange que l'on pourrait appeller send-driven : l'action débute avec l'envois par un client d'un message et ce termine par la réponse du serveur à ce message.
Bien que moins répendue que la méthode du send-driven, une autre forme d'échanges de messages est possible et souvent préférée à l'utilisation le reply-driven dans lequel l'action est initialisée avec un Reply().
Dans cette méthode un process envoit un message au serveur en lui indiquant qu'il est prêt à "travailler". Le serveur ne repond pas tout de suite mais prend acte de la demande du process libre. Ainsi il pourra plus tard decider de lui donner des actions à effectuer. Le process aura alors du travail dont il rendra alors les résultats par l'indermédiaire d'un message renvoyé au serveur.
Les données d'un message sont maintenue dans le processus d'envois jusqu'à ce que le receveur soit paré à le recevoir. Il n'y a pas de sauvegarde de ce message dans le microkernel. Les données du messages sont maintenanues et ne peuvent être changées par inadvertance tant que le process emetteur est dans l'état SEND-blocked .
Le message renvoyé est directement copié du process renvoyant au process REPLY-blocked lorsque la fonction Reply() est appellée. La fonction Reply() ne bloque en aucun cas le processus de reponse du message. L'état REPLY-blocked prend fin lorsque les données sont reçues par le recepteur.
Le process émetteur n'a pas besoin de savoir l'état du process a qui est destiné son message avant l'envois. Si le recepteur ne peut accepter au moment de l'envois le message , l'emetteur passe simmplement dans l'état SEND-blocked.
Des messages d'une longeur de 0 peuvent être échangés si neccessaire.
Il peut y avoir plusieurs messages en attente de réception envoyés par plusieurs process à destination d'un seul. Normalement le process recepteur recoit les messages dans l'ordre de leur émission ; mais l'ordre peut être adapté à la priorité des processus émetteurs.
QNX fournit des possibilités de transmition de messages avancés :
Généralement lorsqu'un process veut recevoir un message, il utilise la fonction Receive() afin d'attendre l'arrivée d'un message. C'est le moyen usuel pour recevoir des données et le plus approprié dans bien des cas.
Dans certains cas un process peut avoir besoin de prendre connaissance des messages en attente d'être reçu afin de ne pas avoir à entrer en RECEIVE-blocked en l'absence de messages. Par exemple un process a besoin d'executer un programme à une grande vitesse sans avoir à se bloquer. Malheureusement le process doit répondre aux messages des autres process. Dans ce cas le process peut utiliser la fonction Creceive() afin de lire le message si un se présente et retourner immédiatement si aucune donnée n'est disponible ( a la différence de receive() qui attend qu'un message se présente et ne rend pas la main sinon).
Il est tout de même déconseillé d'utiliser Creceive() car le processus utiliserait le processeur continuellement en recherchant constamment un message.
Il arrive qu'on ai besoin d'ecrire ou de lire seulement une partie d'un message. Il est possible d'utiliser pour cela directement le buffer alloué pour le message plutôt qu'un buffer séparé.
Prenons un exemple : un manager entrée/sortie accepte des messages qui consistent en une entête de taille fixe suivie par des datas d'une taille variable. L'entête contient le comptage des octets (0 à 64 octets).
Le manager peut choisir de ne recevoir que l'entête et va utiliser la fonction Readmsg() pour lire les données directement dans un buffer. S'il la taille du buffer à lire est inferieur à la taille totale, le manager pourra utiliser de nouveau la fonction Readmsg jusqu'à ce qu'il l'y ai plus de données. De même la fonction Writemsg peut être utilisée pour rassembler des donnée et pour ensuite les inscrires dans le buffer du reply.
Jusqu'à maintenant les messages ont été géré comme une seule et même entité. Toutefois ils consistent souvent en 2 ou plusieurs parties. Par exemple un message peut avoir une entête de taille fixe suivit par des données dune taille variable. Pour s'assurer que le message sera bien envoyé et reçu sans être copié dans un buffer temporaire, un message en plusieurs parties peut être créé a partir d'un ou plusieurs messages. Ce genre de possibilités aide les gestionnaires d'entrées sorties (I/O) comme Dev et Fsys en augmentant leur performances. Les fonctions suivantes permettent de gerer ces messages divisés en plusieurs parties :
Bien que vous ne soyez pas obligé de la faire, QNX commence tous ses messages par un mot de 16 bits appellé code. Les processus système de QNX utilisent les code en les ordonnants ainsi :
bits : | Description: |
0x0000 à 0x00FF | messages du gestionnaire de processus |
0x0100 à 0x01FF | messagesI/O -entrées sorties- (identique pour tous les servers I/O ) |
0x0200 à 0x02FF | messages du gestionnaire de fichier |
0x0300 à 0x03FF | messages du gestionnaire de materiel (Device Manager) |
0x0400 à 0x04FF | messages du gestionnaire réseau (Network Manager) |
0x0500 à 0x0FFF | non utilisé pour l'instant (réservés au futurs process système) |
Un proxy est une forme de message non blocant spécialement efficace pour les événements ou le process emetteur n'a pas besoin d'interragir avec le recepteur. La seue fonction d'un proxy est d'envoyer un message à un processus spécifique qui connait le proxy.
Tout comme les messages un proxy peut conctionner le long d'un réseau.
En utilisant un proxy, un processys ou une interruption peut envoyer un message à un autre processus sans avoir a se bloquer ou avoir à attendre une réponse.
Voici quelques exemples d'utilisation de proxy :
Les signaux permettent une communication asynchrone qui sont présents depuis un bon nombre d'années dans bien des OS.
QNX supporte un grand nombre de signaux compatible POSIX, des signaux UNIX,
mais aussi des signaux propres à QNX.
Un signal est considéré comme envoyé à un processus lorsque le process qui l'a defini est créé. Un processus peut cré un signal pour lui même.
Si vous voulez : | utilisez : |
Générer un signal à partir du shell | les programmes kill ou slay |
Generer un signal à l'interieur d'un process | les fonctions kill() or raise() |
Un processus peut recevoir des signaux de trois manières différentes suivant de quel manière il a défini sa gestion des signaux :
Pour definir le type d'action à réaliser pour chaques signaux, vous devez utiliser la fonction C ANSI : signal() ou son équivalent POSIX sigaction().
La fonction sigaction() vous donnera un plus grand control sur la gestions des signaux.
Vous pouvez changer votre facon de gerer un signal a tout moment. Si vous changez la gestion d'un signal en demandant d'ignorer celui-ci, tout signal de ce type en attente sera écarté.
Revenons sur les processus qui attrappent des signaux.
Une fonction qui manipule un signal peut être comparée à une interruption software. Elle est executée d'une manière asynchrone par rapport aux autres process. Il est possible par consequent pour un signal d'être prit en compte alors qu'une fonction dans le programme est en cours d'execution (quelque soit le type de la fonction).
Si votre processus reste dans le manipulateur et ne retourne pas, il est possible d'utiliser les fonctions siglongjmp() ou longjmp(), mais il est preferable d'utiliser la fonction siglongjmp() parcequ'avec longjmp() le signal reste bloqué.
Il peut arriver que vous ayez besoin d'empecher un signal d'être intercepté sans pour autant avoir a changer la méthode qui manipule ce signal.
QNX fournit un jeu de fonctions qui vous permet de bloquer l'émission de signaux. Un signaux qui est bloqué, reste en attente ; une fois que qu'il est débloqué votre programme peut le recevoir.
Pendant que votre programme traite un signal, QNX bloque automatiquement ce signal. Cela veut dire que vous n'avez pas à vous en soucier .
Chaque appelle à un manipuleur de signal est une instruction simple qui respecte l'arrivée de d'autres signaux de ce type.
Si le manipuleur de sognal de votre processus renvois normalement, le signal est alros automatiquement débloqué. Certains système Unix on une implémentation défectueuse des manipuleurs de signaux dans le fait qu'ils reinitient le signal à son action par défaut plutôt que de le bloquer. Certaines applications Unix appellent ainsi la fonction signal() avec le manipuleur de signaux afin de réarmer celui-ci. Cela peut provoquer deux types de problèmes ; premièrement si un signal arrive alors que notre programme est dans le manipulateur mais avant que la fonction signal() soit apellée votre programme sera terminé.
Secondement si un signal arrive juste apres l'appelle de la fonction signal() il vous faudra appeller le manipulateur "manuellement".
QNX permet de bloquer les signaux et résout ainsi ce genre de problèmes.
Il y'a de nombreuses interractions entre les signaux et les messages. Si votre processus est SEND-blocked ou RECEIVE-blocked alors qu'un signal est généré et que vous avez un manipulateur de signaux, il peut arriver plusieurs choses :
Une application QNX peut parler à un processus situé sur un autre ordinnateur sur le réseau comme si il parlait à un processus situé sur la meme machine. Pour l'application il n'a aucune différence entre une ressource locale et une ressource exterieure.
Ce remarque degré de transparence est du au circuits virtuels (VCs) qui sont des chemins que le gestionnaire de réseau fournit pour transmettre les mesages, proxies et signaux le long du réseau.
Les VCs permettent une utilisation optimisée des ressources réseau pour plusieurs raisons :
Un processus émetteur est responsable de la configurayion du VC entre lui-même et le processus avec qui il veut communiquer. Pour ce faire il doit appeller la fonction qnx_vc_attach(). En plus de créer un VC, cet appel créé un identificateur de processus virtuel ou VID (virtual ID) a chaque bout du circuit. Pour le process situé à des bouts, le VID situé à l'autre bout apparait pour lui comme l'ID l'autre machine avec laquelle il veut communiquer situé sur le même VC. Les processus communiquent ainsi entre eux par l'intermédiaire de ces VID.
Par exemple dans l'illustration qui suit, un circuit virtuel connecte PID 1 (Process IDentificateur 1 )à PID 2. Sur le noeud 20 -sur lequel PID 1 réside-, un VID représente PID 2. Sur le noeud 40 (pù se trouve PID 2) un VID représente PID 1.
PID 1 et PID 2 peuvent se referer au VID de leur noeud comme si c'était un autre processus local (envoyer des messages, en recevoir...). Ainsi PID 1 peut envoyer un message au VID de son noeud qui lui va le relayer le long du réseau par l'intermédiaire du VC au VID représentant PID 1 à l'autre bout. Ce VID va alors donner le message à PID 2.
Un proxy virutel pemet à un proxy d'etre déclenché à distance, tout comme les circuits virtuels permettent à un processus d'echanger des messages avec un noeud'éloigné.
Mais à la différence d'un VC qui lie deux processus ensemble, un proxy virtuel permet à un process sur un noeud éloigné de le déclancher.
Les proxies virtuels sont créés par l'appel de la fonction qnx_proxy_rem_attach(), qui prend un noeud (nid_t) et un proxy (pid_t) comme arguments. Un proxy virtuel est créé sur le noeud éloigné et représentera le proxy sur le noeud appellant.
Un processu peut devenir incapable de communiquer avec un VC existant pour plusieurs raisons :
Les sémaphores sont une autre forme tres puissante de la synchonization qui permet aux processus de "poster" (sem_post()) et "d'attendre" (sem_wait()) un sautre émaphore pour controler lorsqu'un processus se reveille ou dort.
L'opération "post" incrémente le sémaphore tandis que l'opération "wait" le décrémente.
Attendre un semaphore avec une valeur positif n'est pas bloquant. Attendre après un sémaphore non positif sera bloquant jusqu'à ce qu'un autre process execute un post une ou plusieurs fois (afin d'obtenir une valeur positive)avant un wait. Cela va permettre à un ou plusieur processus d'executer le wait sans bloquer. Une différence significative des sémaphore et des autres formes de synchronization et que les sémaphore sont dans l'impossibilité de tomber dans un mode asynchrone et peuvent être manipulée par des manipulateurs de signaux. Si la volonté est d'avoir un manipulateur de signaux qui reveille un processus, alors les sémaphore sont un meilleur choix.
Le scheduler du microkernel entre action lorsque :
Dans QNX chaque processus possède une prioritée. Le scheduler selectionne le prochain process qui doit être executé en regardant la priorité de chaque process qui sont dans l'état READY (un processus READY est capable d'utiliser la CPU). Le processus avec la plus haute priorité sera alors choisi.
Si vous voulez : | Utilisez la fonction : |
Determiner la priorité d'un process | getprio() |
Donner une priorité à un process | setprio() |
Pour répondre aux demandent des applications QNX fournis 3 méthodes de scheduling :
Si vous voulez : | utilisez la fonction : |
determiner la méthode de scheduling pour unprocessus | getscheduler() |
donner une méthode de scheduling pour un process | setscheduler() |
Dans le principe du FIFO, un processus selectionné pour la CPU continue à s'executer jusqu'à ce que :
Dans le principe du round-robin, un process selectionné pour la CPU jusqu'à ce que :
Dans le scheduling adaptif un processus se comporte ainsi :
Dans QNX la plus part des échanges entre processus suivent le modèle du client/serveur.
Les serveurs fournissent des services et les clients envoient des messages à ces serveurs pour utiliser ces services. En général les serveurs sont plus actifs que les clients.
Les clients sont normallement plus nombreux que les serveurs. Ainsi un serveur tournera toujours avec une priorité superieure à celle de ses clients. La méthode de schedulling employée sera l'une des trois énnoncées ci-dessus, mais la round-robin est sans doute la plus utilisée.
Si un client avec une basse priorité envoit un message au serveur, alors sa requête sera prise en charge par la priorité superieure du serveur. Ce la à indirectement augmnté la priorité su client car celui ci à obligé le serveur à s'executer.
Aussi longtemps que le process tourne pour de petite période de temps cela ne pose pas de problèmes. Mais si le serveur tourne un bon moment, alors on pourra voir un client d'une basse priorité (à l'origine de l'appel du serveur) qui concurencera d'autres process d'une priorité superieure à lui mais inferieure à celle du serveur.
Pour résoudre ce probleme le serveur prendra la priorité de ses clients. Lorsqu'il recevra un message, sa priorité prendra alors la valeur de celle de son client. Il est à noter que seule la priorité change, la méthode de schedulling reste. Si un autre message arrive alors que le serveur s'execute, la priorité du serveur sera augmentée si la priorité du client emetteur est superieure à celle du serveur. Cela permet au serveur de pouvoir ensuite prendre la nouvelle requete que vient de faire le client emetteur. Si ce n'est pas possible, c'est la priorité du client qui sera abaissée.
Si vous selectionnez une gestion des priorités comme celle que nous venons de décrire pour votre serveur (client-driven priorities) vous pouvez demander à ce queles messages vous arrivent dans l'ordre de leur priorité (et non pas dans l'ordre de leur arrivée).
Pour utiliser le sytème client-driven vous devez utiliser les masques de la fonction qnx_pflags() ainsi :
qnx_pflags(~0, _PPF_PRIORITY_FLOAT | _PPF_PRIORITY_REC, 0, 0);
Bien sûr les langage C et C++ sont les principales utilisés, mais d'autre langages peuvent être utilisé s'ils sont compilable sous QNX. En effet la plupart des applications venant du libre, de Linux, de BSD ou d'Unix, développés en C ou C++ sont recompilable sous QNX, moyennant peut-etre une retouche des sources.
Voici une liste d'exemples d'applications fonctionnant parfaitement sous QNX: bash, bison, yacc, apache, PHP, Netscape, Doom, Quake, Perl ...
Contrairement a cequ'on voudrait, les ordinateurs ne sont pas infiniment rapide.
Dans un système temps reel, il est absolument crucial que les cycles du CPU ne soit pas dépensés n'importe comment.
Il est donc crucial que vous minimisiez le temps qui se passe entre l'apparition d'un événement jusqu'à son traitement par un programme.
Ce temps est appellé temps de latence.
Plusieurs forme de latence peuvent etre rencontrée dans le système QNX.
Le temps de latence d'une interruption est le temps passé entre la reception d'une interruption materielle jusqu'à l'execution de la premiere instruction d'un manipulateur d'interruption.
Les temps de latence des interruptions sont tres insignifiants sous QNX. Mais certainnes sections de code critiques demandent à ce que les interruptions soient temporairement inhibées.
Le diagramme suivant montre le cas ou une interruption materielle materiel est gérée par un manipulateur.
Til: | Processeur: |
3.3 microsec | 166 MHz Pentium |
4.4 microsec | 100 MHz Pentium |
5.6 microsec | 100 MHz 486DX4 |
22.5 microsec | 33 MHz 386EX |
Dans certains cas une interruption materielle doit demander l'execution d'un processus. Dans ce scenario, le manipulateur d'interruption va indiquer dans la valeur qu'il va retourner le proxy à valider. On appelle cela le temps de latence d'un scheduling.
C'est le tmeps passé entre la fin du manipulateur d'interruption et l'execution de la première intruction du processus driver. C'est en fait le tmeps neccessaire pour sauvegarder le contexte du processus alors en place auquel on ajoute le temps de restauration du processus driver. Bien que plus grand que le temps de lance d'interruption ce temps demeure tres faible.