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
2
5
0
in
let
n
=
input
oc_in
r
0
2
5
0
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 :
-
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 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 :
-
On modifie le comportement associé au signal sigalrm
de façon à déclencher l'exception Timeout.
- On a pris soin, au passage, de mémoriser le comportement
original associé à sigalrm pour définir une
fonction capable de le restaurer.
- On déclenche l'horloge.
- On lance le calcul :
-
Si tout s'est bien passé, on remet sigalrm dans son
état d'origine et on renvoie la valeur du calcul.
- 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
1
0
1
(-
1
));
Printf.printf
"2ème exécution : %d\n"
(timeout
itere
1
0
0
0
0
0
0
0
0
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 :
-
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.
- 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