Les appels systèmes
Les appels systèmes constituent une part importante de l’interface entre le noyau et les applications. Dans ManuX, le choix a été fait d’implanter ce mécanisme au travers d’une interruption. C’est une technique classique (utilisée par exemple par Linux).
Leur mise en œuvre passe par plusieurs étapes que je vais détailler ici. L’objectif d’un appel système est d’offrir à une application utilisateur un service qui ne peut être rendu que part le noyau. Pour des raisons de confort d’utilisation, ces services sont rendus sous la forme de fonctions classiques, comme une librairie traditionnelle. Pour en arriver là, nous pouvons distinguer quatre étapes
-
L’application utilise une fonction C pour invoquer un appel système. Le code de cette fonction doit donc être construit de sorte à préparer puis mettre en œuvre la sollicitation du service auprès du noyau (par le biais d’une interruption ici).
-
L’interruption est déclanchée et le code du handler de cette interruption est exécuté.
-
Le handler va devoir déterminer quelle est la fonction du noyau qui doit être utilisée pour rendre le service voulu à l’application.
-
La fonction en question va être exécutée et le service fourni à l’application.
Nous allons maintenant décrire la mise en œuvre de ces étapes en prenant
comme exemple un appel système qui nous permettra d’envoyer un message
sur la console. Nous pourrons ainsi écrire un printf pour les
applications !
Définition des éléments communs noyau/application
Le fichier
manux/appelsystemenum.h
(dox) donne en particulier les
numéros des différents appels systèmes. Il doit donc absolument être
commun au noyau et aux “applications” (voir la documentation sur
l’espace utilisateur).
Définition de l’interface
Ce que j’appelle interface est la signature de la fonction directement utilisable dans une application pour invoquer un appel système.
Cette interface est donc décrite côté applications, dans les fichiers de
usr.
Des macros ont été créées dans le fichier
usr/include/manux/appelsysteme.h
(dox)
pour simplifier une telle définition. Ce sont par exemple les
“fonctions” appelSysteme0, appelSysteme1, …
On utilisera donc une telle macro chaque fois qu’il sera nécessaire de
définir un nouvel appel système. C’est ainsi que dans le fichier
usr/include/unistd.h on trouve par exemple :
appelSysteme2(NBAS_AFFICHER, int, afficherConsole, void *, int);
Cette ligne défini donc la fonction afficherConsole() qui va permettre
à une application d’envoyer un message sur la console.
Écriture de l’appel système
La partie la plus importante est naturellement d’écrire le code de
l’appel système. Celui-ci consiste simplement en une fonction C
classique (qui en utilise éventuellement d’autres, naturellement). La
seule contrainte étant que si cette fonction a des paramètres, le
premier d’entre eux (qui ne présente aucun intéret pour le programmeur)
doit être de type ParametreAS.
Ce paramètre sert uniquement a “recadrer” les paramètres suivants dans la pile suite aux décalages induits dans celle-là par les appels successifs à l’interface de l’appel système, à l’interruption, et à l’appel système lui-même d’une part, et à la sauvegarde des registres d’autre part.
Observons par exemple le code permettant la mise en œuve de l’appel
système ecrireConsole. Il se trouve dans le fichier lib/console.c :
/**
* L'implantation de l'AS ecrireConsole
*/
int sys_ecrireConsole(ParametreAS as, void * msg, int n)
{
assert(tacheEnCours != NULL);
Console * cons = tacheEnCours->console;
assert(cons != NULL);
afficherConsoleN(cons, msg, n);
return n;
}
Bien sûr, cette fonction utilise le code de manipulation de la console déjà écrit.
Enregistrement de l’appel système
L’enregistrement de l’appel système dans le système d’exploitation est
réalisé par une invocation de la fonction suivante, définie dans
appelsysteme.h et codée dans noyau/appelsysteme.c :
{}
int definirAppelSysteme(int num, void * appel);
où
num
est le numéro de l’appel système enregistré ;
appel
est la fonction de traitement de cet appel système.
Le travail de cette fonction est donc simplement de placer dans la table
des appels systèmes (vecteurAppelsSysteme) la fonction de traitement
de cet appel système.
C’est donc ici la “partie noyau” qui est réalisée, c’est-à-dire la définition des actions à mener lorsque l’appel système est invoqué.
Nous trouvons par exemple dans la fonction initialiserAppelsSysteme()
du fichier noyau/appelsysteme.c la ligne de déclaration de notre
appel système ecrireConsole :
{}
void initialiserAppelsSysteme()
{
...
/* Envoyer une chaine de caracteres sur la console */
definirAppelSysteme(NBAS_ECRIRE_CONS, sys_ecrireConsole);
...
}