SOP lab nr 8

From Studia Informatyczne

Obsługa łączy komunikacyjnych

Czas realizacji zajęć: 180 min.

Zakres materiału, jaki zostanie zrealizowany podczas zajęć: Potoki nienazwane, potoki nazwane, przykłady przetwarzania potokowego, implementacja przykładowych programów obsługi potoków


Spis treści


Łącza komunikacyjne

Łącza w systemie UNIX są plikami specjalnymi, służącymi do komunikacji pomiędzy procesami. Łącza mają kilka cech typowych dla plików zwykłych, czyli posiadają swój i-węzeł, posiadają bloki z danymi (chociaż ograniczoną ich liczbę), na otwartych łączach można wykonywać operacje zapisu i odczytu. Łącza od plików zwykłych odróżniają następujące cechy:

  • ograniczona liczba bloków — łącza mają rozmiar 4KB - 8KB w zależności od konkretnego systemu,
  • dostęp sekwencyjny — na łączach można wykonywać tylko operacje zapisu i odczytu, nie można natomiast przemieszczać wskaźnika bieżącej pozycji (nie można wykonywać funkcji lseek),
  • sposób wykonywania operacji zapisu i odczytu — dane odczytywane z łącza są zarazem usuwane (nie można ich odczytać ponownie), proces jest blokowany w funkcji read na pustym łączu i w funkcji write, jeśli w łączu nie ma wystarczającej ilości wolnego miejsca, żeby zmieścić zapisywany blok (wyjątkiem od tej zasady jest przypadek gdy jest ustawiona flaga O_NDELAY)

W systemie UNIX wyróżnia się dwa rodzaje łączy: łącza nazwane i łącza nienazwane. Zwyczajowo przyjęło się określać łącza nazwane terminem kolejki FIFO, a łącza nienazwane terminem potoki. Różnica pomiędzy łączem nazwanym i nienazwanym polega na tym, że pierwsze z nich ma dowiązanie w systemie plików (czyli istnieje jako plik w jakimś katalogu) i może być identyfikowane przez nazwę a drugie nie ma dowiązania i istnieje tak długo, jak długo jest otwarte. Po zamknięciu wszystkich deskryptorów łącze nienazwane przestaje istnieć i zwalniany jest jego i-węzeł oraz wszystkie bloki. Łącze nazwane natomiast po zamknięciu wszystkich deskryptorów w dalszym ciągu ma przydzielony i-węzeł, zwalniane są tylko bloki dyskowe. Jeżeli dwa procesy mają odpowiednie deskryptory łącza, to dla komunikacji między nimi nie ma znaczenia, czy są to deskryptory łącza nazwanego czy nienazwanego. Różnica jest natomiast w sposobie uzyskania deskryptorów łącza, która wynika z różnic w tworzeniu i otwieraniu łączy.

Ponieważ łącze nienazwane nie ma dowiązania w systemie plików, nie można go identyfikować przez nazwę. Jeśli procesy chcą się komunikować za pomocą takiego łącza, muszą znać jego deskryptory. Oznacza to, że procesy muszą uzyskać deskryptory tego samego łącza, nie znając jego nazwy. Jedynym sposobem przekazania informacji o łączu nienazwanym jest przekazanie jego deskryptorów procesom potomnym dzięki dziedziczeniu tablicy otwartych plików od swojego procesu macierzystego. Za pomocą łącza nienazwanego mogą się zatem komunikować procesy, z których jeden otworzył łącze nienazwane, a następnie utworzył pozostałe komunikujące się procesy, które w ten sposób otrzymają w tablicy otwartych plików deskryptory istniejącego łącza.

