Do spisu tresci tematu 9

9.2.1. RPC - Wstep




Spis tresci


Wprowadzenie

W Linuxie zaimplementowane jest Sun RPC. Przy pomocy tego systemu sa zrealizowane np.: NFS, quota i hasla.

RPC w Linuxie nie jest jednak realizowane przez jadro systemu, a przez standardowa biblioteke LIBC (w jadrze jest zaimplementowany tylko fragment klienta RPC przy okazji systemu NFS). Jak sama nazwa wskazuje, RPC sluzy do zdalnego wywolywania procedur, a dziala w nastepujacy sposob:

[Schemat dzialania RPC]

  1. Klient chcac wywolac procedure zdalna wywoluje procedure lacznika, ktora korzystajac z filtrow XDR (eXternal Data Representation) koduje parametry wywolania procedury (+wlasne dane dodatkowe) na format niezalezny od systemu.
  2. Uzywajac protokolu TCP albo UDP klient komunikuje sie przez siec z komputerem serwera i wysyla zakodowana wiadomosc (polega to na wywolaniu odpowiednich funkcji systemowych).
  3. Na komputerze serwera dziala proces, ktory oczekuje na zadania klientow (w petli). Po odebraniu wiadomosci przez siec rozkodowuje argumenty procedury do formatu wewnetrznego komputera serwera (przez analogiczny jak u klienta filtr XDR) i wywoluje procedure lokalna serwera.
  4. Wyniki tej procedury sa kodowane kolejnym filtrem XDR i sa odsylane klientowi.
  5. Klient odbiera wiadomosc i po rozkodowaniu (filtrem odpowiadajacym filtrowi, ktorym byly kodowane) jej wartosci sa zwracane jako rezultat wykonania procedury zdalnej.

Procedury zdalne sa identyfikowane przez trzy liczby: numer programu, numer wersji programu i numer procedury. Pierwsze dwa sluza do otrzymania od portmappera numeru portu (TCP albo UDP) na komputerze serwera. Portmapper jest procesem dzialajacym na komputerze serwera i sluzacym do informowania o numerach portow serwerow RPC; nie jest on odpowiedzialny za ich przydzielanie (patrz: portmapper). Numery procedur identyfikuja procedury w serwerze.
Jako ze RPC moze uzywac dwoch odmiennych protokolow: TCP albo UDP, istnieja dwa typy klientow i warstw transportowych serwera, dzialajacych na troche innych zasadach. Warstwa transportowa posredniczy miedzy pojedynczym gniazdem a serwerem. Serwer RPC moze obslugiwac w jednej chwili kilka warstw transportowych, po jednej na: kazde aktywne polaczenie TCP, na kazde oczekiwanie polaczenia TCP (gniazdo po wywolaniu listen()) oraz na kazde gniazdo UDP. Wiecej o serwerze: patrz serwer.

TCP

W tym protokole jest zapewniona kontrola polaczenia (tzn.: protokol martwi sie o to czy wiadomosci dojda do adresata i czy bedzie to w odpowiedniej kolejnosci). Zatem jesli klient wysle raz komunikat do serwera, to ma pewnosc, ze zostanie on dostarczony (o ile nie zerwie sie polaczenie). Klient RPC na poczatku korzystania z procedur RPC tworzy polaczenie z serwerem, ktore jest utrzymywane az do konca wywolywania procedur RPC, a zamykane jest dopiero przy usuwaniu struktur klienta. W przypadku TCP powstaje problem w jaki sposob rozrozniac oddzielne komunikaty (jeden komunikat to zestaw parametrow jednej procedury lub jej rezultat), gdyz gniazdo TCP dziala na zasadzie strumienia. Sytuacje ratuje pewien typ potoku XDR, ktory umozliwia podzial danych na oddzielne rekordy (patrz: XDR). Dla protokolu TCP istnieja dwie warstwy transportowe serwera: jedna odpowiada za nawiazywanie polaczen, zas druga jest uzywana po polaczeniu z klientem.

UDP

