RPC – Appel de procédures à distance

 

 

 

Pierre Gançarski*

Octobre 2003

 

Département d'informatique

UFR de Mathématique et Informatique

Université Louis Pasteur – Strasbourg

 

 

 

 

Table des matières

 

 

 

1 - Introduction.. 2

2 - Problèmes liés aux RPC.. 2

2.1 - Passage de paramètres. 2

2.2 Nommage. 3

3 - RPC sous UNIX.. 4

3.1 Les trois niveaux.. 4

3.2 Commande rpcinfo.. 4

3.3 Niveau haut.. 4

3.4 Niveau Intermédiaire. 5

3.4.1 Côté serveur. 5

3.4.2 Côté client 6

3.4.3 Un exemple complet (niveau intermédiaire). 6

4 Échec d'une requête RPC.. 6

4.1 Incapacité à communiquer le serveur.. 6

4.2 Perte de la demande. 7

4.3 Perte de la réponse. 7

4.4 Panne du serveur.. 7

4.5 Panne du client.. 7

 

 

 

 

 

 

 

 

 

 

 

 

 

 

* Pierre.Gancarski@dpt-info.u-strasbg.fr

 

 


 

1 - Introduction

Les différentes étapes lors d'un appel de procédure locale :

 

  1. empilement de l'adresse de retour et des paramètres de l'appel
  2. positionnement de paramètres dans des registres
  3. appel au noyau ou saut dans le code du processus
  4. exécution du code
  5. dépilement des variables locales à la procédures et des paramètres d'appel
  6. écriture du résultat à l'adresse donnée par l'adresse de retour
  7. dépilement de cette adresse

 

L'idée de RPC est de faire réaliser une opération sur une machine distante.

Pour cela, la façon dont une procédure est réalisée est modifiée :

 

1)      empilement de l'adresse de retour et des paramètres de l'appel sur la pile locale

2)      positionnement des paramètres par

a)      transfert de ces paramètres à la machine distante ainsi que le nom de la fonction distante

b)      positionnement des paramètres dans des registres de la machine distante

3)      appel au noyau ou saut dans le code du processus distant devant réaliser cette opération

4)      exécution par

a)      exécution du code distant

b)      renvoi du résultat

5)      dépilement des variables locales à la procédure et des paramètres d'appel

6)      écriture du résultat à l'adresse donnée par l'adresse de retour

7)      dépilement de cette adresse

 

De plus, cela doit être fait de façon, transparente, à l'utilisateur : celui-ci doit simplement savoir comment demander cette opération sans s'inquiéter du passage des paramètres via le réseau (2.a et 2.b), du lancement réel de l'opération (3 à 4.b) et de la récupération du résultat (4.b).

 

On utilise des subrogés (dit aussi talon ou stubs)

 

-          à la compilation, un appel à une procédure distante est remplacé par un appel à un subrogé client

-          à l'appel lors de l'exécution, les paramètres effectifs sont rassemblés dans un message, puis par un appel au noyau, et via des sockets, ce message est transmis au noyau de la machine distante

-          Le noyau de la machine distante transmet ce message au subrogé serveur du processus chargé du service qui

-          désassemble les paramètres

-          appelle la fonction de façon locale et classique.

-          le résultat fait le cheminement inverse.

 

2 - Problèmes liés aux RPC

2.1 - Passage de paramètres

 

 

Ainsi, par exemple

 

- sur SUN on utilise le code ASCII alors que sur IBM on utilise le code EBCDIC

- sur SPARC les entiers sont codés de gauche à droite, c'est l'inverse sur INTEL

 

 

 

 

 

- la représentation des chaînes de caractères ou des tableaux peut être différentes suivant les compilateurs.

 

Deux solutions

 

1.       le client (resp. le serveur) connaît la représentation utilisée par le serveur (resp. le client) : il utilise un algorithme de recodage dans sa propre, représentation MAIS cela nécessite de connaître TOUS les modes de représentations possibles

2.       il existe un format "universel" (un standard) de représentation que toutes les machines comprennent : une machine est capable de traduire SES propres représentations en ce format universel et bien, évidement, elle est capable de la réciproque. Ce qui va être échangé entre les machines sera toujours donné dans ce format. Un tel format existe : XDR pour eXternal Data Representation.

 

·   Pointeurs.

 

Que faire lorsqu'un des paramètres est un pointeur : l'adresse pointée sur l'autre machine sera une erreur!!

 

Trois solutions

 

1.       interdire les pointeurs ;

2.       transmettre toute la zone pointée MAIS cela ne peut être automatique car on peut se trouver dans le cas d'une arborescence de pointeurs (tableaux de pointeurs, pointeurs de pointeurs ... ) => il faut écrire une fonction qui transforme cette arborescence en une zone compacte : utilisation des fonctions XDR.

