Wykład 7 Synchronizacja procesów i wątków Systemy operacyjne Wykład 7 Synchronizacja procesów i wątków dr inż. Wojciech Bieniecki Instytut Nauk Ekonomicznych i Informatyki http://wbieniec.kis.p.lodz.pl/pwsz
Sytuacje hazardowe W systemach wielozadaniowych, podczas realizacji przez ustalona liczbę procesów dostępu współbieżnego do dzielonych zasobów może dojść do sytuacji hazardowych, nazywanych również wyścigiem (ang. race condition). Zjawiska te pojawiają się, kiedy następuje przeplot operacji modyfikacji lub operacji modyfikacji i odczytu stanu współdzielonego zasobu, przy czym operacje te pochodzą od różnych procesów. Jeśli dostęp do zasobu ogranicza się wyłącznie do odczytu, to jest dostępem zawsze bezpiecznym, nawet jeśli dochodzi od przeplotu operacji. Sytuacje hazardowe występują zarówno w przypadku procesów, jak i wątków Obojętnym jest również, czy system komputerowy, w którym są wykonywane procesy jest wyposażony w jeden, czy większą liczbę procesorów.
Przykład wyścigu Rozważmy dwa procesy, które chcą zmodyfikować wartość współdzielonej zmiennej, przy czym pierwszy chce te wartość zwiększyć o jeden, a drugi zmniejszyć o jeden. Przyjmijmy, ze wartość początkowa zmiennej wynosi 5. Zakładamy, ze pojedynczy proces, aby zmodyfikować wartość zmiennej musi ją najpierw pobrać z pamięci operacyjnej, zapisać w rejestrze , wykonać właściwą operacje, a następnie zapisać wynikową wartość do pamięci. Procesy wykonujące wspomniane operacje mogą utracić procesor w każdej chwili, np.: w wyniku zadziałania mechanizmu wywłaszczającego.
Wyścig – scenariusz 1 czas Proces P1 Proces P2 1 Odczytaj wartość z pamięci (M=5) i umieść ja w rejestrze R1. (R1=5) NOP 2 Zwiększ wartość rejestru R1 o jeden. (R1=6) NOP 3 NOP Odczytaj wartość z pamięci (M=5) i umieść ją w rejestrze R1. (R1=5) 4 NOP Zmniejsz zawartość rejestru R1 o jeden. (R1=4) 5 Zapisz zawartość rejestru R1 do pamięci. (M=6) NOP 6 NOP Zapisz zawartość rejestru R1 do pamięci. (M=4) Wynik Wartość wynosi 4 (jako ostatni swój wynik do pamięci zapisał proces drugi). Tymczasem prawidłowym wynikiem jest 5.
Wyścig – scenariusz 2 czas Proces P1 Proces P2 1 Odczytaj wartość z pamięci (M=5) i umieść ja w rejestrze R1. (R1=5) NOP 2 NOP Odczytaj wartość z pamięci (M=5) i umieść ją w rejestrze R1. (R1=5) 3 Zwiększ wartość rejestru R1 o jeden. (R1=6) NOP 4 NOP Zmniejsz zawartość rejestru R1 o jeden. (R1=4) 5 NOP Zapisz zawartość rejestru R1 do pamięci. (M=4) 6 Zapisz zawartość rejestru R1 do pamięci. (M=6) NOP Wynik Wartość wynosi 6 (jako ostatni swój wynik do pamięci zapisał proces pierwszy). Tymczasem prawidłowym wynikiem jest 5.
Sekcja krytyczna Fragment kodu, podczas którego realizacji proces wykonuje dostęp do zasobów współdzielonych nazywamy sekcją krytyczną. Zasoby te mogą być fizyczne lub logiczne. Zasoby mogą mieć prostą budowę (jak zmienne prostych typów) lub złożoną (jak struktury danych). Aby dostęp do zasobu współdzielonego był bezpieczny musimy zagwarantować niepodzielność wykonania przez procesy sekcji krytycznych. Jeśli jeden z procesów korzystających z zasobu dzielonego rozpoczął wykonywanie sekcji krytycznej, to żaden z pozostałych procesów nie może rozpocząć wykonywania sekcji krytycznej dotyczącej tego samego zasobu, dopóki ten pierwszy jej nie skończy. Rozpoczynanie sekcji krytycznej określamy mianem wchodzenia do sekcji krytycznej. Kończenie sekcji nazywamy wychodzeniem lub opuszczaniem sekcji krytycznej. Część kodu bezpośrednio poprzedzającą sekcję krytyczną nazywamy sekcją wejściową. Część umieszczoną bezpośrednio za sekcją krytyczną nazywamy sekcją wyjściową. Pozostała część kodu procesu to reszta.
Poprawność obsługi sekcji krytycznej Każde rozwiązanie problemu sekcji krytycznej musi spełniać trzy warunki, aby być w pełni poprawnym: Wzajemne wykluczanie (ang. mutual exclusion) W danym czasie, w sekcji krytycznej może znajdować się tylko jeden proces. Postęp Jeśli nie wymaga tego warunek wzajemnego wykluczania, to proces nie powinien być wstrzymywany przed wejściem do sekcji krytycznej. Ograniczone czekanie Oczekiwanie każdego procesu na wejście do sekcji krytycznej powinno kiedyś się zakończyć. Inne procesy nie mogą wstrzymywać go w nieskończoność przed wejściem do sekcji krytycznej.
Poprawne rozwiązanie dla dwóch procesów (algorytm Petersona) Współdzielone zmienne wymagane przez algorytm int flaga[2]; //tablica flag gotowości procesów do wejścia do s.k int numer;//numer procesu, któremu zezwolono wejsc do s.k. Proces P0 Proces P1 while(1) { flaga[0] = 1; numer = 1; while(flaga[1]==1 && numer ==1) {;} Sekcja_Krytyczna0(); flaga[0] = 0; Reszta0(); } while(1) { flaga[1] = 1; numer = 0; while(flaga[0]==1 && numer ==0) {;} Sekcja_Krytyczna1(); flaga[1] = 0; Reszta1(); }
Dowód poprawności rozwiązania Wzajemne wykluczanie Jeden z dwóch procesów wchodzi do sekcji krytycznej wtedy i tylko wtedy, kiedy jego flaga ma wartość true lub gdy zmienna numer zawiera jego identyfikator. Może zaistnieć sytuacja, w której oba procesy będą miały ustawione flagi, ale zmienna numer może przyjąć tylko jedną wartość, a więc jeden z nich będzie wykonywał pętlę while, a drugi wejdzie do sekcji krytycznej. Postęp Załóżmy, ze proces o numerze 0 chce wejść do sekcji krytycznej, a proces o numerze 1 nie jest nią zainteresowany (bo wykonuje swoja resztę). Zmienna numer będzie miała wartość 1, ale flaga procesu o numerze 1 nie będzie ustawiona, a więc proces 0 nie zostanie powstrzymany przed wejściem do sekcji krytycznej. Ograniczone oczekiwanie Instrukcja przypisania z sekcji wyjściowej gwarantuje, że proces będzie czekał na wejście do sekcji krytycznej tylko do momentu, gdy drugi z procesów zakończy swoją sekcję krytyczną i wykona sekcję wyjściową.
Poprawne rozwiązanie programowe dla wielu procesów Uogólnienie przedstawionego rozwiązania na n procesów jest możliwe ale otrzymany kod jest mniej czytelny niż w przypadku dwóch procesów. Zmianie ulegają typy zmiennych współdzielonych. Zmienna numer ma większy zakres, również flagi mogą teraz przyjmować trzy wartości: puste – proces jest poza sekcja krytyczna, gotowy – proces zgłasza swa gotowość do wejścia do sekcji krytycznej, w sekcji – proces jest w sekcji krytycznej. Zmienna j jest zmienna niewspółdzielona (poza właścicielem tej zmiennej, żaden inny proces nie ma do niej dostępu).
Kod rozwiązania dla i–tego procesu Współdzielone zmienne wymagane przez algorytm enum stan{puste, gotowe, w_sekcji}; stan flaga[n]; int numer[n]; Proces Pi int j; while(1){ do { flaga[i] = gotowy; j = numer; while (i!=j) { if (flaga[j]!=puste) else j=(j+1)% n; } flaga[i] = w sekcji ; j = 0; while (j<n && (j==i || flaga[j]!=w sekcji)) j++; }while(j<n || (numer!=i && flaga[numer]!=puste)); numer = i; Sekcja_Krytyczna(); j = (numer+1) % n; while (flaga[j]==puste) j=(j+1)%n; numer = j; flaga[i] = puste; Reszta(); }
Dowód poprawności Wzajemne wykluczanie Postęp Ograniczone oczekiwanie Proces z grupy procesów ubiegających się o dostęp do współdzielonego zasobu wchodzi do sekcji krytycznej wtedy i tylko wtedy, gdy jego flaga gotowości ma wartość w_sekcji, a flagi pozostałych procesów mają inną wartość. Ponieważ tylko on może ustawić swoją flagę na wspomnianą wartość oraz dokonuje sprawdzenia flag pozostałych procesów po jej ustawieniu, to warunek wzajemnego wykluczania jest zachowany. Postęp Wartość zmiennej numer ulega zmianie tylko wtedy gdy proces wchodzi lub wychodzi z sekcji krytycznej. Jeśli tylko jeden proces jest zainteresowany wejściem do sekcji krytycznej, a żaden inny nie wykonuje jej, ani nie ubiega sie o wejście do niej, to może on wykonać sekcję krytyczną poza kolejnością wyznaczaną przez zmienną numer. Ograniczone oczekiwanie Każdy proces opuszczający sekcję krytyczną w sekcji wejściowej wyznacza swojego następcę do wejścia do sekcji krytycznej. W ten sposób każdy proces, który ubiega się o wykonanie sekcji krytycznej dostanie pozwolenie po co najwyżej n-1 próbach.
Algorytm piekarni Nazwa pochodzi od sposobu w jaki piekarnie sprzedają chleb. Każdy proces ubiegający się o wejście do sekcji krytycznej musi się zarejestrować i otrzymać swój numer. Im niższa jest wartość tego numeru, tym szybciej jest jego właściciel obsługiwany. Może zdarzyć się, że dwa procesy o trzymają ten sam numer. Taki konflikt rozstrzyga się porównując ich identyfikatory(PID), które też są numerami.
Algorytm piekarni Uwaga: należy zdefiniować funkcje max i cmp2 Współdzielone zmienne wymagane przez algorytm int wybrane[n]; int numer[n]; Proces Pi while(1){ wybrane[i] = 1; numer[i]=max(numer,n)+1; wybrane[i]=0; for(j=0;j<n;j++) { while(wybrane[j]){;} while(numer[j]!=0 && cmp2(numer[j],j,numer[i],i)<0) {;} } SekcjaKrytyczna(i); numer[i]=0; Reszta(i); Uwaga: należy zdefiniować funkcje max i cmp2
Dowód poprawności Wzajemne wykluczanie Postęp Ograniczone oczekiwanie Każdy proces, który wchodzi do sekcji krytycznej otrzymuje numer, który jest następnikiem największego z dotychczas wybranych numerów. Ponieważ operacja wybierania nie jest niepodzielna, to dodatkowo sprawdzane są unikatowe numery identyfikacyjne procesów, gdyby pojawiły sie dwa lub większa liczba o takich wybranych numerach. Operacja porównania numerów wstrzymywana jest do czasu zakończenia wybierania numeru przez nadchodzące procesy. To wszystko gwarantuje spełnienie warunku wzajemnego wykluczania. Postęp Ponieważ tylko procesy gotowe do wejścia do sekcji krytycznej wybierają numery, to zapewniony jest warunek postępu. Ograniczone oczekiwanie Procesy wchodzą do sekcji krytycznej w takim porządku w jakim nadeszły, a więc spełnienie warunku ograniczonego czekania jest zapewnione.
Środki synchronizacji Algorytm Petersena i algorytm piekarni są rozwiązaniami programowymi. Teoretycznie są poprawne. Niestety, w praktyce te rozwiązania mogą zawieść, jeśli program będzie wykonywany na procesorze stosującym wykonywanie instrukcji poza kolejnością (ang. out of order execution). Poprawność tych rozwiązań może również być naruszona podczas wykonywania przez kompilator optymalizacji kodu wynikowego. Stosując te rozwiązania należy pamiętać o dodatkowych środkach, które pozwalają uniknąć opisanych problemów. Inną wadą przedstawionych algorytmów są problemy z zastosowaniem ich do bardziej skomplikowanych zadań. Najprostszym sposobem zapewnienia wyłączności i niepodzielności wykonania sekcji krytycznej jest wyłączenie na czas jej działania systemu przerwań. Niestety to rozwiązanie może prowadzić do większych problemów (możliwość powstania wyjątku w sekcji krytycznej). Nie daje sie ono również zastosować w systemach wieloprocesorowych (tu każdy procesor ma własny system przerwań).
Niepodzielne rozkazy sprzętowe Większość współczesnych architektur sprzętowych oferuje rozkazy, które pozwalają wykonać w sposób niepodzielny, na prostych zmiennych takie operacje, jak dodawanie, odejmowanie, operacje binarne. Istnieją również rozkazy pomagające rozwiązać problem sekcji krytycznej dla operacji na strukturach danych. Rozkaz Testuj i ustaw W sposób niepodzielny odczytuje wartość zmiennej, a następnie ją modyfikuje. Rozkaz Wymień Dokonuje zamiany wartości dwóch zmiennych, które pełnia role zamka i klucza. Oba rozkazy pozwalają zapewnić spełnienie warunku wzajemnego wykluczania. W systemach wieloprocesorowych, w których procesory dysponują wspólną pamięcią operacyjną istnieją rozkazy pozwalające zablokować pozostałym procesorom dostęp do określonej części pamięci na czas wykonywania przez jeden z nich sekcji krytycznej.
Semafor Semafor jest zmienną całkowitą, do której dostęp można uzyskać (poza inicjalizacją) jedynie za pomocą dwóch specjalnych operacji: czekaj P (hol. Proben, ang. wait) sprawdza, czy semafor ma wartość większą od zera. Jeśli nie to czeka aż osiągnie taką wartość, a następnie zmniejsza ją o jeden. Operacja zmniejszania jest wykonywana w sposób niepodzielny. sygnalizuj V (hol. Verhogen, ang. signal). Operacja sygnalizuj w całości jest wykonywana niepodzielnie i polega na zwiększeniu wartości semafora o jeden. Istnieją różne wersje semaforów, niektóre z nich mogą przyjmować różne wartości, inne tylko dwie (tzw. semafory binarne, muteksy). Istnieją również różne sposoby implementacji operacji czekaj. O procesie, który oczekuje na podniesienie semafora w kolejce mówimy, że został uśpiony, a proces, który został wybrany z tej kolejki do opuszczenia semafora mówimy, ze został obudzony.
Implementacja semafora Semafor to: Bieżąca wartość + Lista (np. FIFO) procesów oczekujących 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); Nieco zmodyfikowana 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 Wait i Signal muszą być operacjami atomowymi – ich wykonanie nie może być przerwane przełączeniem kontekstu do innego procesu.
Regiony krytyczne semafory są skuteczne w rozwiązywaniu sekcji krytycznej i proste w zastosowaniu, jednak mogą być dosyć niewygodne w użyciu, tym samym prowadząc do powstania błędów logicznych w programach. Programista może np. zapomnieć o umieszczeniu w programie instrukcji podnoszącej semafor. W takim wypadku może dojść do zablokowania działania wszystkich procesów czekających na podniesienie semafora. W językach programowania wprowadzono instrukcje pozwalające tworzyć tzw. regiony krytyczne
Regiony krytyczne shared int alpha; pozwala na określenie zmiennej, jako współdzielonej przez kilka procesów region alpha do {/* ciąg operacji */}; gwarantuje, ze operacja wykonana na zmiennej współdzielonej będzie niepodzielna. Konstrukcja regionu warunkowego: region alpha when warunek {/* ciąg operacji */}; Operacja zostanie wykonana, tylko wtedy, kiedy warunek będzie spełniony. Warunek sprawdzany jest nie przed wejściem do sekcji krytycznej, ale w trakcie jej trwania. Do realizacji oczekiwania na spełnienie warunku służy instrukcja await(warunek); volatile int zmienna; Informacja dla kompilatora, aby nie korzystać z rejestrów do operacji na tej zmiennej. Wszelkie operacje dokonywane na tej zmiennej, są dokonywane bezpośrednio w pamięci operacyjnej komputera, w tej jej części, która została przydzielona dla tej zmiennej.
Rozwiązanie problemu sekcji krytycznej przy pomocy semaforów Semaphore Sem(1); void Process() { while (1) Sem.Wait(); // Proces wykonuje swoją sekcję krytyczną Sem.Signal() // Proces wykonuje pozostałe czynności }
Klasyczne problemy synchronizacji Każde nowe rozwiązanie problemu synchronizacji musi zostać poddane testom potwierdzającym jego poprawność. Istnieje kilka dobrze poznanych problemów, które tworzą zestaw testowy dla takich rozwiązań. Problem ograniczonego buforowania Problem czytelników i pisarzy Problem pięciu ucztujących filozofów Problem śpiącego fryzjera
Problem producenta-konsumenta z ograniczonym buforem 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.
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 lub czeka, jeżeli counter==n, tzn. bufor pełny Konsument pobiera element z bufora z pozycji out lub czeka, jeżeli counter==0 tzn. bufor pusty. Zmienne in oraz out zmieniane są zgodnie z regułą i=(i+1)%n
Algorytm dla jednej pary procesów Producent Konsument Item element; while(1) { element = Produkuj(); while (counter == n) ; buffer[in] = element; in = (in+1) % n; counter ++; } Item elem; while(1) { while (counter == 0) ; elem = buffer[out]; out = (out+1) % n; counter --; Konsumuj(elem); } dlaczego rozwiązanie jest 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ć?
Problem producenta-konsumenta 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
producent-konsument z wykorzystaniem semaforów Zmienne wspólne const int n; Semaphore empty(n); /*zlicza liczby pustych miejsc w tablicy. Wstrzymuje producenta gdy w tablicy nie ma wolnego miejsca*/ Smaphore full(0); /*zlicza liczbę elementów w buforze (pełnych miejsc w tablicy). Wstrzymuje konsumenta gdy w buforze nie ma żadnego elementu.*/ Semaphore mutex(1); /* zapewnia wzajemne wykluczanie przy dostępie do zmiennych współdzielonych*/ Item buffer[n]; Producent Konsument Item element; int in =0; while(1) { element = Produkuj(); empty.Wait(); mutex.Wait(); buffer[in] = element; in = (in+1) % n; mutex.Signal(); full.Signal(); } Item elem; int out = 0; while(1) { full.Wait(); mutex.Wait(); elem = buffer[out]; out = (out+1) % n; mutex.Signal(); empty.Signal(); Konsumuj(elem); }
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. Semaphore A(1),B(1); P0 wykonał operacje A.Wait() Proces P0 A.Wait(); B.Wait(); . B.Signal(); A.Signal(); Proces P1 B.Wait(); A.Wait(); . A.Signal(); B.Signal(); P1 wykonał operacje B.Wait() P0 usiłuje wykonać B.Wait() P1 usiłuje wykonać A.Wait() P0 czeka na zwolnienie B przez P1 Będą czekały w nieskończoność !!! Do blokady może (ale nie musi) dojść.
Przykład blokady #define MAX 512 main(int argc, char* argv[]) { int pd[2]; pipe(pd); if(fork()==0) {// proces potomny dup2(pd[1], 1); execvp("ls", argv); } else {// proces macierzysty char buf[MAX]; int lb, i; close(pd[1]); wait(0); while((lb=read(pd[0],buf,MAX))>0) { for(i=0; i<lb; i++) buf[i] = toupper(buf[i]); write(1, buf,lb); Proces macierzysty czeka z przetwarzaniem znaków aż proces potomny zakończy się. Jeżeli potok zapełni się (cały listing nie zmieści się naraz) to proces ls nie może się zakończyć. Proces macierzysty nie może rozpocząć opróżniania potoku, bo czeka na zakończenie ls. Spowoduje to powstanie zakleszczenia.
Przykład blokady w łączu nazwanym #include <fcntl.h> #define MAX 512 main(int argc, char* argv[]) { int fd; mkfifo("/tmp/fifo", 0600); if (fork() == 0){ // proces potomny close(1); open("/tmp/fifo", O_WRONLY); execvp("ls", argv); } else { // proces macierzysty char buf[MAX]; int lb, i; wait(0); pd = open("/tmp/fifo",O_RDONLY); while((lb=read(fd,buf,MAX))>0) { for(i=0; i<lb; i++) buf[i] = toupper(buf[i]); write(1, buf, lb); Proces potomny próbuje otworzyć kolejkę FIFO do zapisu W tym miejscu zawiesi się, aż inny proces otworzy kolejkę do odczytu. Proces macierzysty nie może otworzyć kolejki, bo linijkę wyżej czeka na zakończenie procesu potomnego. Nastąpi zakleszczenie. W tym wypadku z pomocą mógłby przyjść inny proces, który na chwilę otworzy kolejkę do odczytu.
Opis formalny: graf alokacji zasobów 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. 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).
Zagłodzenie procesów (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) 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.
Problem pięciu filozofów Może się zdarzyć, że wszyscy filozofowie zechcą jeść i jednocześnie sięgną po leżący po lewej stronie sztuciec. Okaże się, ze żaden z nich nie będzie mógł podnieść sztućca leżącego po prawej stronie (jest ich tylko pięć), koniecznego do spożycia posiłku. Jest to problem zakleszczenia Innym razem może dojść do zagłodzenia jednego z filozofów przez pozostałych biesiadników strategie rozwiązania tego problemu to Pozwolić jednocześnie zasiadać do stołu co najwyżej czterem filozofom Pozwolić podnosić filozofom sztućce tylko wtedy, gdy oba są dostępne Zastosować rozwiązanie asymetryczne: filozofowie o nieparzystych numerach podnoszą sztućce w kolejności lewy - prawy, a ci o numerach parzystych, w kolejności odwrotnej.
Problem czytelników i pisarzy 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, lub Dowolna liczba czytelników i żaden pisarz. Rozwiązanie Potraktować czytelnię jak obiekt wymagający wzajemnego wykluczania wszystkich typów procesów. Rozwiązanie 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. Istnieją rozwiązania: – Z możliwością zagłodzenia pisarzy – Z możliwością zagłodzenia czytelników – Poprawne
Problem śpiącego fryzjera Jeden proces fryzjera i wiele procesów klientów Współdzielone zasoby: n krzeseł w poczekalni i jedno krzesło fryzjera Algorytm fryzjera: Sprawdź czy jest ktoś w poczekalni Jeżeli tak, to weź pierwszego z kolejki i strzyż, a następnie idź do kroku 1. Jeżeli nie, usiądź w fotelu i śpij. Obudzi cię dzwonek do drzwi, wtedy przejdź do kroku 1. Algorytm klienta: Wejdź do salonu uruchamiając dzwonek. Sprawdź, czy jest wolne miejsce w poczekalni. Jeżeli tak, to usiądź na końcu kolejki. Jeżeli nie – wyjdź z salonu.
Literatura Beej's Guide to Unix IPC http://beej.us/guide/bgipc/ Ważniak – laboratorium z systemów operacyjnych http://wazniak.mimuw.edu.pl/index.php?title=Systemy_operacyjne#Laboratorium Robert Love: Linux. Programowanie systemowe. Helion 2008 Stevens R.W.: Programowanie w środowisku systemu UNIX. WNT, 2002 Havilland K., Gray D., Salama B.: Unix - programowanie systemowe. ReadMe, 1999 Arkadiusz Chrobot: Wykład: Systemy Operacyjne. Katedra Informatyki Politechniki Świętokrzyskiej Wojciech Kwedlo: Wykład: Systemy operacyjne Wydział Informatyki, Politechnika Białostocka