Corrigé de l'examen de systèmes de Juin 1999

Exercice

1.
la fonction une_zone() permet d'allouer un segment de mémoire partagée pouvant contenir un objet pid_t.
2.
pour utiliser cette fonction dans un programme il suffit d'utiliser la directive de précompilation suivante :
#include <memoire.h>
La compilation du module memoire.c peut être effectuée en utilisant la commande :
sh> cc -D_XOPEN_SOURCE -c memoire.c
La compilation et l'édition de liens d'un programme désirant utiliser la fonction peut être effectuée de la façon suivante :
sh> cc -o prog prog.c memoire.o

Exercice

1.
Voici ce que pourrait produire le programme en considérant que le processus initial porte le numéro 7678 et que le fork() créé un processus de numéro 7678 :
pere: proc_num=7678 premier=   0 deuxieme=7678
fils: proc_num=   0 premier=   0 deuxieme=7678
fils: proc_num=   0 premier= -12 deuxieme= -15
pere: proc_num=7679 premier= -12 deuxieme=7678
Il faut d'abord remarquer que la variable premier pointe sur un segment de mémoire partagée (ce qui n'est le cas ni de proc_num ni de deuxieme).

La première ligne est générée par le processus initial après l'affectation de proc_num à la valeur de getpid() (ici 7678), du pid_t partagé à 0 et de deuxième à l'adresse de la variable non partagée un_entier laquelle est elle-même initialisée à la valeur de proc_num (soit 7678).

Après le fork() et quelque soit la politique d'ordonnancement utilisée c'est le fils qui réalisera son calcul car le père est bloqué sur wait(NULL) qui permet d'attendre la mort d'un de ses fils (mais comme il n'en a qu'un).

La deuxième ligne est générée par le processus fils lequel a hérité d'une copie de l'espace d'adressage de son père. Mais sa variable proc_num est modifiée au retour du fork() (valeur 0).

La troisième ligne est générée par le processu fils après modification du segment de mémoire partagée (valeur -12) et de la variable pointée par deuxieme (valeur -15).

La quatrième ligne est générée par le processus père après que son unique fils ait terminé son calcul. La variable proc_num a pris la valeur du pid du fils lors du retour du fork(). Le segment partagé a été modifié par le fils (valeur -12) quant à la variable pointée par deuxieme aucune modification ne lui a été apportée depuis le fork().

2.
si l'on supprime l'appel à wait(NULL) la politique d'ordonnancement interviendra et il est possible que le père s'exécute avant le fils. Les messages pourront être mélangés mais pas n'importe comment. Les deux messages du père seront toujours ordonnés ainsi que ceux du fils ; de plus le premier message affiché sera toujours celui du père. On pourra avoir :
cas 1 cas 2 cas 3
pere(1) pere(1) pere(1)
pere(2) fils(1) fils(1)
fils(1) pere(2) fils(2)
fils(2) fils(2) pere(2)

De plus la variable partagée aura (dans le père) deux valeurs possibles : 0 si le père génère son message avant que le fils ne touche au segment (cas 1 et cas 2), -12 sinon (cas 2 et cas 3).

Si le père meure avant le fils celui-ci se retrouvera orphelin (et sera donc adopté par un autre processus). Dans le cas contraire le fils restera dans l'état zombi jusqu'à la mort du père.

Exercice

1.
quatre sorties sont possibles :
cas a cas b cas c cas d
pere fils pere fils
fils fils pere pere

Il faut remarquer que la valeur du fork() est stockée dans un segment de mémoire partagé. Selon la politique d'ordonnancement il est possible que :

(a)
le père sort du fork() affecte la variable *pid (donc non nulle) effectue le code default, ensuite le fils sort du fork() affectue la variable *pid (donc nulle) et effectue le case 0.
(b)
le père sort du fork() et affecte la variable *pid, l'ordonnanceur choisi alors le fils qui sort du fork() et affecte 0 à *pid. Comme il s'agit d'une variable partagée tout deux effectuent le switch(*pid) avec la valeur 0 (affectation du fils).
(c)
le fils sort du fork() et affecte la variable *pid, l'ordonnanceur choisi alors le père qui sort aussi de l'appel système et affecte la variable partagée (valeur non nulle). Ensuite tout deux effectuent le switch sur une valeur non nulle (default).
(d)
le fils sort du fork(), affecte 0 à *pid puis effectue le switch. Ensuite le père sort du fork(), affecte une valeur non nulle à *pid et effectue le switch.
Sous Unix c'est généralement le fils qui est choisi par l'ordonnanceur. De plus le code exécuté est suffisament court pour n'être pas interrompu par l'ordonnanceur. Ce qui conduit l'exécution à être très probablement celle du cas b. Mais en théorie rien est à exclure.
2.
La fonction_qui_calcule_quelque_chose() est destinée à augmenter la probabilité qu'une préemption intervienne alors que le processus effectue son calcul. Si le temps d'exécution de cette fonction est supérieur au temps maximal que l'ordonnanceur peut allouer à un processus, alors seuls les cas b et c peuvent apparaître.
3.
Le père attends la mort de son fils en faisant appel à wait(NULL). Deux cas se présentent : le fils est déjà mort et à l'état zombi auquel cas le système supprime le zombi et le wait(NULL) retourne immédiatement, le fils n'est pas mort mais il le sera un jour donc le père l'attend patiemment (pas de zombi).

