Subsections

3 Przykłady bibliotek komunikacyjnych

W rozdziale 2 opisane zostały zagadnienia związane z działaniem bibliotek komunikacyjnych. Wybranie przy projektowaniu biblioteki różnych rozwiązań skutkuje stworzeniem biblioteki o innych cechach i innym przeznaczeniu. Niektóre biblioteki są łatwe w użyciu, ale mniej wydajne. Inne wręcz przeciwnie -- semantyka biblioteki jest bardziej skomplikowana, ale za to umożliwia osiągnięcie dużej wydajności.

W tym rozdziale przedstawione zostaną przykłady różnych bibliotek komunikacyjnych wraz z krótkim opisem przyjętych założeń i zastosowanych rozwiązań.

W punkcie 3.1 opisano bibliotekę gniazd jako standardową bibliotekę komunikacyjną używaną obecnie.

Punkt 3.2 zawiera opis biblioteki VIPL, zaprojektowanej do szybkiej komunikacji sieciowej z uniknięciem pośredniego kopiowania.

W punkcie 3.3 jest opisana biblioteka ACL. Jest to wydajna biblioteka komunikacyjna wysokiego poziomu, przeznaczona dla systemów klastrowych i używająca VIPL do transportu danych.


1 Gniazda

1 Wstęp

Gniazda (ang. sockets) są obecnie najbardziej rozpowszechnioną biblioteką komunikacyjną. Są one zaimplementowane na praktycznie wszystkich systemach operacyjnych i jako takie stanowią uniwersalny sposób komunikacji sieciowej.

Interfejs gniazd powstał dla systemu Unix BSD i jest prostym rozwinięciem uniksowego modelu dostępu do plików typu otwórz-przeczytaj-zapisz-zamknij. Gniazdo w systemach uniksowych jest deskryptorem, analogicznym do deskryptora pliku. Z tego powodu interfejs gniazd jest prosty w użyciu, a do przesyłania danych można nawet wykorzystać identyczne jak dla plików funkcje read() i write().

W tym punkcie opisane są tylko najważniejsze cechy gniazd, dokładniejszy opis można znaleźć w [Comer1], rozdział 20.

2 Model wysokiego poziomu

Biblioteka gniazd udostępnia ogólny interfejs, który pozwala na komunikację sieciową z użyciem różnych protokołów sieciowych i o różnej semantyce transportu danych. Semantykę transportu danych określa się wybierając typ gniazda (typy gniazd są opisane w p. 3.1.4).

Biblioteka gniazd przeważnie nie udostępnia żadnej funkcjonalności, umożliwia tylko bezpośrednie użycie protokołu komunikacyjnego. Wszystkimi aspektami związanymi z transmisją danych, w tym zapewnieniem niezawodności i porządku danych, zajmuje się protokół komunikacyjny. Dlatego nie każdy rodzaj gniazd może być zrealizowany za pomocą określonego protokołu komunikacyjnego.

3 Zarządzanie połączeniem

Nawiązywanie połączenia w bibliotece gniazd jest oparte na modelu klient-serwer (patrz p. 2.4.1).

Biblioteka gniazd udostępnia następujące operacje zarządzania połączeniem.

W przypadku komunikacji bezpołączeniowej, oferowanej przez niektóre typy gniazd, używane są tylko funkcje socket() i close() do stworzenia i zniszczenia gniazda. Gniazdo takie może być użyte do wysyłania danych pod dowolny adres, a przy każdej transmisji trzeba podać adres odbiorcy.


4 Semantyka transportu danych

Typ gniazda określa semantykę transportu danych. W bibliotece gniazd określone są następujące typy gniazd:

Dla przykładu, w sieciach opartych na protokole TCP/IP gniazda typu SOCK_STREAM są zrealizowane za pomocą protokołu TCP, gniazda typu SOCK_DGRAM używają protokołu UDP, natomiast gniazda typu SOCK_RAW umożliwiają bezpośrednie wysyłanie pakietów IP.

5 Semantyka operacji transmisji danych

Biblioteka gniazd używa synchronicznego przetwarzania transmisji danych. Każde wywołanie funkcji transmisji danych odpowiada jednej transmisji. Zdarzenia związane z transmisją są sygnalizowane poprzez zwrócenie odpowiedniego kodu powrotu z funkcji transmisji.

Do transmisji danych służy cała rodzina funkcji. Różnią się one między sobą możliwością podania dodatkowych opcji i zastosowania wysyłania ze zbieraniem danych (ang. gather) lub odbierania z rozrzucaniem (ang. scatter).

Do rodziny funkcji służących do wysyłania danych należą funkcje: send(), sendto(), sendmsg(), write() i writev().

Rodzina funkcji służących do odbierania danych obejmuje funkcje: recv(), recvfrom(), recvmsg(), read() i readv().

1 Semantyka operacji wysyłania danych

Do wysłania danych używa się jednej z funkcji z rodziny send(), podając identyfikator gniazda i wskazując bufor, w którym znajdują się dane do wysłania. W przypadku komunikacji bezpołączeniowej podaje się również adres odbiorcy. W przypadku komunikacji połączeniowej gniazdo musi być wcześniej połączone za pomocą operacji connect() (przez klienta) lub accept() (przez serwer).

Funkcja wysyłająca dane może się zablokować w oczekiwaniu na niedostępny zasób (na przykład na miejsce w buforze pośrednim). Można temu zapobiec poprzez użycie odpowiedniej opcji, otrzymując mechanizm przepytywania.

Funkcje sendmsg() i writev() zamiast adresu jednego obszaru pamięci, zawierającego dane do wysłania, używają wektora obszarów pamięci (określonych jako adres i długość obszaru), który umożliwia przeprowadzenie operacji zebrania danych do wysłania z niespójnych obszarów pamięci.

