Do spisu tresci tematu 9

9.1.8 Przesylanie danych




Spis tresci


Wprowadzenie

Istnieje 5 rodzajow funkcji systemowych do przesylania danych przez gniazda. Niektore z nich (read()/write() i readv()/writev()) moga operowac takze na deskryptorach plikow innych rodzajow, inne sa specyficzne tylko dla gniazd. Roznice pomiedzy poszczegolnymi funkcjami dotycza jedynie mozliwosci podawania dodatkowych argumentow - wieksza elastycznosc uzycia wiaze sie z koniecznoscia wklepania wiekszej ilosci kodu, co widocznie jest na tyle duzym utrudnieniem, iz musialo powstac 5 roznych interfejsow do jednej funkcji. Rzeczywiste algorytmy przesylania danych zawarte sa w kodach poszczegolnych protokolow, co czyni je czasami bardzo zlozonymi, jak w przypadku protokolu TCP. Mamy tu jednak do czynienia z algorytmami pochodzacymi z konkretnego protokolu przesylania danych, a nie specyficznymi dla jadra Linuxa. Dlatego tez na tej stronie zostanie omowione szczegolowo jedynie przesylanie danych w rodzinie protokolow Unixa; pobieznie przedstawiona bedzie rowniez rodzina protokolow Internetu (TCP, UDP, RAW, PACKET), pomieniete zostana pozostale protokoly, jako rzadziej uzywane na maszynach Linuxowych.

Interfejs programisty do funkcji recvmsg()/sendmsg()

W i-wezle przechowywanym w pamieci dla kazdego otwartego pliku znajduje sie unia u, zawierajaca informacje specyficzne dla systemu plikow, lub tez strukture socket, jesli deskryptor pliku zwiazany jest z gniazdem. W owej strukturze pole ops wskazuje na strukture typu proto_ops, definiujaca operacje na gniezdzie specyficzne dla rodziny gniazda. W szczegolnosci, znajduja sie tam 2 interesujace nas wskazniki do funkcji przesylajacych dane:
int (*sendmsg) (struct socket *sock, struct msghdr *m, int total_len, int nonblock, int flags);
int (*recvmsg) (struct socket *sock, struct msghdr *m, int total_len, int nonblock, int flags, int *addr_len);
Dzialanie wszystkich funkcji gniazd przesylajacych dane sprowadza sie do wywolania jednej z wyzej wymienionych funkcji po przyjeciu domyslnych wartosci dla tych argumentow, ktorych nie dostarczyl dany interfejs. Ponizsza tabelka przedstawia interesujace nas funkcje systemowe i roznice w mozliwosciach przekazywania opcji do nich:

wiele buforowdodatkowe znacznikiadres partnerainformacje kontrolne
read/write
readv/writev+
send/recv +
sendto/recvfrom ++
sendmsg/recvmsg++++

W Linuxie przyjeto konwencje, na mocy ktorej kod implementujacy funkcje systemowa f() jest zawarty w funkcji sys_f(). Ponizej przedstawione sa definicje i opis implementacji poszczegolnych funkcji, z tym, ze pominiety jest opis znaczenia parametrow i wykaz zwracanych bledow, poniewaz bylby to jedynie skrot informacji z podrecznikow systemowych (ang. man pages), ktore zostaly stworzone specjalnie po to, aby szczegolowo opisac interfejs programisty do funkcji systemowych (tzw. API).

DEFINICJA: ssize_t read(int fd, void *buf, size_t count)
           ssize_t write(int fd, const void *buf, size_t count)
    WYNIK: liczba przeslanych bajtow lub -1 (blad)
Funkcje systemowe sys_read() i sys_write(), zdefiniowane w pliku fs/fs.h, dokonuja podstawowego sprawdzenia poprawnosci przekazanych argumentow, czyli np. istnienia otwartego pliku o podanym deskryptorze, praw odczytu lub zapisu do pliku ustalonych przy jego otwarciu, dostepnosci dla danego uzytkownika zadanego obszaru pamieci, po czym wywoluja funkcje wlasciwe dla danego pliku. Sa one zdefiniowane w strukturze typu file_operations, dostepnej ze struktury file. Struktura socket_file_ops wskazuje m. in. na funkcje sock_read() i sock_write() (plik net/socket.c). Te funkcje sprawdzaja nie wiadomo po co po raz kolejny dostepnosc obszaru pamieci oraz flage SO_ACCEPTCON, ktorej zadne inne funkcje przesylania nie sprawdzaja (albo jest to niepotrzebne, albo w innych funkcjach jest blad, choc byc moze nie jest to blad, ktory moglby spowodowac awarie systemu); wypelniaja struktury typu msghdr i iovec (brak nazwy partnera, brak wiadomosci kontrolnych, jeden bufor I/O), a na koniec wolaja funkcje sendmsg()/recvmsg() z rodziny (z wyzerowanymi flagami).
DEFINICJA: int readv(int fd, const struct iovec *vector, size_t count);
           int writev(int fd, const struct iovec *vector, size_t count);
    WYNIK: ilosc przeslanych bajtow lub -1 (blad)
