Communication inter-processus 1/28

Se souvenir 2(1)/28

▶ Les tuyaux ▶ La redirection ▶ Sélection ▶ Projets Qu’est-ce qu’un zombi ?

Différence entre fork et exec ?

Double fork : En quoi ça consiste, à quoi ça sert ? ▶ faire exécuter un programme par le petit-fils plutôt que le fils. ▶ le fils retournent immédiatement, le petit-fils devient orphelin,

il est rattché au processus initial qui s’occupe de le libérer.

Signaux et appels systèmes :

Interaction et conséquence ? Exemple. Quand est-on tranquille ?

▶ Les signaux peuvent interrompre les appels système bloquants. ▶ Si nécessaire, il faut relancer les appels systèmes interrompus. ▶ Exemple : waitpid ▶ On est tranquille si tous les signaux non ignorés sont fatals (ils

arrêtent le programme).

Qu’est-ce qu’un zombi ?

Différence entre fork et exec ?

Double fork : En quoi ça consiste, à quoi ça sert ? ▶ faire exécuter un programme par le petit-fils plutôt que le fils. ▶ le fils retournent immédiatement, le petit-fils devient orphelin,

il est rattché au processus initial qui s’occupe de le libérer.

Signaux et appels systèmes :

Interaction et conséquence ? Exemple. Quand est-on tranquille ?

▶ Les signaux peuvent interrompre les appels système bloquants. ▶ Si nécessaire, il faut relancer les appels systèmes interrompus. ▶ Exemple : waitpid ▶ On est tranquille si tous les signaux non ignorés sont fatals (ils

arrêtent le programme).

Qu’est-ce qu’un zombi ?

Différence entre fork et exec ?

Double fork : En quoi ça consiste, à quoi ça sert ? ▶ faire exécuter un programme par le petit-fils plutôt que le fils. ▶ le fils retournent immédiatement, le petit-fils devient orphelin,

il est rattché au processus initial qui s’occupe de le libérer.

Signaux et appels systèmes :

▶ Les signaux peuvent interrompre les appels système bloquants. ▶ Si nécessaire, il faut relancer les appels systèmes interrompus. ▶ Exemple : waitpid ▶ On est tranquille si tous les signaux non ignorés sont fatals (ils

arrêtent le programme).

Un besoin 3/28

Inventaire de la communication inter-processus

▶ Les signaux : une constante, communication asynchrone.

Superposition possible (perte d’information). ▶ La valeur de retour : du père vers le fils uniquement. ▶ Les fichiers.

▷ un processus écrit dans un fichier,

▷ un autre lit dans le même fichier.

Défaut de la communication par fichier

▶ Lorsque le lecteur attend, il ne sait pas si l’écrivain a fini, ou

calcule d’autres données. ▶ Une fois écrites et lues, les données sont inutiles, mais restent

en mémoire.

Solution : les tuyaux

Les tuyaux (aussi appelés tubes) 4/28

Ce sont des fichiers anonymes, non persistants, de taille bornée, avec un écrivain à un bout et un lecteur à l’autre bout.

pipe: unit -> file descr * file descr

Retourne une paire de descripteurs fd_in, fd_out

Processus Code utilisateur

fd out       fd in

Système Le tuyau est un tampon géré en file d’attente FIFO : ▶ Les processus en écriture rem-

plissent le tuyau. ▶ Ils sont bloqués lorsque le tuyau

est plein. ▶ Ceux en lecture vident le tuyau. ▶ Ils sont bloqués lorsque le tuyau

est vide. Attention !

Les processus écrivent dans fd out

qui est l’entrée du tuyau

et lisent dans fd in qui est la sortie du tuyau

Remarque 5/28

Les descripteurs de tuyaux, comme les descripteurs de fichiers, peuvent être mis en position non-bloquants. Pour changer l’état d’un descripteur ouvert, on utilise les appels :

    set_nonblock : file_descr -> unit
    clear_nonblock : file_descr -> unit

La lecture dans un tuyau vide ou l’écriture dans un tuyau plein échouent avec l’erreur EWOULDBLOCK (au lieu de bloquer) lorsque le descripteur associé est dans l’état non bloquant.

Intérêt 6(1)/28

Que se passe-t-il si c’est le même processus qui lit et écrit ?

— Aucun intérêt et risque de blocage s’il lit avant d’écrire.

Typiquement

▶ Les tuyaux sont créés avant une fourche (fork). ▶ Après le fork, le fils et le père tiennent chacun les deux bouts

