Programmation:C:Systeme

De wiki-prog
Aller à : navigation, rechercher

Introduction

Ce cours traite des opérations permettant de manipuler les processus. Notamment, il s'agit de pouvoir lancer des programmes depuis votre code comme pourrait le faire un shell.

Processus: définition et éléments de bases

Lorsque l'on parle d'un processus, on désigne un programme en cours d'exécution: sa mémoire, ses ressources ... On essaie (souvent) de faire la différence entre le processus et le programme: le programme est unique et passif, tandis que le processus est actif et surtout pour un programme, il peut exister plusieurs processus (même simultanément.)

À un processus correspond une image mémoire (avec le code, la pile, le tas ... ), un ensemble de ressources ouvertes (les file descriptors vus précédement), une ligne de commande (le tableau argv du main) et surtout les informations suivantes:

  • pid: l'identifiant (unique) du processus
  • uid, gid, euid<tt> et <tt>egid: l'identité du propriétaire (et du groupe propriétaire) du processus. Il y a deux groupes d'identités:
    • Les identités réelles: qui déterminent les droits du processus.
    • Les identités effectives: correspondant à l'identité de l'utilisateur exécutant le processus.

Les identités réelles et effectives sont différentes lorsque l'exécutable du programme est marqué avec le setuid-bit: dans ce cas le propriétaire réel est le propriétaire du fichier et le propriétaire effectif est l'utilisateur exécutant le processus. Il est possible de modifier le propriétaire (réel et effectif) d'un processus dans les cas suivants:

  • la nouvelle identité correspond à l'identité réel ou à l'identité effective.
  • l'identité effective est 0 (le root.)

En utilisant le setuid-bit, il est donc possible d'exéctuer un programme avec les droits d'un autre utilisateur, et de faire la bascule entre les deux propriétaires (exécutant et propriétaire du fichier.)

Les opérations suivantes sont disponibles:

int getuid();
int getgid();
int geteuid();
int getegid();
int setuid(int);
int setgid(int);
int seteuid(int);
int setegid(int);
int getpid();

Création de processus par duplication: fork(2)

Le mécannisme qui permet de créer de nouveau processus est basé sur un principe de duplication: le processus courrant va être dupliqué et reprendra son exécution au même point que le programme appellant.

Schéma de fork(2)

L'appel système fork(2) est responsable de cette duplication. Lorsqu'un programme appelle fork(2), le système duplique le processus appellant (mémoire complète, ressources ... ) et relance deux processus. Ces deux processus ont donc le même code et le même état de la mémoire. La seule différence est leur pid (deux processus ne peuvent avoir le même par définition.) La signature de fork(2) est la suivante:

#include <unistd.h>
int fork(void);

En plus du pid différent, les deux processus seront différenciés par la valeur de retour de fork(2). Observons le résultat:

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
 
int main()
{
  int			x;
  x = fork();
  printf("x = %d\n",x);
  return 0;
}

Ce qui donne le résultat suivant:

> make fork-ex01
gcc -O2 -pipe -W -Wall -Werror -pedantic -ansi  fork-ex01.c  -o fork-ex01
> ./fork-ex01
x = 72833
x = 0
>

On observe bien les deux affichages (souvenez vous, il y a maintenant deux processus.) Complétons l'exemple pour différencier le processus original, que nous appellerons le père, du nouveau processus, que nous appellerons les fils:

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
 
int main()
{
  int			x;
  printf("pid = %d\n",getpid());
  x = fork();
  printf("x = %d - pid = %d\n",x,getpid());
  return 0;
}

Programme qui donnera comme résultat:

> make fork-ex01
gcc -O2 -pipe -W -Wall -Werror -pedantic -ansi  fork-ex01.c  -o fork-ex01
> ./fork-ex01
pid = 72907
x = 72908 - pid = 72907
x = 0 - pid = 72908
>

On voit maintenant que le père (de pid = 72907) recupère comme valeur de retour de fork(2) le pid du fils. On note également, l'affichage unique avant le fork(2).

Ce comportement nous permet d'établir la stratégie classique d'utilisation de fork(2):

  if (fork())
    { /* Dans le pere uniquement */
      ...
    }
  else
    { /* Dans le fils uniquement */
      ...
    }
  /* dans les deux */

Cycle de vie des processus

Comme nous venons de le voir, chaque processus est donc le fils d'un autre processus (son père.)

Le seul processus a ne pas avoir de père est init (il a le pid 1), le processus initialement lancé par le kernel au bout du système.

Nous allons maintenant nous intéresser à ce qui se passe lorsqu'un processus meurt.

Processus Orphelins ?

Le premier cas de figure qui nous intéresse est simple: un processus fork puis termine tandis que son processus fils continue de s'exécuter. Dans ce cas qui est le père de ce processus orphelin ?

Testons cette situation.

/* orphan: testing orhpan situation */
#define _XOPEN_SOURCE 500
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
 
int main()
{
  int                   p[2];
  pipe(p);
  if (fork())
    {
      // tricks to pause the parent
      char              c;
      int               r;
      close(p[1]);
      r = read(p[0],&c,1);
      return 0;
    }
  close(p[0]);
  // original father pid
  printf("(1) parent pid: %d\n", getppid());
  close(p[1]); // release our parent
  usleep(100);
 
  // print it again …
  printf("(2) parent pid: %d\n", getppid());
 
  return 0;
}

L'exécution de ce programme nous donne:

un_shell> ./orphan 
(1) parent pid: 3125
(2) parent pid: 1
un_shell> 

On en conclut donc que le processus 1 (init) a adopté l'orphelin ! C'est ainsi que l'on détache les processus pour en faire des daemons:

int main()
{
  if (fork())
    exit(0); // die to become a daemon !
 
  // …
}

Zombies

Lorsqu'un processus termine, il n'est pas immédiatement libérée, il devient un zombie.

La nature exact des processus zombies est dépendante du système (on peut lire dans la page de man pour exit(3) sous Linux que le zombie n'est qu'une boite contenant le statut de sortie du processus en attendant que le père le récupère, mais ce n'est pas toujours aussi simple.) Dans tous les cas, c'est une bonne idée de s'en débarrasser.

La solution classique consiste à utiliser wait(2) (ou l'un des appels plus évolués). Cet appel système va bloquer le processus courant (si celui-ci a des enfants) jusqu'à la mort d'un de ses processus enfants. Chaque appel à wait(2) libère un zombie (le plus ancien.)

Voici un petit exemple:

/* zombie1 */
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
 
int main()
{
  if (fork())
    wait(NULL);
  else
    {
      printf(">> child process: we're done\n");
      return 0;
    }
  printf("> parent process: child is dead\n");
  return 0;
}

Ce programme affiche:

un_shell> ./zombie1 
>> child process: we're done
> parent process: child is dead
un_shell>

Le prototype de wait(2):

int wait(int *status);

indique que celui-ci renvoie un entier (le pid du fils) et prend un pointeur sur un entier. Ce pointeur permet de récupérer les informations sur la mort du fils (mort naturelle ou par signal, statut ou numéro du signal ayant tué le processus … ) Voici un exemple un peu plus complet des informations que nous pouvons récupérer.

/* zombie2 */
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
 
int main()
{
  int                   fpid, status, wpid;
 
  if ( (fpid = fork()) )
    {
      printf("> child process is: %d\n", fpid);
      // now wait
      wpid = wait(&status);
    }
  else
    // die immediately with a special exit status
    return 42;
 
  printf("> process %d died ", wpid);
  if (WIFEXITED(status))
    printf("normaly with status %d\n", WEXITSTATUS(status));
  else
    printf("killed by signal %d\n", WTERMSIG(status));
 
  return 0;
}

Ce programme affiche:

un_shell> ./zombie2 
> child process is: 3461
> process 3461 died normaly with status 42
un_shell>

Si maintenant on remplace la ligne return 42; par abort(); (qui va donc tuer le fils avec le signal 6, SIGABRT), on obtient l'affichage suivant:

un_shell> ./zombie2 
> child process is: 3494
> process 3494 died killed by signal 6
un_shell>

Création de processus par remplacement: execvp(3)

Nous allons maintenant nous intéresser au lancement d'un autre programme. Cette action peut être effectuée via l'un des dérivés de l'appel système execve(2). En général, on utilisera execvp qui utilise le PATH pour chercher les programmes et permet de passer les arguments du nouveau programme sous forme de tableaux.

Vous l'aurez compris, pour démarrer un nouveau programme, il faut remplacer celui du processus courant: le processus en cours d'exécution va charger le code d'un autre programme (on gardera donc le même numéro processus et quelques autres détails.)

La fonction execvp(3) se présente comme suit:

int execvp(char *file, char *arg[]);

file est soit:

  • un nom ne contenant aucun caractère /: dans ce cas un fichier portant se nom est cherché dans les répertoires de la variable d’environnement PATH
  • un chemin (relatif ou absolu) contenant au moins une occurrence du caractère /

arg est le tableau d'argument du nouveau programme, la dernière case de celui-ci doit obligatoirement contenir NULL et la première est censée contenir un pointeur sur le nom du nouveau programme (on peut jouer avec ce nom … )

Voici un petit exemple, on exécute la commande ls -l depuis notre code avec execvp:

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
 
int main()
{
  char                 *arg[3];
  arg[0] = "ls";
  arg[1] = "-l";
  arg[2] = NULL;
  execvp(arg[0], arg);
  // Normally, we never come here …
  exit(127);
}

Voici maintenant un exemple plus complet qui utilise une bonne partie des choses vues dans ce cours. De plus, nous allons utiliser wait3(2) une version plus évoluée de wait(2) permettant de récupérer des informations supplémentaires sur le processus mort.

/* my_time */
#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <fcntl.h>
 
double ut(long sec, long usec)
{
  return sec + 1e-6 * usec;
}
 
int main(int ac, char *av[])
{
  char                 *msg;
  if (ac < 2)
    return 1;
  if (fork())
    {
      struct rusage     ru;
      int               status;
      struct timeval    tv1, tv2, tv;
      gettimeofday(&tv1,NULL);
      wait3(&status,0,&ru);
      gettimeofday(&tv2,NULL);
      timersub(&tv2, &tv1, &tv);
      if (WIFEXITED(status))
        {
          if ( WEXITSTATUS(status) )
            fprintf(stderr,"Command exited with non-zero status %d\n",
                    WEXITSTATUS(status));
          fprintf(stderr,"real %g\n",ut(tv.tv_sec,tv.tv_usec));
          fprintf(stderr,"user %g\n",
                  ut(ru.ru_utime.tv_sec,ru.ru_utime.tv_usec));
          fprintf(stderr,"sys %g\n",
                  ut(ru.ru_stime.tv_sec,ru.ru_stime.tv_usec));
          return WEXITSTATUS(status);
        }
      else
        return 128 + WTERMSIG(status);
    }
  execvp(av[1],av+1);
  asprintf(&msg,"my_time: cannot run %s", av[1]);
  perror(msg);
  exit(127);
}

Signaux

Conclusion

Cours Partie
Cours de Programmation EPITA/spé Programmation:C