Programowanie współbieżne
Spis treści 1 Podstawowe definicje i pojęcia współbieżności 5 1.1 Podstawowe definicje współbieżności.................. 5 1.1.1 Czym jest proces?......................... 5 1.1.2 Procesy sekwencyjne....................... 5 1.1.3 Procesy współbieżne....................... 5 1.1.4 Procesy równoległe........................ 5 1.1.5 Rodzaje współbieżności...................... 5 1.2 Poprawność aplikacji współbieżnych................... 6 1.2.1 Bezpieczeństwo.......................... 6 1.2.2 Blokada.............................. 6 1.2.3 Zagłodzenie............................ 6 1.2.4 Żywotność............................. 6 2 Procesy - pojęcia podstawowe 6 2.1 Kanoniczne stany procesów....................... 6 2.2 Zawartość deskryptora.......................... 7 2.2.1 Zarządzanie procesami...................... 7 2.2.2 Zarządzanie pamięcią....................... 8 2.2.3 Zarządzanie plikami........................ 8 2.3 Fazy wykonywania procesów....................... 8 2.3.1 Utworzenie procesu........................ 8 2.3.2 Ładowanie procesu........................ 8 2.3.3 Wykonywanie procesu...................... 8 2.3.4 Zakończenie wykonywania.................... 8 2.4 Atrybuty procesu............................. 9 2.5 Dziedziczenie procesów.......................... 9 3 Procesy POSIX 9 3.1 Tworzenie kopii procesu bieżącego - funkcja fork()........... 9 3.2 Funkcje.................................. 9 3.3 Makra................................... 10 3.4 Akcje przy zakończeniu procesu..................... 10 4 Pliki 10 4.1 Atrybuty pliku.............................. 11 4.2 Funkcje biblioteki niskiego poziomu................... 11 4.3 Blokady pliku............................... 12 4.3.1 Blokada doradcza......................... 12 4.3.2 Blokada obowiązkowa....................... 12 4.4 Standardowa biblioteka wejścia/wyjścia................. 12 4.5 Funkcje standardowej biblioteki..................... 12 5 Katalogi 13 5.1 Funkcje.................................. 13 6 Łącza nienazwane 13 6.1 Wykorzystanie.............................. 13 6.2 Funkcje.................................. 14 1
7 Łącza nazwane 14 7.1 Wykorzystanie.............................. 14 7.2 Funkcje.................................. 14 7.2.1 mkfifo().............................. 14 7.2.2 select()............................... 15 8 Pamięć dzielona 15 8.1 Komunikacja przez pamięć dzieloną w standardzie POSIX...... 15 8.2 Funkcje.................................. 15 9 Kolejki komunikatów POSIX 16 9.1 Podstawowe cechy kolejek komunikatów................. 16 9.2 Zastosowanie kolejek komunikatów................... 16 9.3 Funkcje.................................. 17 9.3.1 mq open.............................. 17 9.3.2 mq send.............................. 17 9.3.3 mq receive............................. 17 9.3.4 mq getattr............................. 18 9.3.5 mq notify............................. 18 10 Synchronizacja 18 10.1 Wzajemne wykluczanie.......................... 18 10.2 Operacje atomowe............................ 18 10.3 Sekcja krytyczna............................. 19 10.4 Warunki poprawnego rozwiązania sekcji krytycznej.......... 19 10.5 Niesystemowe metody ochrony sekcji krytycznej............ 19 10.5.1 Blokowanie przerwań....................... 19 10.5.2 Metoda zmiennej blokującej (nieprawidłowa).......... 20 10.6 Systemowe metody wzajemnego wykluczania.............. 20 10.6.1 Wirujące blokady......................... 20 10.6.2 Blokowanie przerwań....................... 20 10.7 Sprzętowa ochrona sekcji krytycznej................... 20 11 Semafory 20 11.1 Ochrona sekcji krytycznej........................ 21 11.2 Semafory nienazwane i nazwane POSIX................. 21 11.3 Funkcje.................................. 21 11.3.1 sem open............................. 21 11.3.2 sem init.............................. 22 11.3.3 sem wait.............................. 22 11.3.4 sem timedwait........................... 22 11.3.5 sem post.............................. 22 12 Monitory 23 12.1 Zastosowanie............................... 23 12.2 Oczekiwanie wewnątrz monitora..................... 23 12.3 Zmienne warunkowe........................... 23 12.4 Funkcje.................................. 23 12.4.1 wait(c)............................... 23 12.4.2 signal(c).............................. 24 12.4.3 noempty(c)............................ 24 2
12.4.4 public final void notify()..................... 24 12.4.5 public final void notifyall().................... 24 13 Wątki 24 13.1 Pojęcie wątku i zasoby.......................... 24 13.1.1 Pojęcie wątku........................... 24 13.1.2 Właściwości wątku........................ 25 13.1.3 Zasoby wątku........................... 25 13.2 Tworzenie wątku i synchronizacja.................... 26 13.2.1 Tworzenie wątku......................... 26 13.2.2 Synchronizacja.......................... 26 13.3 Funkcje.................................. 26 13.3.1 pthread create........................... 26 13.3.2 pthread exit............................ 27 13.3.3 pthread join............................ 27 13.3.4 mutex init............................. 27 13.3.5 mutex lock............................. 28 13.3.6 mutex unlock........................... 28 13.4 Zmienne warunkowe........................... 28 13.4.1 cond init.............................. 28 13.4.2 cond wait............................. 28 13.4.3 cond signal............................ 29 13.4.4 cond broadcast.......................... 29 13.5 Bariery................................... 29 13.6 Blokady czytelników i pisarzy...................... 29 13.6.1 Zasada działania blokad czytelników i pisarzy......... 29 13.6.2 Stan blokady........................... 29 13.7 Wirujące blokady............................. 30 13.7.1 Inicjacja wirującej blokady.................... 30 13.7.2 Zajęcie blokady.......................... 31 13.7.3 Zwolnienie blokady........................ 31 14 Interfejs gniazdek 31 14.1 Domeny.................................. 31 14.1.1 Rodzina adresów AF INET................... 32 14.1.2 Rodzina adresów AF INET6................... 32 14.1.3 Rodzina adresów AF UNIX................... 32 14.2 Style komunikacji............................. 32 14.2.1 Strumienie............................. 32 14.2.2 Datagramy............................ 33 14.2.3 Protokół surowy.......................... 33 14.3 Adresy gniazd............................... 33 14.3.1 Nazywanie gniazdka....................... 33 14.3.2 Konwersja z zapisu kropkowego na binarny........... 34 14.4 Komunikacja bezpołączeniowa...................... 34 14.4.1 Odbiór danych z gniazdka.................... 34 14.4.2 Zapis do gniazdka......................... 35 14.5 Komunikacja połączeniowa........................ 35 14.5.1 Połączenie ze zdalnym gniazdkiem................ 35 3
14.5.2 Wprowadzenie serwera w stan gotowości do nawiązania połączenia............................... 36 14.5.3 Nawiązanie połączenia przez serwer............... 36 14.5.4 Odczyt z gniazdka........................ 36 14.5.5 Zapis do gniazdka......................... 37 14.5.6 Odczyt z gniazdka........................ 37 14.5.7 Zapis do gniazdka......................... 37 14.5.8 Konfigurowanie gniazdek..................... 38 14.6 Serwer sekwencyjny............................ 38 14.6.1 Klient............................... 38 14.6.2 Serwer............................... 38 14.7 Serwer współbieżny............................ 39 15 Sygnały 39 15.1 Sygnały i ich obsługa........................... 39 15.2 Instalacja handlera sygnału....................... 40 15.3 Blokowanie sygnałów........................... 41 15.4 Sygnały, a wątki............................. 41 15.5 Funkcje.................................. 41 15.5.1 Obsługa sygnałów......................... 41 15.5.2 Funkcja pause (UNIX)...................... 42 15.5.3 Funkcja alarm........................... 42. 4
1 Podstawowe definicje i pojęcia współbieżności 1.1 Podstawowe definicje współbieżności 1.1.1 Czym jest proces? Proces jest aktywną strukturą dynamiczną istniejącą tylko w środowisku działającego komputera. 1.1.2 Procesy sekwencyjne Procesy są sekwencyjne jeżeli następny proces ze zbiory procesów rozpoczyna się po zakończeniu procesu poprzedniego. 1.1.3 Procesy współbieżne Dwa procesy są współbieżne jeżeli jeden z nich rozpoczyna się przed zakończeniem drugiego. 1.1.4 Procesy równoległe Dwa procesy są równoległe jeżeli jeden z nich rozpoczyna się przed zakończeniem drugiego i wykonywane są jednocześnie na oddzielnych procesorach. 1.1.5 Rodzaje współbieżności Współbieżność konkurencyjna - procesy nie współpracują ze sobą działając w ramach aplikacji jednej współbieżnej. Komunikują i synchronizują się ze sobą w celu wykonania pewnego zadania. Współbieżność kooperacyjna - procesy współpracują ze sobą działając w ramach aplikacji jednej współbieżnej. Komunikują i synchronizują się ze sobą w celu wykonania pewnego zadania. 5
1.2 Poprawność aplikacji współbieżnych 1.2.1 Bezpieczeństwo Aplikacja jest bezpieczna, jeżeli utrzymuje system w pożądanym stanie. Aplikacja nie jest bezpieczna jeżeli: Da niepoprawne wyniki - np. nie jest zachowany warunek wzajemnego wykluczania Nie będzie wykonywał pożądanych działań - ulegnie blokadzie W odniesieniu do modelu klient - serwer bezpieczeństwo oznacza, że klienci są obsługiwani w zadowalający sposób. 1. Serwer nie zaprzestał obsługi zleceń. 2. Na zlecenia odpowiadał w prawidłowy sposób. 1.2.2 Blokada Każdy z zablokowanych procesów oczekuje na zdarzenie które może być wygenerowane tylko przez któryś z zablokowanych procesów. Blokada zwana też zakleszczeniem jest typowym zagrożeniem aplikacji współbieżnych. 1.2.3 Zagłodzenie Zagłodzenie występuje gdy procesowi cały czas odmawia się dostępu do zasobów, których ten potrzebuje by wykonać zlecone mu zadanie. 1.2.4 Żywotność Aplikacja jest żywotna, jeżeli każde pożądane zdarzenie w końcu nadejdzie. W modelu klient - serwer żywotność oznacza, że każdy klient zostanie w końcu obsłużony. 2 Procesy - pojęcia podstawowe 2.1 Kanoniczne stany procesów Proces może być w jednym z trzech podstawowych stanów: Wykonywany - proces, który się aktualnie wykonuje Gotowy - proces, który się aktualnie nie wykonuje Zablokowany - proces, który na coś czeka np. na zakończenie wątku (pthread join()), podprocesu (wait()), dostępu do sprzętu Zombie - proces, który się zakończył, ale jego proces macierzysty nie wykonał funkcji wait() 6
Pokazane na rysunku przejścia mają miejsca w następujących sytuacjach: 1. Proces utrzymuje w pamięci struktury danych. 2. Proces żąda zasobu, który nie jest dostępny. 3. Wystąpiło przerwanie (proces został wywłaszczony lub też proces dobrowolnie zwolnił procesor. 4. Procedura szeregująca zdecydowała, że ten proces ma być wykonywany. 5. Zasób, którego brakowało do kontynuacji procesu stał się dostępny. Przejście zostało zainicjowane przez przerwanie od urządzenia wejścia/wyjścia lub też proces aktualnie wykonywany. 6. Zakończenie procesu. 2.2 Zawartość deskryptora 2.2.1 Zarządzanie procesami Identyfikator procesu - PID ang. Process Identifier Bieżący stan procesu (wykonywany, gotowy, zablokowany, itd.) Wskaźniki do poprzedniego i następnego deskryptora w kolejce wszystkich deskryptorów Wskaźniki do poprzedniego i następnego procesu w danej kolejce (procesów gotowych, zablokowanych, itd.) Informacje dla procedur szeregowania (priorytetu procesu, typ szeregowania) Informacje dotyczące obsługi sygnałów (sygnały dostarczone, zablokowane itd.) Informacje na temat hierarchii procesów (proces macierzysty, potomne itd.) Kontekst sprzętowy procesu (rejestry procesora) Informacje rozliczeniowe o czasie procesora zużytym przez proces Nazwa pliku z którego utworzono proces Informacje uwierzytelniające jak rzeczywisty i efektywny identyfikator użytkownika i grupy (UID, GID, EUID, EGID) 7
2.2.2 Zarządzanie pamięcią Rozmiar segmentu kodu, danych, stosu Położenie segmentu kodu, danych, stosu Informacje o stronach zajmowanych przez proces 2.2.3 Zarządzanie plikami Katalog bieżący Katalog macierzysty Informacja o terminalu sterującym Wzorzec tworzenia nowych plików (UMASK) Wskaźnik na tablicę deskryptorów otwartych plików 2.3 Fazy wykonywania procesów 2.3.1 Utworzenie procesu Alokacja deskryptora procesu, przydział PID Ustalenie zmiennych otoczenia, zwykle dziedziczone z procesu macierzystego 2.3.2 Ładowanie procesu Załadowanie segmentu kodu i danych oraz inicjacji stosu. Ładowanie wykonywane jest przez oddzielny wątek ładujący, aby nie blokować administratora procesu. 2.3.3 Wykonywanie procesu Po załadowaniu nowy proces jest umieszczany w kolejce procesów gotowych. 2.3.4 Zakończenie wykonywania Zakończenie procesu może być zainicjowane przez sam proces, gdy wykona funkcję exit() lub poprzez wysłany z zewnątrz sygnał. Zakończenie składa się z dwóch etapów. Zwolnienie zasobów - proces zwalnia wszystkie zajmowane zasoby takie jak pamięć, nazwy itd. oraz likwiduje interakcje z innymi procesami. Po wykonaniu tej fazy zajmuje tylko deskryptor. Zawiadomienie procesu macierzystego o zakończeniu - dopóki proces macierzysty nie wykona funkcji wait() lub waitpid() kończony proces pozostaje w stanie zombie. W celu uniknięcia tego można ustawić reakcję na sygnał SIGCHLD w funkcji signal na SIG IGN. 8
2.4 Atrybuty procesu PID - identyfikator procesu PPID - PID procesu macierzystego UID - identyfikator użytkownika GID - identyfikator grupy użytkownika SID - identyfikator sesji PGRP - identyfikator grupy procesów priorytet procesu CWD - katalog bieżący katalog główny otoczenie procesu 2.5 Dziedziczenie procesów Proces potomny dziedziczy większość atrybutów procesu macierzystego, ale ma swoje PID, PPID oraz własne kopie deskryptorów otwartych plików. 3 Procesy POSIX 3.1 Tworzenie kopii procesu bieżącego - funkcja fork() Procesy tworzone są za pomocą funkcji fork(). Funkcja fork() posiada następujący prototyp. Funkcja tworzy kopię procesu bieżącego, czyli tego procesu, który wykonuje funkcje fork(). Funkcja fork() zwraca: 0 w procesie potomnym 0 w procesie macierzystym zwracany jest PID procesu potomnego -1 błąd Funkcja fork() tworzy deskryptor nowego procesu oraz kopię segmentu danych i stosu procesu macierzystego. 3.2 Funkcje fork() - utworzenie kopii procesu bieżącego exec() - zastąpienie procesu bieżącego innym procesem system() - wykonanie programu lub skryptu exit() - zakończenie procesu wait(), waitpid() - czekanie na zakończenie procesu 9
3.3 Makra WIFEXITED - zwraca 0 gdy proces potomny był zakończony pomyślnie WEXITSTATUS - zwraca kod powrotu y przekazany przez funkcję exit(y) z procesu potomnego WIFSIGNALED - zwraca 0 gdy proces potomny był zakończony przez nieobsłużony sygnał WTERMSIG - zwraca numer sygnału, gdy proces był zakończony przez sygnał 3.4 Akcje przy zakończeniu procesu Należy zakończyć komunikację z innymi procesami Należy zwolnić zajmowane zasoby Należy zaczekać na zakończenie procesów potomnych (bo inaczej są one adoptowane przez init 1. Zamykane są otwarte pliki i strumienie 2. Najmłodszy bajt kodu powrotu x jest przekazywany do zmiennej odczytywanej przez funkcję wait() w procesie macierzystym. Kod powrotu zapamiętywany jest w deskryptorze. 3. Jeśli proces macierzysty wywołał wait() albo waitpid(), to zostaje on odblokowany a deskryptor jest usuwany. 4. Jeśli w/w funkcje nie zostały wywołane, to proces potomny przechodzi do stanu zombie, a kod powrotu czeka w deskryptorze. 5. Proces macierzysty otrzymuje sygnał SIGCHLD. 4 Pliki Plik jest podstawową abstrakcją używaną w systemach operacyjnych. Pozwala na traktowanie dużego zbioru zasobów w jednolity sposób. W systemie Linux prawie wszystkie zasoby są plikami. Dane i urządzenia są reprezentowane przez abstrakcję plików. Mechanizm plików pozwala na jednolity dostęp do zasobów lokalnych jak i zdalnych za pomocą poleceń i programów usługowych. Plik jest obiektem abstrakcyjnym, z którego można czytać i do którego można pisać. Oprócz zwykłych plików w systemie występują łącza symboliczne, kolejki FIFO, blok pamięci, urządzenia blokowe i znakowe. 10
4.1 Atrybuty pliku typ pliku - plik regularny, katalog, gniazdo, kolejka FIFO, urządzenie blokowe, urządzenie znakowe, link prawa dostępu - prawo odczytu zapisu, wykonania określone dla właściciela pliku, grupy do której on należy i innych użytkowników systemu wielkość - wielkość pliku w bajtach właściciel pliku - UID właściciela pliku grupa do której należy właściciel - GID grupy do której należy właściciel pliku czas ostatniej modyfikacji - czas, kiedy nastąpił zapis do pliku czas ostatniego dostępu - czas kiedy nastąpił odczyt lub zapis do pliku czas ostatniej modyfikacji statusu - kiedy zmieniano atrybuty takie jak prawa dostępu, właściciel itp. liczba dowiązań - pod iloma nazwami występuje dany plik identyfikacja urządzenia - identyfikator urządzenia na którym plik jest pamiętany 4.2 Funkcje biblioteki niskiego poziomu open() - funkcja otwierająca plik. Potrafi utworzyć plik, jeśli nie takowy istnieje, ale to zależy od ustawień oflag. Funkcja zwraca deskryptor pliku (uchwyt), którego używamy do identyfikacji pliku. creat() - funkcja tworząca nowy plik. W argumencie podajemy nazwę pliku i atrybuty (prawa dostępu). Funkcja, podobnie jak open(), zwraca deskryptor pliku. read() - za parametr bierze deskryptor pliku, bufor docelowy i ilość bajtów do odczytu. Czyta w bieżącej pozycji pliku. write() - parametry do deskryptora pliku, bufor zawierające dane do zapisu i ilość bajtów do zapisania. close() - zamknięcie pliku lseek() - pozwala zmieniać obecną pozycję w pliku. Argumenty to deskryptor pliku, offset stanowiący o ile mamy się przesunąć i flagę, która decyduje, czy przesuwamy się względem bieżącej pozycji, początku lub końca pliku. f,;stat() - zwraca informacje o pliku (inode, uprawnienia itd.) fcntl() - zmienia atrybuty pliku dup(), dup2() - duplikuje deskryptor pliku. 11
4.3 Blokady pliku W sytuacji, gdy więcej procesów korzysta współbieżnie z pliku, a przynajmniej jeden z nich pisze, może dojść do błędnego odczytu jego zawartości. Podobnie dwa (lub więcej) procesy nie powinny jednocześnie pisać do pliku. W celu zapewnienia wzajemnego wykluczania w dostępie do pliku stosuje się blokady plików. 4.3.1 Blokada doradcza Blokady doradcze działają wtedy, gdy procesy używające wspólnego pliku stosują prawidłowo mechanizmy ochrony. Gdy jeden z procesów takie mechanizmy stosuje, a inny nie, to w tym drugim operacje odczytu i zapisu nie spowodują blokady procesu. 4.3.2 Blokada obowiązkowa Blokady obowiązkowe działają nawet wtedy, gdy nie wszystkie procesy używające wspólnego pliku stosują mechanizmy obrony. W tym przypadku blokada założona na plik (F RDLCK, F WRLCK) przez jeden proces będzie działała nawet gdy inny proces nie będzie jej świadomy. W takim przypadku operacja read/write dokonana na zablokowanym pliku spowoduje zawieszenie procesu do czasu zdjęcia blokady z pliku. Od tej chwili blokada POSIX założona na ten plik będzie obowiązująca. 4.4 Standardowa biblioteka wejścia/wyjścia Zapewnia wiele rozbudowanych funkcji ułatwiających formatowanie wyjścia i skanowanie wejścia. Obsługuje buforowanie Funkcje zadeklarowane są w pliku nagłówkowym stdio.h Odpowiednikiem uchwytu jest strumień widziany w programie jako FILE* 4.5 Funkcje standardowej biblioteki fopen() - otwarcie lub utworzenie pliku, atrybutami jest nazwa pliku i sposób otwarcia pliku fprintf(), sprintf() - fprintf() pisze do strumienia zawartość kolejnych zmiennych zgodnie z łańcuchem formatującym, sprintf() pisze do bufora, atrybutami są plik/bufor, łańcuch formatujący i bufor na łańcuch wyjściowy fscanf(), sscanf() - fscanf() czyta dane ze strumienia i umieszcza je w zmiennych, których adresy wyszczególnione są jako parametry. Ważne, aby sposób zapisu pola danych był zgodny z typem zmiennej w której to pole ma być zapisane.. Funkcja sscanf() czyta z bufora. Atrybutami są plik/bufor łańcuch formatujący i bufor na łańcuch wejściowy. 12
5 Katalogi 5.1 Funkcje DIR* opendir(char* dirname) - funkcja zwraca wskaźnik na strukturę DIR, która wskazuje na pierwszą pozycję w katalogu. Gdy wywołanie się nie uda zwracany jest NULL. Działanie funkcji podobne jest do funkcji fopen, która otwiera strumień zwracając wskaźnik na strukturę FILE. struct dirent* readdir(dir* dirptr) - odczytuje kolejne pozycje katalogu, kiedy jest otwarty. Funkcja powoduje skopiowanie struktury dirent danych o bieżącej pozycji katalogu i przesuwa ten wskaźnik na następną pozycję. int mkdir(char* patchname) - funkcja tworzy nowy katalog (wraz ze ścieżką). Funkcja zwraca 0 gdy sukces, -1 gdy błąd. Funkcja tworzy katalog o nazwie patchname. Tworzy ona również dwa łącza:.. łącze do katalogu wyższego poziomu. łącze do bieżącego katalogu int rmdir(char* patchname) - kasowanie katalogu, 0 gdy sukces, -1 gdy błąd. Usunięty może być tylko pusty katalog. int chdir(char* patchname) - odczyt i zmiana katalogu bieżącego, funkcja zmieni katalog bieżący na patchname, 0 gdy sukces, -1 gdy błąd 6 Łącza nienazwane 6.1 Wykorzystanie Prosta komunikacja pomiędzy procesem macierzystym i potomnym, nie ma możliwości wymiany deskryptora pliku, w którym znajduje się łącze nienazwane inaczej niż gdy oba procesy są w relacji macierzysty/potomny. Ten kanał komunikacji jest jednostronny dla danego procesu, tzn. np. macierzysty tylko czyta, a potomny tylko pisze. 1. Tworzymy łącze za pomocą funkcji pipe(), której parametrem jest wskaźnik na dwuelementową tablicę int - w niej znajdują się deskryptory. 2. Tworzymy proces potomny danego programu (fork()) 3. W procesie potomnym zamykamy nieużywany deskryptor za pomocą close(), np. jeśli tylko będziemy pisać, to zamykamy deskryptor do pisania (close(fd[1])), po czym czytamy/zapisujemy jak z normalny plik 4. Robimy analogicznie to samo co w punkcie 3. dla procesu macierzystego 5. Na końcu robimy close() na pozostałych deskryptorach, z których korzystaliśmy 13
6.2 Funkcje pipe() - tworzy łącze nienazwane open(), read(), write(), close() - jak w przypadku plików, nie używamy open() w przypadku łącz nienazwanych, bo nie mamy pliku, który chcemy otworzyć, zamiast tego korzystamy z pipe() flock() - blokuje plik, pozwalając na synchronizację dostępu do niego pomiędzy kilkoma procesami fileno() - zwraca deskryptor pliku dla argumentu FILE* 7 Łącza nazwane 7.1 Wykorzystanie Łącza nazwane mogą być używane przez niepowiązane ze sobą procesy. Są po specjalne pliki, które są normalnie dostępne w systemie plików. Pliki te giną po wyłączeniu komputera. 1. Tworzymy plik FIFO za pomocą mkfifo() 2. Otwieramy plik open() 3. Używamy read() i write() w zależności od tego co chcemy zrobić 4. Zamykamy plik przy pomocy funkcji close() 7.2 Funkcje 7.2.1 mkfifo() Tworzy łącze nazwane, jako argument przyjmuje nazwę pliku FIFO oraz prawa dostępu do pliku. int mkfifo(char* path, mode t mode path - nazwa pliku FIFO (ze ścieżką) mode - prawa dostępu do pliku Zwraca 0 gdy sukces, -1 gdy błąd. 14
7.2.2 select() Funkcja ta blokuje bieżący proces do momentu, kiedy dany deskryptor stanie się gotowy albo wystąpi błąd. Można mu ustawić timeout, bo którym zwraca błąd. 8 Pamięć dzielona 8.1 Komunikacja przez pamięć dzieloną w standardzie PO- SIX 1. Alokujemy pamięć dzieloną poprzez funkcję shm open(), w atrybutach której znajduje się nazwa segmentu pamięci dzielonej i jej atrybuty. Zwraca ona deskryptor pliku. 2. Poprzez ltrunc() lub ftruncate() ustalamy obszar zajmowanej pamięci 3. Używając mmap() inicjujemy zmienną w pamięci dzielonej, zwykle ładując tam wcześniej zdefiniowaną strukturę. Jako wynik funkcji otrzymujemy wskaźnik do zmiennej. 4. Po skończeniu używania pamięci zwalniamy nazwę pamięci dzielonej za pomocą shn unlink(nazwa). 8.2 Funkcje int shm open(char *name, inf oflag, mode t mode) - tworzenie segmentu pamięci, atrybutami jest nazwa segmentu pamięci, flaga specyfikująca tryb utworzenia i specyfikacja trybu dostępu. Gdy funkcja zwraca liczbę nieujemną jest 15
to uchwyt identyfikujący segment w procesie. Segment widziany jest jako plik specjalny w katalogu /dev/shmem. off t ftruncate(int fdes, off t offset) - ustala rozmiar segmentu pamięci, atrybutami są uchwyt segmentu zwracany przez poprzednią funkcję shm open i wielkość segmentu w bajtach. Funkcja zwraca wielkość segmentu lub -1 gdy błąd. void *mmap(void *addr, size t len, int prot, int flags, int fdes, off t off - odwzorowuje segment pamięci wspólnej w obszar procesu, atrybutami są zmienna wskaźnikowa w procesie, której wartość będzie przez funkcję zainicjalizowana, wielkość odwzorowywanego obszaru, specyfikacja dostępu do obszaru, specyfikacja użycia segmentu, uchwyt wspólnej pamięci i początek obszaru we wspólnej pamięci (musi mieć wielokrotność strony 4K). Funkcja zwraca adres odwzorowanego obszaru lub -1 gdy błąd. 9 Kolejki komunikatów POSIX 9.1 Podstawowe cechy kolejek komunikatów 1. Kolejki komunikatów są pośrednik obiektem komunikacyjnym widzianym jako plik specjalny. Komunikujące się procesy nie muszą znać swoich identyfikatorów. 2. Komunikaty odczytywane z kolejki zachowują strukturę - są separowane. W kolejce mogą znajdować się komunikaty różnej długości. Własności tej nie mają kolejki FIFO. 3. Można zadać maksymalną długość kolejki komunikatów. Gdy zostanie ona przekroczona, proces piszący do kolejki komunikatów zostanie zablokowany. 4. Kolejka widziana jest w systemie plików jako plik specjalny. Operacje odczytu/zapisu mogą być zabezpieczane prawami dostępu jak w przypadku plików regularnych. 5. Można testować status kolejki (np. liczbę komunikatów w kolejce). Nie jest to możliwe w przypadku plików regularnych. 6. Komunikatom można nadać priorytet. Komunikaty o wyższym priorytecie będą umieszczane na początku kolejki. 9.2 Zastosowanie kolejek komunikatów 1. Proces wysyłający komunikaty nie może być wstrzymany. 2. Proces wysyłający komunikaty nie potrzebuje szybkiej informacji zwrotnej o tym, czy komunikat dotarł do adresata. 3. Zachodzi potrzeba przekazywania danych z procesu w którym one powstają (producent) do procesu w którym są one przetwarzane (konsument). 16
9.3 Funkcje 9.3.1 mq open Kolejkę komunikatów tworzy się za pomocą funkcji: mqd t mq open(char *name, int oflag) mqd t mq open(char *name, int oflag, int mode, mq attr *attr) name - łańcuch identyfikujący kolejkę komunikatów, ma się zaczynać od /. Kolejki tworzone są w katalogu bieżącym. oflag - tryb tworzenia kolejki. Tryby te są analogiczne jak w zwykłej funkcji open. mode - prawa dostępu do kolejki (r - odczyt, w - zapis) dla właściciela pliku, grupy i innych, analogicznie jak w przypadku plików regularnych. Atrybut x - wykonanie jest ignorowany Funkcja zwraca identyfikator kolejki komunikatów, -1 gdy błąd. 9.3.2 mq send Wysyłanie komunikatu do kolejki odbywa się za pomocą funkcji: int mq send(mqd t mq, char *msg, size t len, unsigned int mprio) mq - identyfikator komunikatów *msg - adres bufora wysyłanego komunikatu len - długość wysyłanego komunikatu mprio - priorytet komunikatu (od 0 do MQ PRIORITY MAX) Wywołanie funkcji powoduje przekazanie komunikatu z bufora msg do kolejki mq. Można wyróżnić dwa zasadnicze przypadki: 1. W kolejce jest miejsce na komunikaty. Wtedy wykonanie funkcji nie spowoduje zablokowania procesu bieżącego. 2. W kolejce brak miejsca na komunikaty. Wtedy wykonanie funkcji spowoduje zablokowanie procesu bieżącego. Proces ulegnie odblokowaniu, gdy zwolni się miejsce. Zachowanie się funkcji uzależnione jest od stanu flagi O NONBLOCK. Flaga ta jest domyślnie wyzerowana. Funkcja zwraca 0 gdy sukces i -1 gdy błąd. 9.3.3 mq receive Pobieranie komunikatu z kolejki komunikatów odbywa się za pomocą funkcji: int mq receive(mqd t mq, char *msg, size t len, unsigned int *mprio mq - identyfikator komunikatów 17
*msg - adres bufora odbieranego komunikatu len - maksymalna długość odbieranego komunikatu mprio - priorytet odebranego komunikatu 1. Gdy w kolejce znajduje się przynajmniej jeden komunikat wywołanie funkcji mq receivex nie spowoduje zablokowanie procesu bieżącego. 2. Gdy w kolejce brak komunikatów wywołanie funkcji mq receive spowoduje zablokowanie procesu bieżącego. Proces ulegnie odblokowaniu, gdy w kolejce pojawi się jakiś komunikat. W przypadku gdy więcej niż jeden proces czeka na komunikat - odblokowany będzie proces, który najdłużej czekał. Zachowanie się funkcji uzależnione jest także od stanu flagi O NONBLOCK. Funkcji zwraca rozmiar odebranego komunikatu lub -1 gdy błąd. 9.3.4 mq getattr Testowanie statusy kolejki komunikatów odbywa się poprzez wykonanie funkcji: int mq getattr(mqd t mq, struct mq attr *attr mq - identyfikator kolejki komunikatów *attr - adres bufora ze strukturą zawierającą atrybuty kolejki komunikatów 9.3.5 mq notify 1. Można spowodować aby pojawienie się komunikatu w pustej kolejce powodowało zawiadomienie procesu bieżącego. 2. Zawiadomienie może mieć postać sygnału lub powodować uruchomienie wątku int mq notify(mqd t mq, struct sigevent *notif) mq - identyfikator kolejki komunikatów *notif - adres struktury typu sigevent specyfikującego sposób zawiadomienia 10 Synchronizacja 10.1 Wzajemne wykluczanie Wymaganie, aby ciąg operacji na pewnym zasobie (zwykle pamięci) był wykonywany w trybie wyłącznym tylko przez jeden z potencjalnie wielu procesów. 10.2 Operacje atomowe Operacje, które nie mogą zostać przerwane, np. przez przełączenie procesu. 18
10.3 Sekcja krytyczna Ciąg operacji na pewnym zasobie (zwykle pamięci), który musi wykonany w trybie wyłącznym przez tylko jeden z potencjalnie wielu procesów. 10.4 Warunki poprawnego rozwiązania sekcji krytycznej 1. W sekcji krytycznej może być tylko jeden proces, to znaczy instrukcje z sekcji krytycznej nie mogą być przeplatane. 2. Nie można czynić żadnych założeń co do względnym szybkości wykonywania procesów. 3. Proces może się zatrzymać w sekcji lokalnej, ale nie w sekcji krytycznej. Zatrzymanie procesu w sekcji lokalnej nie może blokować innym procesom wejścia do sekcji krytycznej. 4. Każdy z procesów musi w końcu wejść do sekcji krytycznej. 10.5 Niesystemowe metody ochrony sekcji krytycznej 10.5.1 Blokowanie przerwań Metoda zapewnienia wzajemnego wykluczania poprzez blokowanie przerwań opiera się na fakcie, że proces może być połączony przez: 1. Przerwanie, które aktywuje procedurę szeregującą 2. Wywołanie wprost procedury szeregującej lub innego wywołania systemowego powodującego przełączanie procesów Gdy żaden z powyższych czynników nie zachodzi procesy nie mogą być przełączane. Metoda ochrony sekcji krytycznej poprzez blokowanie przerwań opiera się na następujących zasadach: 1. Protokół wejścia do sekcji - następuje zablokowanie przerwań 2. Protokół wyjścia z sekcji - następuje odblokowanie przerwań 3. Wewnątrz sekcji krytycznej nie wolno używać wywołań systemowych mogących spowodować przełączenie procesów Ochrona sekcji krytycznej przez blokowanie przerwań Wady metody: 1. Przełączanie wszystkich procesów jest zablokowane. 2. System nie reaguje na zdarzenia zewnętrzne co może spowodować utratę danych. 3. Skuteczne w maszynach jednoprocesorowych. Zastosowanie metody: Wewnątrz systemu operacyjnego do ochrony wewnętrznych sekcji krytycznych. 19
10.5.2 Metoda zmiennej blokującej (nieprawidłowa) Metoda polega na użyciu zmiennej o nazwie lock. Gdy zmienna lock = 0 sekcja jest wolna, gdy lock sekcja jest zajęta. Proces przy wejściu testuje wartość tej zmiennej. Gdy wynosi ona 1 to czeka, gdy zmieni się na 0 wchodzi do sekcji ustawiając wartość zmiennej lock na 1. Metoda jest niepoprawna, gdyż operacja testowania wartości zmiennej lock i ustawiania jej na 1 może być przerwana (nie jest niepodzielna). Dodatkową wadą metody jest angażowanie procesora w procedurze aktywnego czekania. 10.6 Systemowe metody wzajemnego wykluczania 10.6.1 Wirujące blokady Wykorzystuje sprzętowe wsparcie w postaci instrukcji sprawdź i przypisz oraz zamień. Stosuje się je do synchronizacji wątków ze względu na mały narzut operacji systemowych. 10.6.2 Blokowanie przerwań Do ochrony wewnętrznych sekcji krytycznych systemu operacyjnego. 10.7 Sprzętowa ochrona sekcji krytycznej Wiele mikroprocesorów zawiera instrukcje wspierające sprzętowo wzajemne wykluczanie. Są to instrukcje typu: 1. Sprawdź i Przypisz - ang. TAS - Test And Set 2. Porównaj i zamień - ang. CAS - Compare And Swap 3. Zamień - ang. EXCH - Exchange Pozwalają one wykonać kilka operacji w sposób nieprzerywalny. Listing 1 : Instrukcja CAS int cas ( int *word, int test_value, int new_value ){ int old_ value ; atomic { old_ value = * word ; if ( * word == test_ value ) * word = new_value ; } return ( old_ value ); // Czemu kurwa wymagasz jak nie ma o tym wzmianki w pdfach. } 11 Semafory Semafor - jest obiektem abstrakcyjnym służącym do kontrolowania dostępu od ograniczonego zasobu. Semafory są szczególnie przydatne w środowisku gdzie wiele procesów lub wątków komunikuje się przez wspólną pamięć. 20
11.1 Ochrona sekcji krytycznej 1. Tworzymy semafor za pomocą funkcji sem init(), której pierwszym parametrem jest wskaźnik na semafor (zmienna typu semaphore), a drugim początkowa wartość semafora. 2. Wywołujemy sem wait(). Funkcja ta dekrementuje wartość semafora jeśli jest on dodatni, a jeśli jest równy 0 to blokuje proces bieżący, który zostanie odblokowany wtedy, gdy inny proces wywoła na tym samym semaforze sem post(). 3. Wywołujemy kod sekcji krytycznej. 4. Wywołujemy sem post(), który albo odblokowuje inny proces oczekujący na tym semaforze albo inkrementuje wartość semafora. 11.2 Semafory nienazwane i nazwane POSIX 1. Semafory nienazwane - dostęp do semafora nienazwanego następuje po adresie semafora. Stąd nazwa semafor nienazwany. 2. Semafory nazwane - identyfikowane są w procesach poprzez ich nazwę. Na semaforze nazwanym operuje się tak samo jak na semaforze nienazwanym z wyjątkiem funkcji otwarcia i zamknięcia semafora. Semafory nienazwane nadają się do synchronizacji wątków w obrębie jednego procesu. Dostęp do semafora nienazwanego następuje poprzez jego adres. Może on być także użyty do synchronizacji procesów o ile jest umieszczony w pamięci dzielonej. Dostęp do semaforów nazwanych następuje poprzez ich nazwę. Ten typ semaforów bardziej nadaje się do synchronizacji procesów niż wątków. Semafory nienazwane działają szybciej niż nazwane. 11.3 Funkcje 11.3.1 sem open Funkcji używa się w celu użycia semafora nazwanego: sem t *sem open(const char *sem name, int oflags, [int mode, int value]) sem name - nazwa semafora, powinna zaczynać się od znaku / oflags - flagi trybu tworzenia i otwarcia: O RDONLY, O RDWR, O WRONLY. Gdy semafor jest tworzony należy użyć flagi O CREAT mode - prawa dostępu do semafora - takie jak do plików. Parametr jest opcjonalny. value - początkowa wartość semafora. Parametr jest opcjonalny. Funkcja zwraca identyfikator semafora. Semafor widoczny jest w katalogu /dev/sem. Funkcja tworzy semafor, gdy nie był on wcześniej utworzony i otwiera go. 21
11.3.2 sem init Przed użyciem semafor powinien być zainicjowany. int sem init(sem t *sem, int pshared, unsigned value) sem - identyfikator semafora (wskaźnik na strukturę w pamięci) pshared - gdy wartość nie jest zerem semafor może być umieszczony w pamięci dzielonej i dostępny w wielu procesorach value - początkowa wartość procesora Funkcja zwraca 0 gdy sukces, -1 gdy błąd. 11.3.3 sem wait Czekanie na semaforze. int sem wait(sem t *sem) sem - identyfikator semafora Gdy licznik semafora jest nieujemny funkcja zmniejsza go o 1. W przeciwnym wypadku proces bieżący jest zawieszany. Zawieszony proces może być odblokowany przez procedurę sem post wykonywaną w innym procesie lub sygnał. Funkcja zwraca 0 gdy sukces, -1 gdy błąd. 11.3.4 sem timedwait Ograniczone czasowo czekanie w semaforze, funkcja ta jest wersją operacji sem wait. int sem timedwait(sem t *sem, struc timespec timeout) sem - identyfikator semafora timeout - specyfikacja przeterminowania - czas absolutny Gdy licznik semafora jest nieujemny funkcja zmniejsza go o 1. W przeciwnym wypadku proces bieżący jest zawieszany. Zawieszony proces może być odblokowany przez procedurę sem post wykonywaną w innym procesie lub sygnał. Funkcja zwraca 0 gdy sukces, -1 gdy błąd. Gdy wystąpił timeout kod błędu errno wynosi ETIMEDOUT. 11.3.5 sem post Sygnalizacja na semaforze. int sem post(sem t *sem) sem - identyfikator semafora Jeśli jakikolwiek proces jest zablokowany na tym semaforze przez wywołanie funkcji sem wait zostanie on odblokowany. Gdy brak procesów zablokowanych licznik semafora zwiększany jest o 1. Funkcja zwraca 0 gdy sukces, -1 gdy błąd. 22
12 Monitory Monitor jest strukturalnym narzędziem synchronizacji. Zmienne i procedury, które na nich operują są zebrane w jednym module. Dostęp do zmiennych monitora jest możliwy tylko i wyłącznie za pomocą procedur monitora. Tylko jeden proces może w danej chwili wywoływać procedury monitora. Każdy inny proces chcący wywołać procedurę monitora zostanie zablokowany, aż pierwszy się nie skończy. Można wstrzymywać i wznawiać procedury monitora za pomocą zmiennych warunkowych, na których można wykonywać operacji wait() i signal(). 12.1 Zastosowanie wygodniejsze rozwiązanie problemu sekcji krytycznej niż semafory, ze względu na ich strukturalny charakter monitory udostępniają kolejki procesów, które mogą być używane tylko wewnątrz procesorów (FIFO) 12.2 Oczekiwanie wewnątrz monitora W trakcie procedury monitora można wywołać wait(), po czym obecny proces zostanie wstrzymany i wrzucony na koniec kolejki procesów oczekujących na mieszanie w monitorze lub jakiejś kolejki uprzywilejowanej (zależy od implementacji), a dostęp do monitora zostaje przekazany innemu procesowi. Ma to sens w przypadku, kiedy wykonujemy jakąś operację na monitorze, ale okazuje się, że musimy czekać na coś jeszcze innego. Wtedy zamiast blokować inne procesy czekające na dostęp monitora i bezczynnie czekać, możemy wpuścić kogoś samemu czekając. 12.3 Zmienne warunkowe W zmiennych warunkowych bez blokowania operacja powiadom nie powoduje opuszczenia monitora. Zamiast tego wątek oczekujący jest przenoszony do kolejki e, aby zaczekać, aż wątek powiadamiający zakończy swoje zadanie. Kolejka s jest niepotrzebna. Czasami dodaje się dodatkową operację powiadom wszystkie, która przenosi wszystkie oczekujące wątki do e. 12.4 Funkcje 12.4.1 wait(c) Wstrzymywanie procesu bieżącego wykonującego procedurę monitora i wstawienie go na koniec kolejki związanej ze zmienną warunkową c. Jeżeli jakieś procesy czekają na wejście do monitora to jeden z nich będzie wpuszczony. 23
12.4.2 signal(c) Odblokowanie jednego z procesów czekających na zmiennej warunkowej c. Gdy brak czekających procesów operacja nie daje efektów. Operacja signal nie posiada licznika. 12.4.3 noempty(c) Funkcja zwraca true gdy kolejka c jest niepusta, false gdy jest pusta. Jeżeli nie jest to ostatnia instrukcja monitora to proces wykonujący tę operację jest wstrzymywany do chwili gdy wznowiony przezeń proces zwolni monitor. Wstrzymany tak proces może przebywać w: wejściowej kolejce procesów oczekujących na wejście do monitora kolejce uprzywilejowanej 12.4.4 public final void notify() Metoda powoduje odblokowanie jednego z wątków zablokowanych na monitorze pewnego obiektu przez wait(). Który z czekających wątków będzie odblokowany nie jest w definicji metody określone. Odblokowany wątek nie będzie natychmiast wykonywany - musi on jeszcze zaczekać aż zwolniona będzie przez bieżący wątek blokada monitora. Odblokowany wątek będzie konkurował z innymi o nabycie blokady monitora. Wątek staje się właścicielem monitora obiektu, gdy: wykona synchronizowaną metodę instancji tego obiektu wykona synchronizowane wyrażenia tego obiektu Gdy wątek nie jest w posiadaniu monitora obiektu, generowany jest wyjątek Illegal- MonitorStateException. 12.4.5 public final void notifyall() Metoda powoduje odblokowanie wszystkich wątków zablokowanych na monitorze pewnego obiektu poprzez uprzednie wykonanie wait(). Wątki będą jednak czekały aż do chwili gdy wątek bieżący zwolni blokady monitora. Odblokowane wątki będą konkurowały o nabycie blokady monitora. Metoda notify() i notifyall() może być wykonana tylko przez wątek, który jest właścicielem monitora obiektu. 13 Wątki 13.1 Pojęcie wątku i zasoby 13.1.1 Pojęcie wątku Wątek - elementarna jednostka szeregowania korzystająca z zasobów procesu. Wątki wykonywane w ramach jednego procesu dzielą jego przestrzeń adresową i inne zasoby procesu. W ramach jednego procesu może się wykonywać wiele wątków. 24
13.1.2 Właściwości wątku Koszt utworzenia i przełączania wątków jest mniejszy niż procesu. Dane statystyczne procesu są dla wątków działających w ramach jednego procesu wzajemnie widoczne. Wykonanie każdego wątku przebiega sekwencyjnie, każdy wątek ma swój licznik rozkazów. Wątki mogą być wykonywane na oddzielnych procesorach co umożliwia przyspieszenie obliczeń. Konieczna jest synchronizacja dostępu do wspólnych danych, z których korzystają wątki. 13.1.3 Zasoby wątku Wątek dzieli ze swym procesem macierzystym następujące zasoby: Dane statystyczne (segment danych) Deskryptory otwartych plików, blokady plików Maskę tworzenia plików (umask) Środowisko Katalog macierzysty i główny Limity zasobów (setrlimit) Timety Sesję, użytkownika, grupę, terminal sterujący Zasoby własne wątku: Identyfikator wątku (thread ID) Maska sygnałów Zmienna errno Priorytet i strategię szeregowania Atrybuty i zasoby własne wątku: 1. Identyfikator wątku TID (ang. Thread Identifier - każdy wątek ma unikalny w ramach procesu identyfikator. Jest to liczba całkowita. Pierwszy wątek ma TID 1, następny 2 itd. 2. Zestaw rejestrów (ang. Register set) - każdy wątek posiada własny obszar pamięci w którym pamiętany jest zestaw rejestrów procesora (tak zwany kontekst procesora). Gdy wątek jest wywłaszczany lub blokowany w obszarze tym pamiętane są rejestry procesora. Gdy wątek będzie wznowiony obszar ten jest kopiowany do rejestrów procesora. 25
3. Stos (ang. Stack) - każdy wątek ma swój własny stos umieszczony w przestrzeni adresowej zawierającego go procesu. Na stosie tym pamiętane są zmienne lokalne wątku. 4. Maska sygnałów (ang. Signal mask) - każdy wątek ma swoją własną maskę sygnałów. Maska sygnałów specyfikuje, które sygnały mają być obsługiwane, a które blokowane. Początkowa maska jest dziedziczona z procesu macierzystego. 5. Obszar TLS wątku (ang. Thread Local Storage) - każdy wątek ma pewien obszar pamięci przeznaczony na utrzymanie różnych danych administracyjnych takich jak TID, PID, początek stosu, kod ewentualnego errno i inne dane. Obszar TLS jest odpowiednikiem deskryptora procesu. 6. Procedura zakończenia (ang. Cancellation Handler - gdy wątek kończy się wykonywana jest procedura zakończenia w ramach której zwalniane są zasoby wątku. 13.2 Tworzenie wątku i synchronizacja 13.2.1 Tworzenie wątku Nowy wątek tworzy się przy pomocy funkcji pthread create. Funkcja ta tworzy wątek, którego kod znajduje się w funkcji jako argument func. Wątek jest uruchamiany z parametrem arg, a informacja o nim jest umieszczana w strukturze thread. 13.2.2 Synchronizacja W bibliotece pthread.h do zapewnienia wyłączności dostępu do danych stosuje się mechanizm mutex. Nazwa ta pochodzi od słów Mutual exclusion czyli wzajemne wykluczanie. 13.3 Funkcje 13.3.1 pthread create int pthread create(pthread t *thread, pthread attr t *attr, void (*func) (void*), void *arg) thread - identyfikator wątku - wartość nadawana przez funkcję attr - atrybuty wątku, gdy NULL przyjęte domyślnie func - procedura zawierająca kod wątku arg - argument przekazywany do wątku Funkcja zwraca 0 gdy sukces, -1 gdy błąd. 26
13.3.2 pthread exit Jawne zakończenie wątku następuje poprzez wywołanie funkcji: pthread exit(void *status) status - kod powrotu(będący wskaźnikiem) przekazywany jako status funkcji: pthread join(pthread t *thread, void *status) Zmienna wskazywanej w funkcji pthread exit nie może być umieszczona w pamięci lokalnej wątku, gdyż po jego zakończeniu obszar ten jest zwalniany i wartość zmiennej jest nieokreślona. Możliwe są dwa sposoby postępowania z kończonymi wątkami: 1. Z chwilą zakończeni się wątku zwalniane są wszystkie jego zasoby. 2. Zasoby zwalniane są z chwilą dołączenia wątku bieżącego do innego wątku (wykonującego funkcję pthread join). Postępowanie to uzależnione jest od ustalenia atrybutu PTHREAD CREATE JOINABLE, który ustalany jest podczas tworzenia wątku. 1. Gdy atrybut ten nie jest ustawiony, wątek zwalnia swe zasoby zaraz po zakończeniu. 2. Gdy atrybut jest ustawiony, wątek zwalnia zasoby po dołączeniu do innego wątku. 13.3.3 pthread join Proces bieżący może czekać na zakończenie wątku poprzez wywołanie funkcji: pthread join(pthread t *thread, void *status) thread - identyfikator wątku - wartość nadawana przez funkcję status - kod powrotu zwracany przez zakończony wątek Gdy wskazany jako parametr wątek nie zakończył się jeszcze, wątek bieżący jest wstrzymywany. Funkcja zwraca 0 gdy sukces, -1 gdy błąd. 13.3.4 mutex init Przed użyciem muteks musi być zainicjowany. Inicjacja następuje poprzez wykonanie funkcji: int pthread mutex init(pthread mutex t *mutex, pthread mutexattr t *attr) mutex - zadeklarowana wcześniej zmienna typu pthread mutex t attr - atrybuty muteksu, gdy attr jest równe NULL przyjęte będą wartości domyślne Funkcja zwraca 0 gdy sukces, -1 gdy błąd. Zainicjowany muteks pozostaje w stanie odblokowania. 27
13.3.5 mutex lock Przed dostępem do zasobu należy zapewnić sobie wyłączność w korzystaniu z tego zasobu. W tym celu wątek wykonuje funkcję: int pthread mutex lock(pthread mutex t *mutex) mutex - zadeklarowana wcześniej zmienna typu pthread mutex t Działanie funkcji zależy od stanu w jakim znajduje się muteks. 1. Gdy muteks jest wolny, następuje jego zablokowanie. 2. Gdy muteks jest zajęty, próba jego powtórnego zajęcia powoduje zablokowanie się wątku, który tę próbę podejmuje. 13.3.6 mutex unlock Użyty i zablokowany wcześniej zasób powinien być zwolniony. Zwolnienie zasobu odbywa się poprzez wywołanie funkcji: int pthread mutex unlock(pthread mutex t *mutex) mutex - zadeklarowana wcześniej zmienna typu pthread mutex t Działanie tej funkcji zależy od tego, czy inne wątki czekają zablokowane na muteksie. 1. Brak wątków zablokowanych na muteksie - stan muteksu zostaje zmieniony na wolny. 2. Są wątki zablokowane na muteksie - jeden z czekających wątków zostaje odblokowany. 13.4 Zmienne warunkowe Zmienne warunkowe są narzędziem do blokowania wątku wewnątrz sekcji krytyczne aż do momentu gdy pewien warunek zostanie spełniony. Warunek ten może być dowolny i niezależny od zmiennej warunkowej. Zmienna warunkowa musi być użyta w połączeniu z muteksem o ile konstrukcja ma zapewnić własności monitora. 13.4.1 cond init Inicjacja zmiennej warunkowej. int pthread cond init(pthread cond t *zw, pthread condattr t attr) zw - zadeklarowana wcześniej zmienna typu pthread cond t attr - atrybuty zmiennej warunkowej, gdy attr jest równe NULL przyjęte będą wartości domyślne 13.4.2 cond wait Zawieszenie wątku w oczekiwaniu na sygnalizację. int pthread cond wait(pthread cond t *zw, pthread mutex t mutex) zw - zadeklarowana wcześniej zmienna typu pthread cond t mutex - zadeklarowana wcześniej zmienna typu pthread mutex t 28
Funkcja powoduje zawieszenie bieżącego wątku w kolejce związanej ze zmienną warunkową zw. Jednocześnie blokada mutex zostaje zwolniona. Obie operacje są wykonywane w sposób atomowy. Gdy inny wątek wykona operację pthread cond signal(&zw) zablokowany wątek zostanie odblokowany, a blokada mutex zwolniona. 13.4.3 cond signal Wznowienie zawieszonego wątku. int pthread cond signal(pthread cond t *zw) zw - zadeklarowana wcześniej zmienna typu pthread cond t Jeden z wątków zablokowanych na zmiennej warunkowej zw zostanie zwolniony. 13.4.4 cond broadcast Wznowienie wszystkich zawieszonych wątków. int pthread cond broadcast(pthread cond t *zw) zw - zadeklarowana wcześniej zmienna typu pthread cond t Wszystkie wątki zablokowane na zmiennej warunkowej zw zostaną zwolnione. 13.5 Bariery Bariera jest narzędziem do synchronizacji procesów działających w ramach grup. Wywołanie funkcji pthread barrier wait() powoduje zablokowanie zadania bieżącego do chwili gdy zadana liczba wątków nie wywoła tej procedury. 13.6 Blokady czytelników i pisarzy Blokady typu wzajemne wykluczanie są zbyt restrykcyjne co prowadzi do ograniczenia równoległości i spadku efektywności aplikacji. Można je osłabić wiedząc czy przeprowadzana jest operacja odczytu czy zapisu. 13.6.1 Zasada działania blokad czytelników i pisarzy Odczyt może być wykonywany współbieżnie do innych odczytów. Zapis musi być wykonywany w trybie wyłącznym względem innych zapisów lub odczytów. 13.6.2 Stan blokady Wolna Zajęta do odczytu być może przez wiele wątków czytających Zajęta do zapisu 29
13.7 Wirujące blokady Wirujące blokady są środkiem zabezpieczenia sekcji krytycznej. Wykorzystują jednak czekanie aktywne zamiast przełączenia kontekstu wątku jak to się dzieje w muteksach. Blokada może być w dwóch stanach: Wolna Zajęta 13.7.1 Inicjacja wirującej blokady int pthread spin init(pthread spinlock t *blokada, int pshared) blokada - identyfikator wirującej blokady pthread spinlock t pshared - PTHREAD PROCESS SHARED - na blokadzie mogą operować wątki należące do różnych procesów PTHREAD PROCESS PRIVATE - na blokadzie mogą operować wątki należące do tego samego procesu 30
Funkcja inicjuje zasoby potrzebne wirującej blokadzie. Każdy proces, który może sięgnąć do zmiennej identyfikującej blokadę może jej używać. 13.7.2 Zajęcie blokady int pthread spin lock(pthread spinlock t *blokada) blokada - identyfikator wirującej blokady pthread spinlock t Działanie funkcji zależy od stanu blokady. Gdy blokada jest wolna następuje jej zajęcie. Gdy blokada jest zajęta wątek wykonujący funkcję pthread spin lock() ulega zablokowaniu do czasu, gdy inny wątek nie zwolni blokady wykonując funkcję pthread spin unlock(). 13.7.3 Zwolnienie blokady int pthread spin unlock(pthread spinlock t *blokada) blokada - identyfikator wirującej blokady pthread spinlock t Funkcja zwalnia blokadę i zajmowane przez nią zasoby. 14 Interfejs gniazdek Jednolity interfejs API (ang. Application Program Interface) do mechanizmów komunikacji sieciowej. Główna idea gniazdek polega na użyciu do komunikacji (lokalnej i zdalnej) tego samego mechanizmu, co dostępu do plików. Jest to mechanizm oparty o deskryptory plików i funkcje read i write. Termin gniazdko ma dwa znaczenie: 1. Biblioteka + funkcje interfejsowe (API) 2. Końcowy punkt komunikacji Biblioteka gniazdek maskuje mechanizmy transportu sieci. Własności gniazdek: Gniazdo jest identyfikowane przez liczbę całkowitą nazwaną deskryptorem gniazda. Gniazdo można nazwać i wykorzystywać do komunikacji z innymi gniazdami w tej samej domenie komunikacyjnej. 14.1 Domeny Komunikacja odbywa się w pewnej domenie. Od domeny zależy sposób adresowania w sieci. Są trzy podstawowe domeny: domena internetu wersja IPv4 - AF INET domena internetu wersja IPv6 - AF INET6 domena Unixa - AF UNIX 31