du tuyau (les descripteurs sont copiés). ▶ L’un des processus parle dans un bout et l’autre écoute dans

l’autre bout. ▶ Chacun ferme le bout qu’il n’utilise pas, libèrant ainis le

descripteur inutile.

Typiquement

▶ Les tuyaux sont créés avant une fourche (fork). ▶ Après le fork, le fils et le père tiennent chacun les deux bouts

du tuyau (les descripteurs sont copiés). ▶ L’un des processus parle dans un bout et l’autre écoute dans

l’autre bout. ▶ Chacun ferme le bout qu’il n’utilise pas, libèrant ainis le

descripteur inutile.

Typiquement

▶ Les tuyaux sont créés avant une fourche (fork). ▶ Après le fork, le fils et le père tiennent chacun les deux bouts

du tuyau (les descripteurs sont copiés). ▶ L’un des processus parle dans un bout et l’autre écoute dans

l’autre bout. ▶ Chacun ferme le bout qu’il n’utilise pas, libèrant ainis le

descripteur inutile.

Fermeture d’un tuyaux 7/28

Un tuyaux est fermé lorsque tous les descripteurs sont fermés aux deux bouts.

Lecture dans un tuyau fermé

Elle peut se poursuivre jusqu’à ce que le tuyaux soit vide. À ce moment le lecteur reçoit une fin de fichier, i.e. le système lui retourne 0 caractère en écriture. (Les lectures tamporisées lèvent l’exception End_of_file.)

Écriture dans un tuyau fermé

Elle n’est pas possible : ▶ Le processus écrivain reçoit le signal sigpipe si celui-ci a son

comportement par défaut, qui est de tuer le processus. ▶ Si le signal est ignoré ou redéfini, l’écriture échoue avec une

erreur EPIPE (Sys_error pour les lectures temporisées).

Typiquement 8(1)/28

let (fd_in, fd_out) = pipe() in

match fork() with

  0 -> close fd_in; ... write fd_out ...
| k -> close fd_out; ... read fd_in ...

Père  fd out père  fd in père

Système

let (fd_in, fd_out) = pipe() in

match fork() with

  0 -> close fd_in; ... write fd_out ...
| k -> close fd_out; ... read fd_in ...

Père  fd out père  fd in père

Système

let (fd_in, fd_out) = pipe() in

match fork() with

  0 -> close fd_in; ... write fd_out ...
| k -> close fd_out; ... read fd_in ...

Père  fd out père  fd in père

Système

let (fd_in, fd_out) = pipe() in

match fork() with

  0 -> close fd_in; ... write fd_out ...
| k -> close fd_out; ... read fd_in ...

Père  fd out père  fd in père

Système

let (fd_in, fd_out) = pipe() in

match fork() with

  0 -> close fd_in; ... write fd_out ...
| k -> close fd_out; ... read fd_in ...

Père  fd out père  fd in père

Système

Exemple : crible d’Eratosthène parallèle9/28

générateur d’entiers  2,3,4, …  lire p

Le premier processus filtre lit p = 2, puis il crée un nouveau processus filtre, connecté à sa sortie, et il se met à filtrer les multiples de p depuis son entrée ;

générateur d’entiers  2,3,4, …  filtre les 2n  3,5,7, …  lire p

Le filtre p = 3 affiche 3, et se met à filtrer les multiples de 3, etc.

…  2,3,4, …  filtre les 2n  3,5,7, …  filtre les 3n  5,7,11, …  …

En pratique, on ne lance un nouveau processus que tous les 1000 nombres premiers. Un processus implémente donc le comportement de 1000 filtres.

Les tuyaux nommés 10/28

Les tuyaux sont anonymes

Ils sont donc limités pour la communication entre un fils et son père ou plusieurs fils, ou (possible mais plus rare), entre les descendants d’une même famille. Dans tous ces cas le tuyau est établi par un ancêtre commun.

Tuyaux nommés

Tuyaux avec un nom dans le système de fichier. Créés par

mkfifo: string -> file_perm -> unit

Ouverts comme des fichiers, se comportent comme des tuyaux : bloquent lorsqu’ils sont vides ou pleins, ne supportent pas l’opération �seek�. Partagent leur entrée dans la table des fichiers.

Usage : permettent la communication entre des � cousins �.

Redirection 11(1)/28

L’opération dup2 permet de duppliquer un descripteur de fichier en le redirigeant.

       dup2 : file descr -> file descr -> unit
dup2 descr1 descr2 se comporte comme � descr2 :=!descr1 �.