Protokol ten umozliwia przesylanie datagramow czyli oddzielnych pakietow danych i nie gwarantuje dostarczenia danych do adresata, ani ich odpowiedniej kolejnosci u adresata. Miedzy komputerami nie nawiazuje sie polaczenia. Na poczatku dzialania klient tworzy gniazdo, ktorym bedzie wysylac i odbierac oddzielne komunikaty. Przy kazdym wywolaniu procedury klient wysyla wiadomosc do serwera i oczekuje na odpowiedz. Jesli ta po okreslonym czasie nie nadchodzi, to klient probuje wyslac wiadomosc ponownie. Jesli po kilku probach klient nie otrzymuje wiadomosci zwrotnej to zwraca blad o niemoznosci komunikacji z serwerem. Dla protokolu UDP serwer moze stosowac buforowanie (cache) wynikow procedur, dzieki czemu nie trzeba wywolywac kilka razy tych samych procedur z tymi samymi parametrami (jesli odpowiedzi serwera nie zawsze dochodza do klienta). W przypadku UDP moze sie pojawic problem w momencie, gdy klient wysle dwa wywolania procedury i oba zostana odebrane przez serwer i przetworzone. Aby temu zarazic mozna wprowadzic numerowanie pakietow przez klienta i sprawdzanie przez serwer, czy juz wiadomosci o tym numerze nie przetworzyl (taki mechanizm jest zastosowany np. w systemie NFS).


XDR

Standard XDR (eXternal Data Representation) zostal wprowadzony po to, by ujednolicic reprezentacje danych w transmisjach sieciowych miedzy roznymi komputerami o odmiennych architekturach (umozliwia np. porozumienie sie miedzy programem w jezyku FORTRAN na Cray’u a programem w C na i386 z Linuxem). Przykladowo: wszystkie typy maja rozmiar bedacy wielokrotnoscia 4 bajtow (dziury sa wypelniane zerami) , liczby calkowite sa kodowane w ten sposob, ze mlodsze bajty maja starsze adresy (na odwrot niz w i386), liczby rzeczywiste w formacie IEEE. Ograniczeniem XDR jest to, ze zarowno nadawca, jak i odbiorca musza znac struktury danych (nie ich reprezentacje!) , poniewaz nie sa one przekazywane.

Potoki XDR i filtry XDR

Potok XDR to ciag bajtow, w ktorym dane sa reprezentowane w formacie XDR. Proces wysylajacy / piszacy dane po zakodowaniu danych umieszcza je w potoku, zas odbiorca odbiera z potoku i rozkodowuje. Sa trzy typy potokow: potoki na plikach (dokladniej na ich dekryptorach; nie uzywane przy RPC), potoki w pamieci (na buforach; uzywane przez RPC z UDP) oraz potoki komunikatow (dzielace dane na rekordy; uzywane przez RPC z TCP).

Filtr XDR jest procedura kodujaca i dekodujaca pewien typ danych.

Potok XDR jest identyfikowany przez strukture XDR:

enum xdr_op {
        XDR_ENCODE=0,
        XDR_DECODE=1,
        XDR_FREE=2
};

typedef struct {
        enum xdr_op     x_op;
        struct xdr_ops {
                bool_t  (*x_getlong)();
                bool_t  (*x_putlong)(); 
                bool_t  (*x_getbytes)();
                bool_t  (*x_putbytes)();
                u_int   (*x_getpostn)();
                bool_t  (*x_setpostn)();
                long *  (*x_inline)();
                void    (*x_destroy)();
        } *x_ops;
        caddr_t         x_public;
        caddr_t         x_private;
        caddr_t         x_base;
        int             x_handy;
} XDR;

Pole x_op zawiera informacje jaka czynnosc jest w danej chwili wykonywana na potoku: kodowanie, dekodowanie, czy zwalnianie pamieci. Pole x_ops zawiera wskazniki na funkcje, ktore mozna wykonywac na potoku (z tych operacji korzystaja na najnizszym poziomie filtry). Funkcje te przekodowuja dane. Pozostale pola sa uzywane przez rozne potoki w rozny sposob do przechowywania danych pomocniczych (deskryptory plikow, adresy w pamieci, pozycje w potoku, itp.). Typy: bool_t to int, caddr_t to char*.

