Wzajemne wykluczanie

Podstawowymi mechanizmami wzajemnego wykluczania w Linuxie są wirujące blokady (spinlocks) i blokady dla czytelników i pisarzy, oraz semafory.

Wirujące blokady

Wirujące blokady są używane tylko w systemach wieloprocesorowych i służą do blokowania zasobów innym procesorom. Z założenia musi w nich występować aktywne czekanie. Ich działanie opiera się na dwóch operacjach: spin_lock i spin_unlock. Spin_lock sprawdza, czy blokada jest otwarta; jeśli tak - zamyka ją (i proces może wejść do sekcji krytycznej); jeśli nie - sprawdza jeszcze raz. Operacja spin_unlock to otwarcie blokady.

Definicje typu spinlock_t i najważniejszych operacji na nim znajduję się w pliku include/asm-i386/spinlock.h. W poniższych fragmentach wycięto instrukcje preprocesora związane w debugowaniem, oraz obsługujące przypadki systemów jednoprocesorowych i nietypowych wieloprocesorowych (PPro SMP, OOSTORE).

typedef struct { volatile unsigned int lock;} spinlock_t;

Jak widać, spinlock_t przypomina atomic_t i. Inicjalizacja wygląda następująco:

#define spin_lock_init(x)	do { (x)->lock = 0; } while(0)

Pętla jest zastosowana w celu przechytrzenia kompilatora - jej znaczenie jest tylko syntaktyczne. Na początku wartość lock jest równa 0. Jest ona zerem wtedy i tylko wtedy, gdy spinlock jest otwarty. Kiedy blokada jest zamknięta, zerowy bit zmiennej lock jest ustawiony. Pozostałe bity są zawsze wyzerowane.

Po tym wstępie pora na kod w asemblerze - implementacja operacji spin_lock.

extern inline void spin_lock(spinlock_t *lock)
{
	__asm__ __volatile__(
		spin_lock_string
		:"=m" (__dummy_lock(lock)));
}

#define spin_lock_string \
		"\n1:\t" \
		"lock ; btsl $0,%0\n\t" \
		"jc 2f\n" \
		".section .text.lock,\"ax\"\n" \
		"2:\t" \
		"testb $1,%0\n\t" \
		"jne 2b\n\t" \
		"jmp 1b\n" \
		".previous"

Ten kod wykona się w następujący sposób:

Zablokowanie magistrali na czas wykonywania test and set jest konieczne, ponieważ operacja btsl sama w sobie nie byłaby atomowa. Wówczas mogłoby się zdarzyć, że dwa procesory pobiorą z pamięci wartość zmiennej, oba stwierdzą, że jest to zero, a dopiero potem bit zostanie ustawiony. Ponieważ jednak dwa procesory dostały w wyniku btsl 0, obydwa wejdą do sekcji krytycznej.

Na pozór ten sam efekt możnaby uzyskać zakładając pętlę na atomową instrukcję test and set. W rzeczywistości jednak prowadziłoby to spowolnienia pracy całego komputera. Instrukcja testb jest podczas aktywnego oczekiwania wykonywana na wartości sprowadzonej z pamięci cache procesora. Natomiast wywołanie operacji test and set powoduje zapisanie nowej wartości w pamięci operacyjnej nawet jeśli w rzeczywistości wcale nie zmieniła wartości danego bitu. Dlatego jej ciągłe powtarzanie zwiększałoby (znacznie) obciążenie magistrali i w rezultacie powodowałoby spowolnienie pracy całego komputera.

Operacja spin_unlock, zgodnie z oczekiwaniami, jest jeszcze prostsza.

extern inline void spin_unlock(spinlock_t *lock)
{
	__asm__ __volatile__(
		spin_unlock_string
		:"=m" (__dummy_lock(lock)));
}

#define spin_unlock_string \
	"lock ; btrl $0,%0"

Instrukcja btrl resetuje zerowy bit zmiennej lock i ustawia flagę CF na jego poprzednią wartość. Może to być wykorzystane do debugowania kodu.

Dodatkowo mamy zdefiniowane liczne makra rozszerzające funkcjonalność wirujących blokad. Na przykład spin_lock_irqsave zapamiętuje flagi na podanej zmiennej, blokuje przerwania (cli) i wykonuje operację spin_lock.

#define local_irq_save(x)	__asm__ __volatile__("pushfl ;\
	popl %0 ; cli":"=g" (x): /* no input */ :"memory") 

#define local_irq_restore(x)	__asm__ __volatile__("pushl %0 ;\
	popfl": /* no output */ :"g" (x):"memory") 

#define spin_lock_irqsave(lock,  flags) \
	do { local_irq_save(flags); spin_lock(lock); } while (0) 

#define spin_unlock_irqrestore(lock,  flags) \
	do { spin_unlock(lock); local_irq_restore(flags); } while (0) 

Semafory

Przedstawione fragmenty pochodzą z pliku include/asm-i386/semaphore.h pozbawione są instrukcji związanych z debugowaniem.

struct semaphore {
	atomic_t count;
	int sleepers;
	wait_queue_head_t wait;
};

extern inline void down(struct semaphore * sem)
{
	__asm__ __volatile__(
		"# atomic down operation\n\t"
		LOCK "decl (%0)\n\t"    // --sem->count 
		"js 2f\n"             	
		"1:\n"
		".section .text.lock,\"ax\"\n" 
		"2:\tcall __down_failed\n\t"
		"jmp 1b\n"	
		".previous"	
		:/* no outputs */	
		:"c" (sem)	
		:"memory");
}

Powyższy kod działa jak następuje:

Globalna etykieta __down_failed jest zdefiniowana w pliku arch/i386/kernel/semaphore.c; za nią następuje włożenie na stos wartości rejestrów eax, edx, ecx, po czym wywoływana jest funkcja __down, zaimplementowana w tymże pliku, w języku C. Funkcja __down usypia proces i umieszcza go w kolejce sem->wait. Funkcja __down dla SMP wykorzystuje mechanizm blokad wirujących.

Podnoszenie semafora:

extern inline void up(struct semaphore * sem)
{
	__asm__ __volatile__(
		"# atomic up operation\n\t"
		LOCK "incl (%0)\n\t"     // ++sem->count 
		"jle 2f\n"
		"1:\n"
		".section .text.lock,\"ax\"\n"
		"2:\tcall __up_wakeup\n\t"
		"jmp 1b\n"
		".previous"
		:/* no outputs */
		:"c" (sem)
		:"memory");
}

Najpierw atomowo zwiększany jest licznik (sem->count). Jeśli po zwiększeniu będzie ujemny lub równy 0, to następuje przejście do globalnej etykiety __up_wakeup, skąd wywoływana jest funkcja budząca pierwszy uśpiony proces z kolejki, napisana w C.