TP de système Unix #3 : utilisation de fork et de tubes

Une fois n'est pas coutume, ce TP ne comporte qu'un seul exercice … mais il est de taille ! Mais, jugez plutôt vous mêmes : Il s'agit de créer un programme C qui saisit au clavier les noms de deux commandes, les lance avec fork en reliant la sortie du premier à l'entrée du second par un tube ! C'est un sujet qui est fortement inspiré de l'excellent livre La programmation sous UNIXde Jean-Marie Rifflet paru aux éditions Edisciences.

A priori, ça n'a l'air de rien n'est ce pas ? Toutefois, cet exercice est plus compliqué qu'il n'y paraît. Voici donc quelques pistes qui vous permettront d'y arriver, je l'espère, sans encombre.

Lancer une commande

Introduction

Il est donc question de simuler la commande shell commande1 | commande2. Dans un premier temps, commande2 recouvrira le processus courant, alors que commande1 sera lancée par le fils.

La première question que vous devez vous poser est la suivante : comment exécuter une commande ? Il y a deux réponses possibles.

  1. Utiliser l'immonde commande system(commande) qui invoque un shell et lui fait exécuter la commande passée en paramètre. Ce procédé, si simple soit it, n'en est pas moins très lent et très limité.
  2. Invoquer une primitive de la famille exec. Comme ces commandes sont très complexes, leur développement fait l'objet de la section suivante

Les fonctions de la famille exec

Ces fonctions agissent toutes de la même manière : elles recouvrent le processus courant par celui qu'elles sont chargées d'exécuter. Ceci a une conséquence majeure :

A moins que la primitive n'échoue (elle n'arrive pas à lancer la commande spécifiée), toutes les instructions qui la suivent ne seront jamais exécutées.

On en déduit les autres conséquences :

En fait, on ne dispose pas d'une seule commande mais de tout un panel qui permettent de spécifier de diverses manière l'environnement d'exécution :

  1. les primitives contenant la lettre p dans leur nom vont utiliser la variable d'environnement PATH pour rechercher la commande à exécuter ; les autres requièrent que le chemin de l'exécutable soit donné in extenso
  2. Les fonctions dont le nom contient la lettre e vont prendre un argument tableau de chaînes de caractères permettant de passer à la commande lancée un ensemble de variables d'environnement complet. Sinon, l'environnement actuel est conservé.
  3. Finalement, la manière de passer la commande elle même peut être spécifiée de 2 manières différentes :
    De toute façon, quelque soit le protocole de transmission de la ligne de commande choisi, vous devrez impérativement spécifier le nom d'activation du programme, i.e. le argv[0]. Ceci peut paraître étonnant, mais figurez vous que certains programmes réagissent différemment selon leur argv[0], c'est le cas, par exemple, pour tex qui peut être appelé en tant que latex ou virtex.

Récapitulatif des fonctions de la famille exec

Le tableau suivant récapitule les prototypes et caractéristiques des fonctions de la famille exec. Vous noterez que la transmission d'un nouvel environnement et l'utilisation de PATH sont des fonctionnalités en exclusion mutuelle !

Nom et arguments Transmission
des arguments
de ligne de commande
Environnement Utilise
PATH
execl(const char* chemin, const char* arg1,...,0) Liste d'arguments variables Conservé Non
execlp(const char* chemin, const char* arg1,...,0) Liste d'arguments variables Conservé Oui
execle(const char* chemin, const char* arg1,...,0, const char* env[]) Liste d'arguments variables Transmis Non
execv(const char* chemin, const char* argv[]) Tableau de chaînes de caractères Conservé Non
execvp(const char* chemin, const char* argv[]) Tableau de chaînes de caractères Conservé Oui
execve(const char* chemin, const char* argv[], const char* env[]) Tableau de chaînes de caractères Transmis Non

Un petit exemple

Bon, je suis sur que vous voulez un petit exemple, et comme je suis bon (et con), je vous l'offre. Il s'agit de lancer la commande latex par l'intermédiaire de l'exécutable latex localisé dans le répertoire /home/hell/bin, répertoire placé dans le PATH. Les arguments sont le paramètre -q et le fichier result.tex. Etudions quelques possibilités.

Utilisation de execl

