Author: Christian.Queinnec@lip6.fr

## Communication entre processus

L'utilisation de processus dans le développement d'une application permet la délégation des tâches. Néanmoins, comme nous l'avons déjà évoqué, ces tâches peuvent ne pas être indépendantes et il est dès lors nécessaire que les processus sachent communiquer entre eux.
Nous abordons dans ce paragraphe deux modes de communication entre processus : les tubes de communication (pipes) et les signaux. Ce chapitre ne fait pas un tour complet des possibilités de communication entre processus. Il n'est qu'une première approche des applications développées aux chapitres 19 et 20.

### Tubes de communication

À la manière des fichiers, il est possible de communiquer directement entre processus à travers des tubes de communication.
Les tubes sont en quelque sorte des fichiers virtuels dans lesquels on peut lire et écrire au moyen des fonctions d'entrées-sorties `read` et `write`. Cependant ils sont de taille limitée (cette limite dépendant des systèmes) et leur discipline de remplissage et de vidage est celle des files d'attente : le premier entré est le premier sorti. Et quand nous disons << sorti >>, il faut prendre l'expression au pied de la lettre : la lecture de données dans un tube les supprime de celui-ci.
Cette discipline de file d'attente est réalisée en associant deux descripteurs à un tube : l'un correspond à l'extrémité du tube dans laquelle on écrit ; l'autre, l'extrémité dans laquelle on lit. Un tube est créé par la fonction :

    # Unix.pipe ;;
    - : unit -> Unix.file_descr * Unix.file_descr = <fun>

La première composante du couple obtenu en résultat est la sortie du tube utilisée en lecture et la seconde, l'entrée du tube utilisée en écriture. Tout processus en ayant connaissance peut fermer ces descripteurs.
La lecture dans un tube est bloquante sauf si tous les processus connaissant son descripteur d'entrée (et donc, susceptible d'y écrire) l'ont fermé. Dans ce dernier cas, la fonction `read` renvoie 0. Si un processus tente d'écrire dans un tube plein, il est suspendu jusqu'à ce qu'un autre processus ait effectué une lecture. Si un processus tente d'écrire dans un tube alors que plus aucun autre processus n'est susceptible d'y lire (tous ont fermé le descripteur de sortie) alors le processus tentant d'écrire reçoit le signal `sigpipe` qui, sauf mention contraire, provoque sa terminaison.
L'exemple suivant montre l'utilisation des tubes dans lesquels des petits fils communiquent leur numéro à leur grand père.

    let sortie, entree = Unix.pipe();;

    let write_pid entree =
      try
        let m = "(" ^ (string_of_int (Unix.getpid ())) ^ ")"
        in ignore (Unix.write entree m 0 (String.length m)) ;
           Unix.close entree
       with
         Unix.Unix_error(n,f,arg) ->
           Printf.printf "%s(%s) : %s\n" f arg (Unix.error_message n) ;;

    match Unix.fork () with
        0 -> for i=0 to 5 do
               match Unix.fork() with
                   0 -> write_pid entree ; exit 0
                 | _ -> ()
             done ;
             Unix.close entree
      | _  -> Unix.close entree;
              let s = ref ""  and  buff = String.create 5
              in while true do
                   match Unix.read sortie buff 0 5 with
                       0 -> Printf.printf "Mes petits fils sont %s\n" !s ; exit 0
                     | n -> s := !s ^ (String.sub buff 0 n) ^ "."
                done ;;

On obtient la trace :

    Mes petits fils sont (1575.).(1576.).(1577.).(1578.).(1579.).(1580.).

Nous avons introduit des points entre chaque partie de chaîne lue. On peut ainsi lire sur la trace la succession des contenus du tube. On remarquera que la lecture peut ainsi être désynchronisée : dès qu'une entrée, fût-elle partielle, est produite, elle est consommée.

##### Les tubes nommés

Certains Unix acceptent de nommer les tubes comme s'il s'agissait de fichiers normaux. Il est alors possible de communiquer entre deux processus sans lien de parenté en utilisant le nom du tube. La fonction suivante permet de créer le tube.

    # Unix.mkfifo ;;
    - : string -> Unix.file_perm -> unit = <fun>

Les descripteurs de fichier pour l'utiliser sont obtenus par `openfile` comme s'il s'agissait de fichiers, mais leur comportement est celui des pipes. En particulier, puisqu'il s'agit de files d'attente, on ne peut appliquer la fonction `lseek` à un tube.

### Warning

`mkfifo` n'est pas implantée pour Windows.

### Canaux de communication

Le module `Unix` fournit une fonction de haut niveau permettant le lancement d'un programme en lui associant des canaux d'entrée ou de sortie avec le programme appelant :

    # Unix.open_process ;;
    - : string -> in_channel * out_channel = <fun>

