Rifflet, exercice 69

Etudier le comportement des différents processus créés par le programme suivant, en supposant que l'exécution de la fonction alpha(n) (dont le détail est sans importance) dure 2n secondes.

Programme

#define BASE 100000

int duree;

/* Boucle de calcul */

int alpha (int n) {
  int i, s = 0;
  
  n = n * duree;
  for (i = 1; i < n; i++)
    s = s + i * i;
  return s;
}

main (int argc, char **argv) {
  pid_t pid;
  int etat;
  
  duree = atoi (argv [1]) * BASE;

  if (fork () == 0)
    if (fork () == 0) sleep (10);
    else { alpha (2); exit (2); }
      
  if (fork () == 0) { alpha (3); exit (3); }
  if (fork () == 0) { sleep (2); exit (10); }
  
  alpha (1);
  pid = wait (&etat);  
  pid = wait (&etat);
  sleep (3);
  alpha (1);
  pid = wait (&etat);
  pid = wait (&etat);
}

Exécution commentée

Cette trace d'exécution est obtenue en ajoutant diverses fonctions d'affichage (voir le programme détaillé) ; sur la machine utilisée, le choix de l'argument 20 pour le lancement du programme permet d'obtenir un temps de calcul voisin de 2n secondes pour la fonction alpha(n).

Des commentaires (en vert) ont été ajoutés manuellement ; dans ces commentaires, on n'a gardé que les trois derniers chiffres des numéros (pid) des processus.

almira% forkw 20
[0.00] 26790 (25733) : créé
[0.01] 26791 (26790) : créé
[0.02] 26793 (26791) : créé et endormi 10 secondes
[0.02] 26794 (26790) : créé et endormi 2 secondes
[0.03] 26791 (26790) : calcule (2)
[0.03] 26790 (25733) : calcule (1)
[0.14] 26792 (26790) : créé et calcule (3)

               790
              / | \
             /  |  \
            /   |   \
          791  792  794
           |
           |
          793

[2.59] 26794 (26790) : terminé
       Temps cpu 0.00 , temps système 0.01

  Ce processus a simplement dormi deux secondes.
  790, 791, 792 calculent simultanément.

[6.24] 26790 (25733) : attend
[6.29] 26790 : reçoit le signal de fin de 26794 (code 10)
[6.35] 26790 (25733) : attend

  Un peu plus de six secondes se sont écoulées avant que 790
  termine son calcul, d'une durée théorique de deux secondes,
  car trois processus actifs se partagent le processeur.
  
  Dans le jargon Unix, on dit que le temps réel vaut 6 secondes
  et le temps cpu 2 secondes.
  
               790
              / |
             /  |
            /   |
          791  792
           |
           |
          793

  791 et 792 calculent.

[10.15] 26793 (26791) : réveillé
[10.16] 26795 (26793) : créé et calcule (3)
[10.20] 26793 (26791) : calcule (1)
[10.58] 26796 (26793) : créé et endormi 2 secondes

Noter que le code exécuté par le processus 793 ne comporte
pas d'appel-système exit après sleep (10). Donc ce processus,
une fois réveillé, crée les processus 795 et 796, comme le
processus 790 l'a fait pour les processus 792 et 794.

               790
              / |
             /  |
            /   |
          791  792
           |
           |
          793
          / \
         /   \
        795  796
        
   791, 792, 793, 795 calculent simultanément

[12.07] 26791 (26790) : terminé
       Temps cpu 4.10 , temps système 0.00
[12.21] 26790 : reçoit le signal de fin de 26791 (code 2)
[12.21] 26790 (25733) : endormi 3 secondes
[12.59] 26796 (26793) : terminé
       Temps cpu 0.00 , temps système 0.00

               790
                |
                |
                |
               792
           |
           |
          793
          / \
         /   \
        795  796 (zombi)

[15.21] 26790 (25733) : réveillé et calcule (1)
[18.97] 26792 (26790) : terminé
       Temps cpu 6.22 , temps système 0.00
[19.34] 26790 (25733) : attend
[19.36] 26790 : reçoit le signal de fin de 26792 (code 3)
[19.36] 26790 (25733) : attend
[19.37] 26790 n'a plus de fils
[19.37] 26790 (25733) : terminé
       Temps cpu 4.10 , temps système 0.05

  Le temps réel d'exécution de 792 vaut environ 19 secondes,
  pour un temps cpu de six secondes, car plusieurs processus
  actifs se sont partagé le processeur.

almira% [21.52] 26793 (1) : attend
[21.54] 26793 : reçoit le signal de fin de 26796 (code 10)
[21.87] 26793 (1) : attend

  Noter que 793, orphelin, est rattaché au processus 1,
  et que l'invite réapparaît après la terminaison de 790.
  Les processus 793 et 795 existent en arrière-plan.

[22.89] 26795 (26793) : terminé
       Temps cpu 6.20 , temps système 0.00
[22.90] 26793 : reçoit le signal de fin de 26795 (code 3)
[22.90] 26793 (1) : endormi 3 secondes

  Le temps réel d'exécution de 795 vaut environ treize secondes,
  pour un temps cpu de six secondes, car plusieurs processus
  actifs se sont partagé le processeur.

[25.90] 26793 (1) : réveillé et calcule (1)
[27.95] 26793 (1) : attend
[27.95] 26793 n'a plus de fils

  793 est seul au monde !