A mon avis, c'est la commande la plus brutale, la ligne de code à appliquer est la suivante :

execl("/home/hell/bin/latex","latex","-q","result.tex",0);

L'ajout d'espaces entre les diverses parties de la ligne de commande se fait automatiquement. Certains esprit chagrins me demanderont la différence entre la ligne précédente et la suivante :

execl("/home/hell/bin/latex","latex","-q result.tex",0);

Et bien, c'est très simple :

... et, bien entendu, la seconde formulation risque fort de troubler l'analyseur de ligne de commande de latex et de bien d'autres programmes ! Aussi, trève de blagues, ne cherchez pas à gagner quelques micro secondes de votre précieux temps et gérez les lignes de commande correctement que diable !

Et le argv[0], ne pourrait-on pas le prendre directement égal à la fin du chemin d'exécutable ? Reprennons l'exemple de latex. En fait, nous avons (dans la vraie vie !) un seul exécutable nommé tex que l'on peut invoquer avec plusieurs noms. Selon le nom d'invocation, il va rajouter divers arguments. Par exemple latex==tex &latex.fmt. Donc, pour invoquer latex à partir de tex, vous pourriez utiliser :

execl("/home/hell/bin/tex","latex","-q","result.tex",0);

Sympa, non ?

Utilisation de execlp

Ici, vous n'auriez pas besoin de spécifier vous même le chemin de recherche de l'exécutable, du moment que ce dernier est dans le PATH. Ainsi, la ligne de commande test devient :

execl("latex","latex","-q","result.tex",0);

Quelque peu raccourcie, non ?

Utilisation de execv

Ici, le problème est tout autre ! Les arguments doivent être gérés à la main ! Ainsi, dans le cas précédent, vous devriez utiliser :


char *arguments[4];

arguments[0]="latex";
arguments[1]="-q";
arguments[2]="result.tex";
arguments[3]=0; execv("/home/hell/bin/latex",arguments);

Et le tour est joué ! Vous allez me dire, pourquoi s'arracher les cheveux avec un protocole aussi compliqué alors que le précédent merche du tonnerre de Brest ? En fait, les fonctions en l sont très bien adaptées à partir du moment où vous connaissez, au moment de la compilation, le nom et les arguments de la commande à lancer, alors que les commandes en v permettent de construire dynamiquement n'importe quelle ligne de commande, par exemple, en décortiquant une ligne lue au clavier ...

Un petit avertissement s'impose. Dans l'exemple précédent, j'ai affecté des adresses de chaînes constantes aux éléments du tableau arguments. Si vous utilisiez une chaîne lue au clavier, vous devriez sans doute utiliser de la mémoire dynamique.

Vous l'aurez compris, dans notre exercice, nous aurons à utiliser la fonction execvp qui nous permettra de construire une ligne de commande dynamique et d'utiliser les simplifications liées à l'utilisation du PATH.

Décortiquer une ligne lue au clavier

Principe

Dernier piège : comment décortiquer une ligne lue au clavier pour la transformer en beau tableau d'arguments pour fonction argv[pe]. La réponse est simple, il suffit de découper la ligne lue selon les espaces en séparant dans un tableau chacun des éléments mis en évidence.

Il existe une fonction très pratique pour effectuer cette opération, il s'agit de char* strtok(const char *aParser, const char separateurs[]). Celle-ci fonctionne sur un principe itératif :

Un truc à savoir sur strtok : le pointeur renvoyé a toujours la même valeur (s'il s'agit de 0), strtok travaillant toujours sur des chaînes statiques connues de lui seul !

Exemple

Afin de vous familiariser avec strtok, je vous soumets le petit code suivant qui découpe une ligne lue au clavier. Comme il est très tard, il est très peu commenté mais ne présente aucune difficulté majeure.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[])
{
  /* Deux séparateurs : espace et tabulation */
  char  separateurs[]=" \t"; 
  char  chaine[256];
  char *retour;


  puts("Tappez une ligne !");
  fflush(stdout);
  fflush(stdin);
  fgets(chaine,255,stdin);

  retour=strtok(chaine,separateurs);
  if (retour)
  {
    printf("Premier mot : %s\n",retour);
    retour=strtok(0,separateurs);
    while(retour)
   	{
	     printf("Mot suivant : %s\n",retour);
	     retour=strtok(0,separateurs);
	   } 
  }
  else 
  {
    puts("Ligne vide ?");
  }

  return 0;
}

