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

Podobne dokumenty