4. Lancement d'un greffon avec exec*()

4.1. Un peu de code

Nous avons tout pour que je puisse vous proposer le code suivant, qui exécute un exécutable que j'appellerai greffon, récupère tout ce qu'il écrit sur ses entrées standards et d'erreur, et qui récupère aussi son code de sortie. Voici le code:

  1 #define _USE_BSD
  2 #include <stdio.h>
  3 #include <stdlib.h>
  4 #include <unistd.h>
  5 #include <sys/types.h>
  6 #include <sys/select.h>
  7 #include <sys/time.h>
  8 #include <sys/resource.h>
  9 #include <sys/wait.h>
 10 #include <errno.h>
 11 #include <sys/stat.h>
 12 #include <fcntl.h>
 13 
 14 int
 15 main (int argc, char *argv[])
 16 {
 17   int flux_in, flux_out, flux_err;
 18   int real_stdin, real_stdout, real_stderr;
 19   int p[2];
 20   int pid;
 21   int c;
 22   fd_set readfds;
 23   int retval;
 24   int status;
 25   int retpid;
 26   int plugin_has_quit = 0;
 27 
 28   real_stdin = dup (STDIN_FILENO);
 29   real_stdout = dup (STDOUT_FILENO);
 30   real_stderr = dup (STDERR_FILENO);
 31 
 32   printf ("OK\n");
 33 
 34   if (pipe (p))
 35     {
 36       printf ("Erreur a la creation du pipe\n");
 37       exit (EXIT_FAILURE);
 38     }
 39   flux_in = p[1];
 40   close (STDIN_FILENO);
 41   dup (p[0]);
 42   close (p[0]);
 43 
 44   if (pipe (p))
 45     {
 46       printf ("Erreur a la creation du pipe\n");
 47       exit (EXIT_FAILURE);
 48     }
 49   flux_out = p[0];
 50   close (STDOUT_FILENO);
 51   dup (p[1]);
 52   close (p[1]);
 53 
 54   if (pipe (p))
 55     {
 56       printf ("Erreur a la creation du pipe\n");
 57       exit (EXIT_FAILURE);
 58     }
 59   flux_err = p[0];
 60   close (STDERR_FILENO);
 61   dup (p[1]);
 62   close (p[1]);
 63 
 64   switch (pid = fork ())
 65     {
 66     case 0:
 67       printf ("test\n");
 68       execlp ("/tmp/plugin", "plugin", NULL);
 69       break;
 70     case -1:
 71       printf ("Erreur lors du fork()\n");
 72       exit (EXIT_FAILURE);
 73     }
 74 
 75   close (STDIN_FILENO);
 76   dup (real_stdin);
 77   close (STDOUT_FILENO);
 78   dup (real_stdout);
 79   close (STDERR_FILENO);
 80   dup (real_stderr);
 81 
 82   FD_ZERO (&readfds);
 83   FD_SET (flux_out, &readfds);
 84   FD_SET (flux_err, &readfds);
 85 
 86   retval =
 87     select ((flux_err > flux_out) ? flux_err + 1 : flux_out + 1, &readfds,
 88 	    NULL, NULL, NULL);
 89   retpid = wait4 (pid, &status, WNOHANG, NULL);
 90   while ((!plugin_has_quit) || (retpid < 0))
 91     {
 92       if (retval > 0)
 93 	{
 94 	  if (flux_out >= 0)
 95 	    {
 96 	      if (FD_ISSET (flux_out, &readfds))
 97 		{
 98 		  int r;
 99 		  if ((r = read (flux_out, &c, sizeof (char))) == -1)
100 		    printf ("retval=%d errno=%d\n", retval, errno);
101 		  else if (r == sizeof (char))
102 		    printf ("%c", c);
103 		  else if (r == 0)
104 		    flux_out = -1;
105 		}
106 	    }
107 	  if (flux_err >= 0)
108 	    {
109 	      if (FD_ISSET (flux_err, &readfds))
110 		{
111 		  int r;
112 		  if ((r = read (flux_err, &c, sizeof (char))) == -1)
113 		    printf ("retval=%d errno=%d\n", retval, errno);
114 		  else if (r == sizeof (char))
115 		    printf ("%c", c);
116 		  else if (r == 0)
117 		    flux_err = -1;
118 		}
119 	    }
120 	  if (!plugin_has_quit)
121 	    {
122 	      FD_ZERO (&readfds);
123 	      if (flux_out >= 0)
124 		FD_SET (flux_out, &readfds);
125 	      if (flux_err >= 0)
126 		FD_SET (flux_err, &readfds);
127 	      if ((flux_out == -1) && (flux_err == -1))
128 		plugin_has_quit = 1;
129 	      else
130 		retval =
131 		  select ((flux_err > flux_out) ? flux_err + 1 : flux_out + 1,
132 			  &readfds, NULL, NULL, NULL);
133 	    }
134 	  else
135 	    {
136 	      retval = 0;
137 	      sleep (1);
138 	    }
139 	  if (retpid < 0)
140 	    retpid = wait4 (pid, &status, WNOHANG, NULL);
141 	}
142     }
143   printf ("\nStatus de retour: %d\n", WEXITSTATUS (status));
144 
145   exit (EXIT_SUCCESS);
146 }