2 Semantyka operacji odbierania danych

Dane odbiera się za pomocą jednej z funkcji z rodziny recv(), podając identyfikator gniazda i wskazując gdzie mają być umieszczone odebrane dane. Dodatkowo, przy komunikacji bezpołączeniowej można otrzymać adres nadawcy danych w oddzielnym buforze. Komunikacja połączeniowa wymaga utworzenia wcześniej połączenia (analogicznie jak przy wysyłaniu).

Jeżeli dane są dostępne od razu, to powrót z funkcji odbierającej dane odbywa się bez blokowania, natychmiast po umieszczeniu danych we wskazanym buforze. Jeżeli dane jeszcze nie nadeszły, to nastąpi zablokowanie procesu do czasu odebrania danych lub, w przypadku podania odpowiedniej opcji, operacja zakończy się błędem. Ta ostatnia sytuacja umożliwia przeprowadzenie przepytywania, czy dane są dostępne.

Semantyka operacji odbioru nie zawiera żadnych ograniczeń na moment odebrania danych -- dane mogą być wysłane w dowolnym momencie i odebrane w dowolnym, dogodnym dla odbiorcy momencie.

Istnieje możliwość odebrania danych do niespójnych obszarów pamięci, czyli operacji rozrzucania danych. Podobnie jak przy wysyłaniu, używa się do tego wektora zawierającego ciąg wskaźników obszarów pamięci i ich długości.

3 Brak pośredniego kopiowania

Większość implementacji gniazd używa buforów pośrednich. Wynika to z faktu, że semantyka operacji wysyłania i odbierania danych nie została określona pod kątem uniknięcia pośredniego kopiowania. O ile przy wysyłaniu danych możliwe jest uniknięcie pośredniego kopiowania, o tyle przy odbiorze danych, w ogólnym przypadku, jest to niemożliwe.

1 Wysyłanie bez pośredniego kopiowania

Przy wysyłaniu danych można uniknąć pośredniego kopiowania poprzez zablokowanie wątku wysyłającego do czasu zakończenia transmisji. Taka konieczność wynika z faktu, że bufor z danymi do wysłania może zostać użyty ponownie natychmiast po powrocie z funkcji wysyłającej, oznaczającym dla użytkownika koniec transmisji. Do czasu powrotu z funkcji wysyłającej dane w buforze nie mogą zostać zmodyfikowane i dlatego można wysłać dane bezpośrednio z tego bufora.

Wysyłanie bez pośredniego kopiowania wymaga wsparcia ze strony karty sieciowej, która musi mieć możliwość zebrania danych do jednej transmisji z różnych fragmentów pamięci -- przed właściwymi danymi musi zostać umieszczony nagłówek protokołu komunikacyjnego.

Takie rozwiązanie co prawda zapewnia brak pośredniego kopiowania przy wysyłaniu, ale ogranicza liczbę jednocześnie przesyłanych danych, ponieważ jeden wątek synchronicznie czeka na zakończenie jednej transmisji. W przypadku komunikacji zawodnej oznacza to oczekiwanie na wysłanie danych w sieć, natomiast dla trasmisji niezawodnej wątek musi poczekać na potwierdzenie odebrania danych, ponieważ może się zdarzyć potrzeba retransmisji danych.

2 Odbieranie bez pośredniego kopiowania

Odbieranie danych bez pośredniego kopiowania napotyka dużo poważniejsze problemy.

  1. Segregacja danych. W momencie odebrania danych z sieci do pamięci jest zapisywana cała ramka sieciowa, zawierająca nagłówek wraz z danymi. Dopiero po odczytaniu nagłówka wiadomo jaki protokół został użyty i do którego gniazda są skierowane te dane.
  2. Dane przychodzące w nieodpowiedniej kolejności, zgubione i duplikaty. Jeżeli komunikacja ma zapewniać porządek danych, a fizyczny transport danych odbywa się za pomocą protokołu nie zapewniającego porządku, to może się okazać, że dane zostaną odebrane z sieci nie w takim porządku, w jakim ma je odebrać użytkownik. Jeśli dane byłyby odbierane bezpośrednio do bufora użytkownika, to muszą stamtąd zostać przeniesione, a w ich miejsce wstawione właściwe dane, kiedy nadejdą. Podobna sytuacja występuje wówczas, gdy transmisja jest zawodna -- część danych może być zgubiona, w związku z tym można odebrać późniejsze dane do bufora przygotowanego dla danych, które zostały zgubione lub odwrotnie -- te same dane nadejdą po raz drugi i zostaną umieszczone w buforze przygotowanym dla danych późniejszych.
  3. Rozdzielanie danych. W przypadku połączeń strumieniowych wątek może oczekiwać na odebranie dowolnej liczby bajtów. W związku z tym dane, na które oczekuje, mogą stanowić tylko fragment danych, które zostały odebrane. Wówczas część danych powinna trafić do tego wątku, a pozostała do innego, który również oczekuje na dane, lub, jeśli takiego nie ma, dane trzeba porzucić lub skopiować gdzieś indziej.
  4. Brak oczekującego odbiorcy. W momencie nadejścia danych nikt na nie nie musi czekać. W szczególności jest to prawdziwe, gdy odbiór danych odbywa się poprzez przepytywanie.