3.       transférer uniquement l'adresse et le type de l'objet pointé : à chaque accès, le serveur demande la valeur pointée. S'il modifie la valeur pointée, il le signale au client pour que celui-ci puisse mettre à jour sa mémoire.

4.       Négation C/S à suivant le cas, on transmet la zone pointée ou non

 

2.2 Nommage

 

Pour qu'une machine puisse rendre un service quel qu'il soit (RPC ou autre), il faut d'une part, que ce service soit "nommable" (c-à-d que le client connaissent le nom de la machine et le "nom local" sur cette machine, du service demandé) et d'autre part, qu'il existe un processus qui réalise le service

 

·   Pour connaître

 

1.       le nom d'une machine serveur -> utilisation de serveurs de noms.

2.       le nom local du service. Deux cas :

  1. le service n'est pas un RPC, mais est basé sur des sockets (exemple : ftp, telnet,...) à utilisation de numéros de port et de types de protocole réseau (par exemple : telnet à port 23 sous TCP) donnés (validés) par le fichier /etc/services.
  2. le service est un RPC à utilisation d'un triplet (N° de programme, N° de version, N° de fonction)

 

·   Pour qu'un machine connaisse le processus

 

-          le service n'est pas un RPC, mais est basé sur des sockets -> utilisation d'un processus particulier inetd chargé de lancer le processus en fonction des indications données par le fichier /etc/inetd.conf.

telnet         strear   tcp      nowai  root    /usr/sbin/in.telnetd      telnetd

-          le service est un RPC à utilisation d'un processus particulier rpcbind (Solaris) ou portmap (Linux) chargé de demander la réalisation du service au processus qui s'est engagé à le réaliser en enregistrant ce service grâce au triplet (N° de programme, N° de version, N° de fonction) et à la fonction registerrpc.

 

On trouvera dans le fichier /etc/rpc un ensemble de services "prédéfinis". Ainsi, NFS est un ensemble de services RPC du NumProg 1000003 dont, par exemple, la lecture a pour NumFonction 6 et l'écriture a pour NumFonction 8.

 

 

 

3 - RPC sous UNIX

3.1 Les trois niveaux

 

Sous UNIX, il existe trois niveaux de RPC (haut, intermédiaire et bas) qui offrent plus ou moins de transparence à l'utilisateur è demande une connaissance plus ou moins approfondie des mécanismes de plus bas niveau du type sockets.

 

Les trois niveaux

 

haut : Un ensemble de fonctions est disponible via la bibliothèque librpcsvc.a. L'utilisateur peut simplement appeler une fonction en spécifiant la machine cible et les paramètres. Il n'est pas possible de développer, à ce niveau, de nouveaux services.

Exemple de service existant : rnusers(<machine cible>)

 

intermédiaire : Il est possible de développer de nouvelles applications (presque toutes) en n'ayant qu'une connaissance limitée (minimale) de XDR et des RPC sans avoir à manipuler des sockets (qui sont sur UDP) - écritures des fonctions sur le serveurs ;

- enregistrement via registerrpc et mise en attente du serveur

- appel par le client de ces fonctions via callrpc

 

bas : Son utilisation est beaucoup plus complexe et suppose une bonne connaissance des sockets. Nécessaire pour les applications où UDP est inadapté.

 

3.2 Commande rpcinfo

 

Les commandes

§   rpcinfo -p [<machine cible>] : donne la liste des services RPC enregistrés sur la machine cible (locale par défaut)

§   rpcinfo -[ut] <machine cible> <Nprog> [<Nversion>] : appel de la procédure 0 du service UDP (pour -u) ou TCP (pour -t) du programme Nprog sur la machine cible.

§   rpcinf o -d <machine cible> <Nprog> [<Nversion>] : efface le service.

 

3.3 Niveau haut

 

A ce niveau un certain nombre de fonctions sont disponibles en standard dans la librairie Iibpcsrv.

Deux exemples de telles fonctions

- getrpcport :donne le numéro de port correspondant au service RPC si ce service est connu, 0 sinon

- rnusers <machine cible> : renvoie le nombre d'utilisateurs connectés sur une machine distante.

 

Exemple d'utilisation

 

/** Un programme qui affiche le numéro de port RPC d'un service dont le numéro est donné **/ /* Rappel sur la structure rpcent donné dans rpcent.h

struct rpcent {

char * r_ name ;           // nom du programme RPC

char ** aliases;

int r number;              // numéro du programme

}*/