User  System

Avant

descr 1 File table entry 1 File table entry 2

Redirection 11(2)/28

L’opération dup2 permet de duppliquer un descripteur de fichier en le redirigeant.

       dup2 : file descr -> file descr -> unit
dup2 descr1 descr2 se comporte comme � descr2 :=!descr1 �.

User  System

Après

descr 1 File table entry 1 File table entry 2

Propriétés 12/28

Les structures systèmes sont partagées.

La position de lecture est donc partagée entre tous les descripteurs qui pointent vers la même structure.

Les descripteurs résident dans le programme :

Un descripteur est ▶ un pointeur vers la structure système ▶ plus des drapeaux (e.g. close_on_exec), qui sont remis à zéro

   par dup2.

Le descripteur descr_{1} peut être fermé sans affecter descr_{2}. Si descr_{1} et descr_{2} sont tous les deux fermés, la structure système peut être désallouée.

Création d’un descripteur 13/28

Par ouverture d’un fichier, tuyau, etc.

Les descripteurs prédéfinis stdin, stdout, stderr.

Attention, un descripteur, y compris stdin, stdout et stderr peuvent avoir été fermés !

Par duplication

dup : file_descr -> file_descr

Le descripteur retourné est un nouveau descripteur, mais partage la même entrée dans la table des fichiers.

(Celui-ci peut être égal à un descripteur standard qui avait été auparavant fermé.)

Exemple 14(1)/28

Échange de stdout et stderr :

Mauvais

  dup2 stdout stderr;
  dup2 stderr stdout

std out std err

Sans effet !

Exemple 14(2)/28

Échange de stdout et stderr :

Mauvais

  dup2 stdout stderr;
  dup2 stderr stdout

std out std err

Sans effet !

Exemple 14(3)/28

Échange de stdout et stderr :

Mauvais

  dup2 stdout stderr;
  dup2 stderr stdout

std out std err

Sans effet !

Exemple 14(4)/28

Échange de stdout et stderr :

Mauvais

  dup2 stdout stderr;
  dup2 stderr stdout

std out std err

Sans effet !

Exemple 14(5)/28

Échange de stdout et stderr :

Mauvais

  dup2 stdout stderr;
  dup2 stderr stdout

std out std err

Sans effet !

Exemple 14(6)/28

Échange de stdout et stderr :

Mauvais

  dup2 stdout stderr;
  dup2 stderr stdout

std out std err

Sans effet !

Exemple 14(7)/28

Échange de stdout et stderr :

Mauvais

  dup2 stdout stderr;
  dup2 stderr stdout

std out std err

Sans effet !

Exemple 14(8)/28

Échange de stdout et stderr :

Mauvais

  dup2 stdout stderr;
  dup2 stderr stdout

std out std err

Sans effet !

Exemple 14(9)/28

Échange de stdout et stderr :

Mauvais

  dup2 stdout stderr;
  dup2 stderr stdout

std out std err

Sans effet !

Redirection des descripteurs standards 15/28

La redirection dans les commandes shell

cmd <file        cmd lit dans file au lieu de stdin.
cmd 1>file       cmd écrit dans file au lieu de stdout.

cmd 2>file       cmd envoie sa sortie d’erreur dans file.
cmd 2>&1         cmd redirige sa sortie d’erreur dans stdout.

Redirection avant exec

let open_out_file cmd args out_file =
 let fd =
   openfile out_file [O_WRONLY; O_TRUNC; _O_CREAT] 0o666 in
 dup2 fd stdout;

 close

Penser à refermer les descripteurs inutiles

       fd

         ;

 execvp cmd args;;

Récupérer la sortie d’une commande 16/28

let command_to_string com args =
let (fd_in, fd_out) = pipe() in

match fork() with

| 0 ->

close fd_in;
dup2 fd_out stdout;

close fd out

1) ferme un descripteur inutile 2) vide le flux en écriture (c’est indispensable !)

            ;

execvp com args
| k ->
close fd_out;
let chan = in_channel_of_descr fd_in in

let

    after

         () =

              close_in

                       chan

.../

Il faut aussi libérer le fils

                                   avec waitpid

in

try_finalize input_line chan after() ;;

Exercices 1) Récupérer toutes les lignes

2) Inversement, envoyer les lignes dans l’entrée de la commande 3) Faire les deux en même temps.

Création de processus avec redirection 17/28

Des fonctions dérivées permettent de lancer un programme en redirigeant ses entrées/sorties standards.