Rozwiązanie pierwszych trzech problemów jest niemożliwe bez wsparcia sprzętowego. Podstawową przeszkodą jest to, że informacja o tym, gdzie dane mają zostać umieszczone jest częścią odebranej transmisji -- znajduje się w nagłówku protokołu, którego standardowa karta sieciowa nie rozumie. W związku z tym musi umieszczać dane kolejno w przygotowanych buforach. Rozpoznaniem, do którego gniazda te dane są skierowane, czy nie przybyły w złym porządku albo po raz drugi, oraz rozdzieleniem danych pomiędzy czekających odbiorców zajmuje się system operacyjny, który w tym celu przegląda nagłówki protokołu komunikacyjnego. Tylko że wtedy dane są już w buforze i muszą zostać ponownie skopiowane w odpowiednie miejsce.

Jedynym rozwiązaniem problemu rozpoznawania nagłówków jest sprzętowa implementacja protokołu komunikacyjnego, czyli karty sieciowe, które rozpoznają zawartość nagłówków i są w stanie umieścić dane od razu w odpowiednim miejscu. Przykładem takich kart są tak zwane karty TOE (TCP offload engine), które sprzętowo implementują stos TCP/IP.

Na ostatni problem, brak oczekującego odbiorcy, składają się dwie sytuacje.

Po pierwsze, odbiorca może jeszcze nie być gotowy do odebrania danych, kiedy nadawca jest gotowy. W takim przypadku można zastosować rozwiązanie polegające na tym, że nadawca ma prawo wysłać dane dopiero wtedy, gdy ktoś chce je odebrać. Wymaga to dodatkowej synchronizacji nadawcy z odbiorcą, co jest możliwe, ale kosztem przepustowości (ponieważ ,,w locie'' mogą być tylko dane, na które ktoś oczekuje w funkcji odbierającej) i czasu odpowiedzi (bo nadawca musi zostać powiadomiony, że może zacząć wysyłać).

Drugi problem, dużo poważniejszy, wynika z możliwości nieblokującego przepytywania. Jeżeli nadawca stosuje przepytywanie do stwierdzenia, czy może wysłać dane, i odbiorca również stosuje przepytywanie do stwierdzenia, czy są dane do odebrania, to powyższe rozwiązanie nie sprawdza się, ponieważ żadna ze stron nie czeka na transmisję, tylko pyta, czy w danej chwili transmisja jest możliwa. Jest to problem, którego nie da się rozwiązać bez zmiany semantyki operacji wysyłania i odbierania.

6 Przechodzenie do trybu jądra

Wszystkie funkcje biblioteki gniazd są funkcjami systemu operacyjnego i w związku z tym wymagają przejścia do trybu jądra.

7 Podsumowanie

Biblioteka gniazd to uniwersalna biblioteka, umożliwiająca komunikację sieciową w różnych środowiskach i z użyciem różnych protokołów komunikacyjnych. Jednak semantyka operacji, wzorowana na semantyce operacji na plikach, ogranicza możliwości zastosowania jej w wysoko wydajnych aplikacjach. Nie ma możliwości jednoczesnego przetwarzania wielu transmisji danych w jednym wątku, a uniknięcie pośredniego kopiowania jest możliwe tylko w ograniczonym stopniu.


2 VIPL

1 Wstęp

W miarę powstawania coraz szybszych sieci komunikacyjnych coraz wyraźniej ujawniały się wady istniejących bibliotek komunikacyjnych, stosujących pośrednie kopiowanie i przechodzenie do trybu jądra przy każdej transmisji. Koszt tych operacji zaczął odgrywać znaczącą rolę przy przesyłaniu danych. Dlatego zaczęły powstawać biblioteki umożliwiające usunięcie tego kosztu poprzez eliminację pośredniego kopiowania i przeprowadzanie transmisji z poziomu użytkownika.

W 1997 roku firmy Compaq, Intel i Microsoft zaproponowały standard VIA (Virtual Interface Architecture) zawierający specyfikację komunikacji sieciowej bez pośredniego kopiowania i z możliwością transmisji danych bez przechodzenia do trybu jądra. Specyfikacja VIA ([VIAspec]) zyskała akceptację wielu firm i w obecnej chwili jest najszerzej stosowanym standardem dla tego typu komunikacji.

Specyfikacja VIA zawiera opis przykładowej implementacji interfejsu biblioteki komunikacyjnej. Na jej podstawie został stworzony interfejs biblioteki VIPL (Virtual Interface Provider Library). Opis VIPL znajduje się w [VIAdev].

2 Zarządzanie połączeniem

VIA operuje pojęciem VI (Virtual Interface -- wirtualny interfejs sieciowy) jako abstrakcji jednego końca połączenia. Do komunikacji niezbędne są dwa połączone VI.

Biblioteka VIPL udostępnia dwa modele nawiązywania połączenia: tradycyjny model klient-serwer oraz model równy-z-równym. Model klient-serwer jest przewidziany w specyfikacji VIA, natomiast model równy-z-równym jest rozszerzeniem wprowadzonym w VIPL.

Funkcje służące do nawiązywania połączenia są blokujące lub też umożliwiają stosowanie przepytywania.

Funkcje służące do zarządzania VI to:

Funkcje służące do zarządzania połączeniem w modelu klient-serwer są następujące:

Funkcje służące do zarządzania połączeniem w modelu równy-z-równym to:

Dokładniejszy opis sposobu nawiązywania połączenia można znaleźć w [VIAdev], p. 3.3.


3 Zarządzanie pamięcią

Pamięć używana do komunikacji z kartą sieciową, na bufory danych i deskryptory (por. p. 3.2.5) musi być zarejestrowana przez bibliotekę VIPL w karcie sieciowej.

Do zarządzania zarejestrowaną pamięcią biblioteka udostępnia dwie funkcje:

