Systemy operacyjne Studia podyplomowe 2015-2016 Wydział Informatyki PB dr inż. Marcin Czajkowski materiały przygotowane przez dr inż. Wojciecha Kwedlo
Procesy i wątki
Pojęcie procesu Program = plik wykonywalny na dysku Proces = uruchomiony i wykonywany program w pamięci Program jest pojęciem statycznym. Proces ma naturę dynamiczną (zmieniającą się). Zmianie ulegają m.in. Licznik rozkazów (adres ostatnio wykonywanej instrukcji) Rejestry procesora Wskaźnik stosu Proces ma przestrzeń adresową Kod Dane zainicjalizowane Uruchom program Dane niezainicjalizowane Stos
Model procesów Pojedynczy licznik rozkazów (punkt widzenia procesora) A B C B D A Wiele liczników rozkazów (punkt widzenia systemu oper.) B C D Z punktu widzenie procesora na komputerze jest wykonywany jeden program Z punktu widzenia systemu jednocześnie jest wykonywanych wiele programów. D C B A Czas
Stany procesu Nowy proces został utworzony. Nowy Gotowy proces czeka na przydział procesora. 1 Gotowy 2 Proces mógłby się wykonywać, ale nie wykonuje się, ponieważ w tej chwili wykonuje się jakiś inny proces. Aktywny wykonywane są instrukcje procesu. 5 3 W systemie z jednym procesorem w danej chwili jeden proces może być aktywny Oczekujący 4 Aktywny Oczekujący (uśpiony) proces czeka na zdarzenie (np. Zakończenie operacji we-wy). 7 7 Zakończony 6 Na poprzednim wykładzie proces, który zainicjalizował transmisję DMA lub wysłała znak do drukarki, był usypiany w oczekiwaniu na przerwanie. Proces w stanie uśpionym nie otrzyma procesora. Zakończony proces zakończył działanie
Stany procesu Nowy 1 Gotowy 2 5 Oczekujący 4 3 Aktywny 7 7 Zakończony 6
Stany procesu Nowy proces został utworzony. Nowy Gotowy proces czeka na przydział procesora. 1 Gotowy 2 Proces mógłby się wykonywać, ale nie wykonuje się, ponieważ w tej chwili wykonuje się jakiś inny proces. Aktywny wykonywane są instrukcje procesu. 5 3 W systemie z jednym procesorem w danej chwili jeden proces może być aktywny Oczekujący 4 Aktywny Oczekujący (uśpiony) proces czeka na zdarzenie (np. Zakończenie operacji we-wy). 7 7 Zakończony 6 Na poprzednim wykładzie proces, który zainicjalizował transmisję DMA lub wysłała znak do drukarki, był usypiany w oczekiwaniu na przerwanie. Proces w stanie uśpionym nie otrzyma procesora. Zakończony proces zakończył działanie
Stany procesu Nowy proces został utworzony. Nowy Gotowy proces czeka na przydział procesora. 1 Gotowy 2 Proces mógłby się wykonywać, ale nie wykonuje się, ponieważ w tej chwili wykonuje się jakiś inny proces. Aktywny wykonywane są instrukcje procesu. 5 3 W systemie z jednym procesorem w danej chwili jeden proces może być aktywny Oczekujący 4 Aktywny Oczekujący (uśpiony) proces czeka na zdarzenie (np. Zakończenie operacji we-wy). 7 7 Zakończony 6 Na poprzednim wykładzie proces, który zainicjalizował transmisję DMA lub wysłała znak do drukarki, był usypiany w oczekiwaniu na przerwanie. Proces w stanie uśpionym nie otrzyma procesora. Zakończony proces zakończył działanie
Przejścia pomiędzy stanami procesu 1 (Nowy => Gotowy). Nowo utworzony proces przechodzi do kolejki procesów gotowych. Planista długoterminowy (ang. long-term scheduler) w systemach wsadowych. 2 (Gotowy => Aktywny) Proces otrzymuje przydział procesora. 3 (Aktywny => Gotowy) Procesowi został odebrany procesor (i przekazany innemu procesowi). Przejściami 2 oraz 3 zarządza planista krótkoterminowy (ang. short-term). 4 (Aktywny => Oczekujący) Proces przechodzi w stan oczekiwania na zajście zdarzenia. (np. na zakończenie transmisji wejścia -wyjścia patrz poprzedni wykład)v
Przejścia pomiędzy stanami procesu 5 (Oczekujący => Gotowy) Zdarzenie na które czekał proces nastąpiło (przerwanie we/wy na poprzednim wykładzie). Przejścia 4 oraz 5 są wykonywane przy przeprowadzeniu synchronicznej operacji wejścia wyjścia (ale nie tylko). 6 (Aktywny => Zakończony). Proces zakończył pracę (np. funkcja exit w Uniksach, błąd ochrony) 7 (Gotowy =>Zakończony oraz Oczekujący => Zakończony). Proces został zakończony przez inny proces (np. funkcja kill w systemie Unix).
Dodatkowy stan zawieszony (ang. suspended) Proces oczekuje bardzo długo na operacje we-wy (np. polecenie login) Przejście do stanu zawieszonego Pamięć zajmowana przez proces podlega wymianie (ang. swapping) tzn. zapisaniu na dysk do obszaru wymiany (swap area). Zwolniona pamięć może być wykorzystana przez inne procesy. Po zajściu zdarzenia proces ponownie wczytywany z obszaru wymiany Inne przyczyny zawieszenie procesu. Żądanie użytkownika Brak pamięci w systemie Proces co jakiś czas cyklicznie wykonuje jakąś czynność np. Sprawozdawczość Przejściami do i z stanu zawieszenie zarządza planista średnioterminowy (ang. medium-term scheduler)
Diagram przejść z uwzględnieniem stanu zawieszenia. Planista długoterminowy Planista krótkoterminowy najważniejszy i występujący w każdym systemie. Rozstrzyga problem: Któremu procesowi w stanie gotowym mam przydzielić procesor Planista średnioterminowy
Blok kontrolny procesu ang. Process Control Block - PCB PCB służy do przechowywania informacji o procesie istotnych z punktu widzenia systemu operacyjnego Stan procesu Identyfikator procesu Licznik rozkazów Rejestry procesora Informacja o przydzielonej pamięci Informacja o otwartych plikach Informacja o połączeniach sieciowych Informacja niezbędna do tworzenia systemowych struktur danych. System operacyjny posługuje się różnymi kolejkami procesów. Jeżeli kolejki są implementowane jako listy z dowiązaniami, PCB może zawierać dowiązanie (wskaźnik) do następnego elementu w kolejce
Przełączenie kontekstu pomiędzy procesami
Przełączenie kontekstu (ang. context switch), a przełączenie trybu (ang. mode switch) P 1 P 2 P 3 P 4 Przełączenie kontekstu to zmiana procesu. Jądro Jądro Jądro Jądro Przełączenie kontekstu Przełączenie trybu zmiana trybu pracy procesora (jądra <=> użytkownika) W większości systemów (Uniksy,Windows) przyjęto model, w którym funkcje systemu wykonują się w kontekście procesu użytkownika. W uproszczeniu model ten zakłada że system operacyjny jest kolekcją procedur wywoływanych przez procesy w celu wykonania pewnych usług. Stąd mówiłem o procesie wykonującym się w trybie jądra (jeżeli wykonywany jest kod jądra). Przejście od programu użytkownika do programu jądra w wyniku wywołania funkcji systemowej (przerwanie programowe) lub przerwania sprzętowego wiąże się z przełączeniem trybu. Zmiana trybu jest znacznie mniej kosztowna niż zmiana kontekstu.
Utworzenie procesu Proces rodzicielski tworzy proces potomny, który z kolei może stworzyć kolejne procesy. Powstaje drzewo procesów. Współdzielenie zasobów. Procesy rodzicielski i potomny mogą Współdzielić część zasobów Współdzielić wszystkie zasoby Nie współdzielić żadnych zasobów Wykonywanie Procesy rodzicielski i potomny wykonują się współbieżnie Proces rodzicielski oczekuje na zakończenie procesu potomnego. Przestrzeń adresowa Odrębna przestrzeń adresowa dla procesu potomnego (fork w systemie Unix proces potomny wykonuje się w nowej przestrzeni adresowej będącej kopią przestrzeni procesu rodzicielskiego) Proces ma nowy program załadowany do nowej przestrzeni adresowej (CreateProcess w Win32) Proces potomny i rodzicielski wykonują się w tej samej przestrzeni adresowej (clone w Linuksie; wątki Java i POSIX)
Wywołania systemowe fork, exec, wait w Unix-ie #include <stdio.h> #include <unistd.h> int main(int argc, char *argv[]) { int pid; pid = fork(); if (pid < 0) { /* Błąd!!! */ fprintf(stderr, "Fork Failed"); exit(-1); } else if (pid == 0) { /* proces potomny */ execlp("/bin/ls","ls",null); } else { /* proces rodzicielski */ wait(null); printf("child Complete"); exit(0); } } fork tworzy nowy proces Wywoływana z procesu rodzicielskiego Powracają z niej proces potomny i rodzicielski exec zastępuje obraz bieżącego procesu nowym programem. proces wołający nigdy nie wraca z exec (chyba że powstanie błąd) wait blokuje proces do momentu zakończenia procesu potomnego
Zakończenie procesu Zakończenie na własne żądanie. Proces sam podejmuje decyzję o zakończeniu pracy wywołując odpowiednie wywołanie systemowe. (system Unix: exit ). W programie w języku C jest to robione automatycznie po zakończeniu funkcji main Proces został zakończony w wyniku akcji innego procesu Unix: proces otrzymał sygnał SIGKILL. Polecenie kill w shellu, funkcje systemowe raise oraz kill. Proces został zakończony przez system operacyjny Naruszenie mechanizmów ochrony. Przekroczenie ograniczeń na przyznany czas procesora. Proces rodzicielski się zakończył (w niektórych systemach) Cascading termination
Wielowątkowość Jeden proces wykonuje się w wielu współbieżnych wątkach (ang. thread). Każdy wątek (inna nazwa: proces lekki, ang. lightweight) Ma swój własny stan (Aktywny, Gotowy, Zablokowany,... ) Ma swoje wartości rejestrów i licznika rozkazów. Ma swój własny stos (zmienne lokalne funkcji!!!). Ma dostęp do przestrzeni adresowej, plików i innych zasobów procesu WSZYSTKIE WĄTKI TO WSPÓŁDZIELĄ!!! Operacje zakończenia, zawieszenia procesu dotyczą wszystkich wątków. Procesy są od siebie izolowane, wątki nie!
Procesy i wątki Wątek 1 Licznik rozkazów Rejestry Stos i wskaźnik stosu Stan Proces Przestrzeń adresowa Otwarte pliki Procesy potomne Obsługa sygnałów Sprawozdawczość Zmienne globalne Wątek 2 Licznik rozkazów Rejestry Stos i wskaźnik stosu Stan Wątek 3 Licznik rozkazów Rejestry Stos i wskaźnik stosu Stan
Proces z jednym wątkiem Standardowy Unix MS-DOS
Proces z wieloma wątkami Linux MS-Windows POSIX OS/2 Solaris
Cechy wątków Zalety Utworzenie i zakończenie wątku zajmuje znacznie mniej czasu niż w przypadku procesu Możliwość szybkiego przełączania kontekstu pomiędzy wątkami tego samego procesu Możliwość komunikacji wątków bez pośrednictwa systemu operacyjnego Możliwość wykorzystania maszyn wieloprocesorowych SMP Wady Źle zachowujący się wątek może zakłócić pracę innych wątków tego samego procesu. W przypadku dwóch procesów o odrębnych przestrzeniach adresowych nie jest to możliwe
Wątki na poziomie użytkownika ang. user-level threads System operacyjny nie jest świadom istnienia wątków. Zarządzanie wątkami jest przeprowadzane przez bibliotekę w przestrzeni użytkownika. Wątek A wywołuje funkcję read Standardowo funkcja systemowa read jest synchroniczna (usypia do momentu zakończenia operacji) Jednak sprytna implementacja w bibliotece wywołuje wersję asynchroniczną i przełącza się do wątku B. Rozwiązanie to jest szybkie, ma jednak wady: Dwa wątki nie mogą się wykonywać współbieżnie na dwóch różnych procesorach. Nie można odebrać procesora jednemu wątkowi i przekazać drugiemu
Wątki na poziomie jądra ang. kernel-level threads Wątek jest jednostką systemu operacyjnego. Wątki podlegają szeregowaniu przez jądro. W systemie SMP wątki mogą się wykonywać na różnych procesorach. Przetwarzanie równoległe. Windows i Linux wykorzystują tę metodę.
Przykład użycia wątków: Program do przetwarzania obrazów Użytkownik wydał polecenie Save Jego wykonanie może potrwać kilkadziesiąt sekund. Kto w tym czasie będzie obsługiwał mysz i klawiature? Wątek główny odpowiadający za interakcję z użytkownikiem. Po wydaniu polecenia Save tworzy wątek roboczy zapisujący obraz do pliku. Następnie powraca do konwersacji z użytkownikiem Dzięki temu aplikacja nie jest zablokowana (klepsydra w Win) na czas zapisywania Wątek roboczy wykonuje zapis i kończy pracę. Uwaga na synchronizację wątków!!! Co się stanie gdy po wydaniu polecenia Save natychmiast wydamy polecenie Usuń obraz? Sytuacja wyścigu (ang. race).
Problemy z wątkami W standardowej bibliotece C, w wersji wielowątkowej, errno jest implementowane jako prywatna zmienna globalna (nie współdzielona z innymi wątkami) Kilka wątków jednocześnie wywołuje funkcje malloc/free - może dojść do uszkodzenia globalnych struktur danych (listy wolnych bloków pamięci) Potrzeba synchronizacji => może prowadzić do spadku wydajności
Problemy z wątkami Proces otrzymuje sygnał: Wszystkie wątki otrzymują sygnał Wybrany wątek otrzymuje sygnał Wątek aktualnie aktywny otrzymuje sygnał Proces wykonuje fork. Czy duplikować jedynie działający wątek, czy też wszystkie wątki? Proces wywołuje exit. Zakończyć proces czy też jedynie aktywny wątek? Anulowanie wątku (ang cancellation). Wykonać natychmiast. Wątek co jakiś czas sprawdza czy nie został anulowany. Standard POSIX zawiera odpowiedzi na powyższe problemy.
POSIX threads utworzenie i dołączenie wątku void *thread(void *param) { // tu kod wątku // możemy przekazać wynik return NULL } int main() { pthread_t id; // Parametr przekazywany wątkowi void *param=null; pthread_create(&id,null,&thread,param); // Funkcja thread w odrębnym wątku // współbieżnie z main // id przechowuje identyfikator wątku Funkcja pthread_create tworzy nowy wątek. Rozpoczyna on pracę od funkcji, której adres przekazano jako trzeci argument. Funkcja pthread_join usypia wywołujący ją wątek do momentu, kiedy wątek o identyfikatorze przekazanym jako pierwszy argument zakończy pracę. Zakończenie pracy wątku powrót z funkcji która go rozpoczyna. } void *result; // Czekaj na zakończenie wątku pthread_join(id,&result); // Wynik w result,zamiast &result można // przekazać NULL
Synchronizacja procesów (i wątków)
Procesy wykonują się współbieżnie. Potrzeba synchronizacji Jeżeli w 100% są izolowane od siebie, nie ma problemu. Problem, jeżeli procesy komunikują się lub korzystają ze wspólnych zasobów. Przykład: Proces A przygotowuje wyniki, a proces B drukuje je na drukarce: Jak zapewnić, aby B nie zaczął drukować przed zakończeniem przygotowania wyników przez A. Potrzeba utrzymywania wspólnych zasobów w spójnym stanie. Np. proces A dodaje element do listy (z dowiązaniami), a jednocześnie B przegląda listę, która w momencie trwania operacji dodania ma stan niespójny Potrzeba synchronizacji dotyczy także współbieżnych wątków. W kolejnych slajdach będę używał zgodnie z literaturą pojęcia proces, jednakże wszystkie przykłady oparte są na założeniu, że procesy wykonują się współbieżnie. Zatem bardziej odpowiednie byłoby użycie terminu wątki. Np. dwa wątki wywołują funkcję malloc, która przydziela pamięć.
Problem producenta-konsumenta (z ograniczonym buforem ang. bounded buffer) Jeden proces (producent) generuje (produkuje) dane a drugi (konsument) je pobiera (konsumuje). Wiele zastosowań w praktyce np. drukowanie. Jedno z rozwiązań opiera się na wykorzystaniu tablicy działającej jak bufor cykliczny, co pozwala na zamortyzowanie chwilowych różnic w szybkości producenta i konsumenta. Tę wersję problemu nazywa się problemem z ograniczonym buforem. Problem: jak zsynchronizować pracę producenta i konsumenta np. producent zapełnia bufor, konsument usiłuje pobrać element z pustego bufora. Dwie wersje: dla jednego producenta i konsumenta i wielu producentów i konsumentów. out=3 counter=8 n=16 in=11 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Stąd konsument pobierze kolejny egzemplarz Tu producent wstawi kolejny egzemplarz
Producent konsument z buforem cyklicznym Zmienne wspólne const int n; // rozmiar bufora typedef Item; Item buffer[n]; // bufor int out=0; // indeks konsumenta int in = 0; // indeks producenta counter = 0; // liczba elementów w buforze Producent umieszcza element w buforze na pozycji in Czeka, jeżeli counter==n, tzn. bufor pełny Konsument pobiera element z bufora z pozycji out Czeka, jeżeli counter==0 tzn. bufor pusty. Zmienne in oraz out zmieniane są zgodnie z regułą i=(i+1)%n Wartości kolejnych indeksów do tablicy buffer Jeżeli i==n-1 to nowei=0
Rozwiązanie (prymitywne) dla jednego producenta i konsumenta z wykorzystaniem aktywnego czekania Counter += 1; Counter -= 1; Producent Item pitm; while (1) { produce an item into pitm while (counter == n) ; buffer[in] = pitm; in = (in+1) % n; counter += 1; } Konsument Item citm; while (1) { while (counter == 0) ; citm = buffer[out]; out = (out+1) % n; counter -= 1; consume the item in citm } Dygresja: dlaczego rozwiązanie oczywiście niepoprawne dla więcej niż jednego konsumenta albo producenta? Counter jest zmienną współdzieloną przez obydwa procesy. Co się może stać gdy jednocześnie obydwa procesy spróbują ją zmienić?
Gdzie tkwi problem? Architektura RISC: ładuj do rejestru, zwiększ wartość,zapisz wynik. Niech x oznacza jest modyfikowaną zmienną counter. Przyjmijmy, że x=5 Rozważmy dwie możliwe kolejności wykonywania instrukcji poszczególnych procesów. a) Poprawna wartość 5. b) Niepoprawna wartość 4. Wybór jednej z tych wielkości niedeterministyczny. Sytuacja wyścigu (ang. race condition) a) Producent Konsument R1 <= x R1 = R1+1 R1 => x R3 <= x R3 = R3-1 R3 => x b) Producent Konsument R1 <= x R3 <= x R3 = R3-1 R1 = R1+1 R1 => x R3 => x x=5 x=5 x=5 x=4!
Wyścigi - races O warunku wyścigu mówimy gdy wynik zależy od kolejności wykonywania instrukcji procesów. W poprawnym programie nie powinno być wyścigów. Innymi słowy Uwaga: Ze względu na niedeterminizm (nigdy nie wiemy w jakiej kolejności wykonają się procesy) do błędu może (ale nie musi dojść). W związku z tym przydatność testowania do badanie poprawności programów współbieżnych jest mocno ograniczona. Nic tu nie zastąpi analizy kodu (a często mniej lub bardziej formalnych dowodów poprawności). Typowe sytuacje: błąd objawia się przeciętnie raz na trzy miesiące nieprzerwanej pracy programu. W naszym przykładzie musimy zapewnić, aby jeden proces w danej chwili mógł odwoływać się do zmiennej Counter. Innymi słowy dostęp do tej zmiennej powinien znajdować się wewnątrz sekcji krytycznej.
Problem sekcji krytycznej Chcemy, aby w jednej chwili w sekcji krytycznej mógł przebywać tylko jeden proces Założenia Proces na przemian przebywa w sekcji krytycznej albo wykonuje inne czynności Proces przebywa w sekcji krytycznej przez skończony czas. Rozwiązanie Czynności wykonywane przy wejściu do sekcji - protokół wejścia Czynności wykonywane przy wyjściu z sekcji - protokół wyjścia A wchodzi do sekcji krytycznej A opuszcza sekcj ę krytyczną Proces A Proces B B próbuje wej ść do sekcji krytycznej B wstrzymany B wchodzi do sekcji krytycznej B opuszcza sekcj ę krytyczną Czas
Warunki dla rozwiązania sekcji krytycznej Wzajemne wykluczanie. W danej chwili tylko jeden proces może być w sekcji krytycznej. Postęp Proces który nie wykonuje sekcji krytycznej nie może blokować procesów chcących wejść do sekcji. Ograniczone czekanie Proces nie może czekać na wejście do sekcji krytycznej w nieskończoność
Rozwiązania problemu sekcji krytycznej Wyłącz przerwania Nie działa w systemach wieloprocesorowych Problematyczne w systemach z ochroną Wykorzystywane do synchronizacji w obrębie jądra (zakładając jeden procesor) Rozwiązania z czekaniem aktywnym Algorytm Petersona dla dwóch procesów wymaga trzech zmiennych!!! Algorytm piekarni dla wielu procesów. Rozwiązania wykorzystujące specjalne instrukcje maszynowe np. rozkaz zamiany: XCHG rejestr, pamięć Architektura systemu musi zapewniać atomowe wykonanie instrukcji. W systemach wieloprocesorowych nie jest to trywialne
Przykład realizacji z wykorzystaniem instrukcji XCHG Niech instrukcja XCHG (zmienna,wartość) nadaje zmiennej nową wartość i jednocześnie zwraca starą. Zakładamy, że jest to instrukcja atomowa nie może być przerwana. Na ogół wartość przechowywana jest w rejestrze. Implementacja tej instrukcji nie jest trywialna wymaga dwóch cykli dostępu do pamięci. Na szczęście to problem projektantów sprzętu. Możemy podać stosunkowo proste rozwiązanie problemu sekcji krytycznej. int lock=0; // zmienna wykorzystana do synchronizacji // lock==1 oznacza, że jakiś proces jest w sekcji krytycznej void Process() { while (1) { // Wynik xchg równy jeden oznacza, że poprzednia wartość była równa 1 // zatem ktoś inny był w sekcji krytyczne while(xchg(lock,1)==1); // Protokół wejścia // Proces wykonuje swoją sekcję krytyczną, wiemy że lock==1 } lock=0; // Protokół wyjścia // Proces wykonuje pozostałe czynności }
Dlaczego potrzebujemy XCHG? Ktoś mógłby zaproponować ulepszenie nie wymagające tej instrukcji. while (lock==1); // czekamy, aż inni opuszczą sekcje krytyczną. lock=1; // wchodzimy do sekcji krytycznej i zabraniamy tego innym. Niestety to ulepszenie jest niepoprawne - prowadzi do wyścigu. Dlaczego? Wskazówka: wiele się może zmienić pomiędzy wyjściem z pętli while a przypisaniem zmiennej lock.
Czekanie aktywne Marnowany jest czas procesora Zmarnowany czas można by przeznaczyć na wykonanie innego procesu. Uzasadnione gdy: Czas oczekiwania stosunkowo krótki (najlepiej krótszy od czasu przełączenia kontekstu) Liczba procesów @ Liczba procesorów Przykład zastosowania jądro Linux-a w wersji SMP Funkcje typu spin_lock Alternatywą do czekania aktywnego jest przejście procesu w stan zablokowany Semafory Monitory
Semafor zliczający Zmienna całkowita S i trzy operacje: nadanie wartości początkowej, oraz Wait i Signal. Definicja klasyczna (E. Dijkstra): Wait (czekaj): while (S<=0) ; S-- Signal(sygnalizuj): S++ Operacje Wait i Signal są operacjami atomowymi Początkowa wartość S liczba wywołań operacji Wait bez wstrzymywania. Definicja klasyczna oparta jest na aktywnym czekaniu. W praktyce używa się innej definicji opartej na usypianiu procesów (M. Ben-Ari) : Wait: Jeżeli S>0, to S=S-1, w przeciwnym wypadku wstrzymaj (przełącz w stan oczekujący) wykonywanie procesu proces ten nazywany wstrzymanym przez semafor. Signal: Jeżeli są procesy wstrzymane przez semafor, to obudź jeden z nich, w przeciwnym wypadku S=S+1. Implementacja według powyższej definicji m.in. w standardzie POSIX threads. funkcje sem_init, sem_wait oraz sem_post (odpowiednik signal) funkcja sem_trywait nie wstrzymuje procesu, ale zwracająca kod błędu jeżeli proces byłby wstrzymany.
Implementacja semafora Semafor to: (a) Bieżąca wartość + (b) Lista (np. FIFO) procesów oczekujących Nieco zmodyfikowana (ale równoważna z definicją Ben Ariego) implementacja zakładamy że wartość zmiennej może być ujemna wtedy przechowuje ona liczbę wstrzymanych procesów. Zakładamy dostępność dwóch funkcji na poziomie jądra systemu: Sleep: realizuje przejście procesu Aktywny=>Oczekujący Wakeup: Oczekujący=>Gotowy Oczywiście Wait i Signal muszą być operacjami atomowymi ich wykonanie nie może być przerwane przełączeniem kontekstu do innego procesu. class Semaphore { int value; ProcessList pl; public: Semaphore(int a) {value=a;} void Wait (); void Signal (); }; Semaphore::Wait() () { value -= 1; if (value < 0) { Add(this_process,pl) Sleep (this_process); } } Semaphore::Signal () { value += 1; if (value <= 0) { Process P=Remove(P) Wakeup (P); } }
Semaphore Sem(1); Rozwiązanie sekcji problemu sekcji krytycznej przy pomocy semaforów void Process() { while (1) { Sem.Wait(); // Proces wykonuje swoją sekcję krytyczną Sem.Signal() // Proces wykonuje pozostałe czynności } } Protokół wejścia i wyjścia są trywialne, ponieważ semafory zaprojektowano jako narzędzie do rozwiązania problemu sekcji krytycznej Zmodyfikujmy warunki zadania, tak że w sekcji krytycznej może przebywać jednocześnie co najwyżej K procesów. Pytanie. Co należy zmienić w programie?
Zastosowanie semafora to zapewnienia określonej kolejności wykonywania instrukcji procesów Chcemy aby instrukcja A jednego procesu wykonała się po instrukcji B drugiego. Używamy semafora S zainicjalizowanego na zero...... S.Wait(); A;..... B; S.Signal();
Problem producent-konsument z wykorzystaniem semaforów const int n; Semaphore empty(n),full(0),mutex(1); Item buffer[n]; Producent int in = 0; Item pitem; while (1) { // produce an item into pitem empty.wait(); mutex.wait(); buffer[in] = pitem; in = (in+1) % n; mutex.signal(); full.signal(); } Konsument int out = 0; Item citem; while (1) { full.wait(); mutex.wait(); citem = buffer[out]; out = (out+1) % n; mutex.signal(); empty.signal(); // consume item from citem } Semafor mutex zapewnia wzajemne wykluczanie przy dostępie do zmiennych współdzielonych. Semafor full zlicza liczbę elementów w buforze (pełnych miejsc w tablicy). Wstrzymuje konsumenta gdy w buforze nie ma żadnego elementu. Semafor empty zlicza liczby pustych miejsc w tablicy. Wstrzymuje producenta gdy w tablicy nie ma wolnego miejsca..
Semafory binarne Zmienna może przyjmować tylko wartość zero lub jeden Operacje mają symbole WaitB, SingalB Wartość jeden oznacza, że można wejść do semafora (wykonać WaitB) Wartość zero oznacza że operacja WaitB wstrzyma proces. Mogą być prostsze w implementacji od semaforów zliczających. Implementacje Mutexy w POSIX threads. (pthread_mutex_create, phread_mutex_lock, pthread_mutex_unlock). W win32 mutexy noszą nazwę sekcji krytycznych W Javie mutex jest związany z każdym obiektem Słowo kluczowe synchronized. Więcej o Javie przy omawianiu monitorów
Blokada (Zakleszczenie, ang. deadlock) Zbiór procesów jest w stanie blokady, kiedy każdy z nich czeka na zdarzenie, które może zostać spowodowane wyłącznie przez jakiś inny proces z tego zbioru. Samochody nie mają wstecznego biegu = Brak wywłaszczeń zasobów
Przykład blokady Sekwencja instrukcji prowadząca do blokady. P 0 wykonał operacje A.Wait() Semaphore A(1),B(1); P 1 wykonał operacje B.Wait() P 0 usiłuje wykonać B.Wait() P 1 usiłuje wykonać A.Wait() P 0 czeka na zwolnienie B przez P 1 P 1 czeka na zwolnienie B przez P 0 Będą czekały w nieskończoność!!! Do blokady może (ale nie musi) dojść. Pytanie: Jak w tej sytuacji zagwarantować brak blokady? Proces P 0 A.Wait(); B.Wait();... B.Signal(); A.Signal(); Proces P 1 B.Wait(); A.Wait();... A.Signal(); B.Signal();
Opis formalny: graf alokacji zasobów P 0 semafor A P 1 Okrąg oznacza proces, a prostokąt zasób. Strzałka od procesu do zasobu => proces czeka na zwolnienie zasobu Strzałka od zasobu do procesu => proces wszedł w posiadanie zasobu. semafor B Stan blokady ma miejsce, wtedy i tylko wtedy gdy w grafie alokacji zasobów występuje cykl. Jedna z metod uniknięcia blokady => nie dopuszczaj do powstania cyklu. Np. każdy proces wchodzi w posiadanie zasobów w określonym porządku (identycznym dla wszystkich procesów). W literaturze (Silberschatz i wsp.) opisano wersję z więcej niż jednym egzemplarzem zasobu (np. drukarki)
Zagłodzenie (ang. starvation) Proces czeka w nieskończoność, pomimo że zdarzenie na które czeka występuje. (Na zdarzenie reagują inne procesy) Przykład: Jednokierunkowe przejście dla pieszych, przez które w danej chwili może przechodzić co najwyżej jedna osoba. Osoby czekające na przejściu tworzą kolejkę. Z kolejki wybierana jest zawsze najwyższa osoba Bardzo niska osoba może czekać w nieskończoność. Zamiast kolejki priorytetowej należy użyć kolejki FIFO (wybieramy tę osobę, która zgłosiła się najwcześniej). Inny przykład: z grupy procesów gotowych planista krótkoterminowy przydziela zawsze procesor najpierw procesom profesorów a w dalszej kolejności procesom studentów. Jeżeli w systemie jest wiele procesów profesorów, to w kolejce procesów gotowych znajdzie się zawsze co najmniej jeden i proces studenta będzie czekał w nieskończoność na przydział procesora.
Problem pięciu filozofów Każdy filozof siedzi przed jednym talerzem Każdy filozof na przemian myśli i je Do jedzenia potrzebuje dwóch widelców Widelec po lewej stronie talerza. Widelec po prawej stronie talerza. W danej chwili widelec może być posiadany tylko przez jednego filozofa. Zadanie: Podaj kod dla procesu i-tego filozofa koordynujący korzystanie z widelców.
Problem czytelników i pisarzy Modyfikacja problemu sekcji krytycznej. Wprowadzamy dwie klasy procesów: czytelników i pisarzy. Współdzielony obiekt nazywany jest czytelnią. W danej chwili w czytelni może przebywać Jeden proces pisarza i żaden czytelnik. Dowolna liczba czytelników i żaden pisarz. Rozwiązanie prymitywne: Potraktować czytelnię jak obiekt wymagający wzajemnego wykluczania wszystkich typów procesów. Prymitywne, ponieważ ma bardzo słabą wydajność. Jeżeli na wejście do czytelni czeka wielu czytelników i żaden pisarz to możemy wpuścić od razu wszystkich czytelników W literaturze opisano rozwiązania: Z możliwością zagłodzenia pisarzy Z możliwością zagłodzenia czytelników Poprawne
Problem śpiącego fryzjera c Jeden proces fryzjera i wiele procesów klientów. Współdzielone zasoby: n krzeseł w poczekalni i jedno krzesło fryzjera Napisz program koordynujący pracę fryzjera i klientów
Wady semaforów Jeden z pierwszych mechanizmów synchronizacji Generalnie jest to mechanizm bardzo niskiego poziomu - trochę odpowiadający programowaniu w assemblerze. Duża podatność na błędy, trudno wykazać poprawność programu Przykład: Jeżeli zapomnimy o operacji signal, nastąpi blokada Bardziej strukturalne mechanizmy synchronizacji Regiony krytyczne Monitory
Regiony krytyczne Współdzielona zmienna v typu T jest deklarowana jako: var v: shared T Dostęp do zmiennej v wykonywany przy pomocy operacji region v when B do S B jest wyrażeniem logicznym Tak długo, jak instrukcja S się wykonuje, żaden inny proces nie może się odwołać do zmiennej v. Jeżeli wyrażenie B nie jest spełnione, to proces jest wstrzymywany do momentu jego spełnienia.
Przykład: producent-konsument z ograniczonym buforem var buffer: shared record pool: array [0..n 1] of item; count,in,out: integer end; Deklaracja zmiennej współdzielonej. region buffer when count < n do begin end; pool[in] := nextp; in:= in+1 mod n; count := count + 1; Wstawienie elementu nextp do bufora. (Producent). region buffer when count > 0 do begin nextc := pool[out]; out := out+1 mod n; count := count 1; end; Usunięcie elementu nextc z bufora (Konsument).
Idea monitora (a właściwie zmiennej warunkowej) Udostępnienie procesom operacji pozwalającej procesowi wejść w stan uśpienia (zablokowania) wait oraz operacji signal pozwalającej na uśpienie obudzonego procesu. Ale tu natrafiamy na (stary) problem wyścigów, który ilustruje poniższy przykład: if (Jeszcze_nie_bylo_zdarzenia_muszę_wykonać_wait) wait() // to zaczekam Co się stanie, jeżeli zdarzenie na które czeka proces zajdzie po instrukcji if, ale przed uśpieniem procesu? Proces zgubi zdarzenie (i być może nigdy się nie obudzi) W takim razie wykonajmy cały ten kod wewnątrz sekcji krytycznej? Ale gdy proces wykona wait() - to przejdzie w stan uśpienia nie zwalniając sekcji krytycznej. Przy próbie wejścia do sekcji przez inny proces na pewno dojdzie do blokady. Rozwiązanie: Atomowa operacja wait powodująca jednoczesne uśpienie procesu i wyjście z sekcji krytycznej
Monitory monitor mon { int foo; int bar; public void proc1( ) { } public void proc2( ) { } }; Pseudokod przypominający definicję klasy C++. Współdzielone zmienne foo oraz bar są dostępne wyłącznie z procedur monitora. Procesy synchronizują się wywołując procedury monitora (np. proc1 i proc2) Tylko jeden proces (wątek) może w danej chwili przebywać w procedurze monitora. Gwarantuje to automatycznie wzajemne wykluczanie. Mówimy że proces przebywa wewnątrz monitora.
Zmienne warunkowe (ang. condition) Problem: proces postanawia zaczekać wewnątrz monitora aż zajdzie zdarzenie sygnalizowane przez inny proces. Jeżeli proces po prostu zacznie czekać, nastąpi blokada, bo żaden inny proces nie będzie mógł wejść do monitora i zasygnalizować zdarzenia. Zmienne warunkowa (typu condition). Proces, będący wewnątrz monitora, może wykonać na niej dwie operacje. Niech deklaracja ma postać: Condition C; C.wait() Zawiesza wykonanie procesu, i jednocześnie zwalnia monitor pozwalając innym procesom wejść do monitora. C.signal() Jeżeli nie ma procesów zawieszonych przez operację wait nic się nie dzieje. W przeciwnym wypadku dokładnie jeden proces zawieszony przez operacje wait zostanie wznowiony. (od następnej instrukcji po wait). Możliwa jest trzecia operacja C.signallAll() wznawiająca wszystkie zawieszone procesy.
Przykład 1: Implementacja semafora zliczającego przy pomocy monitora monitor Semafor { int Licznik; Condition NieZero; // coś na kształt konstruktora Semafor(int i) { Licznik=i; } void wait() { if (Licznik==0) NieZero.wait(); Licznik=Licznik-1; } void signal() { Licznik=Licznik+1; NieZero.signal(); } }; Deklaracja: Semafor S(1); Proces potrzebujący wzajemnego wykluczania. S.wait() // sekcja krytyczna S.signal() // pozostałe czynności
Semantyka Hoare'a i semantyka Mesa Przypuśćmy że proces P wykonał operację wait i został zawieszony. Po jakimś czasie proces Q wykonuje operacje signal odblokowując P. Problem: który proces dalej kontynuuje pracę: P czy Q. Zgodnie z zasadą działania monitora tylko jeden proces może kontynuować pracę. Semantyka Mesa Proces który wywołał operacje signal (Q) kontynuuje pierwszy. P może wznowić działanie, gdy Q opuści monitor. Wydaje się być zgodna z logiką, po co wstrzymywać proces który zgłosił zdarzenie. Semantyka Hoare'a Proces odblokowany (P) kontynuuje jako pierwszy. Może ułatwiać pisanie poprawnych programów. W przypadku semantyki Mesa nie mamy gwarancji, że warunek, na jaki czekał P jest nadal spełniony (P powinien raz jeszcze sprawdzić warunek). Aby uniknąć problemów z semantyką najlepiej przyjąć że operacja signal jest zawsze ostatnią operacją procedury monitora.
Struktura monitora Zasada działania: W danej chwili w procedurze monitora może przebywać jeden proces. Z każdą zmienną warunkową związana jest kolejka procesów, które wywołały wait i oczekują na zasygnalizowanie operacji. Po zasygnalizowaniu warunku proces (który wykonał operację signal) przechodzi do kolejki urgent queue. Zatem implementacja realizuje semantykę Hoare'a
Przykład 2: Problem producent-konsument z buforem cyklicznym monitor ProducentKonsument { int Licznik=0,in=0,out=0; Condition Pelny; Condition Pusty; int Bufor[N]; void Wstaw(int x) { if (Licznik==n) Pełny.wait(); Bufor[in]=x; in=(in+1)%n; Licznik++; Pusty.signal(); } int Pobierz() { if (Licznik==0) Pusty.wait(); int x=bufor[out]; out=(out+1)%n; Licznik=Licznik-1; Pelny.signal(); return x; } }; Rozwiązanie dla wielu konsumentów i wielu producentów. Przyjmujemy, że w buforze są przechowywane liczby całkowite (int). Producent chcąc wstawić element do bufora wywołuje procedurę monitora Wstaw. Konsument chcąc pobrać element z bufora wywołuje Pobierz. Gdy bufor jest pusty, to konsumenci są wstrzymywani na zmiennej warunkowej Pusty. Gdy bufor jest pełny to producenci są wstrzymywani na zmiennej warunkowej Pełny.
Tworzenie wątku w Javie Dwie metody: class Worker extends Thread { } public void run() { } System.out.println("Wątek roboczy"); public class First { } public static void main(string args[]) { } Worker runner = new Worker(); runner.start(); System.out.println("Wątek główny"); runner.join(); Rozszerzenie klasy Thread Implementacja interfejsu Runnable Rozszerzenie klasy Thread Metoda run jest wykonywana w odrębnym wątku Deklarujemy obiekt klasy Metoda start() uruchamia wątek. Reprezentowany przez obiekt klasy Worker. Metoda join() zawiesza aktualny wątek do momentu zakończenia wątku reprezentowanego przez obiekt klasy Thread.
Metody synchronizowane w Javie Z każdym obiektem w Javie związany jest zamek (ang. lock). Metoda jest zsynchronizowana, jeżeli przed jej deklaracją stoi słowo kluczowe synchronized. Zamek gwarantuje wzajemne wykluczanie metod synchronizowanych obiektu Aby wykonać metodę synchronizowaną wątek musi wejść w posiadanie zamka. Wątek kończąc metodę synchronizowaną zwalnia zamek Jeżeli wątek próbuje wywołać metodę synchronizowaną, a zamek jest już posiadany przez inny wątek (wykonujący właśnie metodę synchronizowaną), to jest zostaje on zablokowany i dodany kolejki wątków oczekujących na zwolnienie zamka.
Blokada (ang. lock) obiektu w Javie
Producent-konsument w Javie z semi-aktywnym oczekiwaniem class ProducentKonsument { int Licznik=0,in=0,out=0; static final int N=100; int Bufor[N]; public void Wstaw(int x) { while (Licznik==N) Thread.yield(); synchronized(this) { Bufor[in]=x; in=(in+1)%n; Licznik++; } } public int Pobierz() { while(licznik==n) Thread.yield(); synchronized(this) { int x=bufor[out]; out=(out+1)%n; Licznik=Licznik-1; } return x; } }; Thread.yield pozwala na przekazanie sterowania innemu wątkowi lub procesowi. Ciągle jest to aktywne czekanie - nie zalecane. Synchronizowany blok kodu Często nie ma konieczności synchronizowania całej metody W danej jeden wątek może wykonywać synchronizowany blok kodu jednego obiektu. Wątek ten posiada blokadę obiektu W książce Applied Operating Systems Concepts użyto synchronizowanych metod. Czy jest to poprawne? Powyższe rozwiązanie jest poprawne wyłącznie dla jednego procesu konsumenta i jednego procesu producenta. (Dlaczego?).
Metody wait oraz notify Wątek posiadający blokadę obiektu może wykonać metodę wait. (tego obiektu) Wątek natychmiast zwalnia blokadę (inne wątki mogą wejść w posiadanie blokady) Zostaje uśpiony.. Umieszczany jest w kolejce wątków zawieszonych (ang. wait set) obiektu. Należy obsłużyć wyjątek InterruptedException. Wątek posiadający blokadę obiektu może wykonać metodę notify. Metoda ta sprawia, że jeden wątek z kolejki wątków zawieszonych zostanie przesunięty do kolejki wątków oczekujących na zwolnienie blokady. Metoda notifyall powoduje przeniesienie wszystkich wątków zawieszonych. W Javie istnieją również metody suspend i resume. Są niebezpieczne i nie należy ich stosować!!!
Blokada obiektu w Javie (wersja ostateczna) Jeden wątek jest właścicielem wykonuje kod synchronizowany Entry Set - wątki oczekujące na wejście w posiadanie zamka. Wait Set wątki zawieszone poprzez metodę wait Wywołanie metody notify przenosi wątek z wait set do entry set Obiekt w Javie odpowiada monitorowi z maksymalnie jedną zmienną warunku. To rozwiązanie obniża wydajność synchronizacji - każdy wątek po obudzeniu, musi sprawdzić czy obudziło go zdarzenie na które czekał. W przypadku producenta - konsumenta z ograniczonym buforem mamy dwa typy zdarzeń (bufor niepusty oraz bufor niepełny)
Producent konsument z wykorzystaniem metod wait/notify class ProducentKonsument { int Licznik=0,in=0,out=0; int Bufor[N]; public synchronized void Wstaw(int x) { while (Licznik==N) try { wait();} catch(interruptedexception e) {;} Bufor[in]=x; in=(in+1)%n; Licznik++; notifyall(); } Słabość metod wait/notify - trzeba do skutku sprawdzać warunek. public synchronized int Pobierz() { while (Licznik==0) try { wait();} catch(interruptedexception e) {;} int x=bufor[out]; out=(out+1)%n; Licznik=Licznik-1; notifyall(); return x; } };
Synchronizacja w Javie, a monitory. W monitorze możemy zadeklarować wiele zmiennych warunkowych. Klasa w Javie w przybliżeniu odpowiada monitorowi z jedną zmienną warunkową. Różnice są widoczne w przypadku rozwiązania problemu producentkonsument. Wersja z monitorami wykorzystuje dwie zmienne warunkowe W wersji w Javie konsument oczekujący na pojawienie się elementu w buforze może zostać powiadomiony przez innego konsumenta. Z tego powodu po obudzeniu należy raz jeszcze sprawdzić warunek (pętla while). Brak zmiennych warunkowych prowadzi do niskiej wydajności: np. budzeni są wszyscy czekający konsumenci ale tylko jeden z nich może kontynuować. Specyfikacja Javy mówi, że wątek wywołujący metodę notify kontynuuje pierwszy. Odpowiada to semantyce Mesa.
Biblioteka POSIX Threads - implementacja monitora Dostarcza typy i operacje dla semaforów (sem_t) oraz mutexów (pthread_mutex_t) realizujących wzajemne wykluczanie. Dostarcza typ (pthread_cond_t) dla zmiennych warunkowych i niepodzielną operację pthread_cond_wait(condition,mutex) usypiającą wątek na zmiennej warunkowej i jednocześnie zwalniającą blokadę mutex. Po obudzeniu wątku oczekującego na zmiennej warunku (przez pthread_cond_signal albo pthread_cond_broadcast) nastąpi ponowna automatyczna re-akwizycja muteksa, przed powrotem z funkcji pthread_cond_wait. Ponadto mamy operację na zmiennych warunkowych pthread_cond_signal (obudza jeden zawieszony wątek) i pthread_cond_broadcast (obudza wszystkie zawieszone wątki). pthread_mutex_t mutex; // Realizuje wzajemne wykluczanie wew. monitora void Funkcja_Monitora() { pthread_mutex_lock(&mutex); // Wejście do monitora... if ( ) { pthread_mutex_unlock(&mutex); return; // Teraz też opuszczamy monitor - pamiętać o return!!! }... pthread_mutex_unlock(&mutex); // Opuszczenie monitora }
POSIX Threads - zmienne warunkowe monitora Każda zmienna warunkowa deklarowana jest jako zmienna typu pthread_condition_t i inicjowana przy pomocy pthread_cond_init: pthread_cond_t condition; pthread_cond_init(&condition,null); Jeżeli wątek przebywający wewnątrz funkcji monitora (a zatem posiadający mutex), zechce wykonać operację wait na zmiennej warunku, może wykonać następujący kod: // Zwolnienie muteksa i oczekiwanie na zmiennej warunku. pthread_cond_wait(&condition,&mutex); // Obudzenie nastąpi po wykonaniu operacji signal lub broadcast // i re-akwizycja muteksa. Wątek chcący wykonać operację signal monitora (i przebywający wewnątrz monitora tzn. posiadający muteks) wykonuje następujący kod: // Czy to jest semantyka Hoare'a czy też Mesa? pthread_cond_signal(&condition);
Przekazywanie komunikatów (ang. message pasing) Dostarcza dwie operacje. send(odbiorca,dane) receive(nadawca,dane) Send i receive mogą wymagać podania kanału. Idealne dla problemu producent konsument. Na ogół wymaga kopiowania danych => możliwy spadek wydajności. Dobra metoda synchronizacji dla systemów rozproszonych. Wydajna realizacja pamięci współdzielonej w systemie rozproszonym jest bardzo trudna.
Typy operacji send oraz receive Blokujące send i blokujące receive. Obydwa procesy są zablokowane do momentu przekazania komunikatu. Nazywane spotkaniem (Ada, CSP, Occam, Parallel C) Nieblokujące send i blokujące receive. Proces wywołujący send nie musi czekać na przekazanie komunikatu. Komunikat umieszczany jest w buforze Nieblokujące send i nieblokujące receive. Żaden z pary procesów nie musi czekać na przekazanie komunikatu. Operacja receive sygnalizuje brak komunikatu. Dodatkowa operacje test_completion i wait_completion.
Implementacje przekazywania komunikatów Spotkania w Adzie Gniazda Wykorzystujące protokół TCP/IP Gniazda domeny Uniksa. Biblioteki PVM oraz MPI. Zaprojektowane z myślą o obliczeniach równoległych. Zdalne wywołanie procedury (ang. remote procedure call, RPC) Zdalne wywołanie metody (ang. remote method invocation, RMI) Kolejki komunikatów w Uniksie Nazwane (i nienazwane) potoki w Uniksie