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.
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.
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.
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.
Typ gniazda określa semantykę transportu danych. W bibliotece gniazd określone są następujące typy gniazd:
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().
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.
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.
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.
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.
Odbieranie danych bez pośredniego kopiowania napotyka dużo poważniejsze problemy.
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.
Wszystkie funkcje biblioteki gniazd są funkcjami systemu operacyjnego i w związku z tym wymagają przejścia do trybu jądra.
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.
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].
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:
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:
Zarejestrowania pamięć może być również, poprzez ustawienie odpowiedniej flagi przy rejestracji, zabezpieczona przed zdalnym zapisaniem (por. p. 3.2.5).
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.
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.
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.
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.
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ć.
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.
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:
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.
Do transmisji danych służą następujące funkcje:
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:
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.
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.
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.
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).
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.
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.
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.
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żą:
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.
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.
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.
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.
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.
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.
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.
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.
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ń.
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.
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.
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