L'argument est le nom du programme ou, plus précisément, la ligne d'appel du programme telle qu'on la taperait pour l'interprète de commande. Elle pourra donc éventuellement contenir les arguments du programme à lancer. Les deux valeurs de sortie sont les descripteurs de fichier associés aux entrées-sorties standard du programme ainsi lancé qui est exécuté en parallèle du programme appelant.

### Warning

Le programme lancé par `open_process` est exécuté par appel à l'interprète de commandes Unix `/bin/sh`.
L'utilisation de cette fonction n'est possible que sur les systèmes connaissant cet interprète de commandes.

On peut mettre fin à l'exécution d'un programme lancé par `open_process` en utilisant :

    # Unix.close_process ;;
    - : in_channel * out_channel -> Unix.process_status = <fun>

L'argument est le couple des canaux associés à un processus que l'on veut fermer. La valeur de retour est le statut d'exécution du processus dont on a attendu la terminaison.
Il existe des variantes de ces deux fonctions n'ouvrant et ne fermant qu'un canal d'entrée ou un canal de sortie :

    # Unix.open_process_in ;;
    - : string -> in_channel = <fun>
    # Unix.close_process_in ;;
    - : in_channel -> Unix.process_status = <fun>
    # Unix.open_process_out ;;
    - : string -> out_channel = <fun>
    # Unix.close_process_out ;;
    - : out_channel -> Unix.process_status = <fun>

Voici un petit exemple amusant d'utilisation de `open_process` : on lance `ocaml` dans `ocaml` !

    # let n_print_string s = print_string s ; print_string "(* <-- *)" ;;
    val n_print_string : string -> unit = <fun>
    # let p () =
       let oc_in, oc_out = Unix.open_process "/usr/local/bin/ocaml"
       in n_print_string (input_line oc_in) ; print_newline() ;
          n_print_string (input_line oc_in) ; print_newline() ;
          print_char (input_char oc_in) ;
          print_char (input_char oc_in) ;
          flush stdout ;
          let s = input_line stdin
          in output_string oc_out s ;
             output_string oc_out "#quit\059\059\n" ;
             flush oc_out ;
             let r = String.create 250 in
             let n = input oc_in r 0 250
             in n_print_string (String.sub r 0 n) ;
                print_string "Merci de votre visite\n" ;
                flush stdout ;
                Unix.close_process (oc_in, oc_out) ;;
    val p : unit -> Unix.process_status = <fun>

L'appel de la fonction `p` lance un toplevel d'Objective CAML. On remarquera que c'est la version 2.03 qui se trouve dans le catalogue `/usr/local/bin`. Les quatre premières opérations de lecture permettent de récupérer l'en-tête qu'affiche le toplevel. La ligne `let x = 1.2 +. 5.6;;` est lue au clavier, puis envoyée sur `oc_out` (le canal de sortie lié à l'entrée standard du nouveau processus). Celui-ci type et évalue la phrase Objective CAML passée, et écrit le résultat dans sa sortie standard qui est liée au canal d'entrée `oc_in`. Ce résultat est alors lu par la fonction `input` et affiché, ainsi que la chaîne `"Merci de votre visite"`. On envoie d'autre part la directive #quit;; pour sortir du nouveau processus.

    # p();;
            Objective Caml version 2.03

    # let x = 1.2 +. 5.6;;
    val x : float = 6.8
    Merci de votre visite
    - : Unix.process_status = Unix.WSIGNALED 13
    #

### Signaux sous Unix

Une des possibilités pour communiquer avec un processus est de lui envoyer un signal. Un signal peut être reçu à n'importe quel moment de l'exécution du programme. La réception d'un signal provoque une interruption logicielle. L'exécution d'un programme est interrompue pour traiter le signal reçu, puis reprend à l'endroit de son interruption. Les signaux sont en nombre fini et relativement restreint (32 avec Linux). L'information véhiculée par un signal est rudimentaire : elle se borne à l'identité (le numéro) du signal. Les processus ont tous une réaction prédéfinie aux signaux. Néanmoins, celle-ci peut être redéfinie par le programmeur pour la plupart des signaux.
Les données et fonctions de traitement des signaux sont réparties entre les modules `Sys` et `Unix`. Le module `Sys` contient la déclaration d'un certain nombre de signaux répondant à la norme POSIX (décrits dans [Rif90]) ainsi que des fonctions de traitement des signaux. Le module `Unix` définit la fonction `kill` d'émission d'un signal. L'utilisation des signaux sous Windows est restreinte au seul `sigint`.
Un signal peut avoir de multiples sources : le clavier ; une tentative erronée d'accès mémoire, etc. Un processus peut émettre un signal à destination d'un autre processus en ayant recours à la fonction :

    # Unix.kill ;;
    - : int -> int -> unit = <fun>

