Wykład nr 7: Synchronizacja procesów Systemy operacyjne Wykład nr 7: Synchronizacja procesów Piotr Bilski
Cele synchronizacji Współpracujące procesy sekwencyjne wymieniają wspólne dane Jednoczesny dostęp do zasobów wymaga zarządzania – zwłaszcza przy zapisie Systemy wieloprocesorowe przetwarzają jednocześnie wiele instrukcji – potencjalnie o wspólnych danych Konieczne jest zapobieganie rywalizacji
Przykład – mechanizm producent-konsument bufor Wspólny bufor i licznik (liczba elementów w buforze) dla obu Operacje: licznik++ i licznik--
Problemy dostępu do bufora Operacje zwiększania i zmniejszania licznika mogą dać różne wyniki w zależności od kolejności wykonywania operacji! R1 = licznik R1 = R1 + 1 R2 = licznik R2 = R2 – 1 licznik = R1 licznik = R2 R1 = licznik R1 = R1 + 1 licznik = R1 R2 = licznik R2 = R2 - 1 licznik = R2
Założenia synchronizacji Wspólne zasoby nie mogą być użytkowane jednocześnie (szkodliwa rywalizacja!) Synchronizacja i koordynacja procesów wymaga wsparcia sprzętowego i/lub programowego Wrażliwy kod musi zostać wyróżniony
Sekcja krytyczna Segment kodu zawierający operacje na zasobach dzielonych Poprzedzony sekcją wejściową i zakończony sekcją wyjściową Pozostałe fragmenty kodu nieistotne z punktu widzenia synchronizacji
Wymagania sekcji krytycznej Wzajemne wykluczanie – tylko jeden proces działa w sekcji krytycznej Postęp – grupa procesów do wykonania sekcji krytycznej jest ściśle określona i sukcesywnie obsługiwana Ograniczone czekanie – liczba procesów, które weszły w sekcję krytyczną przed dopuszczeniem do niej dowolnego procesu jest skończona Względna efektywność procesów nieistotna
Przykład sekcji krytycznej while(true) { sekcja_wejściowa() sekcja_krytyczna() sekcja_wyjściowa() reszta() } while(true) { while(pod <> i) ; sekcja_krytyczna() pod = j reszta() }
Elementy programistyczne synchronizacji Tablica informująca o chęci procesów wejścia do sekcji krytycznej bool znacznik[i] znacznik[i] = true; while(znacznik[j]); Numer przechowujący informację o tym, który proces może wejść do sekcji krytycznej int number
Algorytm synchronizacji procesu Pi while(true) { znacznik[i] = true; number = j; while(znacznik[j] && number == j); sekcja_krytyczna(); znacznik[i] = false; reszta(); }
Rozwiązanie wieloprocesowe Dla n procesów algorytm jest rozszerzany do algorytmu piekarni: Każdy proces dostaje numer porządkowy Każdy proces ma miejsce w tablicy do sygnalizacji potrzeby wejścia do sekcji krytycznej Obsługiwany jest proces, który ma taką potrzebę oraz najniższy numer Struktury danych: bool wybieranie[n]; int number[n]; Procesy są obsługiwane w kolejności FCFS
Algorytm piekarni – struktura procesu while(true) { wybieranie[i] = true; number[i] = max(number[0], …, number[n-1])+1; wybieranie[i] = false; for (j=0; j<n; j++) { while(wybieranie[j]); while(number[j]<>0 && ((nubmer[j], j) < (number[i], i)); } sekcja_krytyczna(); number[i] = 0;
Synchronizacja sprzętowa Przypadek jednego procesora wymaga wyłącznie zablokowania przerwań W środowisku wieloprocesorowym wymagane są dodatkowe, niepodzielne instrukcje procesora TestAndSet Swap
Szczegóły rozkazów sprzętowych bool TestAndSet(bool &zmienna) { bool ret = zmienna; zmienna = true; return pob; } while(true) while(TestAndSet(lock)); sekcja_krytyczna(); lock = false; reszta();
Szczegóły rozkazów sprzętowych (c.d.) void Swap(bool &x, bool &y) { bool temp = x; x = y; y = temp; } while(true) lock = true; while(lock == true) Swap(lock,key) sekcja_krytyczna(); lock = false; reszta();
Semafor Jest to zmienna całkowita S, której nadaje się wartość początkową oraz zmienia ją przy pomocy dwóch operacji: wait(S) Signal(S) Zmiany dokonywane przez operacje muszą być niepodzielne (blokowanie przerwań!) Zwykle występują w postaci mutexów w systemach wieloprocesowych
Operacje na semaforze wait: signal: while(true) { wait(mutex); while (S <= 0); S--; } signal: signal(S) { S++; while(true) { wait(mutex); sekcja_krytyczna(); signal(mutex); reszta(); }
Implementacja semaforów Problem aktywnego czekania – wykonywanie pustej pętli, gdy semafor jest opuszczony (wirująca blokada) Wirujące blokady są przydatne w systemach wieloprocesorowych, gdy czas oczekiwania jest krótki Możliwość zablokowania procesu czekającego pod semaforem (przeniesienie do odpowiedniej kolejki) – wywłaszczenie! Odblokowanie procesu przy pomocy operacji wakeup()
Implementacja semaforów (c.d.) Operacje block() i wakeup() są dostarczane przez system operacyjny i występują wewnątrz operacji wait i signal Uwaga: wartość semafora może być ujemna i wskazuje liczbę procesów w kolejce Kolejka procesów czekających na semafor jest realizowana jako dowiązanie w PCB W krótkich sekcjach krytycznych stosuje się aktywne czekanie, w długich - blokowanie
Zakleszczenie i głodzenie Zakleszczenie występuje, gdy dwa procesy czekają na wzajemne zwolnienie zasobów Głodzenie to nieskończone czekanie pod opuszczonym semaforem Blokowanie nieskończone występuje w przypadku zastosowanie kolejki LIFO Semafory w ogólności są zliczające (wiele wartości). Realizuje się je jako semafory binarne
Problemy synchronizacji Problem ograniczonego buforowania – przykład operowania na skończonej liczbie buforów, z których każdy przechowuje jeden bajt Problem czytelników i pisarzy – czytelnicy mogą jednocześnie czytać, ale pisarz może pisać tylko jeden Problem chińskich filozofów
Problem czytelników i pisarzy Należy zapewnić wyłączność dostępu pisarzy do zasobu Żaden czytelnik nie może czekać na zasób, chyba, że używa go pisarz Żaden pisarz nie może czekać na zasób – „wyprzedza” wszystkich czytelników Należy uniknąć głodzenia!
Założenia Struktury danych: semaphore mutex, pis; int liczba_czytelnikow; mutex – zapewnia wzajemne wykluczanie przy aktualnizacji liczby czytelników pis – zapewnia wzajemne wykluczanie pisarzy wait(pis) pisanie() signal(pis) proces pisarza
Proces czytelnika Gdy wielu czytelników czeka na zakończenie przez pisarza pisania do zasobu, pierwszy czeka w kolejce do semafora pis, a reszta – do semafora mutex Gdy pisarz zwolni zasób, może on być przydzielony następnemu pisarzowi, lub czytelnikom
Problem chińskich filozofów Każdy filozof je i medytuje Aby zjeść, potrzebuje pałeczek po obu swoich stronach
Rozwiązanie problemu chińskich filozofów Każda pałeczka jest semaforem Aby móc uzyskać dostęp do zasobu, dwa sąsiednie semafory muszą być opuszczone Problem: każdy filozof może podnieść po jednej pałeczce (zakleszczenie)! Rozwiązania: Tylko czterech filozofów dopuszczonych Filozof może podnieść pałeczki tylko wtedy, gdy obie są dostępne Rozwiązanie asymetryczne
Regiony krytyczne Problem: semafory mogą zostać użyte niewłaściwie – odwrócona kolejność operacji wait() i signal(), pominięcie jednej z nich, powielona jedna z nich Region krytyczny to fragment kodu działający na wspólnych zmiennych – jest osobno oznaczony, przed wejściem do niego sprawdzany jest warunek logiczny Ogranicza liczbę błędów związanych z sekcją krytyczną
Działanie regionu krytycznego Obsługiwany jest przez dwa semafory Przed wejściem do regionu sprawdzany jest warunek logiczny: region v when (B) S1; Procesy, którym nie udało się wejść do regionu oczekują najpierw pod pierwszym semaforem, potem pod drugim Obliczenie wyrażenia logicznego jest konieczne, gdy proces opuszcza sekcję krytyczną
Monitory Zbiór operacji zdefiniowanych przez programistę: monitor mój_monitor { procedure P1 { } procedure P2 { } }
Realizacja monitora Wewnątrz monitora może być aktywny tylko jeden proces Konieczne jest wprowadzenie warunku, na którym wykonuje się operacje wait i signal: condition a,b; a.wait(); a.signal(); Operacja signal() wznawia jeden zawieszony proces
dane dzielone i warunki Schemat monitora Kolejka wejściowa dane dzielone i warunki operacje Kod inicjujący Problem dostępu do monitora procesów w kolejce (metoda FCFS, czekanie warunkowe z numerem priorytetu)
Problemy monitorów Proces może: uzyskać dostęp do zasobu bez pozwolenia nie zwrócić zasobu chcieć zwolnić zasób, którego nie posiada zamówić wiele razy ten sam zasób Warunkiem uniknięcia problemów jest poprawne użycie operacji wysokiego poziomu zdefiniowanych w ramach monitora
Przykład synchronizacji - Solaris Dostęp do sekcji krytycznych realizowany poprzez zamki adaptacyjne, zmienne warunkowe, blokady i turnikety Zamek adaptacyjny to semafor, który powoduje przejście procesów czekających w wirującą blokadę, lub blokowanie (w zależności od tego, czy wątek utrzymujący blokadę jest aktywny, czy nie)
Przykład synchronizacji – Solaris (c.d.) Porządkowaniem procesów czekających na dostęp do sekcji krytycznej zajmują się turnikety – kolejki Z turniketami wiązane są wątki jądra Turnikety są zarządzane zgodnie z protokołem dziedziczenia priorytetów Blokowanie wątków użytkownika identyczne, jak wątków jądra
Przykład synchronizacji - Windows Dostęp do zasobów globalnych ograniczany przez wirujące blokady Obiekty dyspozytora pozwalają wątkom synchronizować się z innymi mechanizmami za pomocą muteksów, zamków itp. Obiekty dyspozytora są w stanie sygnalizowania lub niesygnalizowania Zdarzenia są mechanizmem analogicznym do zmiennych warunkowych
Transakcje Ciąg operacji wykonany albo w całości, albo wcale Zastosowanie: bazy danych, systemy operacyjne Dwie główne operacje: odczyt i zapis Operacje mogą być potwierdzone lub anulowane
Implementacja transakcji Zatwierdzenie (commit) oraz wycofanie (rollback) wymaga dziennika w pamięci trwałej Wszystkie wykonywane zmiany muszą być rejestrowane (zapis z wyprzedzeniem)
Struktura dziennika Postać rekordu: Rekordy kontrolne transakcji: Nazwa transakcji Nazwa jednostki danych Stara wartość Nowa wartość Rekordy kontrolne transakcji: Rozpoczęto (zanim wykonywanie operacji się rozpocznie) Zatwierdzono (po pomyślnym zakończeniu operacji) Punkt kontrolny (służy do odzyskiwania sprawności systemu po awarii) Wpis do dziennika poprzedza operację
Zatwierdzanie zmian Idempotentne operacje undo (wszystkie zmieniane dane odzyskują stare wartości) i redo (wszystkie zmieniane dane uzyskują nowe wartości) undo – gdy brak rekordu „zatwierdzono” redo – gdy są oba rekordy kontrolne
Punkty kontrolne Po awarii nie trzeba przeglądać całego dziennika, lecz pozycje od ostatniej oznaczonej pozycji W punkcie kontrolnym: Zapisuje się rekordy i zmienione dane z pamięci ulotnej do nieulotnej Dopisuje się do dziennika rekord „punkt kontrolny” Operacje przed punktem kontrolnym uważa się za wykonane pomyślnie
Współbieżność Transakcje muszą być wykonane w określonym porządku – są szeregowalne! Niepodzielność wymusza wykorzystanie sekcji krytycznej Algorytmy sterowania współbieżnością zapewniają szeregowalność
Szeregowalność Szereg transakcji nazywany jest planem Plan szeregowy – wszystkie operacje transakcji wykonywane bez przerw Plan nieszeregowy – dopuszcza przeplatanie transakcji Konieczne zdefiniowania operacji konfliktowych