Funkcje sys_readv() i sys_writev() (plik fs/read_write.c) sprawdzaja poprawnosc deskryptora pliku oraz tryb otwarcia i wywoluja do_readv_writev(), ktora z kolei weryfikuje dostepnosc dla uzytkownika obszarow pamieci (wektor, bufory danych), kopiuje wektor I/O do pamieci jadra, oblicza calkowita dlugosc przesylanych danych i wywoluje (w przypadku gniazd) sock_readv_writev(). Ta funkcja wypelnia strukture typu msghdr, ustawiajac brak adresu partnera, brak wiadomosci kontrolnych oraz wstawiajac adres i dlugosc wektora I/O, po czym wywoluje funkcje sendmsg()/recvmsg() z rodziny (z wyzerowanymi flagami).
DEFINICJA: int recv(int s, void *buf, int len, unsigned int flags);
           int recvfrom(int s, void *buf, int len, unsigned int flags,
              struct sockaddr *from, int *fromlen);
           int recvmsg(int s, struct msghdr *msg, unsigned int flags);
           int send(int s, const void *msg, int len, unsigned int flags);
           int sendto(int s, const void *msg, int len, unsigned int flags,
              struct sockaddr *to, int *tolen);
           int recvmsg(int s, const struct msghdr *msg, unsigned int flags);
    WYNIK: ilosc przeslanych bajtow lub -1 (blad)
Funkcje sys_send(), sys_recv(), sys_sendto(), sys_recvfrom(), sys_sendmsg() i sys_recvmsg() z pliku net/socket.c sprawdzaja poprawnosc argumentow i wolaja funkcje sendmsg()/recvmsg() z rodziny, przy czym:

Funkcje sendmsg() i recvmsg() rodziny protokolow


Rodzina protokolow Unixa

