Wykład 7

Transkrypt

Wykład 7
Systemy Operacyjne – semestr drugi
Wykład siódmy
Zagadnienia synchronizacji w Linuksie
W każdym systemie, gdzie jest obecna współbieżność występują problemy związane z tą techniką. W przypadku jądra
systemu Linux ilość problemów rosła wraz z ilością kodu, który wprowadzał kolejne ulepszenia wielozadaniowości. Do
momentu wydania serii 2.0 ilość tych problemów była niewielka i sposoby przeciwdziałania nim stosunkowo proste. Od wersji
2.0 w jądrze Linuksa pojawiła się obsługa trybu SMP – wieloprocesorowego przetwarzania symetrycznego. Obsługa pracy
kilku procesorów równocześnie wymusiła obsługę sytuacji, które nie występują w przypadku wykonania jądra na jednym
procesorze. Dodatkowe problemy pojawiły się wraz z pierwszym jądrem serii 2.6. Ta seria jest pierwszą, w której kod jądra
podlega wywłaszczaniu.
Pierwszym problemem jaki powoduje współbieżne wykonanie jest problem sekcji krytycznej. Występuje on wszędzie tam,
gdzie kilka wątków wykonania (procesów, wątków, zdań) może modyfikować dane, które są wspólne. Jeśli nie zapewnimy
niepodzielności (ang. atomicity)wykonania takiej operacji nie możemy zagwarantować spójności informacji, która jest
podmiotem tej operacji. Niepodzielność oznacza, że kiedy ta operacja się rozpocznie, to musi być wykonana w całości, nie
może być przerwana przez inną operację tego samego typu. Operacje podzielne mogą prowadzić do zjawiska, które określamy
przeplotem operacji, wyścigiem (ang. race condition) bądź hazardem. Zagadnienie zapobiegania sytuacjom hazardowym nosi
nazwę synchronizacji.
Najprostszy przypadek sekcji krytycznej występuje wtedy, kiedy kilka wątków wykonania próbuje wykonać operację
1
modyfikującą zwartości zmiennej globalnej . Brak synchronizacji takiej operacji może prowadzić do powstania błędnej
wartości końcowej. W listach rozkazów współczesnych procesorów znajdują się rozkazy pozwalające wyeliminować ten
2
problem na poziomie sprzętowym . W przypadku bardziej złożonych operacji działających na strukturach danych, należy
3
dostarczyć środków programowych rozwiązania zagadnienia synchronizacji. Takimi środkami są w jądrze Linuksa blokady.
Istnieje wiele rodzajów blokad, jednak sposób ich stosowania jest zawsze taki sam: wątek wykonania, który dokonuje
modyfikacji zawartości zasobu współdzielonego ustawia blokadę przed wykonaniem tej operacji. Tę blokadę może zdjąć tylko
wątek wykonania, który ją założył i jest jej właścicielem. Dopóki tego nie uczyni, żaden inny wątek wykonania nie może
rozpocząć operacji modyfikującej zawartość tego zasobu. Niestety wymóg stosowania blokad nie jest egzekwowany przez
żaden mechanizm ani na etapie kompilacji jądra, ani w czasie wykonania. Istnieje więc możliwość, że wskutek błędu lub
celowego, złośliwego działania programista stworzy fragment kodu, który będzie uzyskiwał dostęp do danych współdzielonych
bez sprawdzania, czy jest ustawiona blokada do tego zasobu.
Przyjrzyjmy się bliżej zjawisku współbieżności. Ogólnie to zjawisko można podzielić na dwie kategorie: pseudorównoległość i
prawdziwą równoległość. Z pseudorównoległością mamy do czynienia w środowiskach jednoprocesorowych natomiast z
równoległością prawdziwą w środowiskach wieloprocesorowych. W przestrzeni użytkownika wywłaszczenie bieżącego procesu
i oddanie sterownia innemu procesowi może nastąpić w dowolnym momencie, również wtedy, kiedy proces bieżący wykonuje
sekcję krytyczną. To zdarzenie może również wystąpić, kiedy w systemie pracuje tylko jeden proces, ale za to odbierający
sygnały, które zawsze pojawiają się w sposób asynchroniczny. W przestrzeni jądra wywłaszczenie następuje z kilku przyczyn:
wystąpienie przerwania,
wywłaszczenie bieżącego zadania jądra przez inne zadanie,
zawieszenie i synchronizacja z przestrzenią użytkownika,
przetwarzanie wieloprocesorowe.
Zapobieganie przeplotowi operacji w jądrze polega na wyodrębnieniu na etapie planowania tych zasobów, które są
współdzielone i muszą podlegać ochronie. Dopiero po wykonaniu tej czynności można przystąpić do pisania kodu. Odwrotne
postępowanie zwykle kończy się niepowodzeniem.
Kod który jest odporny na wystąpienie wystąpienie przerwania, tj. zachowuje się w sposób bezpieczny nawet jeśli zostanie
przerwany przez procedurę obsługi przerwania nazywamy kodem przerywalnym (ang. interrupt-safe) lub asynchronicznie
bezpiecznym (ang. asynchronous-safe). Kod odporny na problemy wynikające z współbieżności nazywa się kodem
wznawialnym (ang. reentrant).
Do zasobów, które nie wymagają zabezpieczenia przed dostępem współbieżnym możemy zaliczyć wszystkie te zasoby, które
1
2
3
Taką operacją może być dodawanie, odejmowanie, mnożenie, itd.
Takie rozwiązania określamy mianem niskopoziomowych.
Dla odmiany takie rozwiązania nazywane są wysokopoziomowymi.
1
Systemy Operacyjne – semestr drugi
4
są lokalne dla poszczególnych wątków wykonania . Do takich zasobów można zaliczyć zmienne automatyczne (lokalne)
funkcji. W przypadku pozostałych zasobów należy odpowiedzieć sobie na następujące pytania:
Czy są one globalne?
Czy są współużytkowane między kontekstem procesu i jądra lub czy są współużytkowane przez dwie procedury
obsługi przerwań?
Czy choć jeden z procesów użytkujących zasób może zostać wywłaszczony podczas dokonywania modyfikacji
zawartości zasobu?
Czy bieżący proces może ulec zablokowaniu (zawieszeniu) i jaki to będzie miało wpływ na stan zasobu?
Jak zapobiec przedwczesnemu odblokowaniu danych?
Jakie będzie zachowanie funkcji, jeśli zostanie ona wywołana równocześnie na innym procesorze i co należy w takiej
sytuacji zrobić?
Blokady rozwiązują problem sekcji krytycznych, ale wprowadzają nowy problem – zakleszczenia. Najprostszym przypadkiem
zakleszczenia jest samozakleszczenie. Występuje ono wtedy, kiedy wątek wykonania zakłada blokadę i po jakimś czasie
ponownie będzie próbował ją założyć. Ponieważ Linux nie stosuje blokad rekurencyjnych taka operacja się nie powiedzie i
wątek będzie czekał nieskończenie na zniesienie blokady. Oprócz niego będą również czekały inne wątki próbujące uzyskać
dostęp do tego samego zasobu. Takiej sytuacji można uniknąć pisząc prosty i starannie przemyślany kod. Innym typem
zakleszczenia jest najczęściej przedstawiane w podręcznikach zakleszczenie ABBA zwane również impasem. Występuje ono
wtedy, kiedy dwa wątki próbują uzyskać dostęp do dwóch zasobów, ale w odwrotnej kolejności. To zjawisko dotyczyć może
większej ilości zasobów i wątków. Aby uniknąć zakleszczeń należy stosować się do następujących zaleceń:
uważać na kolejność zakładania blokad,
unikać trwałego zablokowania (należy zwrócić uwagę na obsługę wyjątków w kodzie),
nie zakładać ponownie tej samej blokady,
utrzymywać prostotę kodu.
Pierwsze zalecenie jest po prostu jedną z metod zapobiegania zakleszczeniom, nakazującą zajmowanie zasobów wedle ściśle
określonej kolejności. W przypadku jądra Linuksa oznacza to, że wątki wykonania zakładające blokady na te same zasoby
muszą zakładać je w tej samej kolejności (zwykle jest to opisane w komentarzach od kodu realizującego tę operację).
Kolejność zwalniania blokad zazwyczaj nie gra roli, ale dobrą praktyką jest zwalnianie ich w odwrotnej kolejności niż były
zakładane. Jądro Linuksa dysponuje prostymi mechanizmami wykrywania pewnych typów zakleszczeń.
Jedną z miar efektywności działania systemu jest jego skalowalność. Określa ona stopień wzrostu wydajności systemu, jeśli
zostaną zwielokrotnione pewne dostępne w nim zasoby, takie jak np.: ilość pamięci, czy procesorów. W idealnym przypadku,
jeśli zwiększylibyśmy ilość procesorów w systemie z jednego do czterech, to szybkość wykonywania zadań powinna ulec
czterokrotnemu zwielokrotnieniu. Jest to jednak sytuacja czysto teoretyczna i w rzeczywistości powinniśmy być zadowoleni,
jeśli ten wzrost prędkości będzie bliski wartości cztery. Wąskimi gardłami, które uniemożliwiają zwiększenie skalowalności
mogą się okazać właśnie blokady. Jeśli o dostęp do jakiegoś zasobu rywalizuje duża ilość procesów, których wykonanie
wstrzymywane jest przez blokadę, to może się okazać konieczne zastąpienie tej pojedynczej blokady dwoma lub większą
ilością blokad. Takie zastępowanie nazywa się zwiększeniem ziarnistości blokad. Pojedyncze blokady, służące do
zabezpieczenia dużych zasobów, to tak zwane blokady gruboziarniste. Blokady zabezpieczające małe zasoby, takie jak
poszczególne pola struktur, to blokady drobnoziarniste. W systemie najwięcej jest oczywiście blokad średnioziarnistych.
Zazwyczaj programiści jądra mają tendencję do zastępowania w kolejnych wersjach kodu blokady gruboziarniste
drobnoziarnistymi, a nie odwrotnie. Przykładem jest tu mechanizm szeregujący: we wcześniejszych wersjach stosowana była
jedna lista procesów, chroniona przez pojedynczą blokadę, od wersji 2.6, wraz z nastaniem planisty O(1) została ona
4
Zasoby lokalne są „widziane” jedynie przez te wątki.
2
Systemy Operacyjne – semestr drugi
zastąpiona kilkoma listami, z których każda posiada własną blokadę. Zmiana ziarnistości blokad może również prowadzić do
problemów: zwiększenie ziarnistości może podnieść wydajność systemów pracujących na architekturach o wielu procesorach,
ale może również obniżyć ją dla systemów pracujących na komputerach o niższej liczbie procesorów lub nawet na jednym
procesorze. W tym ostatnim przypadku poszczególne zadania muszą zakładać i zdejmować szereg blokad, które z
powodzeniem można by z powodzeniem zastąpić pojedynczą blokadą.
Problem synchronizacji występuje również w przypadku dolnych połówek. Rozwiązanie tego problemu polega nie tylko na
założeniu blokady, ale również na wyłączeniu czasowym mechanizmu dolnych połówek. Operacja wyłączenia jest realizowana
za pomocą funkcji local_bh_disable(), a operacja ponownego włączenia za pomocą local_bh_enable(). Wywołania tych funkcji
mogą być zagnieżdżane, to znaczy, że przed wywołaniem funkcji local_bh_enable() można wielokrotnie wywołać
local_bh_disable(). Liczba wywołań tej ostatniej jest umieszczana w liczniku preempt_count. Wartość tego licznika jest
zmniejszana o jeden za każdym wywołaniem local_bh_enable() i dopiero kiedy osiągnie ona wartość zero dolne połówki są
ponownie włączane. Opisane funkcje nie blokują dolnych połówek realizowanych za pomocą kolejek prac, ponieważ te
wykonywane są w kontekście procesu i nie są asynchronicznie uruchamiane.
3