Son premier paramètre est le PID du processus destinataire et le second est le signal qu'on veut lui envoyer.

#### Traitement des signaux

La réaction associée à un signal peut être de trois ordres. À chacun d'eux correspond un constructeur du type `signal_behavior` :

- `Signal_default` : le comportement par défaut défini par le système. Dans la plupart des cas c'est la terminaison du processus avec création ou non d'un fichier d'état du processus (fichier `core`).
- `Signal_ignore` : le signal est ignoré.
- `Signal_handle` : le comportement est redéfini par une fonction Objective CAML de type int -> unit que l'on passe en argument au constructeur. Lors du traitement du signal ainsi modifié, le numéro de signal est passé à la fonction de traitement.

À la réception d'un signal, l'exécution du processus récepteur est déroutée vers la fonction de traitement du signal. La fonction permettant de redéfinir le comportement associé à un signal est fournie par le module `Sys` :

    # Sys.set_signal;;
    - : int -> Sys.signal_behavior -> unit = <fun>

Le premier argument est le signal à redéfinir et le second, le comportement assigné.
Le module `Sys` fournit une autre fonction de modification du traitement des signaux :

    # Sys.signal ;;
    - : int -> Sys.signal_behavior -> Sys.signal_behavior = <fun>

Elle agit comme `set_signal`, sauf qu'en plus elle renvoie la valeur associée au signal avant la modification. On peut ainsi écrire une fonction renvoyant (sans la modifier apparemment) la valeur comportementale associée à un signal :

    # let signal_behavior s =
      let b = Sys.signal s Sys.Signal_default
      in Sys.set_signal s b ; b ;;
    val signal_behavior : int -> Sys.signal_behavior = <fun>
    # signal_behavior Sys.sigint;;
    - : Sys.signal_behavior = Sys.Signal_handle <fun>

Certains signaux ne peuvent pas voir leur comportement modifié. Notre fonction n'est donc pas utilisable pour n'importe quel signal :

    # signal_behavior Sys.sigkill ;;
    Uncaught exception: Sys_error("Invalid argument")

#### Quelques signaux

Nous illustrons ci-dessous l'utilisation de quelques signaux essentiels.

##### sigint

Ce signal est, en général, associé à la combinaison de touches `CTRL-C`. Dans le petit exemple ci-dessous, nous modifions la réaction à ce signal de façon à ce que le processus récepteur ne s'interrompe qu'à la troisième occurrence du signal.
Créons le fichier `ctrlc.ml` suivant :

    let sigint_handle =
     let n = ref 0
     in function _ -> incr n ;
                      match !n with
                          1 -> print_string "Vous venez d'appuyer sur CTRL-C\n"
                        | 2 -> print_string "Vous avez encore appuyé sur CTRL-C\n"
                        | 3 -> print_string "Si vous insistez ...\n" ; exit 1
                        | _ -> () ;;
    Sys.set_signal Sys.sigint (Sys.Signal_handle sigint_handle) ;;
    match Unix.fork () with
        0 -> while true do () done
      | pid -> Unix.sleep 1 ; Unix.kill pid Sys.sigint  ;
               Unix.sleep 1 ; Unix.kill pid Sys.sigint  ;
               Unix.sleep 1 ; Unix.kill pid Sys.sigint  ;;

Ce programme simule l'appui de la combinaison de touches `CTRL-C` par l'envoi du signal `sigint`, on obtient la trace d'exécution suivante :

    $ ocamlc -i -o ctrlc ctrlc.ml
    val sigint_handle : int -> unit
    $ ctrlc
    Vous venez d'appuyer sur CTRL-C
    Vous avez encore appuyé sur CTRL-C
    Si vous insistez ...

##### sigalrm

Un autre signal couramment utilisé est `sigalrm` qui est associé à l'horloge de la machine. Il peut être émit par la fonction :

    # Unix.alarm ;;
    - : int -> int = <fun>

L'argument spécifie le nombre de secondes d'attente avant l'émission du signal `sigalrm`. La valeur de retour est le nombre de secondes restant à courir avant l'émission d'un prochain signal, ou si aucune alarme n'est en cours.
Nous allons utiliser cette fonction et le signal associé pour définir la fonction `timeout` qui lance l'exécution d'une autre fonction et l'interrompt, si besoin est, au bout d'un temps donné. Plus précisément, la fonction timeout prendra en argument une fonction f, l'argument arg attendu par `f`, la durée (`time`) du << timeout >> et la valeur (`default_value`) à rendre si cette dernière est dépassée.
Dans `timeout`, les choses se passent ainsi :

