Cette planche de TP s'inspire librement de l'assignement 2 de Matt Welsh.
Le système OS/161 a un support minimal pour exécuter des programmes mais rien qui ne puisse être considéré comme un vrai processus. Dans ce devoir, vous allez mettre en oeuvre une implémentation minimale des appels système et des processus.
Pour aborder ce devoir dans de bonnes conditions, il est important d'avoir compris le chapitre de cours sur les processus ainsi que le TP préliminaire sur OS/161. Dans la partie programmation, il est important de bien organiser votre code, de le commenter et de produire quelque chose de lisible -- dans un style et des conventions proches du code que vous enrichissez.
OS/161 met à votre disposition un peu de code source pour faciliter la
réalisation de ce TP à travers un pilote (kern/asst2) et des
entrées du menu au boot. Pour activer ce code, il faut reconfigurer
votre noyau, comme vous l'avez fait au TP préliminaire mais en
utilisant le fichier de configuration ASST2 au lieu de ASST0 puis de le compiler
en allant dans le répertoire compile/ASST2.
Notez les réponses aux questions suivantes dans un fichier texte en identifiant bien les différentes questions auxquelles vous répondez. Ce fichier est à rendre avec le devoir.
kern/userprogloadelf.c, runprogram.c et uio.c;
cependant vous aurez peut être besoin de rajouter des fichiers que vous aurez
écrits durant ce devoir. La compréhension de ces fichiers est la clé pour
démarrer avec l'implémentation de la multiprogrammation. Notez que pour
répondre à certaines questions vous aurez besoin d'aller regarder des fichiers
en dehors de ce répertoire.
loadelf.c cs161-gcc.)
runprogram.crunprogram() qui
permet de lancer un programme utilisateur depuis le menu. C'est une bonne base
pour l'écriture de execv(). De plus, lorsque vous aurez écrit votre système
de processus, vous devrez modifier runprogram() pour que les programmes
soient lancés proprement dans ce système; par exemple avec les descripteurs de
fichiers standards disponibles à l'exécution.
uio.c lib/copyinout.c.
UIO_USERISPACE et UIO_USERSPACE ? Quand doit-on utiliser UIO_SYSSPACE à la place ?
struct uio qui est utilisée pour lire dans un segment peut
être allouée sur la pile dans load_segment()? (i.e., où va réellement la
mémoire?)
runprogram(), pourquoi est-il important d'appeler vfs_close()
avant de passer en mode utilisateur ?
copyin et copyout ? et
memmove? Pourquoi les deux premières ne peuvent-elles pas être implémentées
de la même manière que memmove?
userptr_t ?
Les exceptions sont un élément clé des systèmes d'exploitation; ce sont des
mécanismes qui permettent au système de regagner le contrôle de l'exécution et
ainsi de faire son travail. Vous pouvez voir les exceptions comme l'interface
entre le processeur et le système d'exploitation. Quand le système démarre, il
installe un "gestionnaire d'exceptions" (un code en assembleur) à une adresse
spécifique en mémoire. Quand le processeur lève une exception ce code est
invoqué et appelle le système d'exploitation.
Comme "exception" est un terme surchargé en informatique, en systèmes on appelle
ça un piège, lorsque l'OS piège l'exécution.
Les interruptions sont des exceptions, ainsi que les appels système.
Plus spécifiquement, syscall.c gère les pièges qui sont des appels système.
La compréhension d'au moins le code C dans ce répertoire est un point clé pour
devenir un gourou des systèmes d'exploitation donc il est vivement conseillé
d'aller y jeter un oeil attentif.
trap.cmips_trap() est la fonction qui permet de rendre le contrôle au système
d'exploitation. C'est la fonction C qui est appelée par le gestionnaire
d'exceptions (en assembleur).
md_usermode() est la fonction qui permet de rendre le contrôle aux
programmes utilisateur.
kill_curthread() est la fonction pour gérer les mauvais programmes
utilisateur; quand le processeur est en mode utilisateur et rencontre quelque
chose qu'il ne sait pas gérer (par exemple une instruction invalide), il lève
une exception. C'est un point de non retour, ainsi le système se doit de tuer le
processus. Une partie de ce devoir sera d'écrire une version utilise de cette
fonction.
syscall.cmips_syscall() est la fonction que délègue le travail de traitement d'un
appel système à la fonction noyau qui l'implémente.
Notez que reboot() est, pour l'instant, le seul cas géré; cependant vous
allez enrichir cette fonction tout au cours de ce devoir.
Vous trouverez aussi une fonction md_forkentry() qui est un prototype où
vous devrez placer votre code pour implémenter l'appel système fork(). Il
devra être appelé depuis mips_syscall().
mips_trap() met curspl à SPL_HIGH
"manuellement" au lieu d'utiliser splhigh()?
MIPS? (Regardez
attentivement mips_syscall() et pas ailleurs.)
kill_curthread()?
lib/crt0mips-crt0.S, qui contient le code assembleur MIPS qui est
exécuté lorsqu'un programme est lancé.
Il appelle la fonction main() du programme. C'est le code avec lequel votre
execv() devra s'interfacer; vérifiez bien quelles valeurs il attend dans
quels registres, etc.
lib/libcerrno.cerrno est définie.
syscalls-mips.SMIPS.
syscalls.Ssyscalls-mips.S à la compilation et est
celui qui est réellement assemblé dans la librairie C. Les noms des appels
système sont placés dans ce fichier à l'aide d'un script callno-parse.sh qui
les lit depuis les headers du noyau.
Ceci permet d'éviter d'avoir à faire une deuxième liste des appels système.
Dans un vrai système, chaque appel et placé dans son propre fichier pour
permettre des les inclure sélectivement.
Dans OS/161, ils sont tous dans le même fichier pour simplifier les
makefiles.
SYSCALL?
MIPS qui génère un appel système? (Lisez le code
dans ce répertoire et pas ailleurs.)
Vous allez implémenter la gestion des appels système et des exceptions.
La totalité des appels système importants est listée dans
kern/include/kern/callno.h.
Dans ce devoir nous vous demandons d'écrire les appels système suivants:
Il est primordial que vos appels système gèrent les erreurs correctement (i.e.,
sans planter le système). Vous pouvez lire les pages de man inclues dans
OS/161 pour mieux comprendre les appels que vous devez écrire. De plus, vous
devez renvoyer des valeurs correctes (en cas de succès) ou le code d'erreur (en
cas de problème) comme décrit dans ces pages de man; la conformance à la
spécification est aussi importante que la validité de l'implémentation.
Le fichier include/unistd.h contient les définitions des interfaces au
niveau utilisateur des appels système que vous devez écrire.
Cette interface est différente de celle des fonctions noyau que vous allez
définir pour implémenter ces appels. Vous devez écrire cette interface et la
mettre dans kern/include/syscall.h. Comme vous le savez déjà, les codes pour
les appels sont définis dans kern/include/kern/callno.h.
Vous devrez réfléchir sur nombre de problèmes associés avec l'implémentation des
appels système. Peut-être le plus évident est celui-ci: est-ce que deux
processus utilisateur peuvent exécuter un appel système en même temps?
Pour vous aider pour la suite, nous vous donnons la solution pour un appel
système de base: _getcwd().
Comme vu plus haut, le fichier kern/arch/mips/mips/syscall.c contient la
routine de gestion des appels système. Nous rajoutons à la position ad-hoc le
code suivant:
case SYS___getcwd:
err=sys__getcwd(&retval,tf->tf_a0,tf->tf_a1);
break;
Le fichier include/syscall.h contient les prototypes des fonctions noyau
pour les appels système; rajoutons notre prototype:
int sys__getcwd(int*,char*,size_t);
Créons un répertoire kern/fs/ul et le fichier kern/fs/ul/sc.c pour y
mettre le code de nos appels système relatifs aux systèmes de fichier.
Ce fichier ne sera pas compilé par défaut; rajoutons la ligne suivante dans
kern/conf/conf.kern:
file fs/ul/sc.c
Il faut reconfigurer le noyau pour que les changements soient pris en compte
(à l'aide de la commande ./config ASST2).
Maintenant nous pouvons nous concentrer sur le code proprement dit.
int sys__getcwd(int* r,char* buf,size_t size){
struct uio u;
int ret;
u.uio_iovec.iov_un.un_ubase = buf;
u.uio_iovec.iov_len = size;
u.uio_offset = 0;
u.uio_resid = size;
u.uio_segflg = UIO_USERSPACE;
u.uio_space = curthread->t_vmspace;
u.uio_rw = UIO_READ;
ret = vfs_getcwd(&u);
if(ret)*r=-1;
else *r=u.uio_offset;
return ret;
}
Et voilà! Votre appel système _getcwd() est terminé. Il ne vous reste plus
qu'à écrire les votres maintenant.
Même si ces appels semblent être fortement liés aux systèmes de fichiers (que
vous n'avez pas écrits et n'aurez pas à écrire), en réalité ils ne consistent
qu'à manipuler des descripteurs de fichiers ou des états du système de fichier
propres à chaque processus.
Une part importante de cette partie est l'implémentation d'un système pour
suivre cet état.
Une partie de cette information (comme le répertoire courant) est spécifique
seulement au processus et d'autres (tels que les offsets des fichiers) sont
spécifiques au processus et au descripteur de fichiers.
Pour chaque processus, les premiers descripteurs de fichier (0, 1 et 2) sont,
respectivement, l'entrée standard (stdin), la sortie standard (stdout)
et la sortie d'erreur (stderr).
Ces descripteurs de fichier devront être attachés, au début, à la console
(con:) mais votre implémentation se doit d'autoriser les programmes à
utiliser dup2() pour les changer.
Un PID est un numéro unique qui permet d'identifier un processus.
L'implémentation de getpid() n'est pas très maline mais l'allocation et la
désallocation des PID sont des concepts importants que vous devez aussi
implémenter. Ce n'est pas une bonne idée que votre système plante au bout d'un
certain temps parce que vous avez utilisé tous les PID.
Ces appels sont certainement la partie la plus difficile de ce devoir, de fait la plus récompensée aussi. Ils permettent la multiprogrammation et font d'OS/161 un système d'exploitation bien plus utile.
fork() est le mécanisme de création de nouveaux processus.
Il doit faire une copie du processus courant et s'assurer que les processus père
et le fils reçoivent chacun la bonne valeur de retour (0 pour le fils et le PID
du fils pour le père).
execv() est le coeur de ce devoir. Il permet de faire exécuter à des
processus quelque chose de plus utile que le code de son père après un fork
par exemple.
Il doit remplacer l'espace d'adressage existant par un autre tout nouveau pour
l'exécutable (créé en appelant as_create dans le présent système
dumbvm) et de l'exécuter.
Même si cela ressemble au lancement d'un processus depuis le noyau (comme
runprogram() le fait), ce n'est pas si simple.
Rappelez-vous que cet appel vient depuis l'espace utilisateur dans le noyau et
doit ensuite retourner en espace utilisateur. Vous devez gérer la mémoire qui
passe ces frontières avec beaucoup de prudence.
Remarquez aussi que runprogram() ne prend pas un vecteur d'arguments mais
ceci doit aussi être géré correctement par execv().
Même si cela peut paraître simple à première vue, waitpid() nécessite de
réfléchir un peu. Lisez attentivement la documentation pour en comprendre la
sémantique et servez-vous en comme modèle de votre implémentation.
Vous pouvez aussi regarder la page de manuel UNIX mais il n'est pas demandé
d'implémenter tout ce que le waitpid() UNIX supporte.
L'implémentation d'_exit() est intimement liée à celle de waitpid().
Ce sont deux moitié du même mécanisme. Le plus souvent le code pour _exit()
sera simple et celui de waitpid() plus complexe.
Les pages de manuel d'OS/161 contiennent une description des valeurs de retour
que vous devez renvoyer. S'il y a des possibilités qui peuvent arriver et qui ne
sont pas listées dans ces pages, renvoyez le code d'erreur qui vous semble le
plus approprié (cf kern/errno.h).
Si vraiment aucun ne semble convenir, vous pouvez en ajouter un nouveau.
Si vous rajoutez un code pour un cas pour lequel UNIX a un code d'erreur
standard, utilisez le même symbole si possible. Sinon, trouvez-lui un nom mais
gardez en tête que les codes d'erreur doivent commencer par E, ne doivent
pas être EOF, etc. Consultez les pages de manuel pour en apprendre plus sur
les codes d'erreur UNIX (man errno).
Notez aussi que si vous rajoutez un code dans src/kern/include/kern/errno.h,
vous devez aussi rajouter le message d'erreur correspondant dans le fichier
src/lib/libc/strerror.c.
Dans bin/sh vous trouverez un simple shell qui vous permet de tester vos
appels système. Lorsqu'il est exécuté, le shell affiche une invite de commande
et vous permet d'en taper pour lancer d'autres programmes. Chaque commande et sa
liste d'arguments est passée à execv() après un appel à fork() pour
avoir un nouveau processus pour cette exécution. Le shell supporte aussi le
lancement de programmes en arrière plan avec &. Vous pouvez quitter le shell
en tapant exit.
Sous OS/161, une fois que vous avez écrit les appels système de ce devoir vous
devriez être capables d'utiliser le shell pour exécuter les programmes suivants
présents dans le répertoire bin: cat, cp, false, pwd et
true. Les programmes présents dans le répertoire testbin peuvent aussi
vous être utile.