Dodatkowo VIA umożliwia kontrolę dostępu do zarejestrowanej pamięci poprzez znaczniki ochronne (ang. protection tags). Każdy VI i każdy zarejestrowany obszar pamięci posiada swój znacznik ochronny. Żeby użycie zarejestrowanej pamięci na danym VI było możliwe, oba znaczniki ochronne muszą się zgadzać. Dokładniejszy opis mechanizmu znaczników ochronnych można znaleźć w [VIAspec], p. 3.2.2.1 .

Zarejestrowania pamięć może być również, poprzez ustawienie odpowiedniej flagi przy rejestracji, zabezpieczona przed zdalnym zapisaniem (por. p. 3.2.5).

1 Koszt rejestracji pamięci

Rejestracja pamięci musi nastąpić przed rozpoczęciem transmisji, a pamięć może być wyrejestrowana dopiero po zakończeniu transmisji.

Najprostsze rozwiązanie, dynamiczna rejestracja, polega na każdorazowym rejestrowaniu pamięci przed rozpoczęciem transmisji i odrejestrowywaniu w momencie jej zakończenia. Takie podejście jest jednak bardzo nieefektywne, ponieważ operacje rejestracji i derejestracji pamięci są kosztowne.

Koszt rejestracji pamięci jest duży, ponieważ musi nastąpić przejście do trybu jądra, przypięcie stron pamięci (wymaga przejrzenia i modyfikacji tablicy stron oraz może się wiązać ze sprowadzeniem stron do pamięci fizycznej z pamięci pomocniczej), zarejestrowanie pamięci w karcie sieciowej (wymaga komunikacji z kartą) i powrót do trybu użytkownika.

Analogicznie odrejestrowanie pamięci wymaga przejścia do trybu jądra, odrejestrowania pamięci w karcie sieciowej, odpięcia stron i powrotu do trybu użytkownika.

W przypadku dynamicznej rejestracji każda operacja transmisji danych wymagałaby dwóch przejść do trybu jądra, dwukrotnego przeglądania tablicy stron i dwukrotnej komunikacji z kartą sieciową. Wprowadza to narzut, który, zwłaszcza przy przesyłaniu małych ilości danych, niweluje zysk z braku pośredniego kopiowania, bądź też nawet sprawia, że rozwiązanie to jest mniej efektywne od kopiowania danych.

Z tego powodu dynamiczna rejestracja nie nadaje się w większości zastosowań i stosuje się inne sposoby zarządzania rejestracją. Opierają się one na fakcie, że użytkownik używa przeważnie tej samej pamięci do transmisji danych.


2 Prerejestracja

Prerejestracja pamięci polega na zarejestrowaniu na stałe całego obszaru pamięci na cały czas działania aplikacji. Unika się w ten sposób zupełnie kosztu rejestracji pamięci. Użytkownik traci jednak swobodę przydziału pamięci -- musi używać do transmisji danych pamięci z prerejestrowanego obszaru. W przypadku gdy użytkownik nie ma wpływu na usytuowanie pamięci używanej do transmisji danych (ponieważ na przykład jest to schowek danych innej aplikacji), użycie prerejestracji jest niemożliwe.

Prerejestracja dużej ilości pamięci jest akceptowalna jedynie w przypadku aplikacji działającej na dedykowanej tylko dla niej maszynie, ponieważ taka aplikacja zużywa znaczącą część pamięci fizycznej (pamięć zarejestrowana jest przypinana), co w oczywisty sposób ogranicza ilość pamięci dostępnej dla pozostałych aplikacji.

Kolejna wada tego rozwiązania wiąże się z tym, że karty sieciowe posiadają ograniczenie na ilość pamięci, która może być zarejestrowana[*]. Aplikacja może prerejestrować tylko tyle pamięci, na ile pozwala ograniczenie karty sieciowej, a więc może używać tylko tyle pamięci do transmisji danych.


3 Schowek zarejestrowanej pamięci

Rozwiązaniem podobnym do dynamicznej rejestracji jest schowek zarejestrowanej pamięci. Zarejestrowany obszar pamięci nie jest jednak wyrejestrowywany od razu po zakończeniu transmisji, ale pozostaje w schowku. Kiedy przychodzi żądanie rejestracji pamięci, najpierw sprawdza się schowek. Jeśli żądany obszar pamięci pamięci nadal jest zarejestrowany, to może być od razu użyty bez potrzeby przeprowadzania ponownej rejestracji.

Zarejestrowane obszary pamięci trzymane w schowku mogą być w miarę potrzeby odrejestrowywane, co pozwala utrzymać rozmiar jednocześnie zarejestrowanej pamięci w granicach limitu karty sieciowej.


4 VIM

Przykładem implementacji schowka zarejestrowanej pamięci jest biblioteka VIM opisana w [Kilian]. Jej interfejs został zaprojektowany w celu podmiany funkcji VIA VipRegisterMem() i VipDeregisterMem(). Zamiast tych funkcji wywoływane mają być analogiczne funkcje VIM: VimRegisterMemVector() i VimDeregisterMemVector(). Funkcja VimRegisterMemVector() sprawdza, czy pamięć, którą użytkownik chce zarejestrować, jest w schowku i jeżeli tak, to obsługuje żądanie ze schowka. W przeciwnym razie rejestruje pamięć i zapamiętuje ten obszar pamięci w schowku zarejestrowanej pamięci. Funkcja VimDeregisterMemVector() zamiast odrejestrowywać pamięć, tak jak to robi VipDeregisterMem(), oznacza tylko pamięć w schowku jako zarejestrowaną, ale nieużywaną. Odrejestrowywanie pamięci w karcie sieciowej odbywa się w sposób leniwy, z jednoczesnym zapewnieniem, że schowek nigdy nie przekracza narzuconego limitu.

