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/ ├─ RAPPORT ├─ Makefile ├─ question1.c ├─ ... ├─ test_procesus.c ├─ ... └─ minishell.c
Attention :
-
RAPPORT doit être un fichier texte contenant les réponses aux questions du TP et vos commentaires / remarques / explications pertinents,
-
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,
-
un ou plusieurs fichiers C pour chaque question pertinente.
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 et Clovis.Eberhart@univ-smb.fr
1. Les processus
1.1. Préliminaires
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
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
Écrivez un petit programme C qui ne s'arrête pas tout de suite et vérifiez que vous pouvez le voir avec la commande ps.
Remarque : pour empêcher un programme C de se terminer, vous pouvez :
-
mettre une boucle infinie
while(1);
avant l'instructionreturn
finale, -
mettre une instruction
getchar();
avant l'instructionreturn
finale, -
mettre une instruction
sleep(N);
qui met le processus en pause pendant (environ) N secondes.
Dans le premier cas, vous devrez arrêter le programme avec un "Control-c", ou bien avec la commande kill...
Dans le deuxième cas, il ne faut pas oublier la ligne #include
<stdio.h>
au début de votre fichier... Vous pouvez arrêter le
programme en appuyant sur la touche "Entrée".
Dans le troisième cas, il ne faut pas oublier la ligne #include
<unistd.h>
.
1.2. Arborescence des processus, instruction fork
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 départ (le père) et le
nouveau processus (le fils) :
-
dans le processus de départ,
fork()
renvoie un entier strictement positif : le PID du processus créé, -
le nouveau processus se comporte comme si
fork()
avait renvoyé la valeur0
.
Que doit afficher le programme suivant ?
#include <stdio.h>
#include <unistd.h>
int main() {
printf("Bonjour,\n");
fork();
printf("J'ai fait un 'fork' !\n");
return 0;
}
Testez et vérifiez que le résultat correspond à vos attentes.
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.
-
Modifiez le programme précédent pour que :
-
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 (avec une boucle
while(1);
ou unsleep(10)
).
-
-
Compilez et lancez votre programme en arrière plan :
$ make gcc question3.c -o question3 $ ./question3 & 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) sans que le fils ne disparaisse.
Créez un processus zombie. Vous devez :
-
expliquez comment vous vous y prenez en donnant le code C correspondant,
-
expliquez comment vous vérifiez l'existence du processus zombie.
1.3. "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 de la question précédente 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.
Remarque : la commande killall permet de tuer tous les processus avec un nom donné.
$ killall PROCESS_NAME
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.
(*bonus*)
Expliquez ce que fait l'incantation ésotérique suivante dans un shell:
$ :(){ :|:& };:
1.4. 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 programme suivant n'a pas le comportement attendu...
#include <stdio.h>
#include <unistd.h>
int main() {
printf("Je vais faire un 'fork'... ");
int p = fork();
if (p == 0) {
printf("Je suis le fils...\n");
} else {
printf("Je suis le père...\n");
}
return 0;
}
Testez et proposez une explication.
1.5. Ordonnancement des processus et priorités
Comparez l'utilisation du processeur lors de l'exécution des programmes suivants :
int main() {
while(1);
return 0;
}
#include <stdio.h>
int main() {
getchar();
return 0;
}
#include <unistd.h>
int main() {
while(1) {
sleep(1);
}
return 0;
}
Expliquez les différences constatées, en particulier entre le premier et le troisième programme.
La commande
$ top
permet d'afficher, en temps réel, les informations relatives au processus
-
la colonne %CPU indique le pourcentage d'utilisation du CPU par le processus,
-
la colonne S indique l'état du processus :
-
R (Running) pour l'état prêt,
-
S (Sleep) pour l'état bloqué,
-
Z (Zombie) pour l'état zombie.
-
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 5 et 10 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.
-
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 du 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.
La première version du shell n'interprètera que 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 lue 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
), -
strcmp
pour comparer les chaines de caractères.
Remarque : pour simplifier, n'hésitez pas à imposer une limite sur le nombre de caractères autorisés sur une ligne.
Dans la deuxième version du shell, vous devrez interpréter 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 troisième version du shell lancera un nouveau processus lorsque la ligne n'est pas exit.
On suppose dans ce cas que la ligne consiste uniquement en 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
.
Travaillez avec l'hypothèse suivante : aucun fichier ou commande ne contient des caractères "étranges" comme : espace, tabulation, guillemet, "<", ">", "|", "&", etc.
La quatrième version du shell lancera un nouveau processus lorsque la ligne n'est pas exit, mais utilisera la suite de la ligne pour spécifier les argument de la commande.
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 (facultatif mais conseillé), -
execv
, pour remplacer leexecl
de la question précédente.
Remarque : n'hésitez pas à imposer une limite sur le nombre d'arguments possibles des commandes,
La cinquième version du shell doit gérer la commande ls, qui 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 :
-
Utilisation de la variable d'environnement PATH pour chercher les exécutables afin d'éviter d'avoir à taper le chemin absolu des commandes.
Fonction C à utiliser:
-
getenv
, -
execvp
(qu'elle est la différence avecexecv
?).
-
-
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.
-
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).
-
-
...