Operacje zapisu i odczytu na łączu nazwanym wykonuje się tak samo, jak na łączu nienazwanym, inaczej natomiast się je tworzy i otwiera. Łącze nazwane tworzy się poprzez wywołanie funkcji mkfifo w programie procesu lub przez wydanie polecenia mkfifo na terminalu. Funkcja mkfifo tworzy plik specjalny typu łącze podobnie, jak funkcja creat tworzy plik zwykły. Funkcja mkfifo nie otwiera jednak łącza i tym samym nie przydziela deskryptorów. Łącze nazwane otwierane jest funkcją open podobnie jak plik zwykły, przy czym łącze musi zostać otwarte jednocześnie w trybie do zapisu i do odczytu przez dwa różne procesy. W przypadku wywołania funkcji open tylko w jednym z tych trybów proces zostanie zablokowany aż do momentu, gdy inny proces nie wywoła funkcji open w trybie komplementarnym.

Funkcje operujące na łączach nienazwanych zdefiniowane są w pliku unistd.h, natomiast funkcje używane w celu tworzenia łączy nazwanych zdefiniowane są w plikach sys/types.h oraz sys/stat.h.


Funkcje systemowe służące do tworzenia i komunikacji poprzez łącza nienazwane.

int pipe ( int pdesk[2] )

Wartości zwracane:

  • poprawne wykonanie funkcji: 0
  • zakończenie błędne: -1

Możliwe kody błędów (errno) w przypadku błędnego zakończenie funkcji:

  • EMFILE - w procesie używanych jest zbyt wiele deskryptorów pliku
  • ENFILE - tablica plików systemu jest pełna
  • EFAULT - deskryptor pdesk jest nieprawidłowy

Argumenty funkcji:

  • pdesk[0]- deskryptor potoku do odczytu
  • pdesk[1]- deskryptor potoku do zapisu

UWAGI:

Funkcja tworzy parę sprzężonych deskryptorów pliku, wskazujących na inode potoku i umieszcza je w tablicy pdesk. Komunikacja przez łącze wymaga aby dwa procesy znały deskryptory tego samego łącza. Zatem proces, który utworzył potok może się przez niego komunikować tylko ze swoimi potomkami (niekoniecznie bezpośrednimi)lub przekazać im odpowiednie deskryptory, umożliwiając w ten sposób wzajemną komunikację. Dwa procesy z kolei mogą komunikować się przez potok wówczas, gdy mają wspólnego przodka (lub jeden z nich jest przodkiem drugiego), który utworzył potoką następnie odpowiednie procesy potomne, przekazując im w ten sposób deskryptory potoku. Jest to pewnym ograniczeniem zastosowania potoków.

Wielkość potoku zależy od konkretnej implementacji, faktyczną maksymalną liczbę bajtów można uzyskać przez funkcję fpathconf: fpathconf(pdesk[0], _PC_PIPE_BUF).

Gdy deskryptor pliku reprezentujący jeden koniec potoku zostaje zamknięty

  • Dla deskryptora zapisu :
    • jeśli istnieją inne procesy mające potok otwarty do zapisu nie dzieje się nic
    • gdy nie ma więcej procesów a potok jest pusty, procesy, które czekały na odczyt z potoku zostają obudzone a ich funkcje read zwrócą 0 ( wygląda to tak jak osiągnięcie końca pliku)
  • Dla deskryptora odczytu :
    • jeśli istnieją inne procesy mające potok otwarty do odczytu nie dzieje się nic
    • gdy żaden proces nie czyta, do wszystkich procesów czekających na zapis zostaje wysłany sygnał SIGPIPE.


int read(inf fd, void *buf, size_t count)

Wartości zwracane:

  • poprawne wykonanie funkcji: rzeczywista liczba bajtów, jaką udało się odczytać
  • zakończenie błędne: -1

Argumenty funkcji:

  • fd - deskryptor potoku z którego mają zostać odczytane dane
  • buf - adres bufora znajdującego się w segmencie danych procesu, do którego zostaną przekazane dane odczytane z potoku w wyniku wywołania funkcji read
  • count - ilość bajtów do odczytania

UWAGI:

Jeśli wszystkie deskryptory do zapisu są zamknięte i łącze jest puste, to zostaje zwrócona wartość 0.

Różnica w działaniu funkcji read na łączu i na pliku zwykłym polega na tym, że dane odczytane z łącza są z niego zarazem usuwane, wobec czego mogą być one odczytane tylko przez jeden proces i tylko jeden raz, podczas gdy z pliku można je odczytywać wielokrotnie.