Wadą VIM jest to, że użytkownik po wykonaniu funkcji VimDeregisterMemVector() może zrobić z tą pamięcią to co chce, ponieważ traktuje to wywołanie analogicznie do VipDeregisterMem(). Może więc tę pamięć zwolnić[*] bez wiedzy VIM, co powoduje niespójność w schowku. Jeśli proces ponownie przydzieli sobie ten fragment pamięci fizycznej, to może zostać tam przypisana inna ramka pamięci fizycznej, a VIM będzie ją uważał za zarejestrowaną, ponieważ znajduje się w tym samym miejscu pamięci wirtualnej (por. [Calkowski], p. 4.4). Ponadto sama operacja VipDeregisterMem() posługuje się adresem wirtualnym, więc nie wiadomo, jakie byłoby jej zachowanie w takim przypadku. Wada ta jest wynikiem tego, że VIM nie posiada żadnego mechanizmu pozwalającego użytkownikowi stwierdzić kiedy może on zwolnić pamięć.

Dodatkowym obciążeniem VIM jest to, że musi trzymać drzewo wyszukiwań pozwalające mu stwierdzić, które strony pamięci są zarejestrowane, a które nie. Drzewo to musi mieć możliwość pokrycia całej pamięci procesu.

4 Semantyka transportu danych

VIA oferuje pakietową komunikację połączeniową. Dane są przesyłane pakietami o ograniczonym maksymalnym rozmiarze (specyfikacja VIA określa, że ma to być minimum 32 KB).

Standard VIA definiuje trzy poziomy niezawodności połączenia, które określają semantykę transportu danych na tym poziomie. Przy tworzeniu połączenia deklaruje się, którego poziomu chce się użyć.

  1. Zawodne wysyłanie (Unreliable Delivery). Na tym poziomie VIA zapewnia jedynie wykrycie przekłamań w pakietach oraz brak duplikacji danych. Nie ma wykrywania zgubienia pakietów ani zapewniania porządku. W przypadku wystąpienia błędu połączenie nie jest zrywane. Nadawca zostaje powiadomiony o zakończeniu transmisji w momencie, gdy dane zostaną wysłane w sieć.
  2. Niezawodne wysyłanie (Reliable Delivery). Ten poziom zapewnia niezawodne połączenie. Dane, o ile nie wystąpi błąd, są dostarczane z zapewnieniem porządku, nie są gubione ani nie mogą zostać odebrane dwa razy. Każdy błąd powoduje zerwanie połączenia. Nadawca zostaje powiadomiony o zakończeniu transmisji w momencie, gdy dane zostaną wysłane w sieć.
  3. Niezawodne odbieranie (Reliable Reception). Ten poziom zapewnia to, co niezawodne wysyłanie, z dwoma zmianami. Po pierwsze, transmisja jest zakończona dopiero wtedy, kiedy dane zostaną dostarczone do pamięci na drugim końcu połączenia. Po drugie, jeżeli w czasie transmisji nastąpi błąd, to kolejne transmisje nie zostaną odebrane. Zapewnia to ścisły porządek odbierania danych nawet w przypadku błędów transmisji.


5 Semantyka operacji transmisji danych

Transmisja danych może odbywać się za pomocą standardowego interfejsu wyślij/odbierz lub za pomocą zdalnego zapisu/odczytu pamięci (RDMA -- remote direct memory access). Operacja zdalnego zapisu do pamięci (RDMA write) polega na zapisaniu wysyłanych danych pod wskazanym adresem pamięci u odbiorcy. Operacja zdalnego odczytu (RDMA read) polega na odczytaniu wskazanego obszaru pamięci u odbiorcy i przesłaniu go do nadawcy. Operacja zdalnego odczytu jest opcjonalna w specyfikacji i przeważnie nie jest implementowana.

Specyfikacja VIA wymaga, by każda operacja transmisji danych mogła się odbywać z użyciem techniki rozrzucania/zbierania. Minimalna liczba fragmentów pamięci, która musi być obsługiwana dla jednej transmisji wynosi 252.

Z każdą transmisją danych można przesłać krótką informację dodatkową, zwaną danymi natychmiastowymi (ang. immediate data). W bibliotece VIPL dane natymiastowe mają 32 bity. Dane te są przesyłane razem z właściwymi danymi i mogą służyć do identyfikacji odebranych danych.


1 Deskryptory

Do opisu żądań transmisji danych VIA posługuje się deskryptorami (ang. descriptor). Deskryptor jest to struktura w pamięci opisująca wszystkie parametry jednego żądania transmisji. Najważniejsze pola to:

W przypadku operacji wysyłania bufor opisany w deskryptorze zawiera dane przeznaczone do wysłania.

Dla operacji odbierania, deskryptor wskazuje bufor, do którego mają zostać odebrane dane.

Deskryptory są alokowane przez użytkownika biblioteki, z tym że, muszą się znajdować w pamięci zarejestrowanej w karcie sieciowej, na której ma się odbywać transmisja.

2 Transmisja danych i kolejki deskryptorów

Do transmisji danych służą następujące funkcje:

Po wypełnieniu pól deskryptora, użytkownik wywołuje odpowiednią funkcję wysyłającą lub odbierającą. Powoduje to wstawienie deskryptora na koniec tak zwanej kolejki deskryptorów (ang. descriptor queue). Każde VI posiada dwie osobne kolejki: odbiorczą i nadawczą. Funkcja  VipPostSend() powoduje wstawienie deskryptora do kolejki nadawczej, a funkcja  VipPostRecv() do kolejki odbiorczej. Od momentu wstawienia do kolejki żądanie jest przetwarzane przez kartę sieciową. Użytkownik może sprawdzić stan żądania sprawdzając bezpośrednio odpowiednie pole deskryptora albo może posłużyć się funkcjami obsługującymi kolejki deskryptorów.

