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