1. On modifie le comportement associé au signal `sigalrm` de façon à déclencher l'exception `Timeout`.
2. On a pris soin, au passage, de mémoriser le comportement original associé à `sigalrm` pour définir une fonction capable de le restaurer.
3. On déclenche l'horloge.
4. On lance le calcul :
   1. Si tout s'est bien passé, on remet `sigalrm` dans son état d'origine et on renvoie la valeur du calcul.
   2. Sinon, on restaure `sigalrm` et si la durée est dépassée, on renvoie la valeur par défaut.

Voici les définitions correspondantes ainsi qu'un petit essai :

    # exception Timeout ;;
    exception Timeout
    # let sigalrm_handler = Sys.Signal_handle (fun _ -> raise Timeout) ;;
    val sigalrm_handler : Sys.signal_behavior = Sys.Signal_handle <fun>
    # let timeout f arg time default_value =
       let old_behavior = Sys.signal Sys.sigalrm sigalrm_handler in
       let reset_sigalrm () = Sys.set_signal Sys.sigalrm old_behavior
       in ignore (Unix.alarm time) ;
          try  let res = f arg in reset_sigalrm () ; res
          with exc -> reset_sigalrm () ;
                      if exc=Timeout then default_value else raise exc ;;
    val timeout : ('a -> 'b) -> 'a -> int -> 'b -> 'b = <fun>
    # let itere n = for i = 1 to n do () done ; n ;;
    val itere : int -> int = <fun>

    Printf.printf "1ère exécution : %d\n" (timeout itere 10 1 (-1));
    Printf.printf "2ème exécution : %d\n" (timeout itere 100000000 1 (-1)) ;;

    1ère exécution : 10
    2ème exécution : -1
    - : unit = ()

##### sigusr1 et sigusr2

Ces deux signaux sont à la disposition du programmeur pour les besoins de ses applications. Ils ne sont pas utilisés par le système d'exploitation.
Dans cet exemple, la réception, par le fils, du signal `sigusr1` provoque l'affichage du contenu de la variable `i`.

    let i = ref 0  ;;
    let affiche_i s = Printf.printf "signal recu (%d) -- i=%d\n" s !i ;
                      flush stdout ;;
    Sys.set_signal Sys.sigusr1 (Sys.Signal_handle affiche_i) ;;

    match Unix.fork () with
        0 -> while true do incr i done
      | pid -> Unix.sleep 0  ; Unix.kill pid Sys.sigusr1  ;
               Unix.sleep 3 ; Unix.kill pid Sys.sigusr1  ;
               Unix.sleep 1  ; Unix.kill pid Sys.sigkill

Voici la trace d'une exécution de ce programme :

    signal recu (10) -- i=0
    signal recu (10) -- i=47580467

En examinant cette trace, on voit qu'après avoir exécuté une première fois le code associé au signal `sigusr1`, le processus fils continue à exécuter la boucle et à incrémenter i.

##### sigchld

Ce signal est émis vers son père à la terminaison d'un processus. Nous allons l'utiliser pour rendre un père plus attentif au devenir de ses enfants. Voici comment :

1. On définit une fonction de traitement du signal `sigchld` qui traite tous les enfants morts à la réception de ce signal4 et termine le processus père lorsque celui-ci n'a plus d'enfant (exception `Unix_error`). Pour ne pas bloquer le père, si tous ses enfants ne sont pas morts, on utilise `waitpid` plutôt que `wait`.
2. Le programme principal, après avoir redéfini la réaction associée à `sigchld`, boucle pour créer cinq fils. Ceci fait, le père fait autre chose (boucle while true) jusqu'à la mort de ses fils.

    let rec sigchld_handle s =
      try  let pid, _ = Unix.waitpid [Unix.WNOHANG] 0
           in if pid <> 0
              then ( Printf.printf "%d est mort et enterré au signal %d\n" pid s ;
                     flush stdout ;
                     sigchld_handle s )
      with Unix.Unix_error(_, "waitpid", _) -> exit 0 ;;

    let i = ref 0
    in Sys.set_signal Sys.sigchld (Sys.Signal_handle sigchld_handle) ;
       while true do
         match Unix.fork() with
             0 -> let pid = Unix.getpid ()
                  in Printf.printf "Création de %d\n" pid ; flush stdout ;
                     Unix.sleep (Random.int (5+ !i)) ;
                     Printf.printf "Terminaison de %d\n" pid ; flush stdout ;
                     exit 0
           | _ -> incr i ; if !i = 5 then while true do () done
       done ;;

On obtient la trace :

    Création de 1561
    Création de 1562
    Création de 1563
    Création de 1564
    Création de 1565
    Terminaison de 1565
    1565 est mort et enterré au signal 17
    Terminaison de 1561
    1561 est mort et enterré au signal 17
    Terminaison de 1562
    1562 est mort et enterré au signal 17
    Terminaison de 1563
    1563 est mort et enterré au signal 17
    Terminaison de 1564
    1564 est mort et enterré au signal 17