Kolejka deskryptorów jest kolejką prostą, deskryptory są obsługiwane w takiej kolejności, w jakiej zostały wstawione. W związku z tym można obsługiwać zakończenie transmisji w takiej samej kolejności. W tym celu biblioteka VIPL udostępnia dwie funkcje:

3 Kolejka zakończonych transmisji

W VIPL nie ma operacji oczekiwania na kilku kolejkach deskryptorów naraz, natomiast wprowadzono kolejkę zwaną kolejką zakończonych transmisji (ang. completion queue). Jest to specjalna kolejka deskryptorów, z którą można skojarzyć inne kolejki, zarówno nadawcze, jak i odbiorcze, z różnych VI. Zachowuje się ona w ten sposób, jakby trafiały do niej deskryptory, dla których zakończyła się transmisja, ze wszystkich kolejek deskryptorów skojarzonych z tą kolejką zakończonych transmisji.

Do obsługi kolejki zakończonych transmisji służą dwie funkcje: VipCQDone() i VipCQWait(), których działanie jest analogiczne do funkcji do obsługi nadawczych i odbiorczych kolejek deskryptorów.

Wadą używania kolejki zakończonych transmisji jest to, że skojarzenie kolejek nadawczych i odbiorczych z kolejką musi nastąpić przy tworzeniu VI oraz to, że na kolejce skojarzonej z kolejką zakończonych transmisji nie można używać funkcji przeznaczonych dla tej kolejki.

4 Asynchroniczne powiadamianie

Powyższe funkcje tworzą interfejs do synchronicznego lub opartego na przepytywaniu powiadamiania o zakończeniu transmisji. Alternatywnym sposobem przewidzianym przez VIPL jest asynchroniczne powiadamianie.

Asynchroniczne powiadamianie polega na zerejestrowaniu funkcji obsługi, która będzie wywołana, kiedy zakończy się następna transmisja na danej kolejce deskryptorów (nadawczej, odbiorczej lub kolejce zakończonych transmisji). Taka rejestracja jest jednorazowa, to znaczy że funkcja obsługi zostanie wywołana tylko dla jednego deskryptora, dla którego zakończyła się transmisja. W celu obsługi kolejnych deskryptorów należy funkcję zarejestrować jeszcze raz. Wywołanie funkcji obsługi powoduje usunięcie deskryptora z jego kolejki.

Funkcje służące do rejestrowania funkcji obsługi to VipSendNotify() dla kolejki nadawczej, VipRecvNotify() dla kolejki odbiorczej oraz VipCQNotify() dla kolejki zakończonych transmisji.

5 Przechodzenie do trybu jądra

Jedną z najważniejszych cech operacji transmisji danych w VIA jest to, że nie powodują one przejścia do trybu jądra (poza funkcjami blokującymi, które mogą zasnąć). Zarówno inicjowanie wysyłania danych, wystawienie buforu odbiorczego, jak i operacje sprawdzania kolejek deskryptorów są dokonywane w pełni z poziomu użytkownika.

6 Kontrola przepływu danych

VIA nie oferuje kontroli przepływu danych. Odebranie danych w momencie, w którym nie ma gotowego wystawionego bufora odbiorczego (tzw. queue overrun) jest traktowane jako katastrofalny błąd i powoduje zerwanie połączenia (przy połączeniach niezawodnych). Analogicznie traktowane jest odebranie danych o rozmiarze przekraczającym rozmiar przygotowanego bufora (tzw. buffer overrun).

7 Powiadamianie o błędach

Powiadamianie o błędach związanych z określoną transmisją danych odbywa się poprzez oznaczenie deskryptora odpowiednim kodem błędu i zwrócenie błędu z funkcji operujących na deskryptorach. Błędy nie związane z konkretną transmisją danych (np. zerwanie połączenia) sygnalizuje się za pomocą asynchronicznego wywołania procedury obsługi, zdefiniowanej przez użytkownia VIPL.

6 Działanie w trybie jądra

Architektura VIA i biblioteka VIPL zostały zaprojektowane z myślą o pracy w przestrzeni użytkownika (jednym z zasadniczych celów VIA jest eliminacja przejścia do trybu jądra przy transmisji danych). Wkrótce jednak okazało się, że potrzebna jest biblioteka wspierająca podobną architekturę, ale działająca w jądrze systemu tak, by umożliwić współpracę aplikacji napisanych dla VIA z komponentami znajdującymi się w jądrze systemu, na przykład w celu implementacji zdalnego systemu plików wykorzystującego VIA jako warstwę transportu danych. Przykładem takiego zastosowania jest system plików DAFS (ang. Direct Access File System), opisany w [DAFS].

Praca w jądrze, ze względu na swoją specyfikę, wymaga trochę innego, bardziej asynchronicznego modelu działania. Producenci poszczególnych kart sieciowych dla VIA na własną rękę modyfikowali interfejs VIPL w celu umożliwienia działania ich implementacji w jądrze, ale takie podejście powoduje konieczność dostosowywania aplikacji do rozwiązania danego producenta. W celu standaryzacji interfejsu VIPL w trybie jądra Intel stworzył specyfikację KVIPL (Kernel Virtual Interface Provider Library), ale nie jest ona publicznie dostępna[*].

7 Podsumowanie

Biblioteka VIPL umożliwia wydajną komunikację z poziomu użytkownika z uniknięciem pośredniego kopiowania. Możliwe jest jednoczesne obsługiwanie wielu żądań transmisji dzięki użyciu deskryptorów opisujących żądania transmisji.

Wadą VIPL jest brak kontroli przepływu danych. Musi ona być zapewniana na wyższym poziomie za pomocą odpowiedniej biblioteki, bądź musi być realizowana bezpośrednio w aplikacji korzystającej z VIPL.