Funkcje unix_sendmsg() i unix_recvmsg() sa zaimplementowane w pliku net/unix/af_unix.c i niestety przekraczaja dopuszczalna dlugosc, podana w dokumencie CodingStyle w rozdziale o funkcjach. Najprosciej bedzie je przedstawic w pseudo-kodzie (zwykly opis bylby dosc trudny do zrozumienia).
algorytm: unix_recvmsg
wejscie i wyjscie: takie jak w deklaracji funkcji rodziny recvmsg()
{
	if (zadanie danych pilnych (OOB))
		zwroc blad EOPNOTSUPP;
	if (jest niezwrocony blad z poprzedniej operacji)
		zwroc go;
	if (zadanie danych kontrolnych (deskryptory plikow))
		skopiuj ich bufor przestrzeni jadra; /* patrz tez: Uwaga */
	zablokuj gniazdo przy pomocy semafora;
	while (jest jeszcze niezapelniony bufor pamieci) {
		przejdz do kolejnego bufora;
		while (biezacy bufor nie jest jeszcze w calosci zapelniony) {
			if (juz cos przeslano, a flaga MSG_PEEK ustawiona)
				goto wyjscie;
			if (spelniono juz cale zadanie odczytu)
				goto wyjscie;
			/* powyzszy warunek jest wg mnie absurdalny */
			zdejmij wiadomosc z kolejki otrzymanych;
			if (nie bylo czekajacej wiadomosci) {
				odblokuj gniazdo;
				if (gniazdo zamkniete dla czytania)
					zwroc to co dotad przeslano;
				if (juz cos przeslano)
					zwroc to co dotad przeslano;
				if (operacja nieblokujaca)
					zwroc blad EAGAIN;
				czekaj na dane;
				/* oznacza to spanie na kolejce sleep (jest ona
				 * w strukturze typu sock); zwolnienia dokonuje
				 * funkcja def_callback2(), wskazywana przez
				 * wskaznik data_ready, wywolywana przy zapisie
				 */
				if (obudzil nas sygnal)
					zwroc blad ERESTARTSYS;
				zablokuj gniazdo;
				continue;
			}
			if (uzytkownik potrzebuje nazwy zdalnego gniazda)
				skopiuj ja do naglowka wiadomosci;
			skopiuj do bufora uzytkownika wiadomosc (lub te jej czesc,
			   ktora potrzebowal) i zwieksz odpowiednie wskazniki;
			if (do wiadomosci sa dolaczone deskryptory plikow)
				skopiuj deskryptory do bufora lub zwolnij je, o ile
				   uzytkownik ich nie zadal;
			if (nie bylo flagi MSG_PEEK)
				obetnij przeczytany fragment wiadomosci;
			if (wiadomosc nie jest cala przeczytana) {
				zwroc ja do kolejki;
				continue;
			}
			zwolnij pamiec jadra zajeta przez wiadomosc;
			if (gniazdo datagramowe lub uzytkownik zadal danych kontr.)
				goto wyjscie;
		}
	}
wyjscie:
	odblokuj gniazdo;
	jesli byly zadane dane kontrolne, skopiuj je do przestrzeni uzytkownika;
	zwroc ilosc skopiowanych danych;
}
Uwaga: na deskryptory plikow przekazywane przy pomocy gniazd alokowana jest pamiec jadra, ktora w pewnych przypadkach (np. odczyt nieblokujacy) nie jest zwalniana. Mozliwe konsekwencje tego faktu pokazuje program przykladowy.
algorytm: unix_sendmsg
wejscie i wyjscie: takie jak w deklaracji funkcji rodziny sendmsg()
{
	if (jest niezwrocony blad z poprzedniej operacji)
		zwroc go;
	if (zadanie danych pilnych (OOB))
		zwroc blad EOPNOTSUPP;
	if (jakies flagi sa ustawione)
		zwroc blad EINVAL; /* w przyszlych wersjach to sie zmieni */
	if (gniazdo zamkniete dla pisania) {
		wyslij sygnal SIGPIPE;
		zwroc blad EPIPE;
	}
	if (podano zdalny adres, a to jest gniazdo strumieniowe)
		zwroc blad EISCONN lub EOPNOTSUPP;
	if (nie podano zdalnego adresu, a gniazdo nie jest polaczone)
		zwroc blad ENOTCONN;
	if (dolaczona wiadomosc kontrolna) {
		skopiuj ja do pamieci jadra;
		if (blednie zapodana)
			zwroc blad EINVAL;
		skopiuj desktyptory do nowej tablicy, zamieniajac je na wskazniki 
		   do plikow;
	}
	while (nie wyslano jeszcze wszystkiego) {
		if (wiadomosc do wyslania przekracza polowe rozmiaru bufora sndbuf) {
			if (gniazdo datagramowe)
				zwroc blad EMSGSIZE;
			ustaw rozmiar do wyslania jako polowe sndbuf;
		}
		zaalokuj pamiec na bufor - moze to byc wywolanie blokujace, jesli brakuje
		   w systemie pamieci (moze zwracac tez bledy: EPIPE, EAGAIN, ERESTARTSYS);
		ustaw rozmiar wiadomosci do wyslania na rozmiar otrzymanego bufora (w
		   przypadku gniazd strumieniowych moglismy otrzymac mniejszy bufor, niz
		   zadalismy)
		dolacz do wiadomosci przekazywane deskryptory plikow (jesli sa);
		skopiuj z buforow uzytkownika tresc wiadomosci;
		if (nie podano nazwy zdalnego gniazda) {
			if (gniazdo, z ktorym jestesmy polaczeni, jest w stanie usuwania) {
				if (gniazdo datagramowe) {
					rozlacz sie;
					zwroc blad ECONNRESET lub ilosc juz przeslanych
					   danych (jesli takie byly);
				}
				if (gniazdo strumieniowe)
					zwroc blad EPIPE i wyslij sygnal SIGPIPE lub zwroc
					   ilosc juz przeslanych danych;
			}
		} else
			if (gniazda zdalnego nie ma)
				zwroc blad ECONNREFUSED lub blad zwracany przez
				   open_namei() lub zwroc ilosc juz przeslanych danych;
		wstaw wiadomosc na koniec kolejki odbiorczej gniazda zdalnego;
		obudz procesy czekajace na tym gniezdzie, wywolujac jego funkcje
		   data_ready();
		uaktualnij ilosc juz przeslanych danych;
	}
	zwroc ilosc przeslanych danych;
}