x_getlong()
- pobiera z potoku typu long
x_putlong()
- wysyla do potoku typu long
x_getbytes()
- pobiera potoku ciag bajtow
x_putbytes()
- wysyla do potoku ciag bajtow
x_getpostn()
- pobiera aktualna pozycje w potoku
x_setpostn()
- ustawia pozycje w potoku
x_inline()
- zwraca wskaznik na aktualna pozycje w buforze potoku
x_destroy()
- zwalnia zasoby zwiazane z tym potokiem

Dla danego potoku nie zawsze wszystkie operacje beda dzialac, np. x_inline() dla potoku zwiazanego z plikiem zawsze zwraca NULL.

Potok w pamieci

Potok ten jest zaimplementowany w pliku rpc/xdr_mem.c. Tworzenie:

void xdrmem_create( XDR *xdrs, caddr_t addr, u_int size, enum xdr_op op)

Parametry:
addr - wskaznik na przygotowany bufor
size - jego rozmiar
op - operacja na potoku.

Dodatkowe dane:
x_private - wskaznik na aktualne polozenie w buforze,
x_base - poczatek bufora,
x_handy - rozmiar wolnej pamieci

Potok komunikatow (rekordow)

Implementacja: plik rpc/xdr_rec.c.Znacznie bardziej skomplikowany od poprzedniego. Operuje na strumieniu danych (sam potok pisze albo czyta plik, gniazdo, itp.) i zapewnia jego podzial na oddzielne rekordy czyli oddzielne zestawy danych (oczywiscie moga byc one roznych typow). Gwarantuje odnalezienie konca wczytywanego rekordu w przypadku bledu i przejscie na poczatek nastepnego. Organizacja rekordu: rekord sklada sie z fragmentow rekordu (bynajmniej nie odzwierciedlaja one typow przesylanych zmiennych, a jedynie pojemnosc bufora potoku). Kazdy fragment sklada sie z 32-bitowego naglowka oraz ciagu bajtow, ktorego dlugosc jest zakodowana w naglowku. Kodowanie naglowka: funkcja htonl(u_long). Jesli najstarszy bit jest rowny 1 to jest to ostatni fragment rekordu, jesli 0 to rekord zawiera jeszcze jakies fragmenty. Pozostale 31 bitow to dlugosc fragmentu.
Ten typ potokow uzywa zdefiniowanych przez uzytkownika funkcji czytania i pisania z plikow, gniazd lub innych, co daje wieksza uniwersalnosc tego filtru .

Tworzenie:

void xdrrec_create(XDR *xdrs, u_int sendsize, u_int recvsize, caddr_t tcp_handle, int (*readit)(), int (*writeit)())

sendsize, recvsize - rozmiary buforow we i wy
tcp_handle - dowolne parametry (niekoniecznie deskryptor gniazda), ktore beda przekazywane do:
readit, writeit - funkcje czytania z i pisania do jakiegos medium.

Funkcje przejscia do nastepnego, zakonczenia rekordu i sprawdzenia czy juz koniec rekordu:

bool_t xdrrec_skiprecord(XDR *xdrs)
bool_t xdrrec_endofrecord(XDR *xdrs, bool_t sendnow)
bool_t xdrrec_eof(XDR *xdrs)

sendnow oznacza, czy od razu wysylac rekord, czy tylko zapamietywac go w buforze (wyslanie wszystkich zgromadzonych komunikatow nastapi wtedy, gdy bufor sie przepelni lub gdy funkcja xdrrec_endofrecord bedzie wywolana z parametrem sendnow!=0).

Filtry XDR

Funkcje te sluza do kodowania i dekodowania danych. Ich dzialanie polega na tym, ze otrzymujac w parametrach wskaznik na strukture XDR i wskaznik na zmienna, w zaleznosci od pola XDR->x_op dokonuja konwersji. Dla wszystkich operacji na okreslonym typie danych definuje sie jedna uniwersalna funkcje, dzieki czemu latwiej uniknac bledow przy pisaniu filtru. Dla kazdego podstawowego typu istnieje filtr XDR, dla wlasnych trzeba go stworzyc, zazwyczaj korzystajac ze standardowych filtrow.