3 Biblioteka ACL

1 Wprowadzenie

Biblioteka ACL ([Calkowski]) powstała w ramach projektu ISS jako wydajna biblioteka komunikacyjna dla systemów klastrowych. Została zaprojektowana z myślą o wymaganiach projektu ISS (opisanego w rozdziale 3 [Calkowski]), to znaczy przesyłaniu do klientów dużych ilości danych z pamięci masowej serwerów tworzących klaster. Do transportu danych ACL używa biblioteki VIPL.

Do głównych cech ACL należą:

  1. Umożliwienie uniknięcia pośredniego kopiowania poprzez użycie mechanizmów VIA.
  2. Implementacja mechanizmów wyższego poziomu, np. kontroli przepływu danych.
  3. Możliwość nawiązania połączenia do wielu komputerów (klastra).

2 Model systemu

Komunikacja w ACL odbywa się pomiędzy procesami klastrowymi tworzącymi wspólnie klaster. Każdy proces klastrowy działa na oddzielnej maszynie. Rozróżniane są procesy klienckie i procesy serwerowe.

Każdy proces serwerowy jest w pełni połączony z pozostałymi procesami serwerowymi klastra. Proces kliencki podłączając się do klastra, podłącza się do wszystkich procesów serwerowych. Możliwe jest najwyżej jedno połączenie pomiędzy dwoma procesami klastrowymi w klastrze. ACL rozróżnia dwa typy połączeń: klienckie -- pomiędzy procesem klienckim a serwerowym, oraz serwerowe -- pomiędzy dwoma procesami serwerowymi.

Każde połączenie składa się ze zbioru kanałów. Kanał to jednostka połączenia udostępniająca różną funkcjonalność w zależności od typu kanału.

3 Sygnalizacja zdarzeń

ACL stosuje wszystkie trzy podstawowe modele sygnalizacji zdarzeń. Niektóre operacje mogą być blokujące, na przykład operacja przydzielania bufora na komunikat (por. 3.3.8). W innych sytuacjach jest stosowane przepytywanie, na przykład przy wysyłaniu poprzez kanał Pump (por. p. 3.3.10).

Duża część zdarzeń jest sygnalizowana w sposób asynchroniczny, poprzez wywołanie funkcji obsługi zdarzenia (ang. callback). Dotyczy to zdarzeń związanych z zarządzaniem połączeniem i części zdarzeń związanych z transmisją danych. W przypadku zdarzeń związanych z transmisją danych zajmują się tym dwa typy wątków -- jeden obsługuje zdarzenia związane z odebraniem danych, drugi z wysyłaniem. Wątki te są tworzone dla każdej karty sieciowej i każdy obsługuje wszystkie VI działające na tej karcie sieciowej.

4 Zarządzanie połączeniem

W celu nawiązania połączenia proces musi utworzyć definicję połączenia, składającą się z deklaracji typów i parametrów wszystkich kanałów wchodzących w skład połączenia. Następnie poprzez wywołanie odpowiedniej funkcji proces łączy się z wszystkimi procesami serwerowymi klastra. Nie ma możliwości zmiany liczby kanałów wchodzących w skład połączenia, kiedy połączenie zostanie ustanowione.

Wystąpienie błędu w jednym kanale powoduje zerwanie całego połączenia. Zerwanie połączenia jest sygnalizowane wygenerowaniem zdarzenia zerwania połączenia.

5 Zarządzanie pamięcią

ACL zarządza pamięcią przeznaczoną do transmisji danych używając wektorów pamięci. Każdy wektor pamięci opisuje kilka rozłącznych obszarów pamięci stanowiących bufor do transmisji danych.

Wektory pamięci są używane zarówno do opisywania buforów odbiorczych, jak i nadawczych. W przypadku odbierania danych wektor pamięci opisuje obszary pamięci, do których dane zostaną zapisane za pomocą rozrzucania, przy nadawaniu zaś obszary pamięci, z których dane zostaną wysłane za pomocą zbierania.

Pamięć przeznaczona do transmisji jest rejestrowana na czas transmisji i odrejestrowywana po transmisji w sposób niewidoczny dla użytkownika. W celu uniknięcia kosztu każdorazowej rejestracji pamięci ACL umożliwia zastosowanie prerejestracji. Użytkownik wskazuje obszary pamięci, których będzie używał do transmisji danych i zostają one zarejestrowane na stałe. Jeśli pamięć przeznaczona do transmisji nie należy do prerejestrowanego obszaru, to stosuje się dynamiczną rejestrację.

Planowane było użycie schowka zarejestrowanej pamięci zaimplementowanego w bibliotece VIM (patrz p. 3.2.3). Porzucono jednak to rozwiązanie z powodu możliwości zwolnienia pamięci przez użytkownika ACL bez wiedzy VIM (por. [Calkowski], wstęp do rozdziału 4.4).

W ACL jest możliwe przesłanie wektora pamięci do innego procesu klastrowego. Umożliwia to zaimplementowanie operacji zdalnego zapisu. Wektor pamięci przesłany do innego procesu klastrowego jest nazywany zdalnym wektorem pamięci. Przed przesłaniem wektora obszary pamięci opisywane przez wektor są rejestrowane, aby możliwe było użycie ich do odebrania danych wysłanych za pomocą zdalnego zapisu.

6 Semantyka transportu danych

ACL udostępnia zarówno standardową semantykę wysyłania i odbierania danych, jak i zdalny zapis za pomocą RDMA. Wszystkie kanały w ACL zapewniają niezawodne połączenie pakietowe z zachowaniem porządku przesyłanych danych.