Disponibles dans la librairie Unix

Voir create_process et create_process_env.

Ces fonctions retournent au père le numéro processus fils que le père doit impérativement libérer (avec waitpid)

Implémentation à l’aide de fork, dup2 et execvp.

Commandes avec IO temporiéses 18/28

Similaires, mais on ouvre ensuite des canaux pour les entrées/sorties temporisées :

Avec entrées/sorties temporisés

open_process_in : string -> Pervasives.in_channel
open_process_out : string -> Pervasives.out_channel

Il faut libérer les (fils des) processus alloués par :

close_process_in : int -> process_status
close_process_out : int -> process_status

Pourquoi par directement avec waitpid ?

close_process_xxx combine waitpid et close_xxx

Multiplexage 19/28

Au travers d’une ligne téléphonique

A  X
B multiplex line démultiplex
C  Y

Plus retour symmétrique : problème équivalent à

line  démulti- -plexeur  {A \atop B}  multi- -plexeur  line

Multiplexage 20(1)/28

ttya  démulti- -plexeur  shell emacs  ttya

Dé-multiplexage ne pose pas de problème. ▶ on lit le destinataire et la longeur de la transmission. ▶ on lit l’information et on la transmet au destinataire. (Solution 4) : il faut une nouvelle primitive

            select

▶ On se met attente sur toutes les entrées. ▶ Le système nous réveille et décrit les entrées qu’on peut lire

sans bloquer.

ttya  démulti- -plexeur  shell emacs  multi- -plexeur  ttya

Dé-multiplexage ne pose pas de problème.

Multiplexage (Solution 1)

On redirige les sorties vers un multiplexeur. ▶ Lecture sur chacune des entrées

et recopie l’information annotée sur sa sortie. ▶ Problème : les lectures sont bloquantes.

▷ Risque de bloquer lorsque le tuyau est vide et le processus

inactif. C’est une � situation de famine.� (Solution 4) : il faut une nouvelle primitive

            select

▶ On se met attente sur toutes les entrées. ▶ Le système nous réveille et décrit les entrées qu’on peut lire

sans bloquer.

ttya  démulti- -plexeur  shell emacs  répéteur répéteur  ttya

Dé-multiplexage ne pose pas de problème.

Multiplexage (Solution 2)

On met un répéteur par entrée qui ▶ lit son entrée et recopie l’information annotée sur la sortie. ▶ Problème : les écritures ne sont pas atomiques.

▷ les blocs taggés risquent d’être entrelacés.

C’est un problème � d’exclusion mutuelle � (race condition) (Solution 4) : il faut une nouvelle primitive

            select

▶ On se met attente sur toutes les entrées. ▶ Le système nous réveille et décrit les entrées qu’on peut lire

sans bloquer.

ttya  démulti- -plexeur  shell emacs  répéteur répéteur  ttya

Dé-multiplexage ne pose pas de problème.

Multiplexage (Solution 3)

Répéteur + synchronisation. ▶ On met un verrou pendant l’écriture. ▶ Problème : cher + attente active

▷ Il faut regarder régulièrement s’il y a eu des changements.

▷ Réglage statique impossible : trop ou pas assez souvent. (Solution 4) : il faut une nouvelle primitive

            select

▶ On se met attente sur toutes les entrées. ▶ Le système nous réveille et décrit les entrées qu’on peut lire

sans bloquer.

ttya  démulti- -plexeur  shell emacs  multi- -plexeur  ttya

Dé-multiplexage ne pose pas de problème. Multiplexage (Solution 4) : il faut une nouvelle primitive

            select

▶ On se met attente sur toutes les entrées. ▶ Le système nous réveille et décrit les entrées qu’on peut lire

sans bloquer.

Select 21/28

select :
  file_descr list -> file_descr list -> file_descr list ->
  float ->
  file_descr list * file_descr list * file_descr list

select ℓ_{1} ℓ_{2} ℓ_{3} t retourne un triplet ℓ′_{1}, ℓ′_{2}, ℓ′_{3} de descripteurs qui sont �prêts� et parmi ℓ_{1}, ℓ_{2}, ℓ_{3} : ▶ ℓ_{1} sont des descripteurs en attente de lecture ▶ ℓ_{2} sont des descripteurs en attente d’écriture ▶ ℓ_{3} sont des descripteurs en attente de conditions

exceptionnelles (e.g. contrôle de flux). Peu utilisé. ▶ t > 0 est un délai maximum d’attente.

t = 0 signifie attendre indéfiniment.

