Blog ENI : Toute la veille numérique !
💥 Un livre PAPIER acheté
= La version EN LIGNE offerte pendant 1 an !
Accès illimité 24h/24 à tous nos livres & vidéos ! 
Découvrez la Bibliothèque Numérique ENI. Cliquez ici

Système et processus

Introduction

Le système d’exploitation a entre autres pour rôle d’exécuter des processus. Ce chapitre vous présente quelques recettes qui vous permettront de lancer un nouveau processus, communiquer avec lui ou connaître et interagir avec certaines propriétés de celui en cours.

Les deux premières recettes abordent le lancement d’un nouveau processus, la première dans un cadre générique et la deuxième lorsqu’il s’agit d’un script Perl. Les deux suivantes visent à obtenir la sortie standard d’un processus pour la troisième, et son code retour lorsqu’il a pris fin, pour la quatrième. De la cinquième à la huitième recette, vous communiquez des données à un processus. Cela s’effectue via la ligne de commande pour la cinquième. Nous verrons également comment envoyer des données sur l’entrée standard d’un processus. Elle peut être utilisée conjointement à la recette "Lire une chaîne de caractères depuis l’entrée standard". La huitième est un cas particulier de cela car il s’agit de la lecture d’un mot de passe. La neuvième recette est un sujet un peu à part car elle concerne le partage d’un descripteur de fichier entre deux processus, ce qui est surtout utile pour...

Lancer un programme

Problème

Vous souhaitez exécuter un programme externe à l’intérieur de votre programme.

Solution

Exécutez execv() ou l’une des fonctions équivalentes. Avec glib, utilisez une fonction de la série g_spawn_sync().

Discussion

La fonction execv(), ainsi que ses équivalents execl(), execlp(), execle(), exect() et execvp(), remplacent le processus existant par celui appelé. Par conséquent, ces fonctions ne renvoient quelque chose que si l’exécution du processus externe n’a pu être réalisée. Si tel est le but, voici un exemple d’exécution d’un programme :


char *argv[] = { "programme", "argument1", "argument2", NULL }; 
 
execv ("programme", argv); 
fprintf (stderr, "Une erreur est survenue avec execv()\n");
 

Le problème consiste en général à lancer un programme externe sans mettre fin au programme qui l’appelle. Les fonctions g_spawn_async_with_pipes(), g_spawn_sync(), g_spawn_async(), g_spawn_command_line_async() et g_spawn_command_line_sync() de la bibliothèque glib résolvent le problème de manière élégante. Vous pourrez, en effet, lancer le programme précédent ainsi :