Exercice

1.
tout d'abord il faut remarquer que le père envoie bien 100 signaux SIGUSR1 à son fils et 100 SIGUSR1 à lui-même. Une différence apparaît dans la fonction de traitement du signal : le père ne fait que compter une occurence supplémentaire alors que le fils effectue en plus un calcul qui lui prend certainement un temps non négligeable. Il faut aussi se rappeler que lors de la délivrance d'un signal (en sémantique POSIX) le signal lui-mème est bloqué pendant toute la durée du traitement. De plus les signaux POSIX ne sont pas comptabilisés : il n'est pas possible de savoir combien ont été envoyés, la seule information disponible est qu'au moins un signal l'a été (peut-être plus).

Donc si le fils est interrompu par l'ordonnanceur pendant la délivrance de SIGUSR1 et que le père est choisi, il est possible que ce dernier puisse envoyer plusieurs fois SIGUSR1 avant que l'ordonnanceur ne redonne la main au fils. Auquel cas, le fils aura perdu quelques signaux au passage.

Plus la boucle for (i=0; ...) est longue plus nombreux seront les signaux perdus.

2.
l'application termine lorsque le père envoie le signal SIGUSR2. À sa reception le fils meure (et devient zombi si le père n'est pas encore dans le wait(NULL) ; le père se contente d'attendre la mort de son fils.

Exercice

1.
cette commande a pour effet d'enregistrer tout les noms de login (première colonne du fichier /etc/passwd ; une colonne étant une suite de caractères terminée par ':') dans le fichier resultat.

Les redirections effectuées sont :

2.
si l'on remplace le caractère '|' par ';' le fichier resultat ne contiendra rien, mais les noms de login apparaîtront sur le terminal. De plus l'exécution des deux commandes ne sera plus concurrente mais séquentielle.

Une remarque à faire sur la redirection du fichier /etc/passwd. Si l'on effectue la commande suivante :

sh> ( cat truc ; cat muche ) > bidule
Le résultat produit est celui naturellement attendu, c'est-à-dire concaténation des deux fichiers truc puis muche dans bidule. Et cela fonctionne car les deux commandes cat et cut partagent le même fichier ouvert (bidule en écriture). Donc lorsque la première commande cat termine la position courante du fichier (propriété du fichier ouvert) est réutilisée par la deuxième commande cat.

Pour notre commande c'est bien la mème chose qui se produit mais en entrée. Donc lorsque la commande cut a terminé de lire le fichier /etc/passwd la position courante du fichier ouvert en lecture est celle de la fin de fichier. Ainsi la commande cat tente de lire alors que le fichier ouvert est déjà en fin de fichier. Donc cat ne produit rien !

3.
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>

#define REDIRECT(from,to) { close(to); dup(from); close(from); }

main() {
  int etc, res, p[2], i, pid;

  if (fork()==0) {
    etc = open("/etc/passwd",O_RDONLY);
    REDIRECT(etc,0);
    pipe(p);
    if (fork()==0) {
      close(p[1]);
      res = open("res",O_WRONLY|O_CREAT|O_TRUNC,S_IRUSR|S_IWUSR);
      REDIRECT(res,1);
      REDIRECT(p[0],0);
      execlp("cat","cat",NULL);
    }
    if (fork()==0) {
      char c;
      close(p[0]);
      REDIRECT(p[1],1);
      execlp("cut","cut","-f1","-d:",NULL);
    }
    close(p[0]);
    close(p[1]);
    wait(NULL);
    wait(NULL);
    exit(EXIT_SUCCESS);
  }
  wait(NULL);
}

Problème

Voici une solution qui a au plus le mérite de répondre à la question. On n'y trouvera aucune gestion d'erreur ni une quelconque optimisation ou codage intelligent.

Première partie

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

int lit_ligne(char *ligne,int fic) {
  int i=0;
  char c;

  while (read(fic,&c,1)>0) {
    ligne[i++] = c;
    if (c=='\n') {
      ligne[i++] = '\0';
      return 1;
    }
  }
  return 0;
}

int main(int argc,char *argv[]) {
  pid_t pid;
  int sig, tube, base;
  char *nom_tube, a_chercher, ligne[256];

  pid = atoi(argv[1]);
  sig = atoi(argv[2]);
  nom_tube = argv[3];
  a_chercher = argv[4][0];
  base = open("/tmp/base_de_donnees",O_RDONLY);
  tube = open(nom_tube,O_WRONLY);
  while (lit_ligne(ligne,base)) {
    if (ligne[0]==a_chercher) {
      write(tube,ligne,256);
      kill(pid,sig);
    }
  }
  ligne[0]='\0';
  write(tube,ligne,256);
  kill(pid,sig);
  close(base);
  close(tube);
  exit(EXIT_SUCCESS);
}

Deuxième partie

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