Si vous voulez un greffon de test, en voici le listing au cas où vous auriez une panne d'imagination. Appelons l'exécutable généré par ce code du nom très original plugin et plaçons-le dans le répertoire /tmp:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 
 4 int
 5 main (int argc, char *argv[])
 6 {
 7   printf ("resultat\n");
 8   fprintf (stderr, "erreur?\n");
 9   exit (4);
10 }

4.2. Les explications

Lignes 28, 29 et 30, je sauvegarde l'entrée standard, la sortie standard et la sortie d'erreur standard. En effet, il me faudra faire du bricolage par la suite, et le chargeur aura besoin de revenir à l'état de départ. Notez qu'il y a au moins deux autres manières de faire. L'une est de faire le bricolage qui suit uniquement pour le processus fils, sans toucher au père (c'est plus propre et je vous le conseille, mais ca m'empèche de vous montrer comment on fait pour sauvegarder les flux standards). Une autre est de ne rien sauvegarder du tout, et de réouvrir /dev/tty avec les options qui vont bien des que l'on en a de nouveau besoin. La restauration de ces flux a lieu lignes 75 à 80.

Lignes 34 à 42, je crée un tube, et en fermant l'entrée standard, je simule celle-ci par la suite. C'est exactement ce que j'ai décrit plus haut dans le paragraphe Manipulation des descripteurs de fichiers. Pas besoin de commentaires supplémentaires: vous avez le droit de relire le paragraphe en question - ou voici un artifice pour faire durer plus longtemps le plaisir de la lecture du magazine sans en écrire plus -

Lignes 44 à 52 et 54 à 62, le principe est le même que dans le bloc précédent, à ceci près que nous traitons la sortie standard puis la sortie d'erreur standard. De plus, ce sont des sorties et pas des entrées: il faut inverser p[0] et p[1].

Lignes 64 à 73, nous avons le fork() avec en ligne 68 le lancement du greffon via execlp(). Ceci était l'objet du début de l'article: vous en avez l'exemple ici.

A partir de la ligne 82, vous avez du code qui n'est exécuté que par le père, et qui consiste à lire tout ce que le greffon peut vouloir afficher, ainsi que la lecture du code d'erreur. Dois-je revenir sur cette partie qui est en fait un appel à la fonction select() avec les arguments qu'elle demande, puis la gestion de ce qu'elle retourne? Référez-vous à l'opus numéro 8 des briques en C qui traite justement de cette fonction, en long en large et en travers. Par contre, notez que si read() renvoie zéro, c'est que le descripteur de fichier a été fermé en face - il n'y a plus rien à lire -. Je mets donc de descripteur de fichier à -1, et lorsque les deux descripteurs de fichiers qui m'interessent sont à -1, je suis prêt à quitter la boucle principale. Prêt? Presque, car il faut encore aussi que wait4 m'ait renvoyé le status de retour du greffon, lorsque celui-ci a fini son exécution.

Enfin, je voudrais attirer votre attention sur la ligne 137 et l'appel à sleep(1). Sans cet appel, vous pouvez avoir un bug qui n'apparaît que dans les applications parallèles. Or l'exécution d'un greffon parallèlement à son chargeur n'est rien d'autre qu'une application parallèle! Quant au bug, il est que vous avez une boucle principale dans le chargeur, et que s'il n'y a plus rien a lire, la boucle va tourner à vide, avec pour seul appel celui à wait4(). Le risque est que, dans une telle boucle, le processeur soit monopolisé par la boucle et ne puisse plus exécuter quoi que ce soit d'autre. l'appel à wait4() permet de redonner la main au système un court instant. Comme cet appel est non bloquant, le chargeur risque donc de monopoliser le processeur pour ne rien faire, à attendre que le greffon finisse, mais en l'empechant de finir par cette monopolisation du processeur! L'appel à sleep() permet donc de redonner la main au système et de ne pas monopoliser le processeur. Notez cependant que dans l'exemple que je vous ai donné, ce cas a peu de chances de se matérialiser quand même. Mais souvenez-vous en pour plus tard!

création est mise à disposition sous un contrat Creative Commons