Dans le cas de la constitution d'un tableau d'arguments, il ne faut pas oublier d'allouer une chaîne dans le tableau puis d'y recopier le retour de strtok.

Solution

Bon, il vous faut la solution ? soit, je vous la donne. Les puristes remarqueront que le tableau d'arguments doit être suffisament grand dès le départ. Je le concède. Vous pourriez utiliser une pile pour assurer un dimensionnement exact en 2 passes mais, j'ai utilisé le fait que nulle ligne de commande ne peut compter (au moins en théorie) plus de 256 arguments !

/* Une ligne lue au clavier est transformee en tableaux de chaines elementaires
   pour pouvoir etre utilisee, par exemple, par les commandes execv[pe] */

int ligneCommande(char *ligneLue, char **rep)
{
  int   compteur=0;
  char *retour;
  
  retour=strtok(ligneLue," ");
  rep[0]=(char *)malloc(strlen(retour)+1);
  strcpy(rep[0],retour);

  do
  {
    retour=strtok((char *)0," ");
    if (retour)
    {
      rep[++compteur]=(char *)malloc(strlen(retour)+1);
      strcpy(rep[compteur],retour);
    }
  } while (retour);

  rep[++compteur]=0;

  return compteur;
}

Incidemment, cette fonction renvoie le nombre d'arguments (y compris le 0) stockés dans le tableau.

La redirection des flux standard d'entrée/sortie

Dans cet exercice, nous devons relier par un tube les deux commandes. En un mot, il faut rediriger la sortie standard de commande1 vers le tube et l'entrée standard de commande2 depuis ce même tube. C'est un mécanisme un peu subtil à comprendre car il utilise une astuce propre à Unix.

En guise de préambile, il est important de se rappeler que les descripteurs de fichiers sont rangés dans une table propre à chaque processus et connue sous le nom de table des descripteurs, laquelle se présente, de manière simplifiée, sous la forme suivante.

Table des descripteurs

Lorsque vous fermez un descripteur, sa place devient vacante dans la table. La prochaine ouverture de descripteur tentera de remplir cette place vide afin d'éviter de saturer la table qui a une longueur fixe.

La commande dup permet de dupliquer un descripteur. Soit un descripteur a associé à un fichier fichier. L'ordre b=dup(a) ouvre un descripteur b associé au même fichier que a. Si vous avez précédemment fermé un descripteur, c'est celui-ci qui va être réutilisé vers le fichier désigné par a. Par exemple, si dans l'exemple précédent, on commence par faire close(3) suivi de dup(5), le descripteur 3 va pointer sur Fichier 3, comme montré sur la figure suivante :

Application des opérations close et dup

Considérons maintenant stdin, stdout et stderr. Leurs descripteurs sont connus et ont une valeur fixe, bien que l'on dispose de trois constantes numériques permettant de les utiliser :

Périphérique de haut niveau Constant de descripteur Valeur usuelle du descripteur Rôle
stdin STDIN_FILENO 0 Entrée standard
stdout STDOUT_FILENO 1 Sortie standard
stderr STDERR_FILENO 2 Sortie standard pour les erreurs
Les périphériques standard et leurs descripteurs

Si vous fermez un descripteur de périphérique standard juste avant une duplication, le périphérique standard va être associé au fichier désigné par le descripteur dupliqué ! Le tour est joué ! Appliquez ce principe aux descripteurs de tuyaux et vous pouvez faire communiquer vos processus sans problème (ou presque !). Reprennons l'exemple précédent où cette fois, le descripteur 4 est associé à un tube en lecture et le 5 est associé au même tube en écriture. Nous souhaitons rediriger la sortie standard vers le tube en écriture, il suffit d'appliquer les modifications de la figure :

Application du procédé précédent à la redirection de la sortie standard dans un tube

Le sujet n'est plus si compliqué que ça après toutes ces explications, n'est ce pas ? je crois qu'il faudra tout de même entrer dans les détails ! Quoiqu'il en soit, la solution est là.