Objectifs :Présentation du modèle à mémoire distribuée de la programmation parallèle : communications, interneteries, modèle client/serveur, bibliothèque Unix en O'Caml
Dans ce modèle chaque processus séquentiel possède sa propre mémoire privée . Il est le seul à y avoir accès. Les processus doivent alors communiquer pour transférer de l'information. On suppose alors qu'il y a un medium assurant ces transferts. La difficulté de ce modèle provient de l'implantation du medium. Les programmes s'en chargeant s'appellent des protocoles.
Ceux-ci sont organisés en couche. Les protocoles de haut niveau, implantant des services élaborés, utilisent les couches de plus bas niveaux (voir les 7 couches du modèle ISO).
Ce modèle est valable dans le cas de parallélisme physique (réseau d'ordinateurs) ou logique (processus Unix communiquant par "pipes" ou threads O'Caml communiquant par canaux). Il n'y a pas de valeurs globales connues par tous les processus (comme un temps global). La seule contrainte sur le temps est l'impossibilité de recevoir un message avant son émission.
Dans ce modèle la communication est explicite alors que la synchronisation est implicite (elle est en fait produite par la communication). Ce modèle est le dual du précédent.
Internet est un réseau de réseau (interconnexion de réseaux). Certaines machines ont néanmoins un rôle particulier : les passerelles (pour le passage d'un réseau à un autre) et les routeurs (indiquant la route à suivre). Conceptuellement l'interconnexion se fait au niveau des réseaux et non pas des machines. Deux machines quelconques connectées sur Internet peuvent communiquer. Le réseau devient une seule entité. Différents types de machines, systèmes cohabitent sur Internet. Elles parlent toutes les protocoles IP (et UDP/TCP).
L'organisation du réseau Internet est la suivante :
---------------- | APPLICATIONS | ---------------- ^ | | | | v ---------------- | UDP / TCP | ---------------- ^ | | | | v ---------------- | IP/Interfaces| ---------------- ^ | | | | v ---------------- | MEDIAS | ----------------
Le protocole IP est le protocole de bas niveau. L'unité de transfert est le datagramme IP. C'est un protocole non fiable : il n'assure ni le bon ordre, ni le bon port, ni la non duplication des datagrammes transmis. Il traite juste le routage d'un datagramme et la gestion des erreurs quand un datagramme n'a pas être transmis. Un datagramme contient un entête et des données. Dans l'entête
apparaît les adresses du destinataire et de l'expéditeur du datagramme.Chaque machine sur Internet possède une adresse unique.
Ces adresses sont codées sur 32 bits (IPv4). Par exemple l'adresse
132.227.60.30
contient 4 champs comportant des valeurs de 0 à 255.
Au dessus d'IP, deux protocoles permettent des transmissions de plus haut niveau : UDP (User Datagram Protocol) et TCP (Transfert Control Protocol). UDP est un protocole sans connexion et non fiable (il sert à multiplexer les transmissions). TCP est un protocole orienté connexion et fiable. Pour cela il doit gérer les acquittements de paquets et optimiser la transmission (technique de fenêtrage).
Les services standards (applications) d'Internet utilisent le plus souvent le modèle client/serveur. Le serveur est un programme offrant un service spécifique. Il gère les requêtes des clients en établissant une connexion ou non selon le protocole. Il y a une asymétrie entre le client et le serveur. Ces services implantent des protocoles de plus haut niveau. Parmi les services standards, on peut nommer :
D'autres services utilisent ce modèle client/serveur :
La communication entre applications s'effectuent via des prises (sockets) de communication. Elles permettent la communication entre des processus ne résidant pas forcément sur une même machine. Différents processus peuvent lire et écrire dans cette voie de communication.
Il existe trois possibilités d'utiliser la communication entre processus en O'Caml. La première utilise les communications entre threads. La deuxième utilise
les primitives fork
et pipe
d'Unix. Ces deux premières méthodes utilisent un modèle logique de concurrence. Il n'y aura pas d'améliorations de performances, tous les processus tournent sur le même processeur. La
troisième possibilité
utilise les prises de communication (sockets) d'Unix. La communication peut alors s'effectuer entre différentes machines physiques.
Le module Event
de la bibliothèque des Threads
d'O'Caml
autorise la communication entre threads à travers des canaux de communication.
Deux types sont définis :
type 'a channel type 'a eventcorrespondant au type des canaux et des valeurs transmises.
Les communications s'effectuent principalement par les fonctions suivantes :
new_channel : unit -> 'a channel send : 'a channel -> 'a -> unit evant receive : 'a channel -> 'a event sync : 'a event -> 'a choose : 'a event list -> 'a event select : 'a event list -> a
La bibliothèque Unix d'O'Caml implante les principaux appels système de la bibliothèque Unix. Une des facilités de cette bibliothèque est d'utiliser la boucle de toplevel pour implanter ses fonctions système. Le typage facilite aussi la programmation système. Il n'est plus aussi nécessaire de pratique la lecture du man, le type de la fonction est bien souvent suffisamment parlant.
Parmi les nombreuses fonctions de cette bibliothèques, on retrouve les appels système fork
pour la duplication d'un processus, les tuyaux (pipe
) de communications et le duplicateur (dup2
) de description de fichiers.
match fork () with 0 -> (* code du fils *) | _ -> (* code du pere *)
La communication entre processus
pipe : unit -> Unix.file_descr * Unix.file_descr dup2 : Unix.file_descr -> Unix.file_descr -> unit
Les principaux types sont les suivants :
type socket_domain = PF_UNIX | PF_INET;; type socket_type = SOCK_STREAM | SOCK_DGRAM | SOCK_SEQPACKET | SOCK_RAW;; type sockaddr = ADDR_UNIX of string | ADDR_INET of inet_addr * int
Les principales fonctions d'établissement d'une prise et de communications sont les suivantes :
socket : Unix.socket_domain -> Unix.socket_type -> int -> Unix.file_descr connect : Unix.file_descr -> Unix.sockaddr -> unit close : Unix.file_descr -> unit bind : Unix.file_descr -> Unix.sockaddr -> unit listen : Unix.file_descr -> int -> unit accept : Unix.file_descr -> Unix.file_descr * Unix.sockaddr read : Unix.file_descr -> string -> int -> int -> int write : Unix.file_descr -> string -> int -> int -> int
où socket
crée un file_descr
Unix. Pour créer une prise on
utilisera l'appel suivant :
let prise = socket PF_INET SOCK_STREAM 0;;où
PF_INET
correspond au domain Internet, SOCK_STREAM
pour un flot d'octets fiable et l'entier 0
au protocole IP.
La connexion à un serveur s'effectue par la fonction connect
. L'établissement d'un service passe par les étapes suivantes :
bind
) pouvant être utilisée par la suite de l'extérieur;
listen
);
accept
).
Dès qu'une connexion est établie, les fonctions d'E/S (read
et write
) peuvent être utilisées sur la prise.
Les fonctions suivantes
gethostbyname : string -> Unix.host_entry getservbyname : string -> string -> Unix.service_entrypermettent à partir d'un nom de machine d'obtenir d'une part la description complète d'une machine (incluant son adresse) etr d'autre part la description d'un service (incluant son numéro de port).
Le schéma classique d'un serveur est le suivant :
creation de ls socket (socket) attachement de la socket (bind) ouverture du service (listen) attente de connexion (accept) creation d'un processus (fork) fils : boucle sur l'attente de la connexion pere : traite la demande
let main () = let port = int_of_string argv.(1) and args = Array.sub argv (Array.length argv - 2) in let sock = socket PF_INET SOCK_STREAM 0 in let mon_adresse = (gethostbyname(gethostname())).h_addr_list.(0) in bind sock (ADDR_INET(mon_adresse, port)); listen sock 3; while true do let (s,app_adr) = accept sock in begin match app_adr with ADDR_INET(host,_) -> print_string ("Connexion depuis "^ string_of_inet_addr host); print_newline() | ADDR_UNIX _ -> () end; match fork() with 0 -> if fork() <> 0 then exit 0; List.iter (dup2 s) [stdin;stdout;stderr]; close s; () (* travail \`a effectuer *) | _ -> wait (); close s done ;; handle_unix_error main ();;
La technique du double "fork" permet d'éviter de laisser des processus "zombies" (en attente de transmission de leur code retour à leur père). Le fils termine par exit juste après le deuxième fork. Le petit fils devient donc orphelin, et est adopté par le processus init qui possède une boucle infinie de wait, et fera disparaître le processus petit fils dès qu'il termine.
let main () = let serveur = argv.(1) and port = int_of_string (argv.(2)) in let args = Array.sub argv (Array.length argv - 3) in let serveur_adr = try inet_addr_of_string serveur with Failure _ -> try (gethostbyname serveur).h_addr_list.(0) with Not_found -> prerr_endline (serveur ^ " : serveur inconnu"); exit 2 in let sock = socket PF_INET SOCK_STREAM 0 in connect sock (ADDR_INET(serveur_adr,port)); match fork() with 0 -> (* travail du fils a effectuer *) shutdown sock SHUTDOWN_SEND; (* fermeture en ecriture *) exit 0 | _ -> (* travail du pere *) close sock; wait() ;;
Ce schéma père/fils permet de répartir les rôles en envoi/réception sur la prise. Ici le fils écrit sur la prise la requête et le père reçoit la réponse. Cette architecture prend du sens si le fils doit envoyer plusieurs requêtes, le père recevra les réponses des premières requêtes au fur et à mesure de leur traitement.
L'utilisation conjointe de la bibliothèque de processus légers et de la bibliothèque Unix provoque le blocage de toutes les "threads" actives si un appel système ne répond pas immédiatement, en particulier les lectures sur un descripteur de fichiers, incluant donc ceux créés par socket.
Pour éviter ce désagrément, le module ThreadUnix réimplante la plupart des fonctions de la bibliothèque Unix. Les fonctions définies dans ce module ne bloqueront que la thread qui effectue cet appel.
La page du système Ensemble montre d'une boite à outils de communications développée en O'Caml.