main() {

int num serv = 1000003, iport ;

struct rpcent * p ;

p = getrpcbynumber(num_serv) ;                 // retourne le rpcent du service

printf(" Nom du service %s \n",p->rname) ;

iport = getrpcport("ada", num_serv, 1, iPPPRO_UDP);

printf(" Port affecté au service %d \n",iport) ;

}

 

3.4 Niveau Intermédiaire

3.4.1 Côté serveur

Trois étapes :

1. écriture des fonctions

2. enregistrement du service

3. mise en attente

 

 

Écriture des fonctions (étapes 4, 7, 8)

 

·   Les fonctions doivent nécessairement n'avoir qu'UN paramètre (et si possible) du type char*. Pourquoi? Tout simplement parce que lors de l'appel, ce paramètre sera passé à XDR pour être codé. Or cet appel à XDR est automatique : le type des paramètres est donc fixe. Idem pour le résultat.

 

·   XDR permet de coder et décoder des données. Pour cela il faut définir EXPLICITEMENT les fonctions de codage sachant qu'il existe un ensemble de fonctions de codage prédéfinies.

 

Par exemple, pour coder une structure : typedef struct { int I ; float F } couple ; on définit la fonction :

bool_t xdr_couple(XDR xdrp, couple *C) { return(xdr_int(xdrp,C) && xdr_float(xdrp,C));}

 

Attention

-    pour une fonction de codage pouvant être appelée automatiquement par un RPC,

·         le premier paramètre est TOUJOURS un objet du type XDR, appelé flot XDR.

·         le second est l'objet à coder ou décoder (dans ce dernier cas, il faut que la place mémoire existe pour cet objet)

- certaines fonctions XDR demandent plus de deux paramètres (par exemple : xdr_ string(XDR xdrp, char *p, int L)). Elles ne peuvent être utilisées directement dans un appel RPC.

 

 

Compilation Nécessite les options : -lnsl, librairie des sockets et -lrpcsvc, librairie des RPC.

 

Enregistrement du service (étapes 1, 2, 3) Cela consiste à signaler l'existence du service au démon portmap ou rpcbind qui lui associe un port. Chaque fonction doit être enregistrée individuellement par :

 

int registerrpc(unsigned long <prog>,unsigned long <version>, unsigned long <proc>, char*(*f)(), bool_t(*xdr_arg)(),bool_t(*xdr_res)() )

 

Par exemple la fonction suivante

char * ADD(couple *pc) {

static float res ;

res = pc->I + pc->F ;

return (char *)&res ;

}

sera enregistrée par registerrpc(32001,1,1,ADD,xdr_couple, xdr_float) ;

 

Effacement d'un service  Deux possibilités :

- par programme : pmap_unset(unsigned long <prog>, unsigned long <version>)

- par commande système : rpcinfo -d <prog> <version>

 

 

Mise en attente du serveur (étapes 5, 6) La fonction svc_run() met le processus serveur en attente. Cette fonction ne finit "jamais" sauf en cas d'erreur.

 

3.4.2 Côté client

 

Pour appeler un service RPC, le client utilise la fonction :

callrpc(char* <machine>, unsigned long <prog>,unsigned long <version>,

unsigned long <proc>, bool_t(*xdr_arg)(), char * arg, bool_t(*xdr_res)(), char * res)

Cet appel est bloquant.

 

Exemple

couple p = { 4 , 9.6 } ; double res ;

callrpc ("ada ", 32001, 1, 1,xdr_ couple, (char *)p, xdr_float, (char *)r); }

 

3.4.3 Un exemple complet (niveau intermédiaire)

 

 

 

/***** Le serveur ****/

#include <rpc/types.h>

#include <rpc/xdr.h>

void xdr_String(XDR * xdrp, char * p) {

       xdr_string(xdrp, &p, 1000) ;

}

/***** Le client ****/

#include <rpc/types.h>

#include <rpc/xdr.h>

void xdr_String(XDR * xdrp, char * p) {

       xdr-string(xdrp, &p, 1000) ;

}

char * Reponse() {

       char * uneReponse = "OK";

       return uneReponse;

}

void main(void) {

       char res[1000] ;

       callrpc("ada", 32012, 1, 1,

       xdr_void, NULL, xdr_String, (char        *)res) != 0)

// Le résultat est dans res ===> res = "OK"

}

void main(void) {

registerrpc(32012, 1, 1, Reponse, xdr_void, xdr_String) ;

svc-run() ;

}

 

 

 

4 Échec d'une requête RPC

Plusieurs causes possibles d'échecs

 

1. incapacité à "communiquer" avec le serveur

2. perte de la demande

3. perte de la réponse

4. panne du serveur

5. panne du client

 

4.1 Incapacité à communiquer le serveur

 