Niezawodność połączenia i porządek danych zapewniane są przez VIPL.

7 Semantyka transmisji danych

ACL posiada trzy typy kanałów: Exchange, Post i Pump. Każdy jest przeznaczony do innych celów i ma inną semantykę. W kolejnych punktach opisane są cechy poszczególnych kanałów.


8 Kanał Exchange

Kanał Exchange umożliwia dwa rodzaje transmisji: przesyłanie krótkich komunikatów oraz zdalny zapis za pomocą RDMA.

Oba sposoby transmisji korzystają z tego samego połączenia VI i tej samej puli buforów odbiorczych i nadawczych.

1 Komunikaty

Komunikaty pozwalają na przesłanie niewielkiej porcji danych oraz jednego wektora pamięci. Przesyłanie wektorów pamięci ma na celu umożliwienie korzystania z operacji zdalnego zapisu w kanale Exchange.

Wysłanie komunikatu wymaga wcześniejszego pobrania bufora za pomocą funkcji allocMsg(). Jeśli nie ma w danej chwili wolnego bufora, to wątek zostaje zablokowany do czasu, aż będzie dla niego wolny bufor. W wyniku wykonania funkcji allocMsg() użytkownik otrzymuje wskaźnik do bufora, do którego może zapisać swoje dane. Kiedy wypełni treść komunikatu, wykonuje funkcję sendMsg(), która powoduje wstawienie komunikatu do kolejki komunikatów do wysłania.

Komunikaty są przesyłane z zachowaniem kontroli przepływu danych. Komunikat czeka w kolejce komunikatów gotowych do wysłania dopóty, dopóki po drugiej stronie nie będzie gotowego bufora do odebrania komunikatu.

Liczba buforów nadawczych i odbiorczych jest stała, określona przy tworzeniu kanału.

W kanale Exchange porządek wysyłania komunikatów jest zachowany, natomiast nie ma gwarancji zachowania porządku pomiędzy operacjami zdalnego zapisu a wysyłania komunikatów na tym samym kanale.

Komunikaty są odbierane sekwencyjnie -- wynika to z użycia jednego wątku do generowania zdarzeń odebrania danych.

2 Zdalny zapis

Kanał Exchange posiada dwie funkcje służące do przeprowadzania zdalnego zapisu: sendData() i depositData(). Obie wymagają podania zdalnego wektora pamięci wskazującego gdzie mają być przesłane dane i wektora pamięci opisującego bufor, z którego wysyła się dane.

Funkcja depositData() umieszcza dane w pamięci odbiorcy w sposób dla niego niewidoczny za pomocą zdalnego zapisu RDMA.

Użycie funkcji sendData() powoduje, że użytkownik zostanie powiadomiony o odebraniu danych poprzez wygenerowanie zdarzenia odebrania danych. Z właściwymi danymi przesyła się również liczbę identyfikującą dane (zwaną ciasteczkiem). Ciasteczko jest przekazywane użytkownikowi przy odbieraniu danych i może zawierać dodatkowe informacje o otrzymanych danych. Odebranie danych wysłanych za pomocą sendData() wymaga zużycia jednego deskryptora i związanego z nim bufora odbiorczego, pomimo że ten bufor nie jest do niczego potrzebny i mógłby być użyty do odebrania komunikatu.

Kontrola przepływu danych jest zapewniona poprzez fakt, że do wysłania danych trzeba użyć zdalnego wektora pamięci, który musiał nam zostać przesłany.

Operacje zdalnego zapisu umożliwiają zastosowanie techniki rozrzucania/zbierania. W przypadku rozrzucania jest to zrealizowane poprzez wykonanie kilku zapisów RDMA, ponieważ VIA nie umożliwia rozrzucania przy zapisie RDMA.

Porządek operacji zdalnego zapisu jest zachowany, natomiast nie musi być zachowany porządek pomiędzy wysyłaniem komunikatów a operacjami zdalnego zapisu. Analogicznie jak przy odbieraniu komunikatów, zdarzenia odebrania danych są generowane sekwencyjnie przez wątek obsługi zdarzeń.

9 Kanał Post

Kanał Post służy do przesyłania danych bez kontroli przepływu danych. Udostępnia tylko dwie operacje: postReceiveBuffer(), która wystawia bufor odbiorczy oraz sendData(), wysyłającą dane. Użytkownik musi zapewnić, że w momencie wysłania danych czeka na nie przygotowany bufor odbiorczy o odpowiedniej wielkości. Jeśli bufora odbiorczego nie będzie, to połączenie zostaje zerwane.

Do wysyłania danych można użyć techniki zbierania, a do odbioru techniki rozrzucania danych.


10 Kanał Pump

Kanał Pump to kanał Post rozszerzony o prostą kontrolę przepływu danych. Możliwe jest warunkowe wysłanie danych -- operacja wysłania powiedzie się tylko wtedy, kiedy po drugiej stronie jest przygotowany bufor odbiorczy. Sprawdzenie to odbywa się w sposób nieblokujący.

11 Podsumowanie

Biblioteka ACL jest przykładem biblioteki wysokiego poziomu, która w umiejętny sposób łączy wysoką wydajność, możliwą dzięki uniknięciu pośredniego kopiowania, z wygodą użytkowania, będącą wynikiem zaimplementowania mechanizmów wysokiego poziomu. Różne rodzaje kanałów pozwalają łatwo zaimplementować różne rodzaje komunikacji. ACL zajmuje się kontrolą przepływu danych i ukrywa przed użytkownikiem szczegóły dotyczące rejestracji pamięci. Dzięku temu używanie ACL jest znacznie wygodniejsze od bezpośredniego korzystania z VIPL.

Krzysztof Lichota 2002-06-24