t < 0 signifie retourner immédiatement.

Exemple typique 22/28

match select [fd1;fd2] [] [] 0.5 with
  [],[],[] -> (* le delai de 0,5s est ecoule *)
| fdl,[],[] ->
    if List.mem fd1 fdl then
         (* lire depuis fd1 sans risque de bloquer *);
    if List.mem fd2 fdl then
         (* lire depuis fd2 sans risque de bloquer *)

Attention !

Si un descripteur d, partagé par plusieurs processus, est prêt en lecture, et est retourné par select dans le processus p, il se peut qu’un processus q lise les données de d avant p et que p bloque en essayant de lire d.

Le multiplex 23/28

let multiplex line_in line_out inputs outputs =

let input_fds = in_line :: Array.to_list inputs in

try while true do

let (ready_fds, _, _) = select input_fds [] [] (-1.) in
for i = 0 to Array.length inputs - 1 do
  if List.mem inputs.(i) ready_fds then begin
    let n = read inputs.(i) buffer 2 255 in

    buffer

          .[0] <-

                  char_of_int

                              i

                               ;

                                 (*

                                    destinataire

Onétiquette le message avec l’émetteur On suppose que le message sera redirigé vers le destinataire en le réétiquettant.

                                                 *)

    buffer.[1] <- char_of_int n; (* longueur *)
    ignore (write line_out buffer 0 (n+2))

end

done ;

if List.mem line_in ready_fds then begin

 /⋮ end

done with End_of_file -> ();;

Le multiplexeur 24/28

let multiplex line_in line_out inputs outputs =
let input_fds = line_in :: Array.to_list inputs in

try while true do

let (ready_fds, _, _) = select input_fds [] [] (-1.) in
for i = 0 to Array.length inputs - 1 do

/⋮ done ;

if List.mem line_in ready_fds then begin
really_read line_in buffer 0 2;
let i = int_of_char(buffer.[0]) in
match int_of_char(buffer.[1]) with
| 0 -> close outputs.(i)
| n -> really_read line_in buffer 0 n in
       ignore (write outputs.(i) buffer 0 n)

end

done with End_of_file -> ();;

Un timer facile et portable 25/28

Écrire usleep avec select

let usleep t (* microseconds *) =
ignore (select [] [] [] (float t /. 1e6));;

La commande usleep peut être interrompue par un signal. Comment s’assurer que l’on attend au moins le temps demandé ? On utilise gettimeofday pour calculer le temps écoulé.

let usleep t (* microseconds *) =
let s = float t /. 1e6 in let time = gettimeofday() +. s in
let rec sleep s (* seconds *)=

if s > 0. then

    try ignore (select [] [] [] s)
    with Unix_error (EINTR, _, _) ->
      sleep (time -. gettimeofday()) in
sleep s

Appels système et signaux 26(1)/28

Appels interruptibles

Les appels systèmes qui peuvent bloquer un temps arbitrairement long :

    waitsig, read ou write dans un tuyau, select, ...

Lorsqu’un signal non ignoré est reçu pendant l’appel, l’appel échoue avec l’erreur EINTR. Il faut, en général relancer l’appel.

Appels non interruptibles

Les appels immédiats (pas besoin d’attendre) :

    getpid, umask, sigprocmask, ...

Les appels dont l’attente est courte (donc bornée) :

read/write dans un fichier : si le bloc n’est pas dans le cache, on doit changer de contexte le temps de charger le bloc, mais cela prend un temps court borné.

Retenir 27(1)/28

Communication entre processus

▶ par des tuyaux. ▶ communication établie par un processus ancêtre commun

branchement par redirections ▶ lecture écriture comme dans un fichier, mais

▷ bloquantes quand le tuyau est vide/plein

▷ read/write dans des tuyaux = appel système interruptible ▷ donc se protéger !

Accès à des ressources concurrentes

▶ par l’appel système select ▶ attente passsive (avec timer !) ▶ appel système interruptible (se protéger...)

Applications

▶ Redirections, Récupérer les résultats de comandes, Multiplexage, etc.

Sujets de projets en système 28/28

N’oublié pas de choisir votre projet avant les vacances

Voir information disponible en ligne :

▶ RCAML : exécuter des commandes sur une machine distante ▶ RSYNC : copie d’une hiérarchie sur une machine distante ▶ HTTP : Proxy HTML/1.1 ▶ BTOR : Bit-Torrent ▶ ARCH : Serveur de sauvegarde. ▶ MODF : Modélisation du système de fichiers.