Download: KnowKT
Transkrypt
Download: KnowKT
Technika jądrowa KNOW HOW Programowanie jądra i sterowników z nowym jądrem 2.6 – TECHNIKA JĄDROWA Ochrona odcinków krytycznych należy do najważniejszych aspektów programowania współbieżnego. Blokowanie tych obszarów powinno działać niezawodnie, jednak w taki sposób, aby nie spowalniać niepotrzebnie innych części programu. Dlatego w Linuksie 2.6 programista jądra ma do dyspozycji precyzyjnie wystopniowane sposoby blokowania. EVA-KATHARINA KUNST, JÜRGEN QUADE J eśli kilka procesów na raz chce korzystać z tych samych danych, powstanie chaos. Zarówno w systemach wielo- jak i jednoprocesorowych konflikty dostępu kończą się często zawieszeniem systemu. Przeciwdziała temu synchronizacja współbieżnych działań w jądrze poprzez strategie blokowania: właściwie użyte, poprawiają stabilność systemu i podwyższają jego wydajność. Prawidłowe mechanizmy blokowania zapobiegają wkra- czaniu kilku procesów jednocześnie we fragmenty krytyczne i wzajemnej ingerencji w dane. Każdy dostęp do obiektu globalnego może doprowadzić do odcinka krytycznego, niezależnie od tego, czy chodzi o wspólnie użytko- Odcinki krytyczne i sytuacje wyścigów (tzw. race conditions) Odcinek krytyczny to fragment kodu, który sięga po wielokrotnie używane zasoby (pamięć, I/O), których z kolei chcą używać inne instancje. Jeśli, na przykład, podczas wykonywania funkcji nastąpi przerwanie, obsługująca je procedura (Interrupt-Service routine – ISR) pracuje niejako równolegle do niej. W momencie, gdy obie sięgają po tę samą zmienną, mamy do czynienia z odcinkiem krytycznym. Wynik operacji jest wtedy dziełem przypadku i stwarza tzw. sytuację wyścigu. Następstwa są trudne do przewidzenia: od błędów obliczeniowych po zupełne zawieszenie systemu. Aby zapobiec sytuacji wyścigu, należy tak chronić odcinek krytyczny, aby był on wykonywany tylko w jednym procesie jednocześnie. Odcinki krytyczne powstają nie tylko przy dostępie do wspólnych danych. Jak wykazano w odcinku drugim Techniki Jądrowej, również podczas synchronizacji między fragmentami kodu może dochodzić do sytuacji wyścigu. Sytuacja wyjściowa Aktualny Następny Następny Nowy2 Następny Nowy1 Następny Dwa procesy chcą dołączyć nowy dokument do listy Zwykły przebieg Aktualny Następny Następny Nowy2 Następny nowy1->następny = aktualny ->następny; Nowy1 aktualny -> następny = nowy1; Następny nowy2 ->następny = aktualny ->następny; aktualny ->następny = nowy2; Aktualny Następny Następny Błędny przebieg (sytuacja wyścigu) nowy1-> następny = aktualny ->następny; Nowy2 Następny Nowy1 Następny nowy2-> następny = aktualny ->następny; aktualny -> następny = nowy1; aktualny -> następny = nowy2; Proces 1 Proces 2 Rysunek 1: Jeśli dwa procesy równocześnie zmieniają listę powiązaną, dochodzi do pomieszania wskaźników. Takie błędy można wyeliminować za pomocą technik blokowania. WWW.LINUX-MAGAZINE.PL NUMER 18 LIPIEC 2005 63 KNOW HOW Technika jądrowa while( down_interruptible(&Semaphor) == -EINTR ) { ... // przetwarzanie sygnałów } Wejście do odcinka krytycznego Odcinek krytyczny (wewnątrz) ... // Modyfikacja danych Wyjście z odcinka krytycznego up( &Semaphor ); Rysunek 2: Ochrona krytycznego odcinka przez semafor. Zmiany funkcji semafora na początku i na końcu. wany interfejs, czy prostą zmienną. Magiczne słowo to „wzajemne wykluczanie”. Jeżeli większa ilość instancji konkuruje ze sobą, ustawiają się one w kolejce. Dzieje się tak, ponieważ dostęp do danych jest dozwolony w danym momencie tylko dla jednej z nich, – niezależnie od współbieżności. Uda się to tylko wtedy, gdy wszystkie instancje będą działały w sposób skoordynowany. Jądro 2.6 stawia do dyspozycji trzy podstawowe metody: operacje atomowe, semafory i blokady. W systemach jednoprocesorowych można zabezpieczyć odcinki krytyczne za pomocą prostej blokady przerwania (funkcje local_irq_enable () i local_irq_disable ()). Wielu programistów jądra łączy lokalne blokady przerwania w ich kodzie z metodami podstawowymi. Ogólna blokada przerwania, taka, jaka występowała w jądrze 2.4, nie istnieje w wersji 2.6. Metody podstawowe dają się łączyć z blokadami przerwania nie tylko ręcznie, istnieją również inne, sprawdzone sposoby. Jednak w przypadku prostych aplikacji znajdziemy w Linuksie odpowiednio proste sposoby blokowania. Operacje atomowe W przypadku zmiennych całkowitych odcinek krytyczny daje się ochronić bardzo łatwo. W Linuksie dysponujemy typem danych atomic_t. Makro ATOMIC_INIT () inicjalizuje obiekt tego typu, funkcje z Tabeli 1 czytają, zapisują lub zmieniają jego wartość. Trzy warianty ..._and_test () zwracają prawdę (1), jeśli zmienna, na którą wskazuje v, po operacji ma wartość 0. W każdym innym przypadku wartość zwracana wynosi 0. Funkcja int atomic_add_negative (int i, atomic_t *v ) dodaje wartość i i zwraca prawdę (1), jeśli wynik będzie ujemny. Jeśli wynik jest dodatni lub równy zeru, funkcja zwraca fałsz (0). Listing 1 pokazuje, jak funkcje atomic_...() w bezpieczny sposób dekrementują i testują zmienną całkowitą. Jądro systemu operacyjnego wykonuje operacje atomowe niepodzielnie (angielski „atomie”), czyli za jednym razem. W trakcie ich wykonywania przerwania nie są obsługiwane. Operacje atomowe nie są również wykonywane w systemach SMP na kilku procesorach jednocześnie. Typy danych są definiowane w pliku nagłówkowym asm/atomic.h. Kto chce pisać oprogramowanie, dające się przenosić na inne architektury Linuksa, powinien używać tylko zmiennych atomowych o długości 24 bitów. Co prawda, zasadniczo stoją do dyspozycji 32 bity, jednak przy implementacji niepodzielnego dostępu, samo jądro używa w niektórych procesorach 8 bitów. Jeśli potrzeba zaledwie 1 bitu, w Linuksie znajdziemy w pliku nagłówkowym asm/bi- Tabela 1: Ochrona zmiennych całkowitych Nagłówek: Funkcja int atomic_read (atomic_t *v ) Zadanie Czytanie void atomic_set (atomic_t *v, int i ) Pisanie void atomic_add (int i, atomic_t *v ) Dodawanie void atomic_sub (int i, atomic_t *v ) Odejmowanie void atomic_inc (atomic_t *v ) Inkrementacja void atomic_dec (atomic_t *v ) Dekrementacja int atomic_sub_and_test (int i, atomic_t *v ) Odejmowanie i test int atomic_inc_and_test (atomic_t *v ) Inkrementacja i test int atomic_dec_and_test (atomic_t *v ) Dekrementacja i test 64 NUMER 18 LIPIEC 2005 WWW.LINUX-MAGAZINE.PL tops.h funkcje Inline. Tu wymagane jest pole bitowe typu u32, na którego 32 bitach operują funkcje set_bit (), clear_bit (), test_and_set_bit (), test_and_clear_bit () i test_and_change_bit (). Wszystkie funkcje posiadają po dwa parametry: numer bitu (int nr, od 0 do 31) oraz adres zmiennej (volatile unsigned long *addr). Każda z trzech funkcji test_...() zwraca przed operacją wartość wybranego bitu. W poniższym przykładzie funkcja set_bit () ustawia szósty bit (Numer 5), clear_bit () kasuje trzeci bit (Numer 2), testuje i wstawia bit 0. u32 sw; // ustawiana wartość set_bit( 5, &sw ); clear_bit( 2, &sw ); if( !test_and_set_bit(0, &sw) ) { ... // Bit 0 nie został ustawiony. } Semafory W przypadku, gdy obejmowany ochroną obiekt jest dłuższy niż 24 bity, lub gdy chodzi o złożoną strukturę danych, z pomocą przychodzą inne metody. Programiści aplikacji pomyślą tutaj zapewne o stworzonych <@21 KT_blau>Listing 1: Funkcje atomic_<\#201>() 01 #include <\<>asm/atomic.h<\>> 02 ... 03 static atomic_t Status U = ATOMIC_INIT (4 ); 04 int s; 05 ... 06 atomic_add (2, &Status ); 07 s = atomic_read (&Status ); 08 while (atomic_sub_and_test (2, &Status ) ) { 09 ... 10 } 11 atomic_set (&Status, 4 ); Technika jądrowa przez Holendra Dijkstrę semaforach. Semafor nie jest niczym innym jak zmienną, dla której zdefiniowane są dokładnie dwie operacje. Proces wykonuje operację P przed wejściem do krytycznego odcinka, zaś po jego opuszczeniu – operację V. P jest skrótem od holenderskiego słowa „passeren” (przechodzić). Operacja ta dekrementuje semafor oraz, w razie gdyby przyjął on wartość niższą od zera, usypia przyporządkowany jej proces. Operacja V (skrót od holenderskiego słowa „vrijgeven”, pozwalać) inkrementuje zmienną. Budzi ona uśpiony proces, czekający na zablokowany obiekt, w przypadku, gdy zmienna wciąż jest negatywna, lub wynosi 0. Nawet jeśli wewnątrz jądra Linuksa nie zachodzą żadne procesy, semafory są w gotowości. Ostatni odcinek Techniki Jądrowej [1] wykazał, że istnieją wątki jądra, a co za tym idzie, klasyczne instancje sterownikowe przy sterownikach urządzeń (kod wewnątrz jądra), który wykonywany jest bezpośrednio na polecenie procesu (XXX User Process??) W razie, gdyby te fragmenty kodu walczyły o zasoby, można użyć do ich ochrony semaforów jądra. Muteksy Zanim program będzie mógł używać semafora, musi go najpierw zdefiniować (static struct semaphore Lista;) i zainicjalizować (sema_init( &Lista, 2 );). Prototypy i makra znajdują się w pliku nagłówkowym asm/semaphore.h. Wartość początkowa semafora (w przykładzie wartość 2) ustala, ile procesów jednocześnie da się zatrzymać w odcinku krytycznym. Jeśli jest to jeden proces, mówimy o wzajemnym wykluczeniu (mutual exclusion). Stąd pochodzi nazwa takiego semafora: mutex. Ponieważ większość semaforów to właśnie muteksy, posiadają one własne makra. DECLARE_MUTEX(MutexName) definiuje i inicjalizuje je z nazwą jednego z muteksów jako parametr. Funkcja przeznaczona do wkraczania w chroniony przez semafor obszar krytyczny, nazywa się down () (ponieważ zmienna semafora jest opuszczana), lub też down_interruptible (). Ta ostatnia budzi uśpiony wątek jądra lub uśpioną instancję sterownika, gdy dociera sygnał. W takim wypadku funkcja zwraca wartość nierówną zeru (patrz Listing 2). Obok dwóch wymienionych wariantów blokowania wstępu do odcinka krytycznego, istnieje również forma nieblokująca down_try- Aplikacja KNOW HOW <@21 KT_blau>Listing 2: Semafor w jądrze #include <\<>asm/semaphore.h<\>> ... if( down_interruptibleU ( &ListyMutex ) ) { return -ERESTART; } ... // Tutaj jest odcinek krytyczny up( &ListyMutex ); lock (). Ta funkcja zwraca natychmiast 0, bez oczekiwania, jeśli można wejść do odcinka krytycznego. Funkcja służąca do opuszczania odcinka krytycznego nazywa się up (). Często, w szczególności przy programowaniu sterowników, odcinki krytyczne powinny być chronione przed konkurującymi dostępami przez procedury obsługi przerwań (ISR). W takim kontekście semafory nie pomogą, choć próbują uśpić przyporządkowane procesy. Nie uda się to z pomocą ISR, jednak w takim przypadku możemy posłużyć się innymi technikami blokowania. Aplikacja Użytkownik Instancja sterownika Wątek jądra Kolejka robocza Jądro Funkcja jądra IRQ programowe Tasklet Zegar ISR a Funkcja jądra IRQ programowe IRQ sprzętowe »SA_SHIRQ« b IRQ sprzętowe a może zostać przerwane przez b Rysunek 3: Nie wszystkie komponenty systemu linuksowego mogą się wzajemnie przerywać. Z tego powodu blokada przerwań często bywa zbędna. WWW.LINUX-MAGAZINE.PL NUMER 18 LIPIEC 2005 65 KNOW HOW Technika jądrowa Blokady przerwań W najprostszym przypadku jądro blokuje przerwanie na czas dostępu. Jeśli, przykładowo, wątek jądra i ISR chcą opracować listę globalną, wątek jądra musi zablokować przerwanie, jeśli wkracza w odcinek krytyczny. Sam ISR nie musi robić nic, ponieważ nie może być zablokowany przez wątek jądra (patrz Rysunek 3). W systemie jednoprocesorowym ta procedura funkcjonuje. W systemie wieloprocesorowym blokada przerwań się nie sprawdza. Tutaj wątek jądra i ISR mogą działać na różnych procesorach, tak, aby wątek jądra musiał poczekać na zakończenie akurat aktywnych na różnych procesorach ISR-ów, aby potem zablokować je na wszystkich procesorach. Dla systemu zorientowanego na wydajność, jakim jest Linux, opcja ta nie jest żadnym rozwiązaniem. W systemach wieloprocesorowych pozostaje tym samym tylko jedno rozwiązanie: ISR musi czekać aktywnie, aż inne fragmenty kodu zakończą użytkowanie wspólnych zasobów (tutaj lista). Metoda ta nazywa się Spinlocking, natomiast wspomniane aktywne oczekiwanie w kolejce to Spinning. Użycie blokad pociąga za sobą trzy konsekwencje: <@10 L_Bullet (n)>n<\!f> blokady funkcjonują tylko na systemach wieloprocesorowych <@10 L_Bullet (n)>n<\!f> aby latencja była jak najmniejsza, należy możliwie maksymalnie skracać odcinki krytyczne <@10 L_Bullet (n)>n<\!f> podczas blokady obszaru zabezpieczonego spinloc- <@21 KT_blau>Listing 3: Blokady w kontekście przerwania. 01 #include <\<>asm/spinlock.h<\>> 02 ... 03 static spinlock_t ListLock = U SPIN_LOCK_UNLOCKED; 04 ... 05 { 06 unsigned long irqflags; 07 08 ... 09 spin_lock_irqsaveU ( &ListLock, &irqflags ); 10 ... // krytyczny odcinek 11 spin_unlock_irqrestoreU ( &ListLock, &irqflags ); 12 ... 13 } 66 NUMER 18 LIPIEC 2005 kiem, nie można usypiać wchodzącego wątku jądra. Punkt pierwszy uwidacznia, że decyzja o zastosowaniu techniki blokowania zależy w rzeczywistości od używanego sprzętu i konfiguracji jądra: blokada przerwania w systemie jednoprocesorowym, blokady (spinlocki) w systemach wieloprocesorowych (SMP – Symmetric Multiprocessing). Na szczęście, programista nie musi się martwić tym różnicowaniem. W systemach jednoprocesorowych może posłużyć się makrami blokad, które kompilator rozwinie przezroczyście do blokad przerwań. Tak jak w wypadku semaforów, programista definiuje i inicjalizuje blokadę najpierw jako obiekt. W tym celu wkleja w jego kod plik nagłówkowy <\<>asm/spinlock.h<\>>. Z reguły przyznaje blokadzie wartość SPIN_LOCK_UNLOCKED, a więc odcinek krytyczny jest na początku wolny. Przed wejściem w odcinek krytyczny, wywołuje funkcję spin_lock_irqsave (), przy wychodzeniu zaś funkcję spin_unlock_irqrestore () (patrz Listing 3). Precyzja działań Semafory i blokady występują w licznych wariantach. Przytoczone przykłady miały na celu zademonstrowanie, w jaki sposób można poprawić obsługę przerwań w jądrze, a co za tym idzie, przyśpieszyć działanie Linuksa. Ponieważ im więcej miejsc przerwania w jądrze, tym krócej muszą czekać na niego inne programy. Dobra przerywalność oznacza krótszą latencję, a więc krótsze oczekiwanie na odpowiedź. Konsekwencją wniosku, że często tylko dostęp w trybie zapisywania jest krytyczny, było powstanie semaforów odczytu/ zapisu. Są to obiekty typu struct rw_semaphore. Operacjami na nich zajmują się funkcje: down_read (), up_read () (do odczytu) i down_write () i up_write () (do zapisu). Semafory odczytu/ zapisu pozwalają odczytywać dane kilku procesom naraz. Dostęp na wyłączność zachodzi wtedy, gdy proces próbuje modyfikować chronione dane. W blokadach sytuacja komplikuje się jeszcze bardziej. Mamy tu do czynienia nie tylko z wariantami odczytu/ zapisu, ale dodatkowo z jeszcze jedną odmianą bez blokady przerwania, samodzielną blokadą IRQ, oraz blokadą przerwań niezabezpieczającą flag. Funkcje dla blokad (spinlocków) bez blokady przerwań to spin_lock () i spin_unlock (). Można ich używać tylko jako semafory, czyli WWW.LINUX-MAGAZINE.PL w wypadku konkurującej próby dostępu wątków jądra lub instancji sterowników. Jednak w przypadku krótszych odcinków krytycznych, są one zdecydowanie efektywniejsze, ponieważ pozwalają uniknąć, gdzie indziej koniecznej, wymiany procesów. Kolejny wariant blokady działa tylko na programowe IRQ. Odcinek krytyczny może być nadal przerwany przez IRQ sprzętowe (patrz Rysunek 3). Jednak przynależne mu funkcje nie nazywają się, jak można by oczekiwać spin_lock_softirq () i spin_unlock_softirq (), tylko spin_lock_bh () i spin_unlock_bh (). bh oznacza tutaj dolną połówkę (Bottom Half), rodzaj IRQ programowego w jądrze 2.2. W przypadku, gdy wątek jądra konkuruje z taskletem (ale nie z ISR) o zasób, należy w pierwszym rzędzie posłużyć się tą techniką. Istnieje jeszcze jeden wariant: blokady, które wprawdzie działają na przerwania, ale nie zabezpieczają flag przerwań. Sposób ten sprawdza się, jeśli znany jest stan flag przerwań. Przykładowo: przerwania wewnątrz ISR są z reguły zablokowane, tak więc flagi nie muszą być w tym momencie rygorystycznie zapisywane. Środowisko jądra jest jednak bardziej skomplikowane, ponieważ w Linuksie mamy kolejne możliwości. Każda z przedstawionych odmian blokad występuje dodatkowo w wariancie odczytu/ zapisu. Odpowiednie obiekty nazywają się: „Read-Write-Lock” (struct rw_lock) (Tabela 2). <@21 KT_blau>Tabela 2: Read-Write-Locks Blokowanie Odblokowanie read_lock() read_unlock() write_lock() write_unlock() read_lock_irq() read_unlock_irq() write_lock_irq() write_unlock_irq() read_lock_irqsave() read_unlock_irqsave() write_lock_irqsave() write_unlock_irqsave() read_lock_bh() read_unlock_bh(), write_lock_bh() write_unlock_bh(). Nowość: Blokady sekwencji (sequence locks) Jako nowość, wprowadzono do jądra 2.6 blokady sekwencji. Przy tej modyfikacji blokad odczytu/ zapisu, tylko dostępy w trybie zapisu mogą się wzajemnie blokować. Z kolei dostęp w trybie odczytu nie powoduje uruchomienia blokady. Dla przebiegu procesu w trybie zapisu nie zmienia się nic, wywołuje Technika jądrowa on tylko kolejne funkcje przed i po odcinku krytycznym. Jednak w wypadku procesu w trybie odczytu, który chce zagwarantować sobie dostęp, struktura programu wygląda inaczej. Nie działa on, jak proces w trybie zapisu, według schematu „Blokowanie, Próba Dostępu, Udostępnienie”, tylko odczytuje tak często w pętli, aż uzna, że jego proces odczytu nie został przerwany przez proces zapisu. Sprawdzenie od strony technicznej, czy proces zapisu miał miejsce, czy nie, można wykonać za pomocą prostego licznika. Jeśli wartości w liczniku zgadzają się bezpośrednio przed i po procesie odczytu, uznaje się odczytane dane za niezmienione przez inne procesy. W wypadku, gdy wartości się różnią, proces przeskakuje do początku pętli. Również blokadę sekwencji należy przed użyciem zdefiniować i zainicjalizować. Można tego dokonać za pomocą metody statycznej lub dynamicznej. Prototypy i makra znajdują się w pliku nagłówkowym linux/seqlock.h. Metoda statyczna: seqlock_t slock = SEQLOCK_UNLOCKED; Metoda dynamiczna: KNOW HOW <@21 KT_blau>Listing 4: Blokady w kontekście przerwań. seqlock_t slock; ... seqlock_init( &slock ); ... Dostęp w trybie odczytu potrzebuje ponadto dodatkowej zmiennej typu long, aby w ten sposób potwierdzić stan blokad sekwencji. Sprawdza się go za pomocą read_seqretry (). Dostęp w trybie odczytu otrzymuje w ten sposób strukturę jak w Listingu 4. Również blokady sekwencji występują w wariantach z blokadą lub bez blokady przerwań, względnie z lub bez blokady programowego IRQ. (Tabela 3). Należy wspomnieć również o dużej blokadzie jądra. Jest to nic innego, jak jedna, ogólna blokada, uruchamiana przez funkcję lock_kernel(), a zdejmowana przez unlock_kernel(). Ogólna blokada jądra jest przydatna tylko w przypadku modyfikacji podejmowanych w kontekście procesu jądra, nie oddziałując przy tym na przerwania. #include <\<>linux/seqlock.h<\>> ... static seqlock_t slock; static unsigned long seq; ... do { seq = read_seqbegin( &slock ); // Odczytywanie danych } while( read_seqretry( &slock,U seq ) ); <@21 KT_blau>Tabela 3: Blokady sekwencji Blokowanie Odblokowanie write_seqlock() write_sequnlock() write_seqlock_irq() write_sequnlock_irq() write_seqlock_irqsave() write_sequnlock_irqrestore() write_seqlock_bh() write_sequnlock_bh() write_seqlock_bh() write_sequnlock_bh() <@21 KT_blau>Tabela 4: Matryca blokowania/blokady (Locking-Matrix) Instancja sterowników Wątek jądra Kolejka robocza Kolejka robocza zdarzeń IRQ oprogramowania Tasklet Licznik (Timer) IRQ sprzętowe Kernel-Thread Semaphor RW-Semaphor Spinlock RW-Lock Seqlock Semaphor RW-Semaphor Spinlock RW-Lock Seqlock Spinlock RW-Lock Seqlock Spinlock-BH RW-Lock-BH Spinlock-Irqsave RW-Lock-Irqsave Seqlock-Irqsave Spinlock-BH RW-Lock-BH Spinlock-Irqsave RW-Lock-Irqsave Seqlock-Irqsave Spinlock-Irqsave RW-Lock-Irqsave Seqlock-Irqsave Workqueue, Semaphor RW-Semaphor Spinlock RW-Lock Seqlock Semaphor RW-Semaphor Spinlock RW-Lock Spinlock RW-Lock Seqlock Spinlock-BH RW-Lock-BH Spinlock-Irqsave RW-Lock-Irqsave Seqlock-Irqsave Spinlock-Irqsave RW-Lock-Irqsave Spinlock-Irqsave RW-Lock-Irqsave Seqlock-Irqsave Event-Workqueue Spinlock RW-Lock Seqlock Spinlock RW-Lock Seqlock Spinlock RW-Lock Seqlock Spinlock-BH RW-Lock-BH Spinlock-Irqsave RW-Lock-Irqsave Seqlock-Irqsave Spinlock-Irqsave RW-Lock-Irqsave Spinlock-Irqsave RW-Lock-Irqsave Seqlock-Irqsave Soft-IRQ Spinlock-BH RW-Lock-BH Spinlock-Irqsave RW-Lock-Irqsave Seqlock-Irqsave Spinlock-BH RW-Lock-BH Spinlock-Irqsave RW-Lock-Irqsave Seqlock-Irqsave Spinlock-BH RW-Lock-BH Spinlock-Irqsave RW-Lock-Irqsave Seqlock-Irqsave Spinlock RW-Lock Spinlock RW-Lock Spinlock-Irqsave RW-Lock-Irqsave Seqlock-Irqsave Spinlock-BH RW-Lock-BH Spinlock-Irqsave RW-Lock-Irqsave Seqlock-Irqsave Spinlock-Irqsave RW-Lock-Irqsave Spinlock-Irqsave RW-Lock-Irqsave Spinlock RW-Lock Spinlock RW-Lock (siehe Anmerkung 1) Spinlock-Irqsave RW-Lock-Irqsave Seqlock-Irqsave Spinlock-Irqsave RW-Lock-Irqsave Seqlock-Irqsave Spinlock-Irqsave RW-Lock-Irqsave Seqlock-Irqsave Spinlock-Irqsave RW-Lock-Irqsave Seqlock-Irqsave Spinlock-Irqsave RW-Lock-Irqsave Seqlock-Irqsave Spinlock-Irqsave RW-Lock-Irqsave Seqlock-Irqsave Spinlock RW-Lock (siehe Anmerkung 2) Tasklet, Timer Hard-IRQ <+>1<+> Tylko w różnych taskletach. Te same tasklety działają tylko raz w jednym czasie. <+>2<+>Tylko przy różnych ISR-ach. Te same ISR-y działają tylko raz w jednym czasie. WWW.LINUX-MAGAZINE.PL NUMER 18 LIPIEC 2005 67 KNOW HOW Technika jądrowa Dostęp równoległy do ogólnych danych to najprostsza forma odcinka krytycznego. Odpowiednio łatwo można rozpoznać tu niebezpieczeństwo i zapobiec mu za pomocą przedstawionych mechanizmów. Sprawa komplikuje się przy rozpoznaniu odcinków krytycznych, które powstają podczas synchronizacji pomiędzy niezależnymi strumieniami prze- twarzania. Ponieważ przy wcześniejszych wersjach rozpoznanie tego procesu należało do programisty, było to i jest nadal przyczyną wielu błędów, w szczególności w sterownikach. Poprawę sytuacji umożliwia tu obiekt ukończenia (Completion-Objekt) i funkcja wait_event_interruptible () jądra 2.6. Oba mechanizmy bazują na przedstawionych w ni- niejszym artykule metodach, a ich zastosowanie zademonstrowano w [1]. Trudy wyboru Mechanizmy ochronne dla odcinków krytycznych należą do najtrudniejszych zagadnień programowania jądra. Naczelna zasada Wyścigi wywołane przez zmianę kolejności poleceń (reordering) Nieoczekiwanym źródłem wyścigów jest zmiana szyku poleceń (reordering) przez nowoczesne kompilatory i procesory. Optymalizują one kolejność ich przetwarzania, co jest zjawiskiem pozytywnym tak długo, jak długo wynik końcowy pozostaje ten sam. Rysunek 4 przedstawia przebieg takiego procesu. Ukazany przykład staje się problematyczny, jeśli podjęta zostaje próba dostępu przez wskaźnik act do kroku 2a. Może się to odbywać poprzez kod, który uruchomiony jest na drugim procesorze. Jeśli kod szuka dostępu do niezainicjalizowanego wskaźnika new, nic już nie stoi na przeszkodzie powstaniu wyjątku typu Null Pointer. Sytuacja wyjściowa Aktualny Następny Element "nowy" powinien zostać dołączony do listy Następny Nowy Następny Aktualny Następny Następny Aktualny Następny Nowy Następny 1a Nowy Następny Nowy ->następny =aktualny -> następny Aktualny Następny Następny 2a aktualny -> następny = Nowy; Aktualny Następny Nowy Następny 1b Następny Następny Nowy Następny aktualny -> następny = nowy; Zwykły przebieg. Lista jest zawsze spójna 2b Nowy -> następny =aktualny -> następny; Odwrotna kolejność wykonywania poleceń Lista staje się chwilowo niespójna (2a) Rysunek 4: Jeśli kompilatory lub procesor przestawiają kolejność poleceń, dane stają się chwilowo niespójne. Jeśli w tym samym czasie odczytuje je inny proces, pojawią się błędy. Zapory pamięci zamiast blokad (spinlocków) Blokada jako taka, doskonale radzi sobie z zabezpieczaniem odcinka krytycznego. Jednak, jeśli wiadomo, że chodzi o współbieżne dostępy w trybie odczytu, można zastosować prostszy sposób. W tym przypadku wystarczy tzw. zapora pamięci (Memory Barrier). Gwarantuje ona wykonywanie poleceń w ustalonej kolejności - czyli zapobiega zmianie ich szyku. Zapory pamięci występują w wariancie Read, Write i Read/ Write. W pierwszym przypadku (rmb()) zapewnia, że wszystkie poprzednio zainicjowane operacje odczytu zostają zakończone przed rozpoczęciem następnych. Zapora zapisu (write barrier) wmb() troszczy się o to, aby już wydane polecenia zapisu zostały wykonane, zanim wydane zostaną kolejne. Wariant Odczyt/ Zapis mb() łączy obie metody. Prototypy tych makr znajdują się w pliku nagłówkowym asm-i386/system.h. Ponadto istnieje makro barrier(), które zapobiega przestawieniu kolejności danych tylko przez kompilator, nie przez procesor. Zjawisko zmiany szyku poleceń może czasami wystąpić podczas dostępu sprzętowego w trybie I/O. Jeśli więc istotna jest kolejność, w jakiej sterownik zapisuje rejestr sprzętowy, dobrym rozwiązaniem będzie zapora pamięci [2]. 68 NUMER 18 LIPIEC 2005 WWW.LINUX-MAGAZINE.PL Technika jądrowa brzmi: przede wszystkim należy w ogóle rozpoznać odcinek krytyczny. Następnie programiści jądra muszą znaleźć i prawidłowo zastosować odpowiedni sposób działania. Na pierwszy rzut oka fragmenty kodu wydają się proste, jednak przez współbieżność wszelkie działania stają się bardziej skomplikowane. Aby prawidłowo wybrać odpowiedni sposób działania, należy zdefiniować instancje, które biorą w nim udział. Na przykład: w odcinkach krytycznych pomiędzy wątkiem jądra i taskletami ma sens użycie blokad (patrz Tabela 4), podczas gdy semafory nie sprawdzają się. Użycie semaforów jest wskazane w momencie, gdy pewne instancje, które chcą uzyskać dostęp do odcinka krytycznego, odbywają się w kontekście procesu. Dotyczy to instancji sterowników (funkcji jądra, które są wyzwalane przez aplikacje) i wątków jądra. Wyjątkami od reguły są kolejki robocze (Work queues), w szczególności robocze kolejki zdarzeń (Event Work queues). Ponieważ są one używane przez liczne podsystemy jądra i sterowniki, nie powinno się usypiać fragmentów ich kodu za pomocą semaforów. Kwestia, który semafor akurat zastosować: zwykły, czy też semafor odczytu/ zapisu, zależy od tego, czy wewnątrz obszaru krytycznego chcemy ochronić dostęp w trybie zapisu, czy może również dostęp w trybie odczytu, przed konkurującym dostępem. Ponieważ semafory odczytu/ zapisu są zdecydowanie bardziej efektywne, to właśnie one powinny mieć pierwszeństwo, tak często, jak to możliwe. W zasadzie blokad można używać wszędzie. Ponieważ jednak - w przeciwieństwie do semaforów - blokady czekają aktywnie (busy loop, spinning), powinno się nimi chronić tylko dające się szybko wykonać odcinki krytyczne. KNOW HOW dzeń w te komponenty, zostanie wyjaśnione w następnej części Techniki Jądrowej. (ofr/fjl) INFO [1] Eva-Katharina Kunst, Jürgen Quade, „Technika Jądrowa”, Odcinek 4, Linux Magazine 11/03, str. 96. [2] Alesandro Rubini i Jonathan Corbet, „Linux Device Drivers”, Wydanie 2, O'Reilly 2001. [3] Rusty Russell, „Unreliable Guide to Locking”: [http://kernelnewbies.org/documents/kdoc/kernel-locking/lklockingguide.html] AUTORZY Wkrótce Jądro 2.6 wprowadza całkiem nowy „model urządzenia”. Co to dokładnie jest, do czego służy i jak wpasowują się sterowniki urzą- Eva-Katharina Kunst, dziennikarka i Jürgen Quade, profesor w na Uniwersytecie w Niederrhein, należą od czasu powstania Linuksa do fanów oprogramowania Open Source. WWW.LINUX-MAGAZINE.PL WWW.LINUX-MAGAZINE.PL WWW.LINUX-MAGAZINE.PL Linux Magazine w Internecie MAGAZYN O ZAAWANSOWANYCH ZASTOSOWANIACH LINUKSA WIADOMOŚCI Na stronach WWW Linux Magazine znajdziesz najnowsze wiadomości ze świata Linuksa. OBSŁUGA PRENUMERATY CO W NASTĘPNYM NUMERZE? Wszystkie sprawy związane z prenumeratą możesz załatwić sam na naszych stronach WWW. Można tutaj uaktualnić dane adresowe, przedłużyć przenumeratę lub zmienić jej parametry. Dowiedz się pierwszy, co będzie w następnym numerze Linux Magazine. Każdego miesiąca publikujemy pełny spis treści oraz kilka wybranych artykułów z numeru Linux Magazine wchodzącego właśnie do sprzedaży. POMOC DLA CZYTELNIKÓW ARCHIWUM ONLINE Chcemy pomagać naszym Czytelnikom w poznawaniu Linuksa. Na naszych stronach WWW znajdziesz kompetentne informacje. Zapraszamy również do korzystania z naszej listę mailingowej. Pełna zawartość numerów archiwalnych dostępna bezpłatnie (dla osób prywatnych) w postaci plików PDF. Funkcja pełnotekstowego wyszukiwania pozwoli łatwo znaleźć potrzebne informacje. WWW.LINUX-MAGAZINE.PL