## Rapport de stage noyau de ya??ba ## interface du noyau - pour l'utilisateur final Exit() : C'est la fonction qui permet de terminer proprement le noyau : elle termine tous les threads, libere tous les sémaphores et les évenements. Elle doit etre appelée par le programme utilisateur (master()) si celui-ci désire terminer l'exécution du noyau, mais elle est appelée automatiquement à la fin de l'éxécution de master(), donc elle n'est nécessaire que si l'utilisateur désire une fin exceptionnelle. Attention : cette fonction termine le programme, donc aucune des instructions qui la suivent ne sera exécutée. Comme elle termine brutalement tous les threads, il est recommandé d'éviter son utilisation, sauf par exemple à la fin de master() quand tous les threads ont terminés normalements. kbhit() : Fonction de gestion du clavier. Si le noyau est lancé en mode console, cette fonction retourne 1 si une touche à été enfoncée depuis le dernier appel de cette fonction ou depuis le début du programme si c'est le premier appel, elle renvoie 0 sinon. Si le noyau est lancé avec l'interface du simulateur, elle renvoie 1 si le bouton SendKey à été pressé sur l'interface depuis le dernier appel, ou depuis le lancement du noyau si c'est le premier appel. Voir aussi kbget(). kbget() : Fonction de gestion du clavier. Si le noyau est lancé en mode console, la fonction retourne le caractère correspondant à la derniere touche frappée au clavier, ou \0 si aucune touche n'a encore été enfoncée. Dans le cadre d'utilisation avec l'interface du simulateur, cette fonction ne sert à rien (à moins que l'interface n'évolue de manière à permettre l'envoi d'une touche quelqonque, ce qui n'est pas prévu, en effet cette fonctionnalité est inutile dans le cadre d'utilisation normale du noyau qu'est l'option systemes réactifs). En fait les fonctions de gestion du clavier sont principalement utilisées pour la synchronisation avec l'utilisateur (exemple : appuyez sur une touche pour creer le process TOTO), ou l'on ne se sert pas de la valeur réelle de la touche pressée. cleardevice() : Cette fonction n'est pas implémentée. Elle est conservée dans l'interface du noyau pour des raisons de compatibilité ascendante, car à l'origine le noyau tournait sous Dos 3.0, et l'interface graphique avait besoin d'une fonction qui permette de réinitialiser l'écran pour permettre un affichage correct des graphismes. Maintenant l'initialisation graphique est gérée directement par GTK/Linux à l'initialisation de l'interface du simulateur. initgraph() : Encore une fonction gardée pour compatibilité. Celle-ci servait à initialiser la carte vidéo en mode graphique. Aujourd'hui elle n'est plus utile puisque le simulateur tourne naturellement en mode graphique. GetCurrentTime() : C'est une fonction qui renvoie le nombre de secondes écoulées depuis le lancement du programme. Attention cette fonction renvoie juste un entier représentant le nombre de secondes écoulées, ce nombre est a formater pour l'utilisateur si celui-ci tient à le mettre en forme de maniere présentable (exemple : 'long s = (long) GetCurrentTime(); sprintf(buffer, "uptime : %dh %dm %ds", s\3600, (s%3600)\60, s%60);' écrit dans le buffer 'buffer' le nombre d'heures, de minutes et de secondes depuis le démarrage du noyau). outtextxy() : C'est la fonction qui doit être utilisée lorsque l'utilisateur souhaite afficher un texte quelqonque à destination de l'utilisateur. Le texte sera affiché dans une fenetre assignée à ce rôle dans le simulateur. Pour des raisons de compatibilité avec la version dos, les deux premiere parametres X et Y ont été conservés, mais ils sont dorénavent ignorés (a l'origine ils servaient à spécifier les coordonnées d'affichage de la chaine en question, mais maintenant le texte est affiché dans une fenêtre dédiée sur l'interface du simulateur, donc ces coordonnées sont devenues superflues). Attention : la chaîne à afficher ne doit pas comporter de retour charriot ('\n' en C), sinon seule la partie avant le premier retour charriot sera affichée. Dans ce cas, il faut envoyer chaque partie de la phrase séparément. (exemple : 'outtextxy(0,0,"c'est l'histoire de toto qui n'a pas de bras\nil demande à sa maman de lui donner le chocolat qui est dans une armoire trop haute pour lui\nsa maman lui répond 'sers-toi tout seul !'\nmoralité : pas de bras, pas de chocolat" -> outtextxy(0,0,"c'est l'histoire de toto qui n'a pas de bras"); outtextxy(0,0,"il demande à sa maman de lui donner le chocolat qui est dans une armoire trop haute pour lui"); outtextxy(0,0,"sa maman lui répond 'sers-toi tout seul !'"); outtextxy(0,0,"moralité : pas de bras, pas de chocolat");' create_event() : Fonction de gestion des evenements. C'est la fonction qu'il faut appeler l'utilisateur souhaite creer un évenement, pour lequel il faut préciser l'état initial (EVENT_SIGNALE ou EVENT_NONSIGNALE). La fonction retourne un entier qui est caractéristique de l'évenement et devra être conservé pour toute référence ultérieure à celui-ci, aussi bien pour changer son état que pour se mettre en attente sur lui. En général, les evenements sont déclarés en variables globales, et servent à la synchronisation des processus (un processus se met en attente d'un évenement qu'un autre processus passera à l'état EVENT_SIGNALE à un moment opportun). Le nombre de processus en attente sur l'évenement n'est pas limité, et si un processus se met en attente sur un évenement qui se trouve dans l'état EVENT_SIGNALE n'attendra pas, il reprendra immédiatement son exécution comme si l'attente n'existait pas (en fait, tout se passe comme si l'attente était terminée dès qu'elle commence, mais le processus ne s'en occupe pas, puisque ce qui l'intéresse est juste la synchronisation.) Attention, se mettre en attente sur un évenement ne garantit pas que le réveil aura lieu juste après le changement d'état de l'évenement, car celui-ce peut être dans l'état EVENT_SIGNALE depuis longtemps avant même la mise en attente. delete_event() : Fonction de gestion des evenements. C'est la fonction qui sert à détruire un évenement une fois que plus aucun process n'en a besoin. Cette fonction est définitive, une fois appelée sur un évenement, l'état de celui-ce n'est plus accessible à personne. Il est bon de détruire les évenements qui sont crées dynamiquement, car le nombre d'évenements distincts dans le noyau est limité à une centaine, et si le nombre maximal est atteint, toutes les requêtes de création ultérieures d'évenement échoueront avec comme motif 'plus d'emplacement disponible'. L'argument de la fonction doit être la valeur de retour d'un appel de create_event(). clear_event() : Fonction de gestion des évenements. Cette fonction appliquée à un évenement le fait passer dans l'état EVENT_NONSIGNALE. Elle est généralement appelée juste après wait_event() sur le meme évenement. Son argument doit être une valeur de retour d'un appel à create_event(). Si on l'appele sur un évenement non initialisé ou déjà détruit, elle lève un erreur "Evenement non initialisé". signal() : Fonction de gestion des évenements. Cette fonction change l'état d'un évenement à EVENT_SIGNALE ou EVENT_NONSIGNALE et diffuse cette information à tous les processus en attente sur cet évenement en les réveillant. L'état mis par signal() n'est pas forcément différent de celui de l'évenement avant l'appel, auquel cas l'appel sert juste à réveiller tous les processus en attente sur cet évenement sans changer sa valeur. Son utilisation la plus globale est le passage d'un évenement de l'état EVENT_NONSIGNALE à l'état EVENT_SIGNALE pour signifier à un processus en attente qu'une condition particulière est maintenant remplie, en général ce processus invoque directement clear_event() de cet évenement, ce qui évite à plusieurs processus de traiter simultanément l'évenement. wait_event() : Fonction de gestion des évenements. Cette fonction est la principale pour la gestion des évenements, c'est même la principale raison de leur existence. Elle sert à mettre le processus qui l'invoque dans la file d'attente d'un évenement. Au début si ledit évenement est déjà dans l'état EVENT_SIGNALE, le processus continue directement son exécution, sinon il ne prendra plus de temps processeur, ce qui est la meilleure manière d'attendre qu'une condition particulière soit remplie, à condition que cette condition puisse être vérifiée par un thread particulier qui pourra réveiller les processus en attente sur cette file (le noyau ne se charge pas tout seul de modifier l'état des évenements). Cette fonction doit être appelée avec comme argument une des valeurs de retour d'un appel à create_event(). L'appel de cette fonction avec une valeur d'évenement invalide produit l'erreur "Evenement non initialisé", tout en continuant l'exécution du processus qui l'a appelé. create_semaphore() : Fonction de gestion des sémaphores. C'est la fonction qui créée un nouveau sémaphore pour le programme en cours d'exécution. Elle renvoie l'identifiant d'un sémaphore qui devra etre conservé pour touts les appels concernant ce sémaphore ultérieurement. Le sémaphore nouvellement crée est initialisé à la valeur passée en parametre, c'est le nombre de processus pouvant posséder le sémaphore en même temps à un moment donné. Si la valeur d'initialisation est 1, le sémaphore est un mutex, et un seul processus pourra entrer en section critique à un moment donné. Le noyau supporte un maximum de 100 sémaphore à la fois, c'est pourquoi si les sémaphores sont créés dynamiquement il ne faut pas oublier de les delete_semaphore() sous peine de voir toutes les tentatives de création échouer pour cause "plus de place disponible". delete_semaphore() : Fonction de gestion des sémaphores. La fonction delete_semaphore est là pour liberer les ressources assignées à un sémaphore pendant create_semaphore(). Elle doit être appelée après que tous les processus qui utilisent le sémaphore soit terminée ou tout du moins cessent définitivement de l'utiliser, sinon toutes les opérations sur ce sémaphore échoueront avec un message "sémaphore non initialisé", ou bien auront lieu, mais sur un nouveau sémaphore qui aura été créé entre temps, ce qui n'est pas du tout souhaitable du point de vue utilisateur. Il faut donc faire attention avant d'utiliser cette fonction quand plusieurs threads susceptible d'utiliser le sémaphore incriminé sont en cours d'execution sur un des processeurs. P() : Fonction de gestion des sémaphores. P() est la fonction qui est utilisée par un processus sur un sémaphore pour entrer en phase critique par rapport à celui-ci. Il décrémente de manière atomique le sémaphore, sachant que si la valeur finale de celui-ci est positive, le processus est autorisé à continuer, mais que si la valeur est négative, le processus est stoppé et mis en attente d'un passage au positif du sémaphore (qui aura lieu quand un des processus qui détient le sémaphore le relachera). Toutes les variables partagées par plusieurs processus doivent être protégées par un sémaphore afin d'éviter les race conditions sur ces variables. P signifie Probe, soit tester en français. Les appels doivent avoir lieu sur un sémaphore correctement créé avec create_semaphore(), sinon l'appel échoue avec une erreur "sémaphore non initialisé" mais l'appel retourne quand même, ce qui laisse croire au processus appelant qu'il est en section critique, ce qui n'est pas le cas. V() : Fonction de gestion des sémaphores. V() incrémente le sémaphore auquel on l'applique, signifiant que le processus quitte une section critique pour permettre à un autre de prendre sa place. Toutes les variables partagées entre threads doivent etre verrouillées par un sémaphore pour les protéger des accès concurrents. Les appels à V() doivent faire référence à un sémaphore valide créé avec create_semaphore(). V() est l'initiale du mot hollandais signifiant laisser aller. GetCurrentProc() : Fonction de gestion des processus. Ceci est la fonction qui retourne l'identifiant dans le contexte du noyau de ya??ba du processus en cours d'éxécution sur le processeur (le processus appelant). Cette fonction est utile par exemple pour changer la priorité du processus courant (set_priorite(GetCurrentProc())). Cette fonction ne peut échouer normalement (si un processus appelle cette fonction, c'est qu'il est en train de s'exécuter dans le noyau), sauf si les structures du noyau n'ont pas été initialisées ( si le thread en cours à été créé directement par un thread du noyau sans passer par CreateProcess() ), ce qui ne doit pas avoir lieu ( la plupart des fonctions du noyau se basent sur une fonction similaire qui doit pouvoir trouver l'index dans les tables du noyau du processus courant ). create_process() : Fonction de gestion des processus. Cette fonction créée un nouveau thread dans le contexte du noyau en commencant son exécution à la fonction passée en argument, cette fonction pouvant prendre zéro ou un argument. Si cette fonction ne prend pas d'argument, l'argument fnarg doit etre NULL, si elle en prend un celui-ci doit etre un void * passé dans le parametre fnarg. A l'issue de cette fonction, le processus appelé reprend son exécution normale, et un nouveau thread est crée, qui commence à exécuter la fonction spécifiée. Les programme utilisateur ne doivent pas creer eux-même leurs threads par les appels système standard, sous peine de disfonctionnement grave du noyau (des informations sur chaque process en cours d'exécutions sont enregistrées dans les structures du noyau, et celles-ci sont initialisées pendant l'appel à create_process). La fonction renvoie le descripteur du processus nouvellement créé qui devra être conservé pour toute modification de ce processus (aussi bien pour modifier sa priorité que pour le terminer...). Le nombre simultané de processus est limité à 100 dans le noyau, donc si un processus en créé d'autres dynamiquement, il doit les détruire convenablement sous peine de saturation de l'espace des processus executable simultanément dans le noyau. delete_process() : Fonction de gestion des processus. Cette fonction est utilisée pour liberer les ressources allouées aux informations de contrôle allouées par le noyau à un thread en cours d'exécution au cours de create_process(). Cette fonction est cependant violente : elle demande au processus cible de se terminer pendant une seconde, et si celui-ci n'obtempère pas, elle lui envoie un sigkill, ce qui a pour effet de le terminer sans chance de s'en rendre compte, ce qui peut par exemple tuer une fonction qui détient un sémaphore sans lui laisser une chance de le relâcher. Il est donc recommendé de garder cette fonction pour la toute fin du programme, où l'on tue tous les process, car si on n'en tue que certains, il faut être sur que ceux qui sont tués ne détiennent pas de ressources utilisables ou nécéssaires pour les autres processus, ceci afin d'éviter des situations de bloquage. Le paramètre de la fonction doit être une valeur valide renvoyée par create_process(), sinon la fonction retourne une erreur "processus non initialisé". set_priorite() : Fonction de gestion des processus. Si vous souhaitez changer la priorité d'exécution d'un processus sur la machine par rapport aux autres, tout en gardant le caractère préemptif des processus du noyau les uns sur les autres, cette fonction est celle qu'il vous faut. Elle prend en arguments un indentifiant de processus valide (renvoyé par create_process()) et la nouvelle priorité du processus (un entier entre 0 et 255, 0 étant la priorité la plus forte), le processus est alors immédiatement modifié pour s'exécuter avec la nouvelle priorité. N'importe quel processus peut changer la priorité de n'importe quel autre, à condition de connaitre son identifiant du noyau. Pour changer sa propre priorité, un processus peut appeler set_priorite(GetCurrentProc(), prio). get_priorite() : Fonction de gestion des processus. Cette fonction renvoie la priorité d'exécution du processus cible dans l'échelle du noyau (de 0 à 255, 0 étant la plus forte priorité). La fonction doit être appelée avec un identificateur de processus valide (retourné par create_process()) sous peine d'erreur "processus non initialisé". wait_time() : Fonction de gestion des processus. Cette fonction attend le nombre spécifié de ticks (1 tick = 1/55 seconde), puis rend la main au processus appelant. C'est le meilleur moyen d'attendre une durée déterminée et connue à l'avance. wait() : Fonction de gestion des processus et des evenements. Pour attendre la fin de plusieurs évenements, utilisez cette fonction. Elle prend en argument le type d'attente (si l'attente doit se terminer dès qu'un des évenements de la liste passe à l'état EVENT_SIGNALE ou si il faut attendre que chacun des évenements passe dans cet état), puis le nombre d'évenements de la liste et enfin la liste des références des évenements dans un tableau de dimension égale au nombre d'évenements à attendre. Si la fonction retourne parcequ'un seul évenement est passé à EVENT_SIGNALE, elle renvoie le descripteur de l'évenement qui l'a réveillé. Si l'attente porte sur plusieurs évenements et si à un moment donné un des évenements observés n'est plus défini (parcequ'il a été victime de delete_event()), la fonction se termine avec une erreur "evenement non initialisé". Le type d'attente est soit PROC_WAITANY pour attendre un seul des évenements de la liste soit PROC_WAITALL pour attendre la signalisation de tous les évenements de la liste. InitCom() : Fonction de gestion de la ligne série. Cette fonction est destinée à l'initialisation de la ligne série. Si le noyau tourne en mode contrôle réel, elle ouvre la ligne série du port Com1 avec les bons paramètres de connection pour pouvoir communiquer avec le circuit du train, et si le noyau tourne en mode simulateur, elle met en place les mécanismes de communication avec ledit simulateur qui va émuler le circuit réel. Cette fonction doit être appelée une et une seule fois dans le programme (à moins que celui-ci ne se serve pas du circuit, mais cela me parait un peu étrange vu la nature du programme). L'appel doit avoir lieu avant toute autre fonction ayant un rapport avec la ligne série. FermeCom() : Fonction de gestion de la ligne série. Cette fonction referme le port série s'il était ouvert (i.e. si le noyau fonctionnait en mode réel), et elle ferme le port de communication avec le simulateur si le noyau fonctionnait en simulation. Cette fonction doit être le dernier appel du programme à toutes les fonctions de gestion du port série. Elle doit être appelée après InitCom(). PurgeCom() : Fonction de gestion de la ligne série. Cette fonction doit être appelée chaque fois que l'utilisateur désire lire des données depuis le circuit : avant de lire la première, il faut appeler cette fonction qui vidange le buffer de réception de la ligne série (ligne réelle si mode réel, ligne de communication avec le simulateur sinon). La fonction n'a aucun effet si on l'appelle avant InitCom(); envoi() : Fonction de gestion de la ligne série. La fonction envoi() envoie une série de caractères à travers la ligne série. Elle envoie n caractères depuis la chaine c dans le port série si le noyau fonctionne en mode réel, et a travers le canal de communication avec le simulateur si le noyau tourne en mode simulateur. Elle doit être appelée après InitCom() (on peut l'appeler avant, mais elle n'aura alors aucun effet). Les commandes de plusieurs caractères doivent être envoyées en une seule commande envoi(), ce qui garanti qu'elles seront émises seules, meme si deux threads essayent d'envoyer des octets en même temps : un seul thread peut occuper la ligne à la fois et chaque thread qui envoie des données prends la ligne et la conserve jusqu'à avoir envoyé tout ce qu'il avait à envoyer. lit() : Fonction de gestion de la ligne série. La fonction lit() revoie au processus appelant le dernier caractère lu depuis le port série (en utilisation réelle), ou -1 si aucun nouveau caractère n'est disponible. L'appel est non bloquant, ce qui signifier que si aucun caractère n'est disponible en lecture, l'appel ne va pas bloquer en attendant un caractère mais renvoyer un code signifiant que la mémoire tampon de lecture du port série est vide (en l'occurence, le message est -1) ## Fin du mode d'emploi utilisateur # Notice pour le développeur linux du noyau Le noyau est un environnement qui offre à un programme utilisateur toutes les primitives d'un système temps réel : création de processus, changement de priorité d'exécution, création et utilisation de sémaphores, création et utilisation d'évenements. Il offre de plus des primitives de communication avec le circuit du petit train à travers l'interface série de l'ordinateur. Toutes ces fonction existent déjà plus ou moins au niveau du système d'exploitation linux, cependant le noyau permet un meilleur contrôle et une interface plus normalisée pour ces fonctions. De plus le noyau garde des informations pour toutes les actions réalisées. Il conserve dans des tables l'ensemble des processus en cours d'éxécution, l'ensemble des sémaphores et des évenements mis en place depuis le lancement du noyau, sauf s'ils ont été spécifiquement détruits. En pratique, le noyau s'éxécute dans un thread propre, où il réalise sont initialisation puis lance l'exécution de la fonction utilisateur master(), par un simple appel de fonction. A la fin de l'éxécution de celle-ci, il appelle la fonction Exit() qui tue tous les threads lancés et termine l'exécution du noyau. La manière dont il est fabriqué fait que tous les threads qui exécutent des appels aux fonctions du noyau les exécutent eux-mêmes, ce qui pose des problèmes de synchronisation pour les accès aux tables internes du noyau. Ce problème est réglé par l'utilisation d'un mutex mutexNoyau qui doit être pris avant toute modification ou consultation d'une des structures du noyau. Quasiment toutes les fonctions du noyau sont donc en section critique. Chaque processus est crée sous linux par la fonction standard pthread_create(); d'ailleurs d'une manière générale la plupart des fonctions du noyau reposent sur les fonctions de la librairie pthread. Mais en plus de créer le nouveau thread, le noyau met à jour ses structures, telles que le threadid du nouveau thread, son nom, son pid, son état d'exécution. Ces informations sont utiles pour tout ce qui touche à la modification ultérieure du processus. La structure de description d'un processus dans le noyau est la suivante : typedef struct _proc { pthread_t thread; int threadpid; char libre; char tourne; char procname[LEN_NAMES]; } procNoyau; les champs sont : - thread: c'est l'identifiant du thread, utilisé pour les appels à la librairie pthread, - threadpid: c'est le descripteur linux du processus, utilisé pour les appels systèmes directs. - libre: c'est une variable booleene qui indique si cette case du tableau est libre ou non. - tourne: c'est une variable booleene qui indique si le processus décrit ici est en cours de terminaison (delete_process), ou si il s'est terminé tout seul mais n'as pas encore été détruit. - procname: c'est une chaine de caractères qui contient le nom de ce processus. Cette chaine est utilisée par la fonction interne sendInfo qui est utilisée pour communiquer les changements d'état du processus à l'utilisateur, ou les erreurs rencontrées. Etant donné que la librairie pthread n'est pas complètement implémentée sous linux, plusieurs adaptations ont du être mises en place pour combler ses déficiances. Tout d'abord, la gestion des priorités des threads n'est pas ce que l'utilisateur s'attend qu'elle soit, il a donc fallu modifier les appels de changement de priorité de la librairie pthread en l'appel système nice(), qui prend lui en argument le pid du processus, et non son identifiant pthread. C'est pour cela que le champs threadpid à été rajouté à la structure, la difficulté fut alors de remplir ce champs, car l'appel pthread_create utilisé pour créer un nouveau thread n'indique rien sur le pid du nouveau processus. On utilise alors la fonction d'enveloppe theblock() qui est la fonction appelée par pthread_create(), qui s'exécute donc dans le nouveau processus, pour lui faire remplir ce champs du noyau avant de lui faire exécuter la fonction prévue par l'utilisateur par appel de fonction, ce qui permet également d'être informé de la fin de cette fonction de manière naturelle, et permet ainsi de mettre à jour l'évenement associé au processus et à sa mort. Le problème rencontré alors est que la fonction theblock() commence son exécution indépendamment de l'exécution de la fin de create_process(), ce qui est génant, puisque il faut que celle-ci soit terminée avant que le nouveau processus ne commence son exécution, sinon on n'est pas sur que les tables internes au noyau concernant ce processus sont à jour, ce qui pose des problèmes, par exemple si le processus appelle une fonction du noyau qui a besoin de déterminer l'index du processus courant dans la table des processus alors que le champs thread n'est pas rempli (puisque GetCurrentProcId() se sert de ce champs pour reconnaitre le processus courant et renvoyer son index). On utilise donc un sémaphore qui permet de s'assurer que tous les champs de la structure associée au nouveau processus sont remplis à la fois avant que le nouveau processus ne commence son exécution et au moment ou la fonction create_process() retourne. Etant donné que les priorités n'ont pas les mêmes bornes dans le noyau(0->255) et dans linux(-20->20), on a choisi une traduction linéaire des priorités, sachant que les macros de traduction NTOL et LTON sont adaptable si l'on trouve une meilleure traduction des priorités, qui est plus proche des attentes des utilisateurs. La deuxième défaillance dans la librairie pthread est sur la fonction pthread_abort(), qui n'est pas implémentée sous linux. Cette fonction sert normalement à terminer brutalement un thread. La seule fonction disponible sous linux qui se rapproche de celle-là est pthread_cancel(), qui notifie le thread que la fin de son exécution est souhaitée, mais elle ne fait rien pour le terminer effectivement, ce qui est le but recherché par la fonction delete_process(). La solution choisie pour implémenter delete_process() est donc la suivante : on commence par invoquer pthread_cancel() sur le thread cible, puis on attend 5*.1s pour vérifier si le thread a terminé son exécution, et si ce n'est pas le cas, la fonction envoie un signal SIGKILL qui tue le processus brutalement. La plupart des appels définis dans le noyau de ya??ba sont implémentés par les appels système correspondant, mais le noyau fournit une couche d'abstraction supplémentaire, en envoyant par exemple les messages d'erreur à la place du programmeur, qui aurait normalement à les gérer. En fait, dans l'état actuel du noyau, le programmeur final n'a qu'à tester la valeur de retour de la fonction qu'il veut pour savoir si elle a échouée ou réussie, sachant que si elle a échouée, le message d'erreur a été envoyé à l'utilisateur avec la fonction sendInfo, qui envoie un message d'erreur au bon endroit, c'est à dire soit à l'interface du simulateur si celui-ci est lancé, soit à la sortie d'erreur standard. La fonction sendInfo est une fonction qui prend une liste d'arguments variables, à la manière de printf, et elle préfixe la chaine qu'elle traite par le nom du thread qui l'a appelée, qui est le nom définit à la création de ce thread, ou "main" si c'est le thread principal en dehors de l'exécution de master(), c'est à dire avant ou après l'appel de master(), ou "master" si c'est ce programme qui est en cours d'exécution. Les noms de threads sont fixes une fois qu'ils sont définis au lancement du thead qu'ils identifient. La fonction sendInfo termine également la ligne envoyée par \n, donc les chaines passées en paramètres de la fonction ne doivent pas contenir elles-mêmes le caractère spécial de retour à la ligne. Dans le cas d'une chaine envoyée au simulateur, la fonction écrit dans un buffer la chaine qui sera interprétée par l'analyseur syntaxique du simulateur, à savoir "E %s\n" et la chaine en question, une fois substituée la liste des arguments à l'aide des fonctions de stdarg.h et vsnprintf (vsnprintf est utilisée pour éviter les dépassement de capacité du buffer statique, celui-ci est assez grand pour contenir tous les descriptifs d'erreur possibles, à condition que ceux-ci ne fassent pas plusieurs kiloocters, un fois les substitutions appliquées, et le nom du thread inclus en tête. Il faut aussi noter que les noms de thread sont limités en taille, ce qui permet d'être sûr que la taille de la chaîne finale produite sera sensiblement égale à la chaine comportant les symboles de substitution, nommée chaîne de format.) Le protocole de communication avec le simulateur par le pipe pipeMsg spécifie que tous les messages sont sous la forme de texte, dont le type est défini par sa premiere lettre, qui correspond à une série de #define au début du fichier, sur laquelle le simulateur fait un switch(), ce qui permet de bien analyser la séquence de caractères qui suit (en fait chaque type de message est identifié par une lettre de type unique, et possède une forme caractéristique, qui permet sa bonne interprétation. Par exemple, l'envoi d'une chaîne préparée par la fonction outtextxy prévoit d'envoyer les coordonnées d'affichage du texte, même si celles-ci seront ignorées par le simulateur qui se contentera d'afficher la ligne de texte dans la fenêtre dédiée. Donc le simulateur sait que s'il reçoit dans pipeMsg un message commençant par la lettre OUTTEXT, le flot de caractères qui suit devra être interprété de la manière suivante : "%c %d %d %s\n" OUTTEXT x y text. En effet, le protocole de communication spécifie que tout message est de la forme "%c ... \n", le premier caractère identifiant le type de chaîne et le caractère d'échappement '\n' spécifiant la fin du message. C'est pourquoi aucun message ne doit pouvoir contenir le caractère de fin de message '\n', sans quoi le simulateur perdrait la synchronisation avec le noyau, puisqu'il interpreterait la fin du texte comme un nouveau message, donc le premier caractères suivant '\n' comme un caractère de type de message, ce qui peut même faire planter ledit simulateur (si par exemple il s'attend à reçevoir %d %d %s et qu'il ne recoit que %s, il déclanchera probablement une erreur d'addressage qui provoquera l'envoi du signal SEGFAULT, qui terminerait donc prématurément, et non proprement le simulateur, ce qui est une très mauvaise chose. En effet, si celui-ci se termine, le noyau(linux) va fermer tous ses descripteurs de fichier, en particulier les pipes, et en particulier pipeMsg, donc si le noyau continue d'envoyer des bits par ce pipe, il recevra le signal SIGPIPE, ce qui a aussi pour effet de terminer brutalement le noyau. Ceci est critique, car le programme utilisateur pourrait être en train d'envoyer l'ordre de changement d'état d'un feu, et si le noyau plante avant que le signal de relâchement n'ait été envoyé, ceci peut provoquer la perte par fonte dudit dispositif.) Il est donc indispensable que le noyau veille à n'envoyer aucun caractère '\n' superflu dans le pipe de communication textuelle. La fonction outtextxy, par exemple, remplace la première occurence de '\n' dans son argument par '\0', ce qui est le signe de fin de chaîne de caractères en convention C ASCII. Il faut avec autant de soin empêcher l'introduction de ce caractère dans les noms de threads et tous les noms internes au noyau, puisque ceux-cis sont envoyés dans les messages d'erreur, sans être recontrôlés. Il faut donc profiter du fait qu'ils ne soient pas modifiables pour intensifier le contrôle au moment de la création et de l'initialisation du nom de chaque objet du noyau. L'implémentation de Exit(), due à l'absence de la fonction pthread_abort() sous linux, qui permet de terminer un thread de manière brutale, a été remplacée par l'utilisation d'une temporisation apres l'appel de la fonction pthread_cancel(), qui indique au thread considéré qu'il doit se terminer, mais celui-ci ne le fait qu'a certains appels de fonction phtread, qui ne sont pas forcément appelées par le thread considéré ( par exemple les programmes d'utilisation du train ont un thread spécifique qui se contente de lire la ligne série en demandant l'état du circuit, juste pour mettre à jour des variables globales du programme, donc qui n'appellent jamais les fonction qui permettraient au thread de se terminer conformément à la demande). Le noyau appelle donc la fonction pthread_cancel() sur le thread, puis toutes les 100ms regarde si le thread s'est arrêté de lui-même (ce qui est plus propre). Si au bout d'une demie seconde ce n'est pas le cas, le noyau invoque l'appel kill() pour envoyer le signal SIGKILL au processus exécutant le thread, ce qui à l'effet escompté. Il faut noter au passage que bien que la fonction kill() soit utilisée, le thread qui est tué ne peut être en possession du mutex MutexNoyau puisque l'appel de fonction kill() est fait en section critique sous MutexNoyau. Ceci est une caractéristique tres importante, car si par malheur il arrivait que le mutex MutexNoyau soit détenu et jamais rendu, quasiment tous les appels aux fonctions du noyau deviendraient bloquant sans espoir de retour. Il est donc important de conserver la destruction de processes a l'intérieur d'une section critique sous tous les mutex importants du noyau (si par exemple une évolution ultérieure du noyau démultipliait le nombre de mutexes, il faudrait prendre garde à tous les acquérir avant d'envisager la destruction radicale d'un quelconque process dont le chemin d'exécution pourrait le conduire à détenir un de ces mutex. Les fonctions de gestion du clavier sont devenues obsolètes avec l'avenement de l'interface graphique, il faudra donc mettre au point un protocole de communication avec celle-ci pour définir une manière d'envoyer un appui d'une touche arbitraire au noyau depuis l'interface (par exemple en définissant une nouvelle valeur non utilisée par le circuit physique du train qui serait définie comme un caractère d'échappement annoncant que l'octet suivant serait le code ASCII de la touche dont l'interface graphique émule la pression). L'émulation n'est pas obligé de spécifier une touche particulière, puisque l'expérience prouve que ces fonctions sont quasiment toujours utilisées pour créer des transitions entre différents modes de fonctionnement pour lesquels on se fiche de la touche qui a effectivement été pressée (par exemple 'appuyez sur une touche pour lancer le prochain train'). On peut alors implémenter juste une méthode qui permet à l'interface graphique de signifier au noyau que l'utilisateur a pressé une touche (juste "une touche", sans préciser laquelle, ou en définissant une touche par défaut qui serait considérée comme celle actionnée par l'action "touche pressée". (En effet, à moins de supprimer la fonction kbget(), il faut toujours veiller à ce qu'elle ne renvoie pas de valeurs absurdes, dans le cas ou un programme voudrait se servir de la valeur de cette touche). Toutefois pour une utilisation en mode console, les fonctions ont été modifiées pour gerer le clavier de manière canonique, c'est à dire que la fonction getchar() qui est utilisée pour implémenter le thread qui exécute getchar() en boucle, pour mettre à jour les variables d'état qui sont utilisées par les fonctions kbget() et kbhit() pour informer l'utilisateur de l'état du clavier, en mode canonique, la fonction getchar() retourne une valeur dès la frappe d'une touche au clavier. Sans cela, la fonction ne réagirait qu'aux retours à la ligne, où elle lirait toutes les lettres de la ligne une par une mais très vite, ce qui a pour effet de ne considérer que le \n que constitue le retour charriot lui-même. La modification du clavier induite par le passage en mode canonique étant une caractéristique du terminal dans lequel est lancé le noyau, il faut prendre gare a bien annuler le mode canonique à la terminaison du noyau (c'est pourquoi une routine de remise en l'état à été intégrée à Exit(), sans quoi le terminal utilisé pour lancé le noyau conserve les paramètres du mode canonique, ce qui est gênant pour une utilisation normale du terminal). On peut être assuré du retour à la normale du clavier en effectuant un system("reset") depuis le loader à la terminaison du noyau, ce qui garantit son exécution quelquesoit le mode de terminaison du noyau, cependant en faisant cela on perd complêtement toutes les données écrites sur ce terminal, ce qui n'est peut-être pas gênant dans une utilisation avec le simulateur, cependant dans ce mode, il est plus efficace de désactiver complètement le mode canonique, voire même éliminer le thread clavier. Il a été conservé en l'absence d'implémentation du mode d'émulation du clavier entre l'interface graphique et le noyau. A terme ce démon est voué à disparaître. On remarque aussi la présence de nombreuses fonction non implémentées, comme toutes celles permettant de gérer l'interface graphique originale, qui sont maintenant obsolètes et conservées uniquement pour compatibilité ascendente (bien qu'on puisse se poser des questions sur l'utilité de cette compatibilité, puisque les élèves sont censé créer de nouvelles applications chaque année, cependant elle peut être utile dans le cas d'exemples, bien qu'il serait probablement facile de porter ces exemples. On peut éventuellement envisager de modifier l'interface graphique pour gérer ces fonctions, mais leur intérêt est assez limité pour les applications considérées. La fonction outtextxy(), elle-même, est modifiée et ignore les coordonnées, le texte est considéré comme du texte simple qui est affiché dans une fenêtre dédiée de l'interface. La déconnection ou la terminaison anormale du simulateur sont gérées en utilisatant la signalisation du signal SIGPIPE, qui indique la tentative de lecture dans un pipe fermé, ce qui n'arrive pas sauf en cas de terminaison non souhaitée du simulateur, dans ce cas on invoque la fonction Exit() pour terminer proprement le noyau, puisque rien ne sert de continuer sans présence du simulateur, ou l'utilisateur n'a plus aucun contrôle sur ce qui se passe, et la terminaison du noyau est préventive, sinon en cas de problème, comme la transmission d'une séquence invalide ou dangereuse pour le circuit (comme le changement de la valeur d'un feu sans relâchement), auquel cas l'utilisateur serait obligé de se loguer en root pour killer le processus du noyau, ou du moins se jeter sur la commande d'extinction d'urgence pour éviter les dégats matériels, ce qui n'est pas forcément souhaitable, de plus certains utilisateur peuvent ne pas être au courant de ces détails technique et donc entraîner la destruction par ignorance de composants physiques du circuit. En fait tous les signaux sont traités de la meme façon, sinon un problème non prévu pourrait avoir les conséquences que la terminaison abrupte du simulateur. Le noyau utilise une seule fonction pour envoyer effectivement des données au simulateur par le pipe pipemsg (qui transporte des messages sous forme texte uniquement) : cette fonction est sendSimul(), qui envoie un buffer de longueur spécifié. Etant donné la nature des messages envoyés à l'interface par le noyau dans le pipe pipemsg, une fonction de plus haut niveau a été implémentée : sendInfo(). Cette fonction est une fonction qui prend un nombre variable d'arguments, a la manière de printf de la libc ; elle est utilisée pour envoyer un message de type standard par le pipe pipemsg. Elle permet de préfixer le message par le nom du processus qui l'invoque de manière automatique, cependant pour déterminer ce nom, elle utilise les informations contenues dans les tables du noyau, il faut donc faire attention à son usage lors de la modification de ces tables, en particulier lors de la création d'un nouveau processus. Ce message est aussi préfixé par le type du message constitué par une initiale qui détermine le type du message qui est interprété par l'interface graphique pour déterminer dans quelle fenetre afficher le message qui suit, les types les plus importants ayant une fenêtre dédiée dans l'interface (par exemple une fenêtre est dédiée aux messages d'erreur). Le message est la portion qui est lue dans le pipe entre deux \n. C'est pour ca que les messages ne doivent pas contenir de \n en eux-mêmes autres que ceux insérés par la fonction elle-même. Reste à faire : Il faudra améliorer la manière dont l'interface graphique et le noyau communiquent, quitte à revoir complètement le protocole de communication actuel (dans la configuration qui est pour le moment retenue, l'interface se contente de recevoir en double tout ce qui est envoyé au circuit physique quand le noyau fonctionne en mode réel, et il se contente d'émuler le circuit physique quand le noyau est en mode simulation). Il faudra notamment améliorer le rôle de l'interface, pour la rendre plus interactive (l'idéal serait par exemple de pouvoir modifier pendant l'exécution d'un programme le sens d'un aiguillage directement depuis l'interface, et que ce changement soit directement effectif sur la table), pour cela il faudra soit mettre en place une autre ligne de communication (ajouter un pipe), soit simplement gérer le pipe pipesimulnoy lors du fonctionnement en mode réel. En effet, en mode simulation le noyau n'a pas à se préoccuper de se faire signaler la position des aiguillages, car il n'a pas connaissance de l'état global du circuit, il n'a meme pas en fait de connaissance du tout, chaque programme déterminant la manière avec laquelle il utilise les primitives du noyau, cependant il faudrait mettre en garde les utilisateurs potentiels sur le fait que les aiguillages peuvent ou non être changé en cours de fonctionnement par une commande non directement émise par le noyau (bien qu'en fait elle serait émise par le noyau, elle ne le serait pas sous le controle du programme utilisateur, c'est pourquoi si un programme utilise par exemple une phase de test initial ou une méthode d'acquisition de l'état du circuit en cours de fonctionnement, en envoyant par exemple l'ordre à un seul train d'avancer lentement et en suivant l'évolution de sa position, cette méthode ne serait plus fiable). On pourrait objecter du fait que les programmes utilisateurs devraient forcer une position initiale pour les aiguillages, ce n'est pas le cas, en effet l'ensemble des positions, des vitesses des trains et des positions des aiguillages forment un système dynamique, et n'ont pas un comportement déterministe, ou en tout cas pas prédéterminé par le programmeur (sauf bien entendu dans le cas de programmes très simples). Il faut cependant noter que les décisions de changement de position des aiguillages sont prises localement (par exemple un train arrive sur un aiguillage donnant soit sur la voie A soit sur la voie B, et un train se trouve sur la voie A, le programme doit donc positionner l'aiguillage pour que le train qui arrive soit dévié sur la voie B). On peut cependant imaginer une situation dans laquelle cette situation vient de se produire avant, et que le noyau ait mis l'aiguillage pour diriger un autre train vers la voie B et ait enregistré cette donnée dans une variable du programme. Si le programmeur n'est pas averti du fait de la possibilité de modification en temps réel des aiguillages, il pourrait partir du principe qu'un aiguillage mis dans l'état X y restera indéfiniment, ce qui n'est pas le cas. Il faut donc dans ce cas soit mettre en place une routine de vérification de la position de l'aiguillage, ce qui n'est possible qu'avec un train test, cette méthode est très lourde, et n'est en général pas envisageable. Il est en pratique beaucoup plus simple de forcer la position de l'aiguillage au moment ou on en a besoin, meme si celui-ci est censé déjà être dans la bonne configuration. Il y a plusieurs manières de remédier à ce problème. On peut soit rajouter des fonctions de contrôle des commandes autorisées à être transmises de l'interface graphique au circuit, qui pourraient par exemple interdire la transmission de certaines configurations de bits (ce qui n'est pas forcément évident à mettrre en place, tant au niveau de la conception qu'à celui de l'application, à moins d'avoir très peu de configurations interdites). On peut aussi envisager un système de plus haut niveau, comme par exemple "interdire de faire passer l'aiguillage X dans l'état A), ce qui est applicable, mais pas particulièrement simple d'utilisation pour le programmeur (en effet, celui-ci n'est pas certain des futures trajectoires de ses trains et ne peut donc savoir quand interdire la modification de tel aiguillage dans tel sens). A mon avis, la modification à la fois la plus facile à mettre en place et à utiliser serait tout simplement une sorte d'interrupteur général, comme si le noyau avait deux modes de fonctionnement : dans l'un, l'interface peut émettre toutes les commandes qu'elle veut, toutes seront transmises, dans l'autre le noyau devient complètement opaque aux commandes de l'interface. Il ne faudrait dans ce cas pas oublier d'avertir l'interface de ce changement de mode, puisque sinon la simulation qui se déroule en temps réel dans le simulateur perdrait sa synchronisation avec ce qui se passe réellement dans le circuit. Bien que cela ne soit pas très important, puisque les programmes utilisateurs sont censé tourner sans intervention ni contrôle d'un opérateur humain, c'est toujours un plus de voir que tout sur le circuit se passe comformément à ce qui se passe dans le modèle que constitue la simulation. Il restera encore à déterminer comment fonctionnerait cette transmission d'ordres asynchrones : est-ce que le simulateur fabrique lui-même les commandes hexadécimales qu'il transmet au noyau qui sert de relai et de synchronisateur au circuit, ou est-ce que le simulateur transmet au noyau des ordres de plus haut niveau, comme "faire passer le feux Y au rouge", et à ce moment le noyau lit la commande, l'interprète et émet la commande binaire qui correspond ? Il serait certainement souhaitable que ce soit le simulateur qui élabore le code binaire, car d'une part le noyau ne devrait jamais avoir à interpréter ce pourquoi on l'utilise (pour des raisons évidentes de modularité : rien ne lie en pratique le noyau au circuit du train, on peut virtuellement s'en servir pour modéliser n'importe quel système programmable en programmation concurrente, le noyau lui-meme n'étant qu'une base de données de méthodes de bas ou moyen niveau, et l'interprétation et la construction des commandes en hexadécimal relevant du haut niveau); de plus le simulateur doit avoit la faculté d'interpreter les codes binaires bruts qui lui sont transmis directement du noyau, ce qui fait qu'il est plus à même de fabriquer les codes binaires ; et en pratique l'interface graphique est le même programme que le simulateur, c'est pourquoi il est souhaitable que le simulateur fabrique les codes binaires traduisant la volonté de l'utilisateur. Il restera alors à déterminer la manière dont l'utilisateur et l'interface communiquent : on peut par exemple un mode de communication basée sur le texte, ou l'utilisateur sélectionne le nom de l'aiguillage sur lequel il souhaite intervenir, a ce moment cet aiguillage est surligné dans la fenêtre principale du simulateur, puis l'utilisateur décrit ce qu'il veut faire grace à un système de contrôles de type boutons (puisque le nombre d'états possible d'un aiguillage ne dépasse jamais 4 (atteint à l'aiguillage centrale, en X), et le nombre standard d'états des feux est 2), ou bien on peut directement envisager un système de cliquer-glisser sur la fenêtre représentant le circuit, à ce moment là l'utilisation est encore plus intuitive (il faudra cependant prendre garde à la modification accidentelle des aiguillages, car si cette modification est trop facile (trop bien faite), il faudra éviter les modifications dues à de fausses manoeuvres des utilisateurs).