Consignes
Le rendu de ce TP se fera uniquement par TPLab et consistera en une archive (zip, tar, etc.) contenant impérativement l'arborescence suivante :
TP1-votre_login/ ├─ Makefile ├─ question1.c ├─ question2.c ├─ ... └─ minishell.c
Attention :
-
Chaque fichier .c doit contenir une entête avec votre nom / prénom ainsi que les réponses aux questions et vos remarques pertinentes.
En particulier, votre fichier minishell.c devra lister les fonctionnalités implantées (notamment pour la dernière question), ainsi que les contraintes imposées (taille maximale des lignes, nombre maximal d'arguments d'une commande, etc.)
-
Makefile doit compiler tous vos fichiers C (vous pouvez vous inspirer de ce fichier Makefile). (si vous ne connaissez pas les fichiers Makefile, je vous conseille de suivre ce tutoriel)
-
Votre Makefile doit compiler les programmes C avec les options --std=c99 -Wall -Wextra -pedantic -Werror.
Tout non respect d'une ou plusieurs de ces consignes entrainera automatiquement un retrait de points sur votre note de TP !
Liens utiles
-
encadrant de TP : Pierre.Hyvernat@univ-smb.fr, Francois.Boussion@univ-smb.fr et Gerald.Cavallini@univ-smb.fr
1. Les processus
1.1. Arborescence des processus, instruction fork
Pour obtenir la liste des processus sur le système, vous pouvez utiliser la command ps. Dans sa forme la plus simple, ps donne uniquement les processus de l'utilisateur courant associé au terminal où est lancé ps.
Certaines options intéressantes sont :
-
afficher tous les processus : option -A ou -e
-
affichage long : option -f ou -l
-
affichage des processus de l'utilisateur jane : option -u jane
La commande ps affiche les processus par ordre de PID. Il est possible d'afficher les processus en arborescence avec :
-
sous Linux, l'option --forest de ps
$ ps --forest
(Vous pouvez bien entendu utiliser d'autres options de ps en plus de l'option --forest.)
-
si le paquet psmisc est installé (Debian / Ubuntu)
$ pstree [PID]
Si PID est donné, la commande n'affichera que les processus descendants de PID. Une option intéressante est -p qui permet d'afficher les PID des processus. (Attention, pstree n'affiche pas les processus zombie, sauf avec l'option -a.)
On peut arrêter un processus en lui envoyant un signal d'arrêt avec la commande kill:
$ kill <PID>
Par défaut, la commande kill envoie le signal TERM (pour "terminate"). On peut préciser un autre signal. Par exemple,
$ kill -9 <PID>
$ kill -KILL <PID>
envoient toutes les deux le signal KILL à un processus.
Sous Linux, vous pouvez afficher la liste des signaux (et leur numéro) avec la commande
$ kill -l
L'appel système (POSIX) fork()
permet de dupliquer le
processus courant. C'est la valeur de retour de cet appel système qui permet
de faire la différence entre le processus de initial (le père) et le
nouveau processus (le fils) :
-
dans le processus de initial,
fork()
renvoie un entier strictement positif : le PID du processus créé, -
le nouveau processus se comporte comme si
fork()
avait renvoyé la valeur0
.
-
Écrivez un programme avec un
fork
où :-
le père affiche le PID du processus créé (du fils),
-
le fils affiche un message simple,
-
les processus ne se terminent pas instantanément. (Vous pouvez par exemple utiliser une boucle
while(1);
ou unsleep(10)
).
-
-
Compilez et lancez votre programme en arrière plan :
$ make gcc question1.c -o question1 $ ./question1 & Salut, je m'appelle Bob et je suis le père. Mon fils a le PID 185. Hej, je suis le fils de Bob.
-
En utilisant la commande ps, vérifiez que vous avez bien créé 2 processus, et que l'un d'eux a effectivement le PID du fils (affiché par le père).
-
Vérifiez que vous pouvez tuer le processus père (avec la commande kill depuis le shell) et que le fils est adopté par init.
-
Vérifiez que vous pouvez tuer le processus fils (avec la commande kill depuis le shell) et qu'il reste sous forme de processus zombie.
1.2. "fork-bomb"
On considère le programme suivant
#include <stdio.h>
#include <unistd.h>
int main() {
int p;
int n = 0;
printf("Bonjour,\n");
while(++n) {
sleep(1);
p = fork();
if (p > 0) {
printf("J'ai un nouveau fils: %i!\n", p);
} else if (p == 0) {
printf("Je suis le fils n° %i, je me termine!\n", n);
return 0;
} else {
printf("ERREUR lors du fork...\n");
}
}
return 0;
}
-
Faites un petit schéma pour montrer l'arborescence des processus engendré par ce programme.
-
Au
n
ème passage dans la boucle, combien de processus sont actifs ? -
Au
n
ème passage dans la boucle, combien de processus sont présents dans la table des processus ? -
Que va t'il se passer si on attend trop longtemps ? Supprimez la ligne
sleep(1)
pour confirmer votre conjecture. -
"Corrigez" le programme pour éviter de créer un nouveau fils si l'ancien ne s'est pas terminé.
L'appel système
wait(NULL)
attend que l'un des fils du processus courant se termine et renvoie son PID. Si l'un des fils s'est terminé avant l'appel àwait(NULL)
, son PID est immédiatement renvoyé. (N'oubliez pas la ligne#include <sys/wait.h>
.)
-
Écrivez un programme qui se comporte de la manière suivante :
-
le processus initial créé un fils et se termine,
-
le fils créé un fils (petit fils du processus initial) et se termine,
-
le petit fils créé un fils (petit petit fils du processus initial) et se termine,
-
ad infinitum
-
-
Expliquez pourquoi le programme obtenu ne sature pas la table des processus.
-
la commande killall permet de tuer tous les processus avec un nom donné.
$ killall <PROCESS_NAME>
mais elle risque de ne pas fonctionner si les processus apparaissent trop rapidement.
-
la commande kill permet aussi de tuer tous les processus d'un même groupe : il suffit de remplacer PID par -PGID. (N'oubliez pas le signe - devant le numéro de groupe !).
Pour récupérer ce numéro de groupe, vous pouvez :
-
utiliser l'appel système
getpgrp()
, -
récupérer le PID du processus initial avec
getpid()
(il est égal au numéro de groupe de ses sous-processus), -
utiliser la commande
ps -o pid,pgid,tty,time,comm x
-
On considère le programme suivant.
#include <unistd.h>
int main() {
while(1) {
sleep(1);
fork();
}
return 0;
}
-
Comment va t'il se comporter ?
-
Faite des petits schémas pour dessiner l'arborescence des processus aux étapes 3 et 4 de la boucle.
Une variante de ce programme à exécuter dans un shell est
$ :(){ :|:& };:
1.3. fork et entrées / sorties
Écrivez un programme C qui
-
créé un fils
-
le père et le fils rentrent dans une boucle infinie qui fait des affichages (
printf
).
Répondez aux questions suivantes :
-
Dans quel ordre se font les affichages ?
-
Est-ce que cet ordre est toujours le même ?
N'oubliez pas de décrire comment vous procédez...
Que se passe t'il lorsqu'un processus fait un fork()
, et que le
père et le fils lisent sur l'entrée standard (avec
getchar()
par exemple) ?
Décrivez votre méthodologie ainsi que les résultats trouvés.
Le terminal bufferise lui aussi les entrées. Pour changer ce comportement, vous pouvez faire
$ stty -icanon
...
...
...
$ stty icanon
1.4. Ordonnancement des processus et priorités
Rappel: vous pouvez lancer plusieurs processus "en même temps" en utilisant
$ CMD1 & CMD2 & CMD3 &
-
Testez le programme suivant
#include <stdio.h> #include <unistd.h> int main() { int p = getpid(); printf("DÉBUT %i\n", p); unsigned n = 0x7fffffff; while(n--); printf("FIN %i\n", p); return 0; }
et ajustez la valeur de
n
pour que l'exécution du programme prenne entre 2 et 5 secondes. -
Lancez 2 instances de ce programme "en même temps". Quelle est la durée d'exécution totale pour les deux processus ?
Expliquez ce que vous constatez.
N'hésitez pas à utilisez la commande top dans un autre terminal pour visualiser les processus...
-
idem pour 4 ou 8 processus, ou plus.
-
Que se passe t'il si on remplace la boucle
while(n--);
par unsleep(5);
?
On peut lancer une commande en diminuant sa priorité avec
$ nice -n N CMD
où N est un nombre inférieur à 20. Plus la valeur de N est grande, plus on diminue la priorité du processus. (L'administrateur a le droit d'utiliser des valeurs de N négatives pour rendre un processus plus prioritaire !)
-
Lancez deux instances du processus précédent "en même temps", mais en diminuant la priorité d'une des instances.
Décrivez ce que vous constatez et expliquez.
-
Idem pour 4 ou 8 instances, dont 3 ou 7 sont moins prioritaires.
2. Un mini shell
L'objectif de cette seconde partie est d'écrire un mini shell en C.
Votre code doit compiler avec les options --std=c99 -Wall -Wextra -pedantic -Werror.
Votre shell consistera en un unique fichier minishell.c qui offrira les fonctionnalité suivantes :
-
affichage d'un prompt #,
-
lecture d'une ligne au clavier, terminée par un saut de ligne,
-
action correspondante à la ligne,
-
on recommence à l'étape 1.
En pseudo-code, ça donnerait quelque chose comme :
while (true) {
line = getline();
if (line == TRUC) {
action_TRUC();
} else if (line == MACHIN) {
action_MACHIN();
} else if (line == BIDULE) {
action_BIDULE();
} else {
action_ERREUR();
}
}
Les actions possibles sont :
-
commande exit pour quitter votre shell,
-
commande cd <PATH> pour changer le répertoire de travail,
-
commande pwd pour afficher le répertoire de travail courant,
-
commande ls pour afficher la liste des fichiers du répertoire courant,
-
pour toutes les autres lignes, il faudra (essayer d') exécuter le programme correspondant et afficher un message d'erreur si ce n'est pas possible, ou attendre la fin du programme pour réafficher le prompt.
Cette partie ressemble à un "mini projet" et de nombreux détails sont omis. Vous trouverez la documentation complète des fonctions C (avec leurs prototypes et #include nécessaires) dans les pages de manuel :
$ man fgets
ou
$ man 3 sleep
pour préciser que vous c'est la fonction sleep du langage C qui vous intéresse...
N'hésitez pas à lire, au moins en diagonale, les pages de manuel des fonctions mentionnées dans les questions.
Il n'est pas nécessaire de gérer un historique des commandes tapées par l'utilisateur, ou l'utilisation des touches "flèches". Pour obtenir ces fonctionnalités automatiquement, vous pouvez utiliser l'utilitaire rlwrap et lancer votre shell avec
$ rlwrap ./minishell
(Attention, l'utilitaire rlwrap n'est pas installé par défaut...)
La première commande à interpréter est la commande exit. Dans tous les autres cas, votre shell affichera un message d'erreur...
Fonction C à utiliser :
-
fflush
etstdout
(variable globale), théoriquement nécessaire, mais probablement facultative en pratique, -
fgets
pour lire une ligne (attention, le caractère'\n'
fait partie de la chaine renvoyée parfgets
), -
stdin
(variable globale), -
strtok
pour récupérer le premier mot de la ligne et supprimer les espaces autour (note : on peut donner plusieurs séparateurs àstrtok
, par exemple" \t\n\r"
pour couper sur tous les caractères blancs et les supprimer), -
strcmp
pour comparer les chaines de caractères.
Remarque : pour simplifier, n'hésitez pas à imposer une limite (raisonnable) sur le nombre de caractères autorisés sur une ligne.
Interprétez maintenant les commandes pwd et cd <PATH>.
Fonction C à utiliser :
-
getcwd
pour récupérer la valeur courante du répertoire de travail, -
chdir
pour changer le répertoire de travail, -
strtok
pour récupérer le second mot de la ligne (argument <PATH>).
-
La commande
chdir
gère automatiquement les chemins relatifs au répertoire courant. -
N'oubliez pas de faire un minimum de gestion d'erreur en affichant un message lorsqu'une commande ne fonctionne pas.
"exit" ou "pwd" sont des commandes internes au shell ("builtin" en anglais). Le shell permet également de lancer de nouveaux processus en exécutant un fichier. Vous devez maintenant gérer ceci.
-
On suppose dans ce cas que la ligne contient uniquement un chemin d'accès vers un fichier exécutable qu'il faudra lancer dans un nouveau processus. Votre shell devra attendre la fin de ce processus avant de réafficher le prompt et de continuer...
Fonctions C à utiliser :
-
fork
, -
execl
pour exécuter un nouveau programme, -
wait
ouwaitpid
.
Les commandes de la famille de
execl
gèrent automatiquement les chemins absolus et relatifs.Pour tester, vous pouvez soit exécuter des commandes système (avec un chemin absolu complet), soit tester avec des commandes locales (avec un chemin relatif) :
# /bin/date Thu Nov 7 15:39:15 CET 2019 # ./question1 Salut, je m'appelle Bob et je suis le père. Mon fils a le PID 217. Hej, je suis le fils de Bob.
-
-
La plupart des programmes acceptent des arguments sur la ligne de commandes. Vous devrez donc découper la ligne en mots et utiliser le premier mot comme chemin d'accès au fichier exécutable, et les mots suivant comme tableau d'arguments (pour la commande
execv
).Fonction C à utiliser :
-
strtok
pour découper la ligne, -
execv
, pour remplacer leexecl
de la question précédente (faites attention à bien initialiser les premières et dernières cases de l'argumentargv
!).
Remarque : n'hésitez pas à imposer une limite (raisonnable) sur le nombre d'arguments possibles des commandes,
Vous pouvez par exemple tester la commande printf:
# /usr/bin/printf %i+%i=%i 1 2 3 1+2=3
(Attention à ne pas rajouter d'espace pour que les arguments soient bien lus correctement avec
strtok
.) -
La commande ls est une commande externe (fichier /bin/ls sur mon système). Nous allons en programmer une version simple interne : lorsque la ligne commence par ls, votre code appellera une fonction que vous écrirez qui affichera la liste des fichiers / répertoires du répertoire de travail.
Fonctions C à utiliser :
-
getcwd
(ou une variante) pour obtenir le répertoire de travail, -
opendir
pour ouvrir un répertoire, -
readdir
pour lire la liste des fichiers / répertoires d'un répertoire.
Ajoutez des fonctionnalités (au choix) dans votre shell.
Par exemple :
-
Possibilité de quitter le shell avec Ctrl-d, qui ferme l'entrée standard du programme.
-
Gestion des variables d'environnement $PWD et $OLDPWD et de l'abréviation cd - pour aller au répertoire précédent, cd pour aller au répertoire $HOME de l'utilisateur.
Fonctions C pertinentes :
-
getenv
, -
setenv
.
-
-
Utilisation de la variable d'environnement PATH pour chercher les exécutables afin d'éviter d'avoir à taper le chemin absolu des commandes. (Regardez dans la page de manuel de
execv
pour voir comment faire.) -
Redirection de la sortie standard dans un fichier avec
# CMD > FICHIER
Fonctions C à utiliser :
-
open
, -
dup2
, -
STDOUT_FILENO
(constante).
Vous pouvez imposer que les tokens ">" et "FICHIER" soient les derniers sur la ligne.
-
-
Redirection de l'entrée standard avec "< FICHIER", ou redirection de la sortie en mode append avec ">> FICHIER".
C'est encore mieux si on peut faire plusieurs redirections sur la même ligne...
-
Lancement d'un processus externe en arrière plan avec
# CMD &
Remarque : il n'est pas demandé d'implanter les fonctionnalités bg et fg des shells usuels.
-
Plus difficile : redirection de la sortie standard d'une commande vers l'entrée standard d'une autre commande avec
# CMD1 | CMD2
Fonctions C à utiliser:
-
pipe
, -
dup2
, -
STDOUT_FILENO
etSTDIN_FILENO
(constantes).
-
-
...