Rodzina protokolow Internetu

Funkcje inet_sendmsg() i inet_recvmsg() (plik net/ipv4/af_inet.c) korzystaja ze struktury typu sock (wskazywanej przez strukture typu socket w polu data), aby dostac sie do struktury typu proto okreslajacej m. in. funkcje sendmsg()/recvmsg() dla danego protokolu (TCP, UDP, RAW, PACKET). Na poziomie rodziny jest wykonywane jedynie: W strukturze typu proto jest dla kazdego protokolu zdefiniowana funkcja rcv() (np. tcp_rcv(), udp_rcv()), zajmujaca sie odbieraniem przychodzacych (oczywiscie asynchronicznie) pakietow - funkcje recvmsg() protokolow musza pozniej odczytac otrzymane dane z buforow, a nie z urzadzen wejscia/wyjscia.

Funkcje przesylania danych na poziomie protokolow sa dosc zlozone, zaleznie od konkretnego protokolu. Zawieraja w sobie zarowno logike protokolu (np. TCP - prawie 150 KB kodu), jak i kod wejscia/wyjscia na nizszym poziomie. Kod przesylania danych przez gniazda Unixa powinien wystarczyc do zrozumienia zagadnienia takze dla innych protokolow (oczywiscie bez tych aspektow, ktore sa specyficzne dla konkretnych protokolow), dlatego w tym punkcie jedynie wymienie protokoly Internetu wraz z krotkim opisem. Troche informacji na temat kodu protokolow IP, TCP i UDP mozna znalezc w temacie nr 8.

PACKET

Jest to protokol datagramowy wystepujacy tylko w Linuxie, pozwalajacy wysylac i odbierac komunikaty na poziomie urzadzen sieciowych, jednoczesnie najprostszy z protokolow rodziny Internetu. Wysylanie danych nastepuje poprzez funkcje dev_queue_xmit() wywolywana z packet_sendmsg(), a odbieranie - poprzez skb_recv_datagram() wywolywana z packet_recvmsg().

RAW

Jest to protokol datagramowy pozwalajacy na dostep do gniazdek "surowych", czyli tylko nieco bardziej zlozony niz PACKET, za to przenosny pomiedzy roznymi wersjami Unixa. Dane wysyla poprzez ip_build_xmit() (wywolywana z raw_sendto(), dosc zlozona, na najnizszym poziomie wywolujaca dev_queue_xmit()), a odbiera - wywolujac tak jak poprzednio skb_recv_datagram() .

UDP

Protokol datagramowy, o wiekszym poziomie kontroli i mniejszych mozliwosciach niz poprzednie, za to dostepny dla dowolnych uzytkownikow (PACKET i RAW sa dostepne tylko dla administratora). Do transmisji danych wykorzystuje te same funkcje co protokol RAW (patrz: udp_recvmsg(), udp_send()), ale opakowane w wieksza ilosc kodu.

TCP

Protokol strumieniowy, niezawodny, o bardzo zlozonych mechanizmach kontroli i o duzych mozliwosciach (RFC 793 i wiele innych). Kod przesylania danych zawarty jest w plikach: tcp.c, tcp_input.c, tcp_output.c.

Inne rodziny protokolow

Schemat przesylania danych w Linuxie umozliwia latwe implementowanie dowolnych protokolow sieciowych, dzieki wstawieniu wskaznikow na operacje specyficzne dla protokolow do struktury typu proto_ops. Zaimplementowane sa juz np. protokoly appletalk, ax25, decnet, ipx (Novell); w przyszlosci bedzie dodany kod dla wersji 6 protokolu IP.

Bibliografia

  1. Kod zrodlowy jadra Linuxa (glownie pliki z katalogu net/)

  2. W. Richard Stevens "Programowanie zastosowan sieciowych w systemie Unix", WNT, Warszawa 1995, 1996
  3. Vic Metcalfe, Andrew Gierth i inni "Programming Unix Sockets in C - Frequently Asked Questions", http://www.auroraonline.com/sock-faq


Autor: Dariusz Grzegorski