GError *err; /* Plus de détails sur la gestion des erreurs dans la 
              * recette "Récupérer le code d'erreur"...

Lancer un script Perl

Problème

Vous souhaitez exécuter un script Perl à l’intérieur de votre programme.

Solution

Liez votre programme à libperl, l’interpréteur embarqué de Perl. Initialisez l’ensemble et exécutez le script Perl ou seulement une fonction de celui-ci.

Discussion

Étant donné que vous pouvez exécuter un script Perl suivant la recette précédente, vous pouvez vous demander s’il est nécessaire d’embarquer un interpréteur Perl pour gagner un peu en temps d’exécution. En effet, vu la facilité d’implémentation d’un appel à execv() par rapport à l’implémentation d’un interpréteur Perl embarqué, le jeu n’en vaut pas toujours la chandelle. Pourtant, quand le même code Perl est exécuté de nombreuses fois, embarquer l’interpréteur Perl devient intéressant.

Cette recette permet aussi d’envisager l’écriture d’un script Perl comme moyen facile de concevoir des greffons. Ce dernier est une extension qu’un programme peut charger à la demande afin de l’exécuter, et les deux manières les plus répandues pour les écrire sont l’utilisation des fonctions de la famille de dlopen() comme nous le discutons dans la troisième recette du chapitre "Bibliothèque et fonctions" et l’exécution directe que nous avons vue dans la recette précédente. Utiliser l’interpréteur Perl crée une alternative en rendant l’écriture de greffons plus simple que des bibliothèques dynamiques à ceux qui savent s’exprimer dans le langage de Larry Wall (créateur de Perl) en donnant de meilleures performances qu’à l’exécution de programmes avec fork() et exec(). Par ailleurs, en liant votre programme à libperl, vous pouvez supprimer l’interpréteur Perl de votre système, ce qui ne manquera pas de vous intéresser si vous cherchez à éliminer les exécutables inutiles d’une machine sensible. Néanmoins, vous prêterez attention au fait que le script perl, facilement modifiable ou interchangeable, est lui-même dangereux.

Nous allons voir dans cette...

Récupérer le code de retour d’un programme qui s’est terminé

Problème

Vous souhaitez obtenir le code de retour d’un programme que vous avez lancé et qui s’est fini.

Solution

Utilisez une fonction parmi wait(), waitpid(), wait3() et wait4(), puis obtenez le code retour avec la macro WEXITSTATUS().

Discussion

Lorsqu’un programme se termine, pour éviter qu’il ne reste zombie trop longtemps, nous devons lire son état de fin d’exécution avec une fonction de la famille de wait(). Les quatre citées ci-dessus peuvent prendre en argument un pointeur sur un entier. Cet entier, contient, dans ses huit bits de poids faible, le code de retour du programme tel qu’il a été fourni à exit(). Voici comment l’obtenir :


int status; 
if ((0 < waitpid (pid, &status, 0)) && (WIFEXITED (status))) 
  printf ("Le code de retour est %d\n", WEXITSTATUS (status));
 

La fonction wait(), simple d’utilisation, bloque l’exécution du programme jusqu’à ce qu’un processus fils prenne fin ou qu’un signal survienne. La fonction wait4() effectue la même chose en permettant de spécifier des options, en particulier un identifiant (PID) pour attendre la fin d’exécution du processus correspondant. Ces options sont WNOHANG que nous avons utilisé ci-dessus, pour rendre la main...

Récupérer la sortie standard d’un programme

Problème

Vous voulez récupérer la sortie standard d’un programme externe que vous lancez.

Solution

Utilisez une fonction g_spawn_*() de glib ou créez un tube que vous redirigez vers la sortie standard du programme lancé.

Discussion

Voici un exemple qui utilise g_spawn_async_with_pipes() pour exécuter la commande ls :


void 
gls (void) 
{ 
  GPid pid; 
  char *args[] = { "/bin/ls", "-l", "/tmp/", NULL }; 
  int fd; 
  GError *err = NULL; 
  int l; 
  char tmp[1024]; 
/* Exécution de la commande ls. */ 
  if (!g_spawn_async_with_pipes 
      (NULL, args, NULL, 0, NULL, NULL, &pid, NULL, &fd, NULL, &err) 
      && err) 
    { 
      fprintf (stderr, 
               "g_spawn_async_with_pipes() n'a pas fonctionné (%s)\n", 
               err->message); 
      exit (EXIT_FAILURE); 
    } 
/* Lecture du résultat de la commande ls dans le tube fd. */ 
  while (0 < (l = read (fd, tmp, 1023))) 
    { 
      tmp[l] = '\0'; 
      printf ("%s", tmp); 
      if (1024 != l) 
        break; 
    } 
  close (fd); 
}
 

Voici le même exemple, mais sans l’utilisation de glib. Remarquez que la boucle de lecture est la même puisque nous lisons dans un tube. Cependant, avec g_spawn_async_with_pipes()...

Récupérer les arguments passés sur la ligne de commande

Problème

Votre programme a été lancé avec des arguments sur la ligne de commande et vous souhaitez en prendre connaissance.

Solution

Utilisez getopt() pour des arguments simples et faites-le vous-même dès qu’il y a des options longues.

Discussion

Si tous vos arguments sont des options de type -x où x est un seul caractère, vous pouvez utiliser getopt(). À chaque itération de la boucle contenant getopt(), la valeur de retour est le caractère correspondant à l’option. Si le caractère a été spécifié en argument à getopt() suivi d’un double point, la variable optarg contient alors l’argument suivant. Lorsque getopt() n’a pas reconnu un argument, elle renvoie ’?’. La fonction renvoie -1 lorsque tous les arguments ont été traités ou lorsque l’argument -- a été rencontré. Enfin, la variable optind contient l’index de l’option dans le tableau argv. L’exemple suivant illustre l’identifiant d’une option -a et d’une option -b suivie d’un argument.


int r; 
 
while (-1 != (r = getopt (argc, argv, "ab:"))) 
  switch (r) 
    { 
    case 'a': 
      printf ("Option -a\n"); 
      break; 
    case 'b': 
      printf ("Option -b <%s>\n", optarg); 
      break; 
    case '?': 
    default: 
      print_help (); 
    } 
 
argc -= optind; 
argv += optind;
 

Lorsque vous voulez utiliser des options longues, getopt() ne suffit plus. Vous pouvez utiliser l’extension GNU getopt_long() ou le faire vous-même. Dans ce cas, nous préférerons suivre les standards de programmation de GNU (GNU Coding Standards) en implémentant des options courtes facultatives, des options longues de la forme --option valeur (nous évitons --option=valeur plus complexe à implémenter et sans valeur ajoutée) ; n’oublions pas les deux options --version pour obtenir les informations de version du programme et --help pour obtenir de l’aide sur les arguments reconnus par le programme.

Il est aussi possible d’implémenter soi-même facilement une fonction pour lire les arguments de la ligne...

