Commandes du shell pour les processus
Les exercices suivants s’attachent à illustrer l’utilisation de quelques commandes qui permettent de consulter la liste des processus, de les lancer en bloquant ou non l’exécution du shell, et enfin de remplacer le code du processus shell par celui d’un autre processus.
- Pour constater la différence utilisateur/processus, on essaiera la séquence de commandes suivantes:
ps -l (liste des processus attachés au terminal) ps -edf (liste des processus présents sur le système) ps -axl (idem avec plus de détails) ou ps -ael ...suivant le système utilisé ps -axjf --forest (idem avec identifiant du processus parent et représentation sous forme d'arbre)
On remarquera les différents processus :
- init de pid 1.
- les démons (notés ?),
- ceux qui exécutent un shell ou dérivent d’un shell (sh, csh ou ksh),
- Envoi d’une commande en arrière plan: dans cet exercice, nous allons illustrer l’utilité de l’option & lorsqu’on lance un processus à partir du shell. Commencez par lancer un processus:
$ xeyes
l’application xeyes se lance, et vous ne pouvez plus taper de commandes tant que vous n’avez pas fermé cette application. Maintenant, lancez cette même application avec l’option &:
$ xeyes &
Cette fois-ci, l’application xeyes s’est lancée mais vous pouvez continuer à taper de nouvelles commandes.
Prenons un autre exemple: sleep n met en pause pendant n secondes le processus qui exécute cette commande. Envoyer la séquence suivante et observer la filiation des processus :
ps -l (liste des processus attachés au terminal courant) sleep 20& (processus qui dort pendant 20s en arrière plan) sleep 30& (processus qui dort pendant 30s en arrière plan) ps -l (pour voir la nouvelle configuration de processus et constater que le processus qui exécute le shell a plusieurs processus fils, chacun faisant tourner le programme sleep)
- Effet de la commande exec. Nous allons voir maintenant l’effet de la commande exec, qui comme la fonction du même nom, remplace le code et le données du processus qui l’exécute par le code d’un fichier binaire référencé lors du lancement de exec.
- Faire : exec ps
que se passe-t-il et pourquoi ?
Indication : pour exec, comme pour quelques autres commandes (built-in commands) le shell ne créée pas de nouveau processus, c’est à dire qu’il ne fait pas appel à fork pour la commande, mais l’exécute directement dans le code du shell (cf. man exec). - Ouvrir une nouvelle fenêtre du terminal,
- Entrer maintenant la commande : zsh,
- Relever le numéro du processus qui fait tourner ce nouveau shell courant (en faisant ps -l),
- Faire maintenant : exec ps -l.
Relever le pid du processus qui exécute ce programme ps -l.
Quelle est sa valeur et pourquoi ?
- Faire : exec ps
Exercices de programmation C: fork/exec/wait
Les sources de ce TP, comme pour les autres, sont dans l’archive que vous avez téléchargée au début des TPs de SELC/INF104.
Par la suite, pour compiler les fichiers et créer les exécutables, utiliser la commande gcc.
Par exemple (premier exercice) :
$ gcc -Wall fork-example.c -o fork-example
Sur une machine multi-processeur on peut avoir à forcer l’exécution sur un seul processeur, dans ce cas il faut utiliser la commande taskset, par exemple :
taskset -c 0 ./fork-example
Création de processus (fonction fork)
Fonctions à utiliser
Dans ces premiers exercices, vous allez utiliser les fonctions suivantes:
- fork() :
Cette fonction va créer un processus. La valeur de retour n de cette fonction indique :- n > 0
On est dans le processus père, - n = 0
On est dans le processus fils, - n = -1
fork a échoué, on n’a pas pu créer de processus.
- n > 0
- getpid() :
Cette fonction retourne le numéro du processus courant. - getppid() :
Cette fonction retourne le numéro du processus père.
Exercice 1
- Compilez le fichier fork-example.c, puis exécutez le.
Pourquoi cinq lignes sont-elles affichées alors qu’il n’y a que trois appels à printf dans le programme ? Exécuter le programme plusieurs fois. L’ordre d’affichage est toujours le même, mais essayez en exécutant avec « taskset -c 0 » et en insérant un appel à sleep(0) entre les deux printf exécutés les deux processus. Est-ce que l’ordre change (essayer plusieurs fois)? Si oui, pourquoi? - Ajouter maintenant la ligne suivant l’appel à fork :
if (ret == 0) sleep (4);
Que se passe-t-il ? Pourquoi ?
Exercice 2
Compiler fork-multiple.c, et exécutez ce programme.
Après exécution et à l’aide du schéma suivant, relever les numéros des processus et numéroter l’ordre d’exécution des instructions printf de façon à retrouver l’ordre d’exécution des processus.
Remarque:
si on relève un numero de processus égal à 1, il s’agit du processus init, père de tous les processus. Init adopte les processus orphelins, c’est à dire qu’un processus dont le père s’est terminé devient fils de 1, et getppid() renvoie 1.
Synchronisation de processus père et fils (mécanisme wait/exit)
Fonctions utilisées
- exit( i ) :
termine un processus, i est un octet (donc valeurs possibles : 0 à 255) renvoyé dans une variable du type int au processus père. - wait( &Etat ):
met le processus en attente de la fin de l’un de ses processus fils.Quand un processus se termine, le signal SIGCHILD est envoyé à son père. La recéption de ce signal fait passer le processus père de l’état bloqué à l’état prêt. Le processus père sort donc de la fonction wait. La valeur de retour de wait est le numéro du processus fils venant de se terminer. Lorsqu’il n’y a plus (ou pas) de processus fils dont il faut attendre la fin, la fonction wait renvoie -1. Chaque fois qu’un fils se termine le processus père sort de wait, et il peut consulter Etat pour obtenir des informations sur le fils qui vient de se terminer.
Etat est un pointeur sur un mot de deux octets :- L’octet de poids fort contient la valeur renvoyée par le fils ( i de la fonction exit( i )),
- l’octet de poids faible :
- contient 0 dans le cas général,
- En cas de terminaison anormale du processus fils, cet octet de poids faible contient la valeur du signal reçu par le fils.
Cette valeur est augmentée de 80 en hexadécimal (128 décimal), si ce signal a entrainé la sauvegarde de l’image mémoire du processus dans un fichier core.
- Schéma résumant le contenu du mot pointé par Etat à la sortie de wait :
Mécanisme wait/exit
Compiler et exécuter fork-synchronization.c. Vérifiez que le l’affichage est cohérent avec ce qui est écrit dans le code.
Père et fils exécutent des programmes différents
Fonction utilisée
La fonction exec charge le code d’un fichier exécutable dans une zone mémoire qui sera utilisé comme séquence d’instructions à exécuter par le processus qui l’appelle. Donc, exec remplace le code à exécuter par le processus (tout en utilisant une zone mémoire différente) par celui contenu dans le fichier exécutable. Les zones de données sont également ré-initialisés.
Une des formes de cette fonction est execl :
int execl (char *fic, char * arg0, [arg1, ... argn,])
Commentaires :
- fic est le nom du fichier exécutable qui sera chargé dans la zone de code du processus qui appelle execl.
Si ce fichier n’est pas dans le répertoire courant, ou s’il n’est pas accessible via la liste de répertoires référencées par la variable PATH, il faut préciser le chemin vers le fichier (relatif ou absolu). - Les paramètres suivants sont des pointeurs sur des chaines de caractères contenant les arguments passés à ce programme (cf. argv en C).
La convention UNIX impose que la première soit le nom du fichier lui-même et que le dernier soit un pointeur nul.
Par exemple, si on veut charger le fichier appelé prog qui se trouve dans le répertoire courant et qui n’utilise auncun argument passé sur la ligne de commande :execl (« prog », « prog », (char *)0)
Fonctionnement de exec
Compiler puis exécuter le fichier fork-exec.c, en appelant l’exécutable fork-exec.
Pour en vérifier le bon fonctionnement, on fera exécuter par execl un programme très simple, par exemple celui que vous avez obtenu après compilation de votre premier code C lors de votre premier TP de SELC: hello-world.c
Par la suite, vous pouvez exécuter le programme fork-exec en lui passant diverses commandes en argument, par exemple :
- fork-exec fork-exec
- fork-exec /usr/bin/xclock
- fork-exec /bin/ps
- fork-exec /usr/bin/emacs
- …
Exercice
Dans cet exercice, vous devez compléter le fichier source fork-exercice.c. Voici l’énoncer de l’exercice: en vous inspirant des exemples précédents, écrire un programme dont le fonctionnement est le suivant (compiler en utilisant l’option -Wall de gcc, il ne doit pas y avoir de warnings après la compilation) :
- il lit sur la ligne de commande (utiliser argc et argv) le nombre N de processus à créer.
- il crée ces N processus en faisant N appels à fork (la tâche assignée à ces processus sera expliqué plus loin).
- il se met en attente de ces N processus fils et visualise leur identité (leur PID et la valeur de leur état) au fur et à mesure de leurs terminaisons. Pour attendre la fin de tous les fils, utiliser le fait que wait renvoie la valeur -1 quand il n’y a plus de processus fils à attendre.Ce que fait chacun des processus fils Pi:
- il visualise son pid (getpid) et celui de son père (getppid),
- il se met en attente pendant 2*i secondes (sleep (2*i)), affiche (via un printf) la fin de l’attente,
- il se termine par exit(i), où i correspond à l’identifiant du ième fils.
Après avoir vérifié que le programme fonctionne correctement :
- Faire en sorte que l’un des processus fils se termine anormalement (par exemple en faisant une division entière par zéro ou en déréférençant un pointeur nul) et constater que le système ne renvoie pas zéro en poids faible de Etat, mais y dépose un indicateur d’erreur.