Procesy
Semafory Systemowe
Autor: Paweł Piechocki
Semafory systemowe służą do synchronizacji procesów na poziomie jądra.
semaphore.h:
struct semaphore {
atomic_t count; # wartość semaphora ( >= -1)
int sleepers; # flaga używana do budzenia, przyjmuje wartość 0, 1 lub (wyłącznie wewnątrz procedury down) 2
wait_queue_head_t wait; # kolejka związana z semaforem
};
atomic_t jest typem liczb całkowitych, na którym zdefiniowane są operacje arytmetyczne wykonywane atomowo.
Proces śpiący na semaforze może zostać obudzony (przejść przez semafor) tylko gdy sleepers == 1 i count == 0.
Znaczenia pola count:
>0 semafor podniesiony, jeszcze count procesów może pod nim przejść
0 semafor opuszczony, ale żaden proces na nim nie czeka
-1 semafor opuszczony, czeka na nim jeden bądź więcej procesów
Znaczenia pola sleepers:
0 żaden proces nie czeka na semaforze - jeżeli jakiś proces śpiący na semaforze zastanie sleepers == 0 musi poprawić tą wartość na 1 i zasnąć ponownie
1 na semaforze czeka co najmniej jeden proces
2 wartość tymczasowa - jest przyjmowana wyłącznie wewnątrz procedury down przez nowo przybyłe procesy mające właśnie zasnąć na semaforze
wartości pola sleepers służą do modyfikacji i poprawiania pola count
Do definiowania semaforów służą następujące makra:
#define __SEMAPHORE_INITIALIZER(name,count) \
{ ATOMIC_INIT(count), 0, __WAIT_QUEUE_HEAD_INITIALIZER((name).wait) ... }
#define __DECLARE_SEMAPHORE_GENERIC(name,count) \
struct semaphore name = __SEMAPHORE_INITIALIZER(name,count)
#define DECLARE_MUTEX(name) __DECLARE_SEMAPHORE_GENERIC(name,1)
#define DECLARE_MUTEX_LOCKED(name) __DECLARE_SEMAPHORE_GENERIC(name,0)
Semafor jest inicjowany na wartość podaną jako parametr count, a pole sleepers jest zerowane.
Istnieje też procedura sema_init() robiąca dokladnie, ponieważ niektóre wersje gcc zgłaszają ostrzeżenia przy próbie użycia tych makr.
semaphore.h | --> | semaphore.c |
up | --> | __up |
down | --> | __down |
down_interruptible | --> | _down_interruptible |
down_trylock | --> | __down_trylock |
Procedury zawarte w semaphore.c są wywoływane tylko kiedy trzeba. Sprawdzaniem aktualnej wartości pola count, oraz jej inkrementacją i dekrementacją (atomowo) zajmują się procedury z semaphore.h. Procedury down* zawsze zmniejszają count a jeden, a up zawsze zwiększa count o 1.
Standardowa operacja podniesienia semafora (zwiększenie wartości o pola count o jeden, oraz obudzenie dokładnie jednego procesu czekającego na semaforze (jeśli taki istnieje)
static inline void up(struct semaphore * sem)
Atomowo zwiększa count o 1 i wywołuje __up, jeśli count <= 0.
oid __up(struct semaphore *sem)
{
wake_up(&sem->wait); # Obudzić jeden z procesów czekających na kolejce wait, cała dalsza obsługa zawarta jest w algorytmach down*
}
Standardowa operacja opuszczenia semafora (zmniejszenie pola count o jeden, oraz zaśnięcie na semaforze, jeśli count < 0). Zawiera także dalszą obsługę semafora już po obudzeniu procesu po podniesieniu semafora. Procedura __down (tak jak __down_itrrruptible) zostawia przy wyjściu (dla obudzonych procesów) wartości count (== 0) i sleepers (== 0) poprawne dla przypadku, gdy w kolejce związanej z semaforem nie czeka juz żaden inny proces. Jeśli tak jednak nie jest, poprawieniem tych wartości zajmuje się jeden obudzony proces, który zastając sleepers == 0 nie przechodzi pod semaforem, tylko wykonuje obrót pętli, modyfikuje pola sleepers oraz count i zasypia ponownie. Obie funkcję są tak skonstruowane, że mogą poprawiać siebie, a także poprawiać błędne wartości pozostawiane przez __down_trylock.
Dodatkowo wszystkie funkcje __down* chronione są przez spinlocki przed odebraniem im sterowania przez np. przerwanie, więc wartości tymczasowe zmiennych semaforowych są bezpieczne. Z poniższego kodu wycięto mniej istotne dla samego algorytmu operacje.
static inline void down(struct semaphore * sem)
Atomowo zmniejsza count o 1 i wywołuje __down jeśli count < 0.
void __down(struct semaphore * sem)
{
struct task_struct *tsk = current;
DECLARE_WAITQUEUE(wait, tsk); # Deklaracja elementu kolejki zawierającego aktualny proces
tsk->state = TASK_UNINTERRUPTIBLE;
add_wait_queue_exclusive(&sem->wait, &wait); # Dodanie siebie do kolejki związanej z semaforem. Końcówka _exclusive oznacza że wake_up() będzie budził procesy z tej kolejki pojedyńczo.
sem->sleepers++; # Wyłącznie tutaj (chwilowo) sleepers może przyjąć wartość 2 dla odróżnienia nowych procesów od obudzonych
for (;;) {
int sleepers = sem->sleepers;
# atomic_add_negative dodaje pierwszy argument (int) do drugiego (atomic_t) i zwraca true, jeśli suma < 0. Wynik jest zapamiętywany na drugim argumencie
# poniższe dodanie do count przywróci wartość -1 którą kolejny down() zmniejszył do -2 i a także skoryguje ją po poprzednim procesie
if (!atomic_add_negative(sleepers - 1, &sem->count)) { # wyjście z pętli (przejście przez semafor) gdy count == 0 i sleepers == 1
sem->sleepers = 0; # to 0 oznacza że kolejny obudzony proces nie może przejść przez semafor (lub że żaden proces nie czeka już na semaforze)
sem->sleepers = 1; # 1 oznacza że są procesy uśpione na semaforze i kolejny obudzony proces może przejść pod semaforem jeśli zastanie count == 0
schedule(); # Tu następuje oddanie strerowania przez proces. Także tu proces powróci gdy odzyska procesor
tsk->state = TASK_UNINTERRUPTIBLE;
remove_wait_queue(&sem->wait, &wait); # usunąć siebie z kolejki procesów czekających
tsk->state = TASK_RUNNING;
wake_up(&sem->wait); # obudzić kolejny śpiący proces żeby poprawił wartości count i sleepers (jeżeli kolejka niepusta, wpp wartości tych pól są w porządku)
}
Uwagi do algorytmu
Kluczowym punktem tego algorytmu jest operacja:
atomic_add_negative(sleepers - 1, &sem->count)
Poniższe uwagi mogą pomóc w jej zrozumieniu:
- ponieważ sleepers przyjmuje wartości 0,1,2, operacja ta może modyfikować pole count o 1, -1 lub pozostawiać je bez zmian
- nowo przybyły proces zastający pustą kolejkę (count == -1 - zmniejszone w procedurze down, sleepers == 0) zwiększył sleepers do 1, zatem nie modyfikuje wartości count
- nowo przybyły proces zastający niepustą kolejkę (count == -2 - wartość niepoprawna, zmniejszona w procedurze down, sleepers == 1) zwiększył sleepers do 2, zatem dodaje on jeden do count, tym samym przywracając poprawną wartość -1
- proces który został obudzony i ma przejść pod semaforem (count == 0, sleepers == 1) nie modyfikuje count (tj. dodaje 0)
- proces "poprawiający" (który zastał count == 0, sleepers == 0) odejmuje jeden od count i zasypia, tym samym przywracając poprawną wartość -1
- we wszytkich tych przypadkach polu sleepers nadawana jest odpowiednia wartość poprzez zwykłe przypisanie
Prawie identyczna do down. Poniżej zaznaczono tylko różnice. Zwraca 0 gdy udało się przejść pod semaforem lub -EINTR gdy wyjście nastąpiło na skutek otrzymania sygnału.
static inline int down_interruptible(struct semaphore * sem)
Atomowo zmniejsza count, ustawia domyślnie resultat na 0 i wywołuje __down_interruptible jeśli count < 0.
int __down_interruptible(struct semaphore * sem)
{
struct task_struct *tsk = current;
DECLARE_WAITQUEUE(wait, tsk);
tsk->state = TASK_INTERRUPTIBLE; # odpowiednia flaga
add_wait_queue_exclusive(&sem->wait, &wait);
for (;;) {
int sleepers = sem->sleepers;
# tutaj następuje sprawdzenie, czy do procesu wysłano jakiś sygnał
if (signal_pending(current)) {
retval = -EINTR; # ustawić zwracaną wartość
atomic_add(sleepers, &sem->count); # skorygować wartość count (aktualny proces przestaje czekać na podniesienie semafora)
if (!atomic_add_negative(sleepers - 1, &sem->count)) {
tsk->state = TASK_INTERRUPTIBLE; # odpowiednia flaga
tsk->state = TASK_RUNNING;
remove_wait_queue(&sem->wait, &wait);
}
Uwagi do algorytmu
Jedyną różnicą od algorytmu down jest zachowanie w przypadku obudzenia procesu przez sygnał. W tej sytuacji mamy count == -1, sleepers == 1, bo wiadomo że przynajmniej nasz proces spał na semaforze. Ustawienie sleepers na 0 zapewni, że kolejny obudzony proces poprawi po nas wartość count. Jednakże najpierw sami skorygujemy wartość count na 0, co jest wartością poprawną, jeśli nie ma więcej oczekujących procesów.
Procedura ta powiedzie się tylko gdy semafor jest podniesiony (zwraca wtedy 0).
static inline int down_trylock(struct semaphore * sem)
Atomowo zmniejsza count o 1, ustawia zwracaną wartość na 0 i wywołuje __down_trylock jeśli count < 0.
int __down_trylock(struct semaphore * sem)
{
sleepers = sem->sleepers + 1;
# Należy poprawić wartość count, ten proces nie będzie czekał na podniesienie semafora
if (!atomic_add_negative(sleepers, &sem->count))
wake_up(&sem->wait); # jeśli jescze ktoś czeka, trzeba go obudzić żeby przywrócił count == -1 i sleepers == 1
return 1; # zwracamy błąd
}
Uwagi do algorytmu
__down_trylock wywoływane jest w przypadku niepowodzenia, musi skorygować wartość count zmniejszoną o jeden przez down_trylock. Możliwe są tu następujące 2 sytucje:
- nikt nie czeka na semaforze, zatem count = -1 (zmniejszone przez down_trylock), sleepers == 0 - zwiększamy do 1, dodajemy do count i otrzymujemy poprawną wartość
- na semaforze czeka inny proces, zatem sleepers = -2 (zła wartość ustawiona przez down_try_lock), sleepers == 1 - zwiększamy do 2, dodajemy do count (teraz count == 0) i budzimy śpiący proces, aby poprawił (zmniejszył o jeden) wartość count (nie ma znaczenia czy obudzony proces zasnął wewnątrz down, czy down_interruptible)