< Gérer les 'Page Fault' | TutoOS | Implémenter les signaux >
Le package contenant les sources est téléchargeable ici : kernel_Shell.tgz
Pour naviguer dans l'arborescence : Shell
Nous avons vu dans un chapitre précédent comment gérer les interruptions du clavier pour saisir des caractères et afficher un message directement sur la console. La réalisation d'un shell demande d'aller un peu plus loin dans le traitement de ces interruptions pour que les caractères saisis soient effectivement "lus" par le shell. Cette partie montre une implémentation simple permettant à de multiples processus d'utiliser une console pour saisir et afficher des caractères.
Au démarrage, le noyau initialise une console par défaut grâce à la structure struct terminal
.
Cette structure contient deux champs, pread
et pwrite
, qui pointent vers les processus qui utilisent la console en lecture ou en écriture. Dans cette architecture, un seul processus peut donc lire ou écrire à la fois.
Le pointeur current_term
indique quel est le terminal actuellement en cours d'utilisation.
L'implémentation actuelle n'utilise qu'un seul terminal, mais ce pointeur devient indispensable dans l'hypothèse où plusieurs terminaux existent.
À sa création, un processus est rattaché à la console en court par le biais du champ term
:
sys_console_read()
pour entrer des données au clavierQuand un processus souhaite lire des données entrées au clavier, il invoque l'appel système sys_console_read()
en passant en paramètre l'adresse d'un buffer d'entrée (inbuf
dans le schéma ci-dessous). Si un autre processus utilise déjà la console en lecture, le processus se met en attente jusqu'à ce qu'elle se libère. Ensuite, l'appel système sys_console_read()
fait pointer le champ pread
de la structure du terminal sur le processus en attente de caractères à lire :
Quand une touche est pressée, une IRQ est levée et la fonction isr_kbd_int()
est activée. Cette fonction lit le code émis par le clavier. Si le code correspond à la saisie d'un caractère, la fonction putc_console()
est appelée.
En mode buffer (le mode le plus général), putc_console()
affiche le caractère à l'écran et l'ajoute dans le buffer de la console, console->inb
, du processus en lecture :
Une fois que le caractère \n
est saisi, le processus est détaché de la console et pread
prend la valeur NULL
. C'est seulement dans un deuxième temps que le contenu du buffer inb
est copié dans le buffer d'entrée de l'espace utilisateur du processus :
Pourquoi ne copions nous pas directement le caractère dans le buffer inbuf
du processus en attente de lecture ?
Quand un caractère est saisi, une interruption est levée et interrompt le processus en cours qui n'est pas forcément le processus en lecture.
Pour copier le caractère dans le tampon inbuf
du processus en lecture, il faudrait alors changer d'espace d'adressage, copier le caractère, puis revenir dans l'espace d'adressage du processus courant. J'ai préféré l'autre solution qui est de copier le caractère dans un buffer intermédiaire situé directement dans la structure struct console
, donc dans l'espace du noyau :
Le code de l'appel système sys_console_read()
: syscalls/sys_console_read.c
Le code de la fonction putc_console()
: console.c
L'appel système sys_exec()
(syscalls/sys_exec.c) permet de créer et d'exécuter un nouveau processus.
C'est un appel système très simple qui :
load_task()
La fonction load_task()
a été modifiée pour permettre le passage d'arguments du processus parent au processus enfant.
La fonction principale main()
prend ses arguments sur la pile (comme d'ailleurs toute fonction). Y placer ces arguments pour les rendre disponibles au nouveau processus ne pose pas de difficulté particulière. Mais où copier les chaînes de caractères à passer en paramètre ?
Une solution est de les copier à un extrème de l'espace d'adressage afin de ne pas géner le développement de la pile ou du heap. Deux emplacements semblent alors possibles : soit avant le heap utilisateur, soit au sommet de la pile. Cette dernière solution, standard sous Unix, est celle que nous avons implémenté :
Le code qui copie les données d'espace utilisateur à espace utilisateur (ici du processus parent au processus enfant) comporte une subtilité ! À un moment donné, on ne peut accéder qu'à un seul espace. Il faut donc faire transiter ces données par l'espace du noyau, qui est commun. La copie se fait donc en deux temps. La création du nouveau processus se fait ensuite comme à l'accoutumé.
L'implémentation de malloc()
repose sur l'appel système sys_sbrk()
: syscalls/sys_sbrk.c
Cet appel système très simple ne fait que mettre à jour le pointeur qui pointe sur le sommet du heap. Mais où commence le heap ? La fonction load_elf()
, qui charge l'exécutable, a été modifiée pour mettre à jour les pointeurs sur le début et la fin des zones de code et de données. A partir de ces valeur, la fonction load_task()
peut évaluer la base du heap. L'utilisation de la mémoire virtuelle par le processus correspond alors au schéma suivant :
L'implémentation des fonctions malloc()
et free()
repose sur les mêmes principes que pour les fonctions kmalloc()
et kfree()
du noyau. Attention au fait qu'il s'agit là d'une implémentation très simple pour gérer des blocs de mémoires au niveau de l'espace utilisateur !
Les sources du shell sont dans Shell/userland. Le répertoire contient également les sources d'une mini-bibliothèque de fonctions et les sources de la commande cat
qui permet d'afficher le contenu d'un fichier.
< Gérer les 'Page Fault' | TutoOS | Implémenter les signaux >