Format komunikatow przesylanych przez siec

Wszystkie komunikaty RPC sa w calosci zakodowane przy pomocy XDR (lacznie z danymi pomocniczymi, takimi jak identyfikatory klienta, nr procedury, itp.). Roznia sie one w przypadkach TCP i UDP na najnizszym poziomie z powodu uzycia innych typow potokow XDR. Pomijajac reprezentacje danych w przypadkach obu protokolow mamy do czynienia z takimi samymi strukturami.

Naglowek wiadomosci RPC:

struct rpc_msg {
        u_long                  rm_xid;
        enum msg_type           rm_direction;
        union {
                struct call_body RM_cmb;
                struct reply_body RM_rmb;
        } ru;
};
rm_xid - identyfikator wiadomosci klienta
rm_direction - typ wiadomosci: CALL lub REPLY

struct call_body {
        u_long cb_rpcvers;      
        u_long cb_prog;
        u_long cb_vers;
        u_long cb_proc;
        struct opaque_auth cb_cred;
        struct opaque_auth cb_verf;
};
cb_rpcvers - wersja protokolu RPC; musi byc rowna 2
cb_cred, cb_verf - do identyfikacji klienta

struct reply_body {
        enum reply_stat rp_stat;
        union {
                struct accepted_reply RP_ar;
                struct rejected_reply RP_dr;
        } ru;
};
struct accepted_reply {
        struct opaque_auth ar_verf;
        enum accept_stat   ar_stat;
        union {
                [...]
                struct {
                        caddr_t where;
                        xdrproc_t proc;
                } AR_results;
        } ru;
};
ar_verf - do identyfikacji klienta
ar_stat - oznacza czy procedura byla i czy zadzialala
AR_results - rezultat dzialania procedury zdalnej, te pola sa ustawiane przez klienta przed odbiorem wynikow; odpowiedni filtr XDR dla accepted_reply zamiast kodowac/rozkodowac AR_results wywoluje
     (ru.AR_results.proc)(xdrs, ru.AR_results.where),
czyli w ten sposob koduje/rozkodowuje wynik procedury zdalnej
where - adres wynikow procedury zdalnej
proc - filtr XDR rozkodowujacy wynik

struct rejected_reply {
        enum reject_stat rj_stat;
        union { [...] } RJ_versions;
                enum auth_stat RJ_why;
        } ru;
};
RJ_why - dlaczego odrzucono komunikat
RJ_versions - dodatkowe informacje na temat powodu odrzucenia

Dla wiadomosci od klienta poczatek naglowka jest taki sam (nie do konca, patrz opis klientow) dla wszystkich wywolan procedur, wiec jest kodowany tylko raz. Za tym fragmentem sa przesylane: dalsza czesc naglowka (zakodowany nr procedury, dane na temat autentycznosci i identyfikacji klienta) i zakodowane parametry procedury.
W przypadku wiadomosci od serwera rezultat jest juz zawarty w strukturze rpc_msg.

Mimo, ze struktury te sa bardzo rozbudowane, dzieki odpowiednim filtrom XDR (xdr_union) wysylane sa tylko pola uzywane a nie wszystkie.


Bibliografia

  1. Biblioteka LIBC, a raczej pliki: ./rpc/* - implementacja RPC
  2. Pliki naglowkowe: /usr/include/rpc/*
  3. M. Gabbasi, B. Dupouy: ,,Przetwarzanie rozproszone w systemie UNIX'' - opis uzytkowy RPC i XDR
  4. RFC 1014 - XDR
  5. RFC 1057 - RPC
  6. Dokumentacja biblioteki LIBC: /usr/info/libc.info - opis funkcji dotyczacych gniazd
  7. R. Stevens: ,,Programowanie zastosowan sieciowych'' - ogolne wiadomosci o RPC


Autor: Przemyslaw Kozankiewicz