Précédent Index Suivant

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 mode de communication entre processus : les tubes de communications (pipes en anglais) 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 4 et 5.

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-sortie 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 est la sortie du tube utilisé 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 (1709.).(1710.).(1711.).(1712.).(1713.).(1714.).


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 descripeurs de fichiers pour l'utiliser sont obtenus par openfile comme si il s'agissait d'un fichier, mais son 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 (au sens des << channel >> du module Pervasives) 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 fichiers associés aux entrées-sorties standard du programme ainsi lancé qui est exécuté en parallèle avec le 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ème 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 de 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 deux variantes de ces deux fonctions. L'une pour créer et fermer un canal d'entrée ; l'autre, 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. Ici, on remarque 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'entête qu'affiche un 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 dans sa sortie standard, liée au canal d'entrée oc_in, le résultat. Ce résultat est 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 du programme est interrompue pour traiter le signal reçu, puis reprend à l'endroit où il en était. 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 les fonctions de traitements des signaux. Le module Unix définit la fonction kill d'émission d'un signal. En cela l'utilisation des signaux sous WINDOWS est restreinte au signal sigint.

Un signal peut avoir de multiples sources : la combinaison de touches <CTRL> et C provoque l'émission du signal sigint ; la tentative d'un accès mémoire erroné provoque l'émission du signal sigsegv, 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 : à 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 seconde 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 apparamment) 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>

Warning


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.
$ 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 courrament utilisé est sigalrm qui est associé à l'horloge de la machine. La fonction

# Unix.alarm ;;
- : int -> int = <fun>
émet le signal sigalrm après le nombre de secondes spécifié en argument. La valeur de retour est le nombre de secondes restant à courir depuis le dernier déclenchement d'une horloge, ou 0 si aucune horloge 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 arguments une fonction f, l'argument arg attendu par f, la durée (time) du << timeout >> et la valeur (default_value) à rendre si ce dernier est dépassé.

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 le temps a été excédé, 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=43697927
En examinant la 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 signal5 et termine le processus père lorsque celui-ci n'a plus d'enfants (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 1695
Création de 1696
Création de 1697
Création de 1698
Création de 1699
Terminaison de 1699
1699 est mort et enterré au signal 17
Terminaison de 1695
1695 est mort et enterré au signal 17
Terminaison de 1696
1696 est mort et enterré au signal 17
Terminaison de 1697
1697 est mort et enterré au signal 17
Terminaison de 1698
1698 est mort et enterré au signal 17







Précédent Index Suivant