Exemple, en cas d'évolution du serveur, les paramètres peuvent avoir changés.

 

Comment le serveur va-t-il signaler qu'il a bien reçu la requête mais qu'elle n'est pas valide :

- par la variable "errno" : or celle-ci est locale au processus client

- par un retour égal par exemple à -1 : mais cette valeur peut très bien être une valeur acceptable comme résultat

èNécessité de mettre en place un mécanisme de signal ou d'exception

 

 

 

 

 

 

4.2 Perte de la demande

 

Utilisation de temporisateur et ré-émission à problème du time-out. En effet, si celui-ci est trop petit, le client va abandonner rapidement et considérer à tort dans certain cas, que le serveur n'est pas accessible. Par contre, s'il est trop grand, il peut y avoir beaucoup de perte de temps si le nombre de ré-émissions nécessaire pour un succès est élevé.

4.3 Perte de la réponse

 

Au bout d'un certain temps, le client décide que la réponse aurait dû lui parvenir. Trois possibilités :

- il y a eu perte réelle de la réponse

- le serveur n'a pas fini (la réponse peut encore venir)

- la demande s'est perdue

 

Deux cas à voir :

- l'opération est idempotente : elle peut être effectuée autant de fois que nécessaire sans problème (ex : lecture de 1024 premiers caractères d'un fichier, ré-initialisation d'une variable, ...)

- l'opération n'est pas idempotente : l'exécution modifie l'état du serveur par exemple (ex : incrément d'un variable,

 

è utilisation d'un marqueur de ré-émission des demandes

 

4.4 Panne du serveur

 

Deux cas :

-    le serveur a effectué l'opération mais n'a pas eu le temps d'envoyer la réponse

§   s'il a eu le temps de sauvegarder alors après redémarrage : il peut l'envoyer ou attendre une ré-émission de la demande . (A noter : (1) la réponse est-elle toujours valable? combien de temps faut-il la mémoriser? (2) cela sert aussi dans le cas général où la réponse se perd)

§   s'il n'a pas eu le temps (ou l'envie) de sauvegarder alors après redémarrage : il doit attendre une ré-émission de la demande => on retrouve le problème d'avant : l'opération est-elle idempotente?

-    le serveur n'avait pas fini : il doit attendre une ré-émission de la demande => on retrouve le problème d'avant l'opération est-elle idempotente ?

 

D'où dans le cas d'une non-réponse pour panne, plusieurs possibilités de reprise par le client

 

-    "une fois au moins" : attendre que le serveur redémarre (ou en trouver un autre) pour renouveler la demande jusqu'à obtention d'une réponse

-    "une fois au plus" : abandon immédiat en cas de non-réponse avec message d'erreur

-    "sans garantie" : quand un serveur tombe en panne ; le RPC a pu être exécuté de 0 à plusieurs fois

 

Solution théorique : "exactement une fois" è impossible car la procédure de reprise dépend entièrement de ce qui s'est passé avant la panne mais le client est incapable de le déterminer.

 

Idéalement, le serveur devrait être capable de réaliser une opération de façon "atomique" : soit l'opération est totalement réalisée soit RIEN n'est fait è notion de transaction.

 

4.5 Panne du client

 

Envoi d'une demande puis panne du client

-    que doit faire le serveur de la réponse (s'il y a mémorisation cf 4.4)

-    que faire des (petits-)fils que le client a lancé

 

 

 

Extermination : Le subrogé client tient un journal à jour sur disque où il note chaque demande RPC. Après redé­marrage, il "tue" explicitement ces orphelins MAIS :

- méthode coûteuse car nécessite des écritures sur disque

- peut échouer en cas de petits orphelins

- le réseau peut être cloisonné et interdit les "kill"

 

è méthode à proscrire

 

Réincarnation : On divise le temps en périodes numérotées. Quand un client redémarre, il diffuse à toutes les machines un message qui marque le début d'une nouvelle période. A réception toutes les exécutions distantes sont détruites. Les réponses d'orphelins non détruits (pour une raison X ou Y) ne seront de toute façon pas prises ne compte car datées d'une période précédente.

Réincarnation douce : Idem, mais à réception de la nouvelle période, le serveur ne tue les exécutions locales que pour celle où il n'arrive pas à localiser le client

 

Expiration : Chaque RPC dispose d'un quantum T pour s'exécuter. Au redémarrage du client, celui-ci attend un temps T avant de (re-)faire des demandes. Il sait qu'après ce temps, plus aucun orphelin ne peut encore exister.

 

 

En conclusion, il faut bien voir que le client n'a aucune information sur la cause de non réponse!!! Il ne peut donc choisir la meilleure solution!!