W przypadku pliku funkcja read zwróci 0 (co oznacza dojście do końca pliku), wówczas, gdy zostaną odczytane wszystkie dane. Po odczytaniu wszystkich danych z łącza, czyli przy próbie odczytu z pustego łącza proces będzie blokowany w funkcji read (potencjalnie w potoku mogą pojawić się jakieś dane a 0 zostanie zwrócone przez funkcję read dopiero wówczas, gdy zamknięte zostaną wszystkie deskryptory do zapisu.

Odczytanie mniejszej liczby bajtów z pliku, niż rozmiar bufora przekazany jako trzeci parametr oznacza dojście od końca pliku. Jeżeli w łączu jest mniej danych, niż rozmiar bloku, który ma zostać odczytany, funkcja systemowa read zwróci wszystkie dane z łącza. Będzie to oczywiście liczba mniejsza, niż rozmiar bufora, przekazany jako trzeci parametr funkcji read, co nie oznacza jednak zakończenia komunikacji przez łącze.


int write(inf fd, void *buf, size_t count)

Wartości zwracane:

  • poprawne wykonanie funkcji: rzeczywista liczba bajtów, jaką udało się zapisać
  • zakończenie błędne: -1

Argumenty funkcji:

  • fd - deskryptor potoku do którego mają zostać zapisane dane
  • buf - adres bufora znajdującego się w segmencie danych procesu, z którego zostaną pobrane dane zapisane przez funkcję write
  • count - ilość bajtów do zapisania

UWAGI:

Funkcja zapisuje w potoku count bajtów w całości (nie przeplatają się one z danymi pochodzącymi z innych zapisów) Różnica w działaniu funkcji write na łączu i na pliku zwykłym polega na tym, że jeżeli nie jest możliwe zapisanie bloku danych ze względu na brak miejsca w łączu, proces blokowany jest w funkcji write tak długo aż pojawi się odpowiednia ilość wolnego miejsca, tzn. aż inny proces odczyta i tym samym usunie dane z łącza


Sposób korzystania z łącza nienazwanego

Schemat komunikacji przez łącze nienazwane wygląda następująco:


Rysunek 2.Schemat komunikacji poprzez łącze nienazwane
Rysunek 2.Schemat komunikacji poprzez łącze nienazwane


Listing 1 pokazuje przykładowe użycie łącza do przekazania napisu (ciągu znaków) Hallo! z procesu potomnego do macierzystego.


    main() {
	int pdesk[2];
3
	if (pipe(pdesk) == -1){
		perror("Tworzenie potoku");
6	exit(1);
	}

9	switch(fork()){
	  case -1: // blad w tworzeniu procesu
		perror("Tworzenie procesu");
12		exit(1);
	  case 0: // proces potomny
		if (write(pdesk[1], "Hallo!", 7) == -1){
15			perror("Zapis do potoku");
			exit(1);
		}
18 		exit(0);
	  default: { // proces macierzysty
		char buf[10];
21		if (read(pdesk[0], buf, 10) == -1){
			perror("Odczyt z potoku");
			exit(1);
24		}
		printf("Odczytano z potoku: %s\n", buf);
	  }
27	}
   } 

Listing 1:Przykład użycia łącza nienazwanego w komunikacji przodek-potomek


Opis programu: Do utworzenia i zarazem otwarcia łącza nienazwanego służy funkcja systemowa pipe, wywołana przez proces macierzysty (linia 4). Następnie tworzony jest proces potomny przez wywołanie funkcji systemowej fork w linii 9, który dziedziczy tablicę otwartych plików swojego przodka. Warto zwrócić uwagę na sposób sprawdzania poprawności wykonania funkcji systemowych zwłaszcza w przypadku funkcji fork, która kończy się w dwóch procesach — macierzystym i potomnym. Proces potomny wykonuje program zawarty w liniach 14-19 i zapisuje do potoku ciąg 7 bajtów spod adresu początkowego napisu Hallo!. Zapis tego ciągu polega na wywołaniu funkcji systemowej write na odpowiednim deskryptorze, podobnie jak w przypadku pliku zwykłego. Proces macierzysty (linie 20-25) próbuje za pomocą funkcji read na odpowiednim deskryptorze odczytać ciąg 10 bajtów i umieścić go w buforze wskazywanym przez buf (linia 21). buf jest adresem początkowym tablicy znaków, zadeklarowanej w linii 20. Odczytany ciąg znaków może być krótszy, niż to wynika z rozmiaru bufora i wartości trzeciego parametru funkcji read (odczytane zostanie mniej niż 10 bajtów). Zawartość bufora, odczytana z potoku, wraz z odpowiednim napisem zostanie przekazana na standardowe wyjście.


Listing 2 zawiera zmodyfikowaną wersję przykładu przedstawionego na listingu 1. W poniższym przykładzie zakłada się, że wszystkie funkcje systemowe wykonują się poprawnie, w związku z czym w kodzie programu nie ma reakcji na błędy.


 main() {
   int pdesk[2];
3
	pipe(pdesk);

6	if (fork() == 0){ // proces potomny
		write(pdesk[1], "Hallo!", 7);
		exit(0);
9	}
	else { // proces macierzysty
		char buf[10];
12		read(pdesk[0], buf, 10);
		read(pdesk[0], buf, 10);
		printf("Odczytano z potoku: %s\n", buf);
15	}
   }

Listing 2: Przykład odczytu z pustego łącza


Opis programu: Podobnie, jak w przykładzie na listingu 1, proces potomny przekazuje macierzystemu przez potok ciąg znaków Hallo!, ale proces macierzysty próbuje wykonać dwa razy odczyt zawartości tego potoku. Pierwszy odczyt (linia 12) będzie miał taki sam skutek jak w poprzednim przykładzie. Drugi odczyt (linia 13) spowoduje zawieszenie procesu, gdyż potok jest pusty, a proces macierzysty ma otwarty deskryptor do zapisu.


Listing 3 pokazuje sposób przejęcia wyniku wykonania standardowego programu systemu UNIX (w tym przypadku ls) w celu wykonania określonych działań (w tym przypadku konwersji małych liter na duże). Przejęcie argumentów z linii poleceń umożliwia przekazanie ich do programu wykonywanego przez proces potomny.


 #define MAX 512

3  main(int argc, char* argv[]) {
	int pdesk[2]; 

6	if (pipe(pdesk) == -1){
		perror("Tworzenie potoku");
		exit(1);
9	}

	switch(fork()){
12	case -1: // blad w tworzeniu procesu
		perror("Tworzenie procesu");
		exit(1);
15	case 0: // proces potomny
		dup2(pdesk[1], 1);
		execvp("ls", argv);
18 		perror("Uruchomienie programułs");
		exit(1);
	default: { // proces macierzysty
21		char buf[MAX];
		int lb, i; 
  
24		close(pdesk[1]);
		while ((lb=read(pdesk[0], buf, MAX)) > 0){
		  for(i=0; i<lb; i++)
27			buf[i] = toupper(buf[i]);
			if (write(1, buf,łb) == -1){
				perror ("Zapis na standardowe wyjscie");
30				exit(1);
			}
		  }
33		  if (lb == -1){
			perror("Odczyt z potoku");
			exit(1);
36		}
	  }
	}
39  } 

Listing 3: Konwersja wyniku polecenia ls


Opis programu: Program jest podobny do przykładu listingu 1, przy czym w procesie potomnym następuje przekierowanie standardowego wyjścia do potoku (linia 16), a następnie uruchamiany jest program ls (linia 17). W procesie macierzystym dane z potoku są sukcesywnie odczytywane (linia 25), małe litery w odczytanym bloku konwertowane są na duże (linie 26-27), a następnie blok jest zapisywany na standardowym wyjściu procesu macierzystego. Powyższa sekwencja powtarza się w pętli (linie 25-32) tak długo, aż funkcja systemowa read zwróci wartość 0 (lub -1 w przypadku błędu). Istotne jest zamknięcie deskryptora potoku do zapisu (linia 24) w celu uniknięcia zawieszenia procesu macierzystego w funkcji read.


Przykład na listingu 4 pokazuje realizację programową potoku ls|tr a-z A-Z, w którym proces potomny wykonuje polecenie ls, a proces macierzysty wykonuje polecenie tr. Funkcjonalnie jest to odpowiednik programu z listingu 3.


 main(int argc, char* argv[]) {
	int pdesk[2];
3
	if (pipe(pdesk) == -1){
		perror("Tworzenie potoku");
6		exit(1);
	}

9	switch(fork()){
	  case -1: // blad w tworzeniu procesu
		perror("Tworzenie procesu");
12		exit(1);
	  case 0: // proces potomny
		dup2(pdesk[1], 1);
15		execvp("ls", argv);
		perror("Uruchomienie programułs");
		exit(1);
18	  default: { // proces macierzysty
		close(pdesk[1]);
		dup2(pdesk[0], 0);
21		execlp("tr", "tr", "a-z", "A-Z", 0);
		perror("Uruchomienie programu tr");
		exit(1);
24	 }
	}
   } 

Listing 4: Programowa realizacja potoku ls|tr a-z A-Z na łączu nienazwanym


Opis programu: Program procesu potomnego (linie 16-19) jest taki sam, jak w przykładzie na listingu 3. W procesie macierzystym następuje z kolei przekierowanie standardowego wejścia na pobieranie danych z potoku (linia 22), po czym następuje uruchomienie programu tr (linia 23). W celu zagwarantowania, że przetwarzanie zakończy się w sposób naturalny konieczne jest zamknięcie wszystkich deskryptorów potoku do zapisu. Deskryptory potomka zostaną zamknięte wraz z jego zakończeniem, a deskryptor procesu macierzystego zamykany jest w linii 21.


Funkcje systemowe służące do tworzenia i komunikacji poprzez łącza nazwane.

int mkfifo ( char * path, mode_t mode )

Wartości zwracane:

  • poprawne wykonanie funkcji: 0
  • zakończenie błędne: -1

Możliwe kody błędów (errno) w przypadku błędnego zakończenie funkcji:

  • EEXIST - plik o podanej nazwie już istnieje, użyto flag O_CREAT i O_EXCL
  • EFAULT - nazwa pathname wskazuje poza dostępną przestrzeń adresową
  • EACCES - żądany dostęp do pliku nie jest dozwolony
  • ENFILE - osiągnięto limit otwartych plików w systemie
  • EMFILE - proces już otworzył dozwoloną maksymalną liczbę plików
  • EROFS - żądane jest otwarcia w trybie zapisu pliku będącego plikiem tylko do odczytu

Argumenty funkcji:

  • path - nazwa ścieżkowa pliku specjalnego będącego kolejką fifo
  • mode - prawa dostępu do łącza

UWAGI:

Funkcja tworzy ( ALE NIE OTWIERA ) plik typu kolejka FIFO


int open (char *path, int flags)

Wartości zwracane:

  • poprawne wykonanie funkcji: deskryptor kolejki FIFO
  • zakończenie błędne: -1

Argumenty funkcji:

  • path - nazwa ścieżkowa pliku specjalnego będącego kolejką fifo
  • mode - prawa dostępu do łącza
  • flags - określenie trybu w jakim jest otwierana kolejka:
    • O_RDONLY - tryb tylko do odczytu
    • O_WRONLY- tryb tylko do zapisu

UWAGI:

Utworzone łącze musi zostać następnie otwarte przez użycie funkcji open. Funkcja ta musi zostać wywołana przynajmniej przez dwa procesy w sposób komplementarny, tzn. jeden z nich musi otworzyć łącze do zapisu, a drugi do odczytu. Odczyt i zapis danych z łącza nazwanego odbywa się za pomocą funkcji: READ, WRITE, jak dla plików


Sposób korzystania z łącza nazwanego

Schemat komunikacji przez kolejkę FIFO:


Rysunek 2.Schemat komunikacji poprzez łącze nazwane
Rysunek 2.Schemat komunikacji poprzez łącze nazwane


Program na listingu 5 pokazuje przykładowe tworzenie łącza i próbę jego otwarcia w trybie do odczytu.


 #include <fcntl.h> 

3  main(){
	mkfifo("kolFIFO", 0600);
	open("kolFIFO", O_RDONLY);
6  }

Listing 5: Przykład tworzenie i otwierania łącza nazwanego


Opis programu: Funkcja mkfifo (linia 4) tworzy plik specjalny typu łącze o nazwie kolFIFO z prawem zapisu i odczytu dla właściciela. W linii 5 następuje próba otwarcia łącza w trybie do odczytu. Proces zostanie zawieszony w funkcji open do czasu, aż inny proces będzie próbował otworzyć tę samą kolejkę w trybie do zapisu.


Listing 6 pokazuje realizację przykładu z listingu 1, w której wykorzystane zostało łącze nazwane.


#include <fcntl.h> 

3  main() {
	int pdesk; 
 
6	if (mkfifo("/tmp/fifo", 0600) == -1){
		perror("Tworzenie kolejki FIFO");
		exit(1);
9	}

	switch(fork()){
12	  case -1: // blad w tworzeniu procesu
		perror("Tworzenie procesu");
		exit(1);
15	  case 0:
		pdesk = open("/tmp/fifo", O_WRONLY);
		if (pdesk == -1){
18			perror("Otwarcie potoku do zapisu");
			exit(1);
		}
21		if (write(pdesk, "Hallo!", 7) == -1){
 		        perror("Zapis do potoku");
			exit(1);
24		}
		exit(0);
	  default: {
27		char buf[10];

		pdesk = open("/tmp/fifo", O_RDONLY);
30		if (pdesk == -1){
			perror("Otwarcie potoku do odczytu");
			exit(1);
33		}
		if (read(pdesk, buf, 10) == -1){
			perror("Odczyt z potoku");
36			exit(1);
		}
		printf("Odczytano z potoku: %s\n", buf);
39  	  }
	}
   }

Listing 6: Przykład tworzenie i otwierania łącza nazwanego


Opis programu: łącze nazwane (kolejka FIFO) tworzona jest w wyniku wykonania funkcji mkfifo w linii 6. Następnie tworzony jest proces potomny (linia 11) i łącze otwierane jest przez oba procesy (potomny i macierzysty) w sposób komplementarny (odpowiednio linia 16 i linia 29). W dalszej części przetwarzanie przebiega tak, jak w przykładzie na listingu 1.


Listing 7 jest programową realizacją potoku ls|tr a-z A-Z, w której wykorzystane zostało łącze nazwane podobnie, jak łącze nienazwane w przykładzie na listingu 4.


  #include <stdio.h>
  #include <fcntl.h>
3
   main(int argc, char* argv[]) {
	int pdesk;
6
	if (mkfifo("/tmp/fifo", 0600) == -1){
		perror("Tworzenie kolejki FIFO");
9		exit(1);
	}

12	switch(fork()){
	  case -1: // blad w tworzeniu procesu
		perror("Tworzenie procesu");
15		exit(1);
	  case 0: // proces potomny
		close(1);
18		pdesk = open("/tmp/fifo", O_WRONLY);
		if (pdesk == -1){
			perror("Otwarcie potoku do zapisu");
21			exit(1);
		}
		else if (pdesk != 1){
24			fprintf(stderr, "Niewlasciwy deskryptor do zapisu\n");			 	
			exit(1);
		}
27 		execvp("ls", argv);
		perror("Uruchomienie programułs");
		exit(1);
30 	  default: { // proces macierzysty
		close(0);
		pdesk = open("/tmp/fifo", O_RDONLY);
33		if (pdesk == -1){
			perror("Otwarcie potoku do odczytu");
			exit(1);
36		}
		else if (pdesk != 0){
			fprintf(stderr, "Niewlasciwy deskryptor do odczytu\n"	
39			exit(1);
		}
		execlp("tr", "tr", "a-z", "A-Z", 0);
42		perror("Uruchomienie programu tr");
		exit(1);
	  }
45	}
   } 

Listing 7: Programowa realizacja potoku ls|tr a-z A-Z na łączu nazwanym


Opis programu: W linii 7 tworzona jest kolejka FIFO o nazwie fifo w katalogu /tmp z prawem do zapisu i odczytu dla właściciela. Kolejka ta otwierana jest przez proces potomny i macierzysty w trybie odpowiednio do zapisu i do odczytu (linia 18 linia 33). Następnie sprawdzana jest poprawność wykonania operacji otwarcia (linie 19 i 34) oraz poprawność przydzielonych deskryptorów (linie 23 i 38). Sprawdzanie poprawności deskryptorów polega na upewnieniu się, że deskryptor łącza do zapisu ma wartość 1 (łącze jest standardowym wyjściem procesu potomnego), a deskryptor łącza do odczytu ma wartość 0 (łącze jest standardowym wejściem procesu macierzystego). Później następuje uruchomienie odpowiednio programów ls i tr podobnie, jak w przykładzie na listingu 4.


Przykłady błędów w synchronizacji procesów korzystających z łączy

Operacje zapisu i odczytu na łączach realizowane są w taki sposób, że procesy podlegają synchronizacji zgodnie ze modelem producent-konsument. Nieodpowiednie użycie dodatkowych mechanizmów synchronizacji może spowodować konflikt z synchronizacją na łączu i w konsekwencji prowadzić do stanów niepożądanych typu zakleszczenie (ang. deadlock).

Listing 8 przedstawia przykład programu, w którym może nastąpić zakleszczenie, gdy pojemność łącza okaże się zbyt mała dla pomieszczenia całości danych przekazywanych przez polecenie ls.


  #define MAX 512

3  main(int argc, char* argv[]) {
	int pdesk[2]; 
 
6 	if (pipe(pdesk) == -1){
		perror("Tworzenie potoku");
		exit(1);
9	}
  
	if (fork() == 0){ // proces potomny
12		dup2(pdesk[1], 1);
		execvp("ls", argv);
		perror("Uruchomienie programułs");
15		exit(1);
	}
	else { // proces macierzysty
18		char buf[MAX];
		intlb, i;

21		close(pdesk[1]);
		wait(0);
		while ((lb=read(pdesk[0], buf, MAX)) > 0){
24		   for(i=0; i<lb; i++)
			buf[i] = toupper(buf[i]);
		   write(1, buf,łb);
27		}
	}
   }

Listing 8: Przykład programu dopuszczającego zakleszczenie w operacji na łączu nienazwanym


Opis programu: Podobnie jak w przykładzie na listingu 3 proces potomny przekazuje dane (wynik wykonania programu ls) do potoku (linie 12-15), a proces macierzysty przejmuje i przetwarza te dane w pętli w liniach 23-27. Przed przejściem do wykonania pętli proces macierzysty oczekuje na zakończenie potomka (linia 22). Jeśli dane generowane przez program ls w procesie potomnym nie zmieszczą się w potoku, proces ten zostanie zablokowany gdzieś w funkcji write w programie ls. Proces potomny nie będzie więc zakończony i tym samym proces macierzysty nie wyjdzie z funkcji wait. Odblokowanie potomka może nastąpić w wyniku zwolnienia miejsca w potoku przez odczyt znajdujących się w nim danych. Dane te powinny zostać odczytane przez proces macierzysty w wyniku wykonania funkcji read (linia 23), ale proces macierzysty nie przejdzie do linii 23 przed zakończeniem potomka. Proces macierzysty blokuje zatem potomka, nie zwalniając miejsca w potoku, a proces potomny blokuje przodka w funkcji wait, nie kończąc się. Wystąpi zatem zakleszczenie. Zakleszczenie nie wystąpi w opisywanym programie, jeśli wszystkie dane, generowane przez program ls, zmieszczą się w całości w potoku. Wówczas proces potomny będzie mógł się zakończyć po umieszczeniu danych w potoku, w następstwie czego proces macierzysty będzie mógł wyjść z funkcji wait i przystąpić do przetwarzania danych z potoku.

Przykład na listingu 9 pokazuje zakleszczenie w wyniku nieprawidłowości w synchronizacji przy otwieraniu łącza nazwanego.


   #include <fcntl.h>
   #define MAX 512
3
   main(int argc, char* argv[]) {
	int pdesk;
6
	if (mkfifo("/tmp/fifo", 0600) == -1){
		perror("Tworzenie kolejki FIFO");
9		 exit(1);
	}
 
12	if (fork() == 0){ // proces potomny
		close(1);
		open("/tmp/fifo", O_WRONLY);
15		execvp("ls", argv);
		perror("Uruchomienie programułs");
		exit(1);
18	}
	else { // proces macierzysty
		char buf[MAX];
21		int lb, i; 
 
		wait(0);
24		pdesk = open("/tmp/fifo", O_RDONLY);
		while ((lb=read(pdesk, buf, MAX)) > 0){
		   for(i=0; i<lb; i++)
27			buf[i] = toupper(buf[i]);
		   write(1, buf,lb);
		}
30 	}
   } 

Listing 9: Przykład programu dopuszczającego zakleszczenie przy otwieraniu łącza nazwanego


Opis programu: Proces potomny w linii 13 próbuje otworzyć kolejkę FIFO do zapisu. Zostanie on zatem zablokowany do momentu, aż inny proces wywoła funkcję open w celu otwarcia kolejki do odczytu. Jeśli jedynym takim procesem jest proces macierzysty (linia 23), to przejdzie on do funkcji open dopiero po zakończeniu procesu potomnego, gdyż wcześniej zostanie zablokowany w funkcji wait. Proces potomny nie zakończy się, gdyż będzie zablokowany w funkcji open, więc będzie blokował proces macierzysty w funkcji wait. Proces macierzysty nie umożliwi natomiast potomkowi wyjścia z open, gdyż nie może przejść do linii 23. Nastąpi zatem zakleszczenie.


Zadania do samodzielnego wykonania.

  1. Napisz program który tworzy trzy procesy - proces macierzysty i jego dwa procesy potomne. Pierwszy z procesów potomnych powinien zapisać do potoku napis „HALLO!", a drugi proces potomny powinien ten napis odczytać.
  2. Napisz program który tworzy trzy procesy, z których dwa zapisują do potoku, a trzeci odczytuje z niego i drukuje otrzymane komunikaty.
  3. Napisz programy realizujące następujące potoki:
    1. ls|wc
    2. finger | cut -d' ' -f1
    3. ls -l | grep ^d | more
    4. ps -ef| tr -s ' '  :| cut -d: -f1 |sort| uniq -c |sort n
    5. cat /etc/group | head -5 > grupy.txt
  4. Napisz program tworzący dwa procesy: klienta i serwera. Serwer tworzy ogólnodostępną kolejkę FIFO, i czeka na zgłoszenia klientów. Każdy klient tworzy własną kolejkę, poprzez którą będą przychodzić odpowiedzi serwera. Zadaniem klienta jest przesłanie nazwy stworzonej przez siebie kolejki, a serwera odesłaniem poprzez kolejkę stworzoną przez klienta wyniku polecenia ls.
  5. Zmodyfikować poprzedni program, tak, by kolejka utworzona przez klienta była dwukierunkowa, klient publiczną kolejką powinien przesyłać nazwę stworzonej przez siebie kolejki. Dalsza wymiana komunikatów powinna odbywać się poprzez kolejkę stworzoną przez klienta. Klient kolejką tą powinien wysyłać polecenia, zadaniem serwera jest wykonywanie tych poleceń i odsyłanie wyników.


Literatura.

[HGS99] Havilland K., Gray D., Salama B., Unix - programowanie systemowe, ReadMe, 1999

[Roch97] Rochkind M.J., Programowanie w systemie UNIX dla zaawansowanych, WNT, 1997

[NS99] Neil M., Stones R,Linux. Programowanie, ReadMe, 1999

[MOS02] Mitchel M., Oldham J., Samuel A.,Linux. Programowanie dla zaawansowanych, ReadMe,2002

[St02] Stevens R.W., Programowanie w środowisku systemu UNIX, WNT, 2002