Envoyer des données sur l’entrée standard d’un programme

Problème

Vous souhaitez écrire sur l’entrée standard d’un programme que vous avez lancé. 

Solution

Utilisez une fonction g_spawn_*() de glib ou créez un tube que vous redirigez vers l’entrée standard du programme lancé.

Discussion

Cette recette est semblable à la recette "Récupérer la sortie standard d’un programme". Référez-vous y pour la discussion. Voici cependant deux exemples, l’un utilisant glib et l’autre non, qui comptent le nombre de lignes et l’affichent via la sortie standard du programme lancé.


void 
gwc (void) 
{ 
  GPid pid; 
  char *args[] = { "/usr/bin/wc", "-l", NULL }; 
  int fd; 
  GError *err = NULL; 
  int i; 
  char tmp[1024]; 
/* Exécution de la commande wc. */ 
  if (!g_spawn_async_with_pipes 
      (NULL, args, NULL, 0, NULL, NULL, &pid, &fd, NULL, NULL, &err) 
      && err) 
    { 
      fprintf (stderr, 
               "g_spawn_async_with_pipes() n'a pas fonctionné (%s)\n", 
               err->message); 
      exit (EXIT_FAILURE); 
    } 
/* Envoi de lignes à la commande wc dans le tube fd. */ 
  for (i = 0; i < 100; i++) 
    { 
      snprintf (tmp, sizeof(tmp), "%d\n", i); ...

Lire une chaîne de caractères depuis l’entrée standard

Problème

Vous souhaitez lire des chaînes de caractères sur l’entrée standard de votre programme.

Solution

Utilisez fgets() avec stdin comme descripteur de fichiers.

Discussion

Lire une chaîne de caractères depuis l’entrée standard peut s’effectuer exactement comme si cette entrée standard était un fichier, de descripteur de fichiers 0 pour la fonction read() et de descripteur de flux stdin pour les fonctions fgets(), fread() et fscanf(). Cependant, deux fonctions sont à éviter pour des problèmes de débordement de tampon possibles : scanf() et gets(). Ces deux fonctions, qui utilisent un espace tampon fourni par l’utilisateur, ne contrôlent pas la longueur des données lues, et il est toujours possible qu’un utilisateur entre une chaîne de caractères plus longue que l’espace tampon prévu. Rappelons que gets() n’est plus une fonction du langage C à partir de la norme C11. Pour lire une ligne, vous pouvez donc reprendre la première recette du chapitre "Contenu des fichiers" en choisissant stdin comme descripteur de flux.

Par ailleurs, vous pouvez utiliser readline() de la bibliothèque du même nom, à savoir libreadline. Celle-ci fait partie du projet GNU et est disponible depuis le site...

Lire un mot de passe sur l’entrée standard

Problème

Vous souhaitez lire une chaîne de caractères sur l’entrée standard, sans que les caractères tapés n’apparaissent à l’écran.

Solution

Changez les options du terminal pour qu’il n’affiche plus les caractères saisis au clavier.

Discussion

Contrairement à ce que nous pourrions penser, ce n’est pas en fermant l’entrée standard que nous empêchons les caractères tapés au clavier de s’afficher. Il faut changer l’option adéquate du terminal avec tcsetattr(). Voici le code qui arrête l’affichage des caractères entrés au clavier lors de l’exécution de fgets() et qui rétablit ensuite la situation :


void 
get_pwd (char *pwd, int len) 
{ 
  char *cr; 
  struct termios t; 
 
/* Désactive l'affichage des caractères. */ 
  tcgetattr (0, &t); 
  t.c_lflag &= !ECHO; 
  tcsetattr (0, TCSANOW, &t); 
 
/* Lit le mot de passe. */ 
  fgets (pwd, len, stdin); 
 
/* Réactive l'affichage des caractères. */ 
  tcgetattr (0, &t); 
  t.c_lflag |= ECHO; 
  tcsetattr (0, TCSANOW, &t); 
 
/* Supprime le caractère de fin de ligne  
 * et tout ce qui suit s'il est présent. 
 */ ...

Partager un identifiant de fichier entre deux programmes

Problème

Vous souhaitez que deux programmes partagent le même identifiant de fichier, par exemple pour que le programme principal délègue le traitement d’un fichier ou d’une socket à un programme satellite.

Solution

Après un appel à fork(), tous les descripteurs de fichiers ouverts le restent, et ils sont partagés entre le processus fils et son père. Il n’y a donc rien à faire.

Pour deux processus n’étant pas issus l’un de l’autre, ou si les descripteurs de fichiers ne peuvent être utilisés avant l’appel à fork(), celui qui veut partager un descripteur de fichier ouvert envoie un message de service contenant les informations sur ce descripteur de fichier ouvert, et l’autre reçoit ce message qui lui permet d’ouvrir le même fichier (ou la même socket) avec le même descripteur de fichier. Ces messages de service transitent obligatoirement par une socket UNIX.

Discussion

Le transfert d’un descripteur de fichiers ouvert passe impérativement par la connexion des deux processus via une socket UNIX. La recette "Communiquer entre deux processus distincts" du chapitre précédent explique la création d’un serveur et d’un client utilisant les sockets UNIX. L’envoi d’un descripteur via cette socket...

Connaître le PID du processus et celui de son père

Problème

Vous voulez connaître l’identifiant de processus (PID) de votre programme, ainsi que celui du programme qui l’a lancé.

Solution

L’identifiant d’un processus s’obtient avec getpid() et celle de son père avec getppid().


printf ("PID         : %ld\n", getpid ()); 
printf ("PID du père : %ld\n", getppid ());
 

Discussion

Pour connaître les numéros de processus de la famille, nous pouvons nous étonner de n’avoir que getpid() et getppid(), mais rien pour les processus fils. En réalité, il n’y a qu’à les conserver lors des appels à fork().

Prototypes


#include <sys/types.h> 
#include <unistd.h> 
 
pid_t getpid (void); 
pid_t getppid (void);
 

Voir aussi la recette "Créer un nouveau processus" du chapitre "Exécution parallèle" ; la page de manuel des fonctions getpid() et getppid().

Créer un démon

Problème

Vous voulez détacher votre programme de son père et effectuer toutes les démarches pour que votre programme devienne un processus résident.

Rappelons qu’un démon désigne un processus s’exécutant en tâche de fond dans l’attente d’un signal précis ou d’une condition qui se vérifie.

Solution

Sur Unix, plusieurs étapes sont nécessaires pour rendre un processus résident. Nous commençons par rendre le processus indépendant par rapport à son père. Un appel à fork() permet cela. Puis nous créons une nouvelle session avec setsid(). Un nouvel appel à fork() permet de se séparer du terminal de contrôle obtenu lors de la création de la session. Ensuite, il faut s’assurer que tous les descripteurs de fichiers hérités des processus parents sont fermés et que l’entrée dite standard et les sorties dites normale (ou standard) et d’erreur sont bien fermées. Enfin, nous nous assurons que le répertoire courant ne pose pas de problèmes.

Discussion

Pour rendre résident un processus, le plus simple est de faire appel à la fonction daemonize() suivante :


void 
daemonize (void) 
{ 
  int pid; 
  int i; 
  int fd; 
  char str[12]; 
 
/* Premier fork...

Connaître l’utilisateur qui a lancé le programme

Problème

Vous souhaitez connaître l’identifiant de l’utilisateur (UID) qui a lancé le programme. 

Solution

Utilisez getuid().

Discussion

Pour un programme, trois identités d’utilisateur coexistent. Nous y trouvons celle de l’utilisateur dit réel, qui a lancé le programme, celle de l’utilisateur dit effectif, à qui s’appliquent les permissions et une sauvegarde de celle de cet utilisateur si le programme a changé d’identité. La fonction getuid() renvoie l’identifiant de l’utilisateur réel et la fonction geteuid() renvoie celui de l’utilisateur effectif. En général ces deux identités sont les mêmes. Mais vous pouvez les changer en utilisant la recette suivante.

Prototypes


#include <sys/types.h> 
#include <unistd.h> 
 
uid_t getuid (void); 
uid_t geteuid (void);
 

Voir aussi la page de manuel des fonctions getuid() et geteuid().

Changer d’identité

Problème

Vous voulez que votre programme change d’identité.

Solution

Utilisez setuid().

Discussion

La fonction setuid() permet de changer l’identité d’utilisateur effectif à savoir l’identité qui donne ses permissions au programme en cours d’exécution. Pour un programme lancé par un utilisateur quelconque (identifiant non nul), il est impossible de changer d’identité, sauf si le programme a le bit Set-UID positionné. Dans ce cas, l’identifiant de l’utilisateur effectif correspond au propriétaire du programme alors que l’identifiant de l’utilisateur réel est celui utilisé pour lancer le programme. Dans ce cas, l’identifiant d’utilisateur effectif pourra devenir égal à celui de l’identifiant réel avec setuid(), ce qui correspond au changement d’identité voulu. Étant donné qu’une sauvegarde de l’identifiant effectif est assurée par le système, vous pouvez revenir en arrière.

Lorsque le possesseur du fichier est l’administrateur système (identifiant nul), que le bit Set-UID est positionné, et qu’un utilisateur exécute le programme, l’identifiant d’utilisateur effectif est nul, indiquant que le programme dispose des droits administrateur. Un appel à setuid()...