[27.95] 26793 (1) : attend
[27.95] 26793 n'a plus de fils
[27.96] 26793 (1) : terminé
       Temps cpu 4.13 , temps système 0.01

  Diagramme récapitulatif d'exécution des principaux processus :
  
         alpha(1)      attend     dort   alpha(1)
  790 |------------|            |      |--------|
     0.03         6.24        12.21  15.21    19.37

              alpha(2)
  791 |-------------------------|
     0.03                     12.07

                     alpha(3)
  792 |----------------------------------------|
     0.14                                    18.97

             dort                   alpha(1)         attend  dort  alpha(1)
  793 |                    |------------------------|      |      |------|
     0.02                10.15                    21.52  22.90  25.90  27.95

                                          alpha(3)
  795                      |-------------------------------|
                         10.16                           22.89
  
Le temps d'exécution global est donc d'environ 28 secondes ; le processeur est occupé en permanence pendant les 23 premières secondes :
  • les calculs simultanés durent environ 11*2=22 secondes ;

  • le reste du temps (pendant ces 23 secondes) est consacré aux créations de processus, aux changements de contexte, aux exécutions des appels-système (attente, mise en sommeil, et affichage) ; aucun processus externe ne semble perturber l'exécution de ce programme.
Si un processus est terminé par un signal, et si le processus père attend sa terminaison, il obtient le numéro du signal ayant provoqué la terminaison de son fils. Exemple :
almira% forkw 20
[0.00] 16594 (16476) : créé
[0.01] 16595 (16594) : créé
[0.01] 16596 (16595) : créé et endormi 10 secondes
[0.03] 16597 (16594) : créé et calcule (3)
[0.05] 16598 (16594) : créé et endormi 2 secondes
[0.08] 16594 (16476) : calcule (1)
[0.34] 16595 (16594) : calcule (2)
[2.06] 16598 (16594) : terminé
       Temps cpu 0.00 , temps système 0.01
[6.47] 16594 (16476) : attend
[6.55] 16594 : reçoit le signal de fin de 16598 (code 10)
[6.59] 16594 (16476) : attend

  La commande kill 16597 est émise depuis une autre fenêtre.

[8.00] 16594 : reçoit le signal de fin de 16597 (signal 15)

  15 est le numéro de SIGTERM, émis par défaut par la commande kill.

On peut aussi envoyer un signal à tous les processus créés par ce programme car le premier est leader de groupe.
almira% ps -j
 PPID   PID  PGID   SID TT TPGID  STAT   UID  TIME COMMAND
16475 16476 16476 16475 p3 16641    SE  1006  0:00 -bin/csh (csh)
16476 16641 16641 16475 p3 16641    SE  1006  0:02 forkw 20
16641 16643 16641 16475 p3 16641     R  1006  0:04 forkw 20
16645 16646 16641 16475 p3 16641     R  1006  0:00 forkw 20

almira% kill -INT -16641
Les processus 16641, 16643 et 16646 reçoivent le signal SIGINT, et sont tués faute de moniteur pour traiter le signal.

Programme détaillé

#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>

#define BASE 100000

int duree;
double depart;

int alpha (int n) {
  int i, s = 0;
  
  n = n * duree;
  for (i = 1; i < n; i++)
    s = s + i * i;
  return s;
}

double chrono () {
  struct timeval date;
  
  gettimeofday (&date, 0);
  return date.tv_sec + date.tv_usec * 1e-6;
}

void identite (char *message) {
  printf ("[%.2lf] ",  chrono () - depart);
  printf ("%d (%d) : %s\n", getpid (), getppid (), message );
}

void attendre (void) {
  pid_t pid;
  int etat;

  identite ("attend");
  pid = wait (&etat);  
  printf ("[%.2lf] ",  chrono () - depart);
  if (pid < 0) {
    printf ("%d n'a plus de fils\n", getpid ());
    return;
  }
  printf ("%d : reçoit le signal de fin de %d", getpid (), pid);
  if (WIFEXITED (etat) )
    printf (" (code %d)\n", WEXITSTATUS (etat) );
  else
    printf (" (signal %d)\n", WTERMSIG (etat) );
}

void terminer (int code) {
  struct rusage ressource;
  double cpu, sys;
  
  identite ("terminé");
  getrusage (RUSAGE_SELF, &ressource);
  cpu = ressource.ru_utime.tv_sec + ressource.ru_utime.tv_usec * 1e-6;
  sys = ressource.ru_stime.tv_sec + ressource.ru_stime.tv_usec * 1e-6;
  printf ("       Temps cpu %.2lf , temps système %.2lf\n",  cpu, sys);
  
  exit (code);
}

main (int argc, char **argv) {
  struct timeval date;
  
  if (argc < 2)
    duree = BASE;
  else
    duree = atoi (argv [1]) * BASE;

  depart = chrono ();
  identite ("créé");
    
  if (fork () == 0) {
    identite ("créé");
    if (fork () == 0) {
      identite ("créé et endormi 10 secondes");
      sleep (10);
      identite ("réveillé");
    }
    else {
      identite ("calcule (2)");
      alpha (2);
      terminer (2);
    }
  }
  
  if (fork () == 0) {
    identite ("créé et calcule (3)");
    alpha (3);
    terminer (3);
  }

  if (fork () == 0) {
    identite ("créé et endormi 2 secondes");
    sleep (2);
    terminer (10);
  }
  
  identite ("calcule (1)");
  alpha (1);

  attendre ();
  attendre ();

  identite ("endormi 3 secondes");
  sleep (3);
  identite ("réveillé et calcule (1)");
  alpha (1);

  attendre ();
  attendre ();

  terminer (0);
}