int main(int argc,char *argv[]) {
  int tube_serveur, sig, i;
  pid_t pid;
  char nom_tube[256], c;

  mkfifo("/tmp/tube",S_IRUSR|S_IWUSR);

  tube_serveur = open("/tmp/tube",O_RDONLY);
  while (1) {
    if (read(tube_serveur,&pid,sizeof(pid_t))==0) {
      close(tube_serveur);
      tube_serveur = open("/tmp/tube",O_RDONLY);
      continue;
    }
    read(tube_serveur,&sig,sizeof(int));
    i = 0;
    while (read(tube_serveur,&c,1)>0) {
      nom_tube[i++] = c;
      if (c=='\0') break;
    }
    read(tube_serveur,&c,1);
    if (fork()==0) {
      char pids[10], sigs[10], cs[2];
      sprintf(pids,"%d",pid);
      sprintf(sigs,"%d",sig);
      sprintf(cs,"%c",c);
      execlp("service","service",pids,sigs,nom_tube,cs,NULL);
    }
  }
}

Troisième partie

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>

int tube_reponse;
char nom_tube[256];

int lit_ligne(char *ligne,int fic) {
  int i=0;
  char c;

  if (read(fic,ligne,256)>0) return 1;
  return 0;
}

void capture(int sig) {
  char ligne[256];

  while (lit_ligne(ligne,tube_reponse)) {
    if (ligne[0]=='\0') {
      close(tube_reponse);
      unlink(nom_tube);
      exit(EXIT_SUCCESS);
    }
    printf("J'ai lu : %s",ligne);
  }
}

int main(int argc,char *argv[]) {
  pid_t pid;
  int signal;
  int tube_serveur;
  struct sigaction act;
  sigset_t ens;

  sigemptyset(&ens);
  act.sa_handler = capture;
  act.sa_mask = ens;
  act.sa_flags = 0;
  sigaction(SIGUSR1,&act,NULL);
  pid = getpid();
  signal = SIGUSR1;
  sprintf(nom_tube,"/tmp/tube_client_%d",pid);
  mkfifo(nom_tube,S_IRUSR|S_IWUSR);
  tube_reponse = open(nom_tube,O_RDONLY|O_NONBLOCK);
  tube_serveur = open("/tmp/tube",O_WRONLY);
  write(tube_serveur,&pid,sizeof(pid_t));
  write(tube_serveur,&signal,sizeof(int));
  write(tube_serveur,nom_tube,strlen(nom_tube)+1);
  write(tube_serveur,argv[1],1);
  close(tube_serveur);
  while (1) pause();
}

Quatrième partie

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>

int tube_reponse[2], ferme;
char nom_tube[2][256];

int lit_ligne(char *ligne,int fic) {
  int i=0;
  char c;

  if (read(fic,ligne,256)>0) return 1;
  return 0;
}

void capture(int sig) {
  char ligne[256];
  int tube;

  tube = sig==SIGUSR1?tube_reponse[0]:tube_reponse[1];
  while (lit_ligne(ligne,tube)) {
    if (ligne[0]=='\0') {
      close(tube);
      unlink(nom_tube[sig==SIGUSR1?0:1]);
      ferme++;
      return;
    }
    printf("J'ai lu : %s",ligne);
  }
}

int main(int argc,char *argv[]) {
  pid_t pid;
  int signal;
  int tube_serveur;
  struct sigaction act;
  sigset_t ens;

  ferme = 0;
  sigemptyset(&ens);
  sigaddset(&ens,SIGUSR2);
  act.sa_handler = capture;
  act.sa_mask = ens;
  act.sa_flags = 0;
  sigaction(SIGUSR1,&act,NULL);
  sigemptyset(&ens);
  sigaddset(&ens,SIGUSR1);
  act.sa_mask = ens;
  sigaction(SIGUSR2,&act,NULL);

  pid = getpid();

  sprintf(nom_tube[0],"/tmp/tube1_client_%d",pid);
  mkfifo(nom_tube[0],S_IRUSR|S_IWUSR);
  tube_reponse[0] = open(nom_tube[0],O_RDONLY|O_NONBLOCK);

  sprintf(nom_tube[1],"/tmp/tube2_client_%d",pid);
  mkfifo(nom_tube[1],S_IRUSR|S_IWUSR);
  tube_reponse[1] = open(nom_tube[1],O_RDONLY|O_NONBLOCK);

  tube_serveur = open("/tmp/tube",O_WRONLY);
  write(tube_serveur,&pid,sizeof(pid_t));
  signal = SIGUSR1;
  write(tube_serveur,&signal,sizeof(int));
  write(tube_serveur,nom_tube[0],strlen(nom_tube[0])+1);
  write(tube_serveur,argv[1],1);

  write(tube_serveur,&pid,sizeof(pid_t));
  signal = SIGUSR2;
  write(tube_serveur,&signal,sizeof(int));
  write(tube_serveur,nom_tube[1],strlen(nom_tube[1])+1);
  write(tube_serveur,argv[2],1);
  close(tube_serveur);

  while (ferme!=2) pause();
}


Jean-Baptiste Yunes
1999-06-22