|Home|      <<Prev<<      <-Back--      >>Next>>

Fonctions de base d'Unix concernant les processus


Tous les documents de cette page sont sous licence Creative Commons BY-NC-SA . Merci de la respecter.


©A. Dragut
Université Aix-Marseille
I.U.T.d'Aix en Provence - Département Informatique
Dernière mise à jour :18/10/2012





Rappel structure des répertoires

Sommaire du document

Liste des fonctions et concepts système étudiés

Dans cette série de dix exercices on approfondit la notion de processus à l'aide des fonctions fork(), wait(), on étudie de près les intéractions des signaux avec les processus, avec la fonctionkill(), et on se familarise avec la famille de fonctions exec().

Liste des exercices

Bon courage !


Les processus sont des programmes en cours d'exécution. Concept fondamental des systèmes de la famille Unix, ils permettent la gestion flexible de l'allocation des ressources principales du système -- CPU, -- mémoire, etc.
Nous allons ici étudier et mettre en oeuvre les techniques de base pour en contrôler la création et les relations de base entre père et fils. L'action de base pour la création est l'appel de la fonction fork(). Celle-ci duplique simplement le processus en cours. Des actions différentes peuvent par la suite avoir lieu, en structurant le code dans un schéma alternatif piloté par la valeur de retour de fork(), laquelle indique si l'on se trouve dans le père ou dans le fils.
Pour exécuter de programmes complétement différents de celui en cours, on dispose de la famille execve(), qu'on étudiera également.
Dans le monde UNIX un père doit attendre la mort de ses fils avec un fonction de type wait3(), waitpid().

exo_01 fonction fork(), père, fils, héritage

Dans cet exercice

vous écrivez un petit programme qui se dédouble avec fork(), pour que chacun des processus père et fils. s'identifie ensuite avec son pid.

Quoi de neuf


    L'ordonnanceur (scheduler) peut élire successivement     La seule contrainte est que le shell ne peut reprendre la main que lorsque le processus père est terminé. En revanche le choix de l'ordonnanceur doit être considéré comme aléatoire. Les ordres (2) - (1) - (3) et (2) - (3) - (1) sont aussi possibles.

Travail à faire

exo_01_a Héritage du traitement des signaux

Travail à faire

exo_01_b Héritage des descripteurs de fichiers ouverts

Quoi de neuf

La fonction getdtablesize() permet de connaître le nombre maximal de fichiers que peut ouvrir un processus, donc la taille maximale de la table des file descriptors. Pour tester si un descripteur correspond à un fichier ouvert on peut utiliser Lseek() (avec de tells paramètres qu'elle ne modifie pas l'état du fichier).
De même, les filedescrs. sont pris en ordre croissant au fur et à mesure que les open ont lieu (à partir du '3', car les 0, 1 et 2 sont ouverts par défaut). Une fois ouverts, si on en referme certains, lors du parcours de la suite 0,1,2,..., pour l'examination (avec Lseek() par exemple) la suite des descripteurs ouverts ne sera pas nécessairement formée de nombres strictement consécutifs.

Travail à faire

exo_01_c Partage du pointeur de fichier entre le fils et le père

Travail à faire

OPTIONNEL exo_01_TuQuoqueFili Compréhension de la fonction kill() par D.Mathieu, M. Laporte Merci!

Travail à faire



exo_02 fonctions wait() et waitpid(), status des fils morts

Dans cet exercice

vous écrivez un programme qui se dédouble plusieurs fois, et dont chacun des fils dort un nombre différent de secondes avant de se terminer. Le père attend chacun de ses fils (avec Waitpid()) et affiche leurs codes de sortie.

Quoi de neuf

Quand un processus se termine, on dit qu'il meurt. Ses zones mémoires sont libérées et il n'aura plus de temps CPU. Pourtant, il laisse une petite trace de son exécution : son status. Cette valeur est un nombre entier qui rend normalement compte de la bonne terminaison d'un processus ou, au contraire, d'une erreur. L'usage veut que la valeur 0 signale un comportement normal et que toute autre valeur soit un code d'erreur.
Le status est stocké par le système dans la table des processus. Il en résulte que l'entrée occupée par le processus dans cette table n'est pas immédiatement supprimée. Cette entrée reste valide jusqu'à ce qu'un processus vienne la lire. Durant ce laps de temps, on dit que le processus est dans l'état Zombie. Les processus zombies sont indiqués par un Z dans la colonne STATUS d'un ps.
C'est normalement le rôle du père d'aller lire le status de ses fils. Si le fils perd son père, il devient automatiquement le fils adoptif du processus de PID 1, qui se chargera d'aller lire le status. Si les Zombies auront rempli la table des processus, aucune création de processus ne sera plus possible.
La valeur de retour du fils est contenue dans le deuxième octet de plus faible poids du paramètre status de la fonction wait(), waitpid(), wait3() , étant donc un nombre entre zéro et 255.

Travail à faire

  • QUESTION: Modifiez le programme pour que tous les fils dorment NbreFils secondes. Que se passe-t-il et pourquoi?Effacez le changement.
  • QUESTION: Commentez dans le code l'attente des fils. Recompilez, et, une fois le message de la terminaison du père affiché, tapez dans une autre fenêtre
     
    ps -leaf | grep ' Z ' 
    
    Qui sont les processus en état zombie? Peu-t-on les tuer du shell avec kill -9 ? Pourquoi?


  • Pour tout processus qui est terminé, mais pour lequel le père n'a pas (encore) appelé une fonction de type wait*(), le système garde son entrée dans la table de processus, avec des renseignements expliquant la raison de la terminaison (normale, code de sortie, ou par signal, etc.). Il est très important de nettoyer ces zombies, même si parfois on n'est pas intéressé par ces renseignements, car sinon on remplit la table de processus (qui est finie!) et on grippe le système -- erreur No more processes. Le processus init s'occupe des zombies périodiquement et automatiquement, donc si jamais le père termine avant le fils, alors le problème est"résolu" de cette manière-là (une autre solution,POSIX-1-2001, est de mettre à SIG_IGN le traitement pour SIGCHLD, ou bien d'utiliser le drapeau SA_NOCLDWAIT lors d'un appel de sigaction() -- depuis les noyaux Linux 2.6).

    REMARQUE

    On peut écrire notre propre macro-instruction GETSTATUS pour émuler WEXITSTATUS(status).
    Les entiers peuvent être codés sur plus de deux octets, et seul le second octet doit être récupéré à l'aide des translations (shifts), et les masques binaires.

    Supposons que status s'écrit ainsi
    |          ||                       ||                       |
    |..._|__|__||__|__|__|__|__|__|__|__||__|__|__|__|__|__|__|__|
     .....17 16 15 14 13 12 11 10  9  8   7  6  5  4  3  2  1  0 
                ^^^^^^^^^^^^^^^^^^^^^^^^^
                 octet qui nous intéresse
    
    Il faut alors aboutir à valeur ainsi
    |          ||                       ||                       |
    |...0|_0|_0||_0|_0|_0|_0|_0|_0|_0|_0||__|__|__|__|__|__|__|__|
     .....17 16 15 14 13 12 11 10  9  8   7  6  5  4  3  2  1  0 
                                         ^^^^^^^^^^^^^^^^^^^^^^^^^
      tous les autres bits sont NULS      octet qui nous intéresse
    
    et une manière simple est de translater status vers la droite de huit bits, ramenant ainsi le second octet en la position du premier (lequel est jeté). Pour ce faire l'opérateur est >> et on écrit l'opération ainsi
    status >> 8
    
    Ce n'est tout de même pas suffisant, car il faut rendre nuls TOUS LES AUTRES BITS de status (qui peut en avoir par exemple 32!), afin d'obtenir donc un nombre entre zéro et 255. En général, pour rendre nuls certains bits et garder d'autres tels quels, on fait un ET binaire entre status et un nombre spécialement construit, qu'on appelle un masque binaire. Les bits zéro du masque, suite au ET binaire, font que le résultat final ait CES BITS également nuls, tandis que les bits non-nuls du masque CONSERVENT en l'état les bits correspondants du status.
    On peut donc le faire en deux étapes ou bien on peut inverser les opérations, mais alors le masque est bien entendu différent : il vaut 0xFF00, i.e. 65280.

    Les macros

    Syntaxe simple

    #define NOM(PARAMETRE) CORPS
    
    avec PARAMETRE apparaissant bien entendu dans le CORPS.

    Exemple

    #define maMacro(a) ((a) < 0 ? - (a) : (a))
    
    va définir une telle macro, et un programme contenant la ligne
    ... 
    cout << "Valeur absolue " << maMacro(x+y) << "\n";
    ...
    
    lors d'une invocation g++ ..., sera TRANSFORMÉ par le préprocesseur en le programme
    ... 
    cout << "Valeur absolue " << ((x+y) < 0 ? - (x+y) : (x+y)) << "\n";
    ...
    
    AVANT que le compilateur commence son travail dessus. Bien entendu, TOUS les endroits ou apparaît maMacro(quelqueChose) vont être ainsi transformés. On comprend aisément la raison de l'abondance de paranthèses dans la définition de la macro -- sinon le compilateur se plaint d'erreurs de syntaxe ou autres, bien difficiles à réparer. Ce remplacement effectué par le préprocesseur est purement syntaxique (pas de vérification de type, comme entier, double, etc. ni de consistance, etc.
    En conclusion notre macro sera:
    GETSTATUS(status) ((status)>>8) & 0xff
    


    exo_03 pére sans waitpid et sans zombies: SA_NOCLDWAIT et sigaction()

    Dans cet exercice

    On crée un père avec plusieurs fils. Dans le père on déroute SIGCHLD d'abord vers un traitant de signal.
    Un appel de type wait() dans le père permet au système de libérer les ressources associées au fils. S'il n'est pas effectué, le fils qui s'est terminé reste dans l'état de « zombie » sauf si le processus père a pris soin d'annoncer le système qu'il ne veut pas recevoir des notifications lors des changements d'état de ses processus fils.

    Quoi de neuf

    La fonction sigaction() permet une manipulation très fine des aspects reliés à la réception des signaux. La valeur SA_NOCLDWAIT dans le champ sa_flags d'une structure de type struct sigaction installée avec la fonction sigaction() demande de liberer le processus père de son devoir d'attendre ses fils tout en évitant la création des processus zombies,.

    Travail à faire



    OPTIONNEL exo_04 Traitant de SIGCHLD et attente correcte et analyse de terminaison pour plusieurs fils

    Dans cet exercice

    vous écrivez un programme semblable au programme précédent, mais qui prend bien soin de tous ses fils: vous rajoutez des traitants de signaux (SIGCHLD et SIGINT), et mettez l'attente dans une boucle, utilisant l'attente atomique de signaux. Les fils ne feront que dormir un nombre de secondes donné en argument, un pour chacun d'entre eux.

    Travail à faire

    • Dans le fichier exo_04.cxx dans le try...catch du main() dans un espace de nom anonyme:
      • déclarez une variable globale nommée qIlYADesFilsTermines, qui est modifiable de manière ininterruptible par les traitants de signal et non-optimisable par le compilateur
      • déclarez un traitant de signal TraitantPourLesFils qui met la valeur 1 dans qIlYADesFilsTermines
      • déclarez un traitant de signal TraitantQuelconque qui ne fait qu'afficher "Bonjour" et le numéro du signal reçu.
    • Dans le fichier exo_04.cxx dans le try...catch du main() mettez les actions suivantes :
      • récupèrez la valeur entière CstNbrFils (argument de commande)
      • déroutez le signal SIGCHLD vers le traitant de signal TraitantPourLesFils avec Sigaction() (et tout ce qu'il faut: déclarez la structure, videz-lui le masque avec sigemptyset(), etc.)
      • vérifiez les arguments du main() ainsi
            if (3 > argc) 
                throw CExc ("main()",string ("Usage : ") + argv [0] +
                           " <nbrFils (entre 1 et 20)> <sleepSecondesFils1> <sleepSecondesFils2> ...");
        
            qIlYADesFilsTermines = 0;
        
            const int CstNbrFils = atoi (argv [1]);
        
            if (CstNbrFils < 1 || CstNbrFils > 20)
                throw CExc ("main()","Nombre de fils non compris entre 1 et 20");
        
            if(argc != CstNbrFils + 2) {
                throw CExc ("main()",string ("Usage : ") + argv [0] +
                            " <nbrFils (entre 1 et 20)> <sleepSecondesFils1> <sleepSecondesFils2> ...");
        
      • déclarez deux masques de signaux nommés MasqueBlock et MasqueUnBlock
      • initialiser MasqueBlock avec un seul signal, SIGCHLD, et MasqueUnBlock à zéro
      • bloquez SIGCHLD à l'aide de MasqueBlock
      • déclarez deux tableaux pour garder les pid des fils et respectivement leur status:
      •  
        
            ::pid_t TabPid [CstNbrFils];
            ::pid_t TabEtat [CstNbrFils];
           
        
      • dans le père, initialiser TabEtat[i] à un .
      • le père crée CstNbreFils fils. Le ième fils "dort" ::atoi(argv[i+2]) secondes avant de se terminer et fait return i.
      • construisez deux boucles imbriquées ainsi:
      •     for(int ilYAEncoreAutantDeFils (CstNbrFils); ilYAEncoreAutantDeFils;) {
                cout << "En attente, car il me reste " << ilYAEncoreAutantDeFils << " fils...\n";
                ::sigsuspend(&MasqueUnBlock);
        	if(!qIlYADesFilsTermines) {
        	    continue;
        	}
        	qIlYADesFilsTermines = 0;
        
                for(int kFils(0); kFils  < CstNbrFils; kFils++) {
                    int status;
                    if(0 == TabEtat[kFils]) {
                        continue;
                    }
                    cout << "Recuperation du status fils " << TabPid[kFils] << " ... ";
        
      • appelez Waitpid() avec trois arguments : l'élément TabPid[kFils] qui indique le pid, l'adresse de status, et l'option qui l'empêche d'attendre indéfiniment (et donc s'il est encore en vie, que renvoie-t-elle ?). Récupérez sa valeur de retour leFils
      • si au retour de Waitpid() on se rend compte que leFils nous indique en effet il n'était pas terminé pour le moment, passez à l'itération suivante de la boucle for() intérieure (avec l'instruction continue, précédée d'un message).
      • d&eaute;crémentez ilYAEncoreAutantDeFils et mettez TabEtat[kFils] à zéro
      • faites une analyse complete du status
      • fermez les deux boucles, et terminez le programme avec un message final.
    • compilez, ouvrez deux terminaux, et testez par exemple avec 9 3 3 3 5 9 9 9 15 25 comme arguments. Lancez exo_04.run dans un des terminaux et pendant l'exécution, depuis l'autre terminal envoyez des signaux (SIGKILL, SIGUSR1, etc.) à des fils (visez ceux qui dorment plus de temps) ; essayez également de faire Ctrl+C dans la fenêtre de lancement de exo_04.run.


    exo_05 fonctions exec(), héritage de la table des descripteurs

    Dans cet exercice

    vous écrivez un petit programme qui affiche ses arguments, et un autre programme qui lance celui-ci avec exec().

    Quoi de neuf

    Les fonctions de la famille exec...() remplacent le code du processus qui appelle cette fonction par celui du programme qui est passé en premier paramètre à la fonction. Si l'appel se déroule normalement, on ne revient donc jamais des fonctions exec...(). Donc tout retour signifie une erreur. Il est donc inutile d'écrire un wrapper, mais il est indispensable de vérifier que le programme appelant ne revient pas de la fonction, en plaçant un traitement d'erreur juste après l'appel. Les autres paramètres de la fonction fournissent les arguments du nouveau programme qui sera ainsi exécuté, et il y a uneconvention générale (que vous connaissez) qui dit que le premier est le nom même du (fichier contenant le) programme.

    Travail à faire

    • Dans le fichier exo_05_x.cxx, écrivez un programme qui affiche le nombre et la liste des arguments passés en argument lors de son lancement (argv, et argc), ainsi que la liste des descripteurs de fichiers ouverts (au moyen de la fonction TestFdOuverts() de l'espace de noms nsFctShell du fichier nsSysteme.cxx).
    • Compilez et testez.
    • QUESTION: Comment prendre dans la liste des arguments des caractères comme | et *?.
    • Dans le fichier exo_05.cxx, écrivez un programme qui
      • crée un fichier pour écriture
      • ouvre un fichier en lecture
      • ouvre un fichier en lecture/écriture en positionnant le drapeau de "fermé-en-exec" O_CLOEXEC
      • lance le programme exo_05_x.run (avec des paramètres quelconques) au moyen de la fonction execl(). Son dernier argument, qui est le pointeur nul, doit être spécifié en convertissant la valeur 0 (qui est a priori un entier, pour le compilateur) vers le pointeur de caractères, ainsi
        execl(..., static_cast<char *>(0));
        
        Explication: Les pointeurs peuvent être sur 64 bits, tandis que les entiers restent sur 32. L'absence de cette conversion entraîne l'erreur de compilation "missing sentinel").
    • Compilez et testez
    • QUESTION: Quels fichiers restent ouverts après un appel execl()?


    Remarque

    Si le man 2 open n'est pas mis à jour utilisez
    http://www.kernel.org/doc/man_pages
    
    Le drapeau est utilisable pour un kernel/noyau Linux > 2.6.23 et un glibc >2.7 . Sinon on peut positionner le drapeau "fermé-en-exec" après l'ouverture d'un fichier avec le drapeau/flag F_SETFD de la fonction fcntl() et la valeur FD_CLOEXEC


    OPTIONNEL exo_06 exec() -- émulation de la fonction system()

    Dans cet exercice

    Vous écrivez une fonction System() qui fait la même chose que system(), et vous la testez avec un petit programme.

    Quoi de neuf

    Pour la réalisation de cette fonction, il faut choisir une fonction de type exec() comme execv(). Ensuite, il faut lui construire la liste des arguments, et ne pas oublier le pointeur nul de la fin, à convertir, ainsi que la convention argv[0]=nom de la commande.
    La fonction à utiliser est strtok(). Elle décompose une chaîne de caractères en "mots" (lexèmes) délimités par certains séparateurs (comme l'espace, ou la virgule), fournis également à la fonction strtok().
    On doit appeler cette fonction qui renvoie un char * une première fois ainsi
    premierMot = strtok(chaineAdecomposer, Separateurs);
    
    et puis l'appeler obligatoirement ainsi
    autreMot = strtok(0, Separateurs);
    
    c'est-à-dire avec zéro pour son premier paramètre.
    La fonction strtok() rend en fait des pointeurs à l'intérieur de l'espace pointé par chaineAdecomposer (ne copiant rien nulle part). Il faut savoir que
    • strtok() modifie effectivement son premier argument chaineAdecomposer, en insérant des '\0' à la fin de chaque mot qu'elle trouve
    • les caractères de séparation sont surchargés, leur identité est donc perdue.
    • on peut indiquer des séparateurs différents d'un appel à l'autre.
    • cette fonction ne doit pas être invoquée sur une chaîne constante.
    • elle utilise un buffer statique et n'est donc pas POSIX safe dans un contexte multithread. Dans ce cas il vaut mieux utiliser strtok_r().

    Travail à faire

    • Dans l'espace des noms anonyme, écrivez la fonction System(const char * const Commande); qui émule la fonction système system().
      • créez un père et un fils.
      • dans le fils tant que strtok() n'est pas arrivé à la fin de la chaineAdecomposer
        • construissez à partir du argv[1] un tableau avec maximum CstMaxArg arguments separés par des espaces
        • appelez la fonction execve() sans oublier le pointeur nul de la fin et le fait que le premier argument est le nom de la commande
      • Dans le fichier exo_06.cxx dans le try ... catch du main() appelez System(const char * const Commande);
    • Compilez et testez avec ./exo_06.run 'ls -l' |grep exo_06



    Solutions

    Solution exo_01


    exo_01_a.cxx
    exo_01_b.cxx
    exo_01_c.cxx
    exo_01_c.txt
    TuQuoqueFili_a
    TuQuoqueFili_b
    Makefile
    CExc.h
    INCLUDE_H
    nsSysteme.h
    nsSysteme.cxx

    Solution exo_02


    exo_02.cxx
    Makefile
    CExc.h
    INCLUDE_H
    nsSysteme.h
    nsSysteme.cxx

    Solution exo_03


    exo_03.cxx
    Makefile
    CExc.h
    INCLUDE_H
    nsSysteme.h
    nsSysteme.cxx

    Solution exo_0


    exo_04.cxx
    Makefile
    CExc.h
    INCLUDE_H
    nsSysteme.h
    nsSysteme.cxx

    Solution exo_05


    exo_05.cxx
    exo_05_x.cxx
    Makefile
    CExc.h
    INCLUDE_H
    nsSysteme.h
    nsSysteme.cxx

    Solution exo_06


    Pour cet exercice vous trouverez trois solutions possibles: une qui utilise strtok mais est limitée par le nombre max d'arguments et deux autres qui sont plus générales, la dernière étant également la plus efficace en utilisation mémoire.
    À propos de l'utilisation de la mémoire, quelques petites explications supplémentaires se trouvent ici, avec bon nombre de conseils pratiques et également une analyse détaillé d'une de variantes de solutions (pour la reproduire, il faut enlever les commentaires devant les sleep(), etc. -- tout est indiqué dans son code source).
    exo_06.cxx
    varexo_06.cxx
    autrevarexo_06.cxx
    Makefile
    CExc.h
    INCLUDE_H
    nsSysteme.h
    nsSysteme.cxx

    © A. B. Dragut     dragut@univmed.fr
    I.U.T.d'Aix en Provence - Département Informatique

    |Home|      <<Prev<<      <-Back--      >>Next>>