Docsity
Docsity

Przygotuj się do egzaminów
Przygotuj się do egzaminów

Studiuj dzięki licznym zasobom udostępnionym na Docsity


Otrzymaj punkty, aby pobrać
Otrzymaj punkty, aby pobrać

Zdobywaj punkty, pomagając innym studentom lub wykup je w ramach planu Premium


Informacje i wskazówki
Informacje i wskazówki

Laboratorium 5: ,,Wątki jądra i mechanizmy synchronizacji'', Egzaminy z Linux

1 opisuje mechanizm działania wątków jądra, a rozdział 2 wybrane mechanizmy synchronizacji, czyli operacje niepodzielne, muteksy, zmienne ...

Typologia: Egzaminy

2022/2023

Załadowany 24.02.2023

niesmialy
niesmialy 🇵🇱

4.1

(11)

80 dokumenty


Podgląd częściowego tekstu

Pobierz Laboratorium 5: ,,Wątki jądra i mechanizmy synchronizacji'' i więcej Egzaminy w PDF z Linux tylko na Docsity! Laboratorium 5: „Wątki jądra i mechanizmy synchronizacji” (jedne zajęcia) dr inż. Arkadiusz Chrobot 20 lutego 2022 Spis treści Wprowadzenie 1 1. Wątki jądra 1 1.1. Opis API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1.2. Przykład . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 2. Mechanizmy synchronizacji 5 2.1. Operacje niepodzielne . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 2.2. Muteksy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 2.3. Zmienne sygnałowe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 2.4. Blokady sekwencyjne . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 2.5. Mechanizm RCU . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 Zadania 21 Wprowadzenie Ta instrukcja jest poświęcona wątkom jądra i wybranym mechanizmom synchronizacji. Rozdział 1 opisuje mechanizm działania wątków jądra, a rozdział 2 wybrane mechanizmy synchronizacji, czyli operacje niepodzielne, muteksy, zmienne sygnałowe, blokady sekwencyjne i mechanizm rcu. Ostatni rozdział instrukcji zawiera listę zadań do samodzielnego rozwiązania w ramach zajęć laboratoryjnych. 1. Wątki jądra Wątki jądra tworzone są w przestrzeni jądra, co oznacza, że współdzielą wszystkie zasoby z pozo- stałymi podsystemami jądra systemu operacyjnego. Takie wątki najczęściej należą do jednej z dwóch kategorii: 1. wątki, które aktywowane są przez określone podsystemy jądra, wykonują pewne czynności i prze- chodzą w stan oczekiwania, 2. wątki, których działanie aktywowane jest przez mechanizm czasowy co określony odcinek czasu i polega ono na wykonaniu określonych prac i przejściu w stan oczekiwania. Mechanizm działania obu typów wątków jest zatem podobny. Oba rodzaje wątków przechodzą w stan task_running za sprawą zdarzenia pochodzącego z zewnątrz, wykonują pracę np. związaną z monito- rowaniem stanu zasobów, a następnie przechodzą w stan oczekiwania, najczęściej task_interruptible. To działanie jest cykliczne. Wątki jądra są uruchamiane wraz ze startem systemu operacyjnego lub po załadowaniu modułu, w którym są oprogramowane, a kończą działanie przy usuwaniu modułu lub zamykaniu systemu. Przejście takich wątków w stan oczekiwania jest możliwe dzięki temu, że są one wykonywane w kontekście procesu. Tak jak zadania użytkownika, podlegają one szeregowaniu, ale nieko- niecznie podlegają wywłaszczaniu. Zależy to od konfiguracji jądra z jaką zostało ono skompilowane. Jeśli opcja wywłaszczania wątków w konfiguracji kompilacji jądra jest wyłączona, to wszystkie wątki jądra będą tak długo aktywne, jak długo same nie zrzekną się procesora. Dlatego ważnym jest, aby wątek jądra po wykonaniu pracy samodzielnie wprowadził się w stan oczekiwania i wywołał planistę procesora, celem przeszeregowania zadań. Informację o uruchomionych wątkach jądra można uzyskać przy pomocy polecenia ps aux. Nazwy wątków jądra na liście wyświetlonej przez to polecenie są ujęte w nawiasy kwadratowe. 1.1. Opis API Wątki jądra są implementowane w postaci funkcji, które mają następujący prototyp: int thread_function(void *data) W definicji tej funkcji należy umieścić kod, który będzie wykonywany przez wątek. Tego rodzaju funkcje 1 53 54 static void __exit threads_exit(void) 55 { 56 kthread_stop(threads.thread[WAKING_THREAD]); 57 kthread_stop(threads.thread[SIMPLE_THREAD]); 58 } 59 60 module_init(threads_init); 61 module_exit(threads_exit); 62 63 MODULE_LICENSE("GPL"); 64 MODULE_DESCRIPTION("An example of using the kernel linux threads."); 65 MODULE_AUTHOR("Arkadiusz Chrobot <[email protected]>"); 66 MODULE_VERSION("1.0"); W wierszach nr 2 i 3 kodu źródłowego tego modułu są włączone pliki nagłówkowe związane z obsługą wątków i kolejek oczekiwania. W wierszu nr 5 zdefiniowany został typ wyliczeniowy, którego elementy zostaną użyte jako stałe określające indeksy elementów tablicy będącej polem struktury threads. Ta struktura zadeklarowana jest w wierszu nr 10 opisywanego kodu źródłowego, a wcześniej zdefiniowany jest jej typ (struct thread_structure). Struktura threads zawiera pojedyncze pole, które jest tablicą wskaźników na deskryptory procesów. Zostaną w niej zapamiętane adresy deskryptorów utworzonych przez moduł wątków jądra. Wątek, adres deskryptora wątku okresowo budzonego będzie zapamiętany w elemencie tej tablicy, którego indeks określa wartość stałej waking_thread, a adres deskryptora wątku uaktywnianego przez wątek okresowo budzony będzie zapamiętywany w elemencie o indeksie, którego wartość jest określona stałą simple_thread. W wierszu nr 12 zadeklarowana jest kolejka oczekiwania o nazwie wait_queue. W wierszu nr 13 zadeklarowana jest zmienna typu bool, o nazwie condition, której wartość będzie określała, czy wątek może skończyć oczekiwanie w kolejce. Wiersze 15-30 zawierają definicję funkcji simple_thread(), która jest funkcją wykonywaną w ramach wątku aktywowanego przez drugi z wątków uruchamianych przez moduł. W wierszu 17 zadeklarowany został element kolejki oczekiwania o nazwie wait będzie on reprezentował wątek wykonujący tę funkcję w kolejce oczekiwania. Wiersze 18-19 zawierają „nieskończoną” pętlę for. Jak opisano to w poprzednim rozdziale, wątki wykonują najczęściej swą pracę cyklicznie, dlatego ta pętla została użyta w funkcji wąt- ku. Pierwszą czynnością wykonywaną w tej pętli jest dodanie wątku, reprezentowanego przez zmienną wait do kolejki oczekiwania (wiersz nr 19). W wierszach 20-26 umieszczona jest pętla while, która koń- czy się, kiedy wartość zmiennej condition będzie wynosiła true. Wewnątrz tej pętli wątek dodaje się do kolejki oczekiwania i zmienia swój stan na stan oczekiwania (task_interruptible), o ile jeszcze w nim nie był - proszę zwrócić uwagę, że nie jest to jednoznaczne z przerwaniem działania tego wątku. Następnie wywołuje on funkcję kthread_should_stop() celem sprawdzenia, czy nie powinien się zakończyć. Jeśli ta funkcja zwrócić wartość true, to funkcja wątku zwraca wartość 0 i kończy działanie. W przeciwnym przypadku wątek dodaje komunikat do bufora jądra, a następnie wywołuje funkcję schedule(), czyli planistę procesora, który odbiera wątkowi procesor i przekazuje ją innemu zadaniu (procesowi lub wąt- kowi użytkownika, bądź innemu wątkowi jądra). Dopiero po wykonaniu tych czynności wątek znajdzie się w stanie oczekiwania. Jeśli nastąpił powrót z funkcji schedule(), to znaczy, że wątek został obudzo- ny i planista oddał mu procesor. Wątek wykonuje następną iterację pętli while celem upewnienia się, że zmienna condition ma wartość true, co znaczy, że faktycznie może zakończyć oczekiwanie. Wątki jądra mogą być obudzone tylko z dwóch powodów: albo muszą zakończyć swe działanie, albo wystą- piło zdarzenie, na które oczekiwały. W tym przykładowym module wartość zmiennej condition służy opisywanemu wątkowi do rozróżnienia tych dwóch przyczyn. Po zakończeniu pętli while wątek ustawia wartość zmiennej condition na false (wiersz nr 27) oraz zmienia swój stan na task_running i usuwa się z kolejki oczekiwania (wiersz nr 28), a następnie wykonuje kolejną iterację pętli for. W wierszach 32-44 umieszczona jest definicja funkcji waking_thread(), która będzie wykonywana w ramach wątku aktywowanego cyklicznie po upływie określonego czasu. Ta funkcja jest zbudowana inaczej niż poprzednia. Wykonywana w niej jest tylko jedna pętla. Jest to „nieskończona” pętla for. Wewnątrz tej pętli wątek najpierw sprawdza, czy nie powinien się zakończyć (wiersze 35-36) jeśli tak, to funkcja wątku zwraca 0 i kończy działanie. W przeciwnym przypadku wątek zmienia swój stan na task_interruptible przy pomocy wywołania funkcji set_current_state(), która ustawia stan zdania będącego aktualnie w posiadaniu procesora, na taki, jaki określa przekazana jej jako argument wywo- 4 łania wartość. Po tym wątek wywołuje funkcję schedule_timeout(). Zadaniem tej funkcji jest opóź- nienie wykonania zadania na określony czas, którego wartość jest określona jej argumentem wywołania. Aby umożliwić korzystanie innym zadaniom z procesora przypisanego wątkowi, funkcja ta usypia wątek i umieszcza go w kolejce oczekiwania na upływ określonego w jej wywołaniu czasu, tym samym zwalniając procesor. Wszystkie te czynności są zaszyte w jej definicji. Czas oczekiwania w przykładowym module jest określony za pomocą stałej hz. Jej wartość jest zależna od platformy sprzętowej, na której jest uru- chomiony Linux, ale zawsze określa ona częstotliwość zegara systemowego, czyli ile sygnałów przerwań wygeneruje on przez sekundę. W opisywanym module wątek będzie czekał na aktywację przez sekundę 1. Jeśli nastąpił powrót z funkcji schedule_timeout() i zwróciła ona wartość 0, to znaczy, że czas, na jaki działanie wątku zostało opóźnione już upłynął2. Jeżeli jednak funkcja zwróciła wartość różną od zera, to znaczy, że dla wątku została wywołana funkcja kthread_stop(). W tym przypadku wątek umieszcza odpowiedni komunikat w buforze jądra. Konieczność zatrzymania działania wątku będzie obsłużona na początku kolejnej iteracji pętli for przez instrukcję warunkową zawartą w wierszach 35 i 36. Po obu- dzeniu wątek jest w stanie task_running. W wierszu nr 40 zmienia on wartość zmiennej condition na true i w wierszu nr 41 wywołuje funkcję wake_up() dla kolejki, w której (prawdopodobnie) oczekuje na obudzenie pierwszy z przedstawionych wątków. Po wykonaniu tych czynności funkcja waking_thread() przechodzi do realizacji kolejnego powtórzenia pętli for. W konstruktorze modułu (wiersze 46-52) inicjowana jest kolejka oczekiwania (wiersz nr 48) oraz tworzone są dwa wątki (wiersze 49-50). Pierwszy z nich (aktywowany zdarzeniem) będzie widoczny na liście zadań pod nazwą simple_thread, a drugi (aktywowany upływem czasu) pod nazwą waking_thread. Adresy deskryptorów tych wątków zapisywane są w tablicy thread struktury threads. Proszę zwrócić uwagę, że po utworzeniu oba wątki są aktywowane. W destruktorze modułu (wiersze nr 54-58) dla obu wątków wywoływana jest funkcja kthread_stop(), która sygnalizuje im konieczność zakończenia ich pracy. Wartości zwracane przez wywołania tej funkcji są ignorowane. Komunikaty umieszczane w buforze jądra przez wątki najwygodniej jest obserwować wywołując po- lecenie dmesg z opcjami -w -d. Pierwsza opcja nakazuje temu poleceniu oczekiwać na pojawienie się kolejnego komunikatu, celem wypisania go na ekranie, a druga opcja powoduje, że dmesg umieszcza informację o tym ile czasu upłynęło między umieszczeniem przez wątki w buforze kolejnych komunika- tów. Działanie tak wywołanego polecenia dmesg można przerwać przez naciśnięcie kombinacji klawiszy Ctrl+C. 2. Mechanizmy synchronizacji Tak, jak w przypadku zdań użytkownika, tak i w przypadku wątków jądra mogą się pojawić sytuacje hazardowe, gdy korzystają one ze wspólnych zasobów. Aby nim zapobiec należy stosować mechanizmy synchronizacji, które zaimplementowali programiści jądra. W tej części instrukcji są opisanie niektóre z nich. Można je podzielić na dwie kategorie: środki ogólnego przeznaczenia (operacje niepodzielne, muteksy i zmienne sygnałowe) oraz środki zorientowane na rozwiązanie problemu czytelników i pisarzy (blokady sekwencyjne, mechanizm rcu). 2.1. Operacje niepodzielne Jeśli zasobem współdzielonym przez wątki i inne fragmenty kodu jądra są zmienne typu całkowitego lub nawet pojedyncze bity w słowie, to do ich ochrony można zastosować operacje niepodzielne zaim- plementowane w postaci makr i funkcji. W pliku nagłówkowym linux/types.h zostały zdefiniowane dwa abstrakcyjne typy danych: atomic_t i atomic64_t. Zmienne tych typów przechowują, odpowied- nio, 32-bitowe i 64-bitowe liczby całkowite. Operacje, które zdefiniowano dla tych typów są wykonywane w sposób niepodzielny (ang. atomic), co w tym przypadku oznacza dwie rzeczy: 1. zanim nie zakończy się bieżąco wykonywana operacja na zmiennej dowolnego ze wspomnianych typów, to nie może się rozpocząć kolejna dotycząca tej samej zmiennej, 1Mnożenie stałej hz przez jeden jest w tym wypadku niepotrzebne, ale pokazuje w jaki sposób można zmienić ten okres czasu, zamiast jedynki stosując inną wartość. 2Pomiar tego czasu może nie być dokładny, więc wątek może być obudzony o kilka taktów zegara wcześniej lub później. 5 2. wykonanie tych operacji nie zostanie nigdy przerwane, zawsze wykonają się do końca. Dla typu atomic_t zdefiniowano następujące operacje niepodzielne w postaci makr i funkcji: ATOMIC_INIT(i) - makro, które służy inicjacji zmiennej typu atomic_t. Przyjmuje jeden argument, któ- rym jest wartość jaka ma być przypisana do zmiennej wspomnianego typu. Wartość ta może być zapisana wprost lub w postaci stałej, wyrażenia albo zmiennej. Wartość zwracaną przez to makro przypisuje się zmiennej typu atomic_t. int atomic_read(const atomic_t *v) - funkcja, która odczytuje wartość przekazanej jej przez adres zmien- nej typu atomic_t i zwraca ją jako wartość typu int. void atomic_set(atomic_t *v, int i) - funkcja, która nadaje zmiennej typu atomic_t, przekazanej jej przez adres, wartość typu int przekazaną jej przez drugi argument wywołania. void atomic_add(int i, atomic_t *v) - funkcja dodaje do wartości zmiennej typu atomic_t, przekaza- nej jej przez adres, wartość typu int przekazaną jej jako pierwszy argument wywołania. Wynik dodawania jest zapisywany w zmiennej typu atomic_t. void atomic_sub(int i, atomic_t *v) - funkcja odejmuje od wartości zmiennej atomic_t przekazanej jej przez adres, wartość typu int przekazaną jej przez pierwszy argument wywołania. Wynik tego odejmowania jest zapisywany w zmiennej typu atomic_t. void atomic_inc(atomic_t *v) - funkcja ta zwiększa wartość przekazanej jej przez adres zmiennej typu atomic_t o jeden i nic nie zwraca. void atomic_dec(atomic_t *v) - funkcja ta zwiększa wartość przekazanej jej przez adres zmiennej typu atomic_t o jeden i nic nie zwraca. int atomic_sub_and_test(int i, atomic_t *v) - funkcja, która odejmuje liczbę przekazaną jej przez jej pierwszy parametr od wartości zmiennej typu atomic_t przekazanej jej przez adres za pomocą dru- giego parametru. Wynik odejmowania zapisywany jest w zmiennej typu atomic_t, a funkcja zwraca wartość różną od zera, jeśli wynik tego odejmowania wynosi 0 i zero w przeciwnym przypadku. int atomic_add_negative(int i, atomic_t *v) - funkcja dodaje liczbę przekazaną jej przez pierwszy pa- rametr do zmiennej typu atomic_t, której adres jest jej przekazywany przez drugi parametr. Wynik dodawania zostaje zapisany w zmiennej typu atomic_t, a funkcja zwraca wartość różną od zera, jeśli był on ujemny, lub zero w przeciwnym przypadku. int atomic_add_return(int i, atomic_t *v) - funkcja dodaje do liczby umieszczonej w zmiennej typu atomic_t, której adres jest przekazywany jej jako drugi argument wywołania, liczbę, która jest jej przekazana przez jej pierwszy parametr. Wynik jest zapisany w zmiennej typu atomic_t i zwrócony przez funkcję. int atomic_sub_return(int i, atomic_t *v) - funkcja odejmuje od liczby umieszczonej w zmiennej typu atomic_t, której adres jest jej przekazany przez jej drugi parametr, liczbę, która jest jej przekazana przez pierwszy parametr. Wynik jest umieszczany w zmiennej typu atomic_t oraz zwracany przez funkcję. atomic_inc_return(v) - makro, które zwiększa wartość przekazanej mu jako argument zmiennej typu atomic_t o jeden i zwraca wartość wynikową. atomic_dec_return(v) - makro, które zmniejsza wartość przekazanej mu jako argument zmiennej typu atomic_t o jeden i zwraca wartość wynikową. int atomic_dec_and_test(atomic_t *v) - funkcja zmniejsza wartość zmiennej typu atomic_t, której ad- res jest jej przekazany jako argument wywołania i zwraca zero, jeśli wynik jest różny od 0, lub wartość różną od zera w przeciwnym przypadku. int atomic_inc_and_test(atomic_t *v) - funkcja zwiększa wartość zmiennej typu atomic_t, której adres jest jej przekazany jako argument wywołania i zwraca zero, jeśli wynik jest różny od 0 lub wartość różną od zera w przeciwnym przypadku. 6 75 } 76 77 static int __init threads_init(void) 78 { 79 init_waitqueue_head(&wait_queue); 80 threads.thread[FIRST_THREAD] = kthread_run(first_thread,NULL,"first_thread"); 81 threads.thread[SECOND_THREAD] = kthread_run(second_thread,NULL,"second_thread"); 82 threads.thread[WAKING_THREAD] = kthread_run(waking_thread,NULL,"waking_thread"); 83 return 0; 84 } 85 86 static void __exit threads_exit(void) 87 { 88 kthread_stop(threads.thread[WAKING_THREAD]); 89 kthread_stop(threads.thread[SECOND_THREAD]); 90 kthread_stop(threads.thread[FIRST_THREAD]); 91 } 92 93 module_init(threads_init); 94 module_exit(threads_exit); 95 96 MODULE_LICENSE("GPL"); 97 MODULE_DESCRIPTION("An example of using the kernel linux threads and the atomic64_t data type."); 98 MODULE_AUTHOR("Arkadiusz Chrobot <[email protected]>"); W module tworzone są trzy wątki, jeden aktywowany upływem czasu i dwa reagujące na zdarzenie, jakim jest pobudzenie ich do działania przez wątek aktywowany czasowo. W porównaniu z poprzed- nim modułem zmieniła się liczba tych ostatnich wątków. To w ramach funkcji wykonywanych w tych wątkach przeprowadzane są operacje na zmiennej typu atomic64_t. Sama zmienna tego typu jest za- deklarowana w wierszu nr 15 modułu. W funkcji wątku, o nazwie first_thread(), jej wartość jest odczytywana i umieszczana w buforze jądra w wierszu nr 22, a następnie zwiększana o jeden w wierszu nr 23. W drugiej funkcji wątku, o nazwie second_thread(), wartość tej zmiennej również jest odczy- tywana i umieszczana w buforze jądra (wiersz nr 45), ale w kolejnym wierszu jest zmniejszana o jeden (wiersz nr 46). Aby zwiększyć intensywność operacji wykonywanych na tej zmiennej, wątek realizujący funkcję first_thread() jest ustawiany w stan oczekiwania co trzy iteracje pętli for, a wątek realizujący funkcję second_thread(), co siódmą iterację. Efekt ten osiągnięto deklarując w każdej funkcji zmienną lokalną counter, zwiększając jej wartość po każdej iteracji pętli for i usypiając dany wątek, kiedy jej wartość jest podzielna, odpowiednio, przez 3 lub przez 7. (wiersze nr 24 i 47). Pozostała część kodu jest podobna do kodu pierwszego modułu zaprezentowanego w tej instrukcji. 2.2. Muteksy Muteksy w jądrze Linuksa są semaforami binarnymi, których implementacja jest niezależna od plat- formy sprzętowej, na której są używane. Zazwyczaj są one używane do ochrony przez sytuacjami hazardo- wymi zasobów o złożonych typach, takich jak struktury, ale mogą być również wykorzystane do ochrony zmiennych prostych typów. Aby móc w module używać tych środków synchronizacji należy włączyć do kodu modułu plik nagłówkowy linux/mutex.h. Jest w nim zdefiniowany typ strukturalny struct mutex służący do deklarowania muteksów, oraz następujące podprogramy, które operują na nich: DEFINE_MUTEX(mutexname) - makro, które definiuje i inicjuje jako niezajęty muteks, o nazwie, która jest przekazana jako jego argument. mutex_init(mutex) - makro, które inicjuje muteks o adresie przekazanym za pomocą jego argumentu, jako niezajęty. mutex_lock_interruptible(lock) - makro zajmuje muteks o adresie przekazanym mu przez argument. Jeśli próba zajęcia się nie powiedzie, to wątek, który używa tego makra jest wprowadzany w stan oczekiwania task_interruptible. 9 mutex_lock(lock) - makro zajmuje muteks o adresie przekazanym mu przez argument. Jeśli próba za- jęcia się nie powiedzie, to wątek, który używa tego makra jest wprowadzany w stan oczekiwania task_uninterruptible. Makro to zwraca wartość różną od zera, jeśli wątek został aktywowany z innego powodu, niże ten na który czekał. mutex_lock_killable(lock) - makro zajmuje muteks o adresie przekazanym mu przez argument. Jeśli próba zajęcia się nie powiedzie, to wątek, który używa tego makra jest wprowadzany w stan ocze- kiwania task_killable. Makro to zwraca wartość różną od zera, jeśli wątek został aktywowany z innego powodu, niż ten na który czekał. int mutex_trylock(struct mutex *lock) - funkcja, która próbuje zająć muteks o adresie przekazanym jej przez argument wywołania. Jeśli ta próba się nie powiedzie funkcja zwraca zero, a w przeciwnym przypadku jeden. Funkcja nie wprowadza w stan oczekiwania wątku, który ją wywołał. void mutex_unlock(struct mutex *lock) - funkcja zwalnia muteks o adresie przekazanym jej przez argu- ment wywołania. int mutex_is_locked(struct mutex *lock) - funkcja zwraca jeden, jeśli muteks, którego adres jest jej przekazany jako argument wywołania jest zajęty przez wątek, lub zero w przeciwnym przypadku. Listing 3 zawiera kod źródłowy modułu będącego modyfikacją modułu z listingu 2, w którym zamiast operacji niepodzielnych na zmiennej atomic64_t użyto muteksów do synchronizacji dostępu do zmiennej typu int. Listing 3: Przykładowy moduł prezentujący działanie muteksów 1 #include<linux/module.h> 2 #include<linux/kthread.h> 3 #include<linux/wait.h> 4 #include<linux/mutex.h> 5 6 enum thread_index {WAKING_THREAD, FIRST_THREAD, SECOND_THREAD}; 7 8 static struct thread_structure 9 { 10 struct task_struct *thread[3]; 11 } threads; 12 13 static wait_queue_head_t wait_queue; 14 static bool condition; 15 static DEFINE_MUTEX(number_lock); 16 static int number; 17 18 static int first_thread(void *data) 19 { 20 int counter = 0; 21 DEFINE_WAIT(wait); 22 for(;;) { 23 mutex_lock(&number_lock); 24 pr_info("[first_thread] Number value: %d\n",number); 25 number++; 26 mutex_unlock(&number_lock); 27 if(counter%3) { 28 add_wait_queue(&wait_queue,&wait); 29 while(!condition) { 30 prepare_to_wait(&wait_queue,&wait,TASK_INTERRUPTIBLE); 31 if(kthread_should_stop()) 32 return 0; 33 printk(KERN_INFO "[first_thread]: awake\n"); 34 schedule(); 10 35 } 36 condition=false; 37 finish_wait(&wait_queue,&wait); 38 } 39 counter++; 40 } 41 } 42 43 static int second_thread(void *data) 44 { 45 int counter = 0; 46 DEFINE_WAIT(wait); 47 for(;;) { 48 mutex_lock(&number_lock); 49 pr_info("[second_thread] Number value: %d\n",number); 50 number--; 51 mutex_unlock(&number_lock); 52 if(counter%7) { 53 add_wait_queue(&wait_queue,&wait); 54 while(!condition) { 55 prepare_to_wait(&wait_queue,&wait,TASK_INTERRUPTIBLE); 56 if(kthread_should_stop()) 57 return 0; 58 printk(KERN_INFO "[second_thread]: awake\n"); 59 schedule(); 60 } 61 condition=false; 62 finish_wait(&wait_queue,&wait); 63 } 64 counter++; 65 } 66 } 67 68 static int waking_thread(void *data) 69 { 70 for(;;) { 71 if(kthread_should_stop()) 72 return 0; 73 set_current_state(TASK_INTERRUPTIBLE); 74 if(schedule_timeout(1*HZ)) 75 printk(KERN_INFO "Signal received!\n"); 76 condition=true; 77 wake_up_all(&wait_queue); 78 } 79 80 } 81 82 static int __init threads_init(void) 83 { 84 init_waitqueue_head(&wait_queue); 85 threads.thread[FIRST_THREAD] = kthread_run(first_thread,NULL,"first_thread"); 86 threads.thread[SECOND_THREAD] = kthread_run(second_thread,NULL,"second_thread"); 87 threads.thread[WAKING_THREAD] = kthread_run(waking_thread,NULL,"waking_thread"); 88 return 0; 89 } 90 91 static void __exit threads_exit(void) 92 { 93 kthread_stop(threads.thread[WAKING_THREAD]); 94 kthread_stop(threads.thread[SECOND_THREAD]); 11 26 prepare_to_wait(&wait_queue,&wait,TASK_INTERRUPTIBLE); 27 if(kthread_should_stop()) 28 return 0; 29 printk(KERN_INFO "[writer_thread]: awake\n"); 30 schedule(); 31 } 32 condition=false; 33 finish_wait(&wait_queue,&wait); 34 } 35 } 36 37 static int reader_thread(void *data) 38 { 39 DEFINE_WAIT(wait); 40 for(;;) { 41 wait_for_completion(&number_completion); 42 pr_info("[reader_thread] Number value: %d\n",number); 43 if(kthread_should_stop()) 44 return 0; 45 schedule(); 46 } 47 } 48 49 static int waking_thread(void *data) 50 { 51 for(;;) { 52 if(kthread_should_stop()) 53 return 0; 54 set_current_state(TASK_INTERRUPTIBLE); 55 if(schedule_timeout(1*HZ)) 56 printk(KERN_INFO "Signal received!\n"); 57 condition=true; 58 wake_up(&wait_queue); 59 } 60 61 } 62 63 static int __init threads_init(void) 64 { 65 init_waitqueue_head(&wait_queue); 66 threads.thread[READER_THREAD] = kthread_run(reader_thread,NULL,"reader_thread"); 67 threads.thread[WRITER_THREAD] = kthread_run(writer_thread,NULL,"writer_thread"); 68 threads.thread[WAKING_THREAD] = kthread_run(waking_thread,NULL,"waking_thread"); 69 return 0; 70 } 71 72 static void __exit threads_exit(void) 73 { 74 kthread_stop(threads.thread[READER_THREAD]); 75 kthread_stop(threads.thread[WAKING_THREAD]); 76 kthread_stop(threads.thread[WRITER_THREAD]); 77 } 78 79 module_init(threads_init); 80 module_exit(threads_exit); 81 82 MODULE_LICENSE("GPL"); 83 MODULE_DESCRIPTION("An example of using the kernel linux threads and a completion variable."); 84 MODULE_AUTHOR("Arkadiusz Chrobot <[email protected]>"); 14 Kod źródłowy tego modułu, choć bazuje na innych zaprezentowanych w tej instrukcji, to jednak znacząco się od nich różni. Role wątków aktywowanych zdarzeniami zmieniły się. Do tej pory te wątki były równoprawne. Teraz jeden z nich został pisarzem (ang. writer), a drugi czytelnikiem (ang. reader). Zostało to odzwierciedlone w nazwach elementów typu wyliczeniowego thread_index w wierszu nr 6. Jednak najważniejsze zmiany zaszły w funkcjach wątków, które teraz nazywają się wirter_thread() i reader_thread(). Obie korzystają ze zmiennej sygnałowej, która została zadeklarowana i zainicjowana w wierszu nr 15. Użycie tej zmiennej wymaga także włączenia pliku nagłówkowego linux/completion.h (wiersz nr 4). Wątek pisarza zwiększa wartość zmiennej number (wiersz nr 22), która jest współdzielona przez oba wątki i sygnalizuje zakończenie swojej pracy (wiersz nr 23). Następnie przechodzi w stan ocze- kiwania, podobnie jak inne opisywane do tej pory wątki. Funkcja realizowana przez wątek czytelnika jest znacznie prostsza niż inne prezentowane dotąd funkcje wątków. W pętli for oczekuje ona na zakończenie pracy przez wątek pisarza (wiersz nr 41), a następnie wypisuje wartość zmiennej number, sprawdza, czy wątek w ramach którego jest realizowana, nie powinien się zakończyć i wywołuje funkcję schedule(). Reszta kodu modułu jest podobna do innych, wcześniej zaprezentowanych modułów. 2.4. Blokady sekwencyjne Blokady sekwencyjne są przykładem mechanizmów synchronizacji zoptymalizowanych dla problemu czytelników i pisarzy, w którym priorytet mają pisarze. Są to zmienne, które pełnią rolę liczników. Wątek pisarza zwiększa wartość tego licznika przez rozpoczęciem modyfikacji zasobu i po jej zakończeniu. Wątek czytelnika odczytuje wartość tej zmiennej przed odczytem stanu zasobu i po jego zakończeniu. Jeśli czytelnik otrzyma dwie takie same wartości, to oznacza to, że operacja odczytu nie została przepleciona z operacją zapisu. Tylko w takim przypadku odczytany stan zasobu jest prawidłowy. Jeśli te dwie wartości różnią się, to należy powtórzyć odczyt. Blokady te używane są w przypadkach, kiedy liczba modyfikacji zasobu przewyższa liczbę odczytów. Blokady sekwencyjne są zmiennymi typu seqlock_t, który wraz z podprogramami obsługującymi te zmienne zdefiniowany jest w pliku nagłówkowym linux/seqlock.h. Wśród tych podprogramów znajdują się: DEFINE_SEQLOCK(x) - makro, które i inicjuje blokadę sekwencyjną, o nazwie przekazanej mu jako argu- ment. unsigned read_seqbegin(const seqlock_t *sl) - funkcja inline, którą wywołuje czytelnik przez odczy- tem zasobu współdzielonego. Przyjmuje ona jako argument wywołania adres blokady sekwencyjnej i zwraca jej wartość. unsigned read_seqretry(const seqlock_t *sl, unsigned start) - funkcja inline, którą czytelnik wy- wołuje po odczycie współdzielonego zasobu. Przyjmuje ona jako pierwszy argument wywołania ad- res blokady sekwencyjnej, a jako drugi wartość zwróconą przez read_seqbegin(). Funkcja zwraca zero, jeśli wartość blokady odczytana przez nią jest równa wartości otrzymanej przez drugi argu- ment wywołania. W przeciwnym przypadku zwraca ona wartość różną od zera. void write_seqlock(seqlock_t *sl) - funkcja inline wywoływana przez pisarza przed wykonaniem mo- dyfikacji zasobu współdzielonego. Zwiększa ona o jeden wartość blokady sekwencyjnej, której adres jest jej przekazany jako argument wywołania. void write_sequnlock(seqlock_t *sl) - funkcja inline wywoływana przez pisarza po wykonaniu mo- dyfikacji zasobu współdzielonego. Zwiększa ona o jeden wartość blokady sekwencyjnej, której adres jest jej przekazany jako argument wywołania. Listing 5 przedstawia kod źródłowy przykładowego modułu, w którym wątki korzystają z mechanizmu blokady sekwencyjnej. Listing 5: Przykładowy moduł prezentujący działanie blokad sekwencyjnych 1 #include<linux/module.h> 2 #include<linux/kthread.h> 3 #include<linux/wait.h> 15 4 #include<linux/seqlock.h> 5 6 enum thread_index {WAKING_THREAD, WRITER_THREAD, FIRST_READER_THREAD, SECOND_READER_THREAD}; 7 8 static struct thread_structure 9 { 10 struct task_struct *thread[4]; 11 } threads; 12 13 static wait_queue_head_t wait_queue; 14 static bool condition; 15 static int number; 16 static DEFINE_SEQLOCK(number_lock); 17 static const int first_reader_number = 1, second_thread_number = 2; 18 19 static int reader_thread(void *data) 20 { 21 DEFINE_WAIT(wait); 22 unsigned long int seqlock_value = 0; 23 int local_number = 0; 24 for(;;) { 25 do { 26 seqlock_value = read_seqbegin(&number_lock); 27 local_number = number; 28 } while(read_seqretry(&number_lock, seqlock_value)); 29 pr_info("[reader_number: %d] Value of \"number\" variable: %d\n", 30 *(int *)data,local_number); 31 add_wait_queue(&wait_queue,&wait); 32 while(!condition) { 33 prepare_to_wait(&wait_queue,&wait,TASK_INTERRUPTIBLE); 34 if(kthread_should_stop()) 35 return 0; 36 pr_info("[reader_thread %d]: awake\n", *(int *)data); 37 schedule(); 38 } 39 condition=false; 40 finish_wait(&wait_queue,&wait); 41 } 42 } 43 44 static int writer_thread(void *data) 45 { 46 for(;;) { 47 write_seqlock(&number_lock); 48 number++; 49 write_sequnlock(&number_lock); 50 if(kthread_should_stop()) 51 return 0; 52 set_current_state(TASK_INTERRUPTIBLE); 53 if(schedule_timeout(HZ>>2)) 54 pr_info("Signal received!\n"); 55 pr_info("[writer_thread] awake!\n"); 56 } 57 } 58 59 static int waking_thread(void *data) 60 { 61 for(;;) { 62 if(kthread_should_stop()) 63 return 0; 16 Listing 6: Przykładowy moduł prezentujący działanie mechanizmu rcu 1 #include<linux/module.h> 2 #include<linux/kthread.h> 3 #include<linux/wait.h> 4 #include<linux/slab.h> 5 #include<linux/rcupdate.h> 6 7 enum thread_index {WAKING_THREAD, WRITER_THREAD, FIRST_READER_THREAD, SECOND_READER_THREAD}; 8 9 static struct thread_structure 10 { 11 struct task_struct *thread[4]; 12 } threads; 13 14 static wait_queue_head_t wait_queue; 15 static bool condition; 16 static int *number_pointer; 17 static const int first_reader_number = 1, second_thread_number = 2; 18 19 static int reader_thread(void *data) 20 { 21 int *local_number_pointer = NULL; 22 for(;;) { 23 rcu_read_lock(); 24 local_number_pointer = rcu_dereference(number_pointer); 25 if(local_number_pointer) 26 pr_info("[reader_number: %d] Value of \"number\" variable: %d\n", 27 *(int *)data,*local_number_pointer); 28 rcu_read_unlock(); 29 if(kthread_should_stop()) 30 return 0; 31 set_current_state(TASK_INTERRUPTIBLE); 32 if(schedule_timeout(HZ>>2)) 33 pr_info("Signal received!\n"); 34 } 35 } 36 37 static int writer_thread(void *data) 38 { 39 int *local_number_pointer = NULL; 40 int number = 0; 41 DEFINE_WAIT(wait); 42 for(;;) { 43 void *old_pointer = NULL; 44 local_number_pointer = kmalloc(sizeof(int),GFP_KERNEL); 45 if(IS_ERR(local_number_pointer)) { 46 pr_alert("Error allocating memory: %ld\n",PTR_ERR(local_number_pointer)); 47 return 0; 48 } 49 *local_number_pointer = number++; 50 old_pointer = number_pointer; 51 rcu_assign_pointer(number_pointer,local_number_pointer); 52 synchronize_rcu(); 53 if(old_pointer) 54 kfree(old_pointer); 55 add_wait_queue(&wait_queue,&wait); 56 while(!condition) { 57 prepare_to_wait(&wait_queue,&wait,TASK_INTERRUPTIBLE); 58 if(kthread_should_stop()) 19 59 return 0; 60 pr_info("[writer_thread]: awake\n"); 61 schedule(); 62 } 63 condition=false; 64 finish_wait(&wait_queue,&wait); 65 } 66 } 67 68 static int waking_thread(void *data) 69 { 70 for(;;) { 71 if(kthread_should_stop()) 72 return 0; 73 set_current_state(TASK_INTERRUPTIBLE); 74 if(schedule_timeout(HZ)) 75 pr_info("Signal received!\n"); 76 condition=true; 77 wake_up(&wait_queue); 78 } 79 80 } 81 82 static int __init threads_init(void) 83 { 84 init_waitqueue_head(&wait_queue); 85 threads.thread[WRITER_THREAD] = kthread_run(writer_thread,NULL,"writer_thread"); 86 threads.thread[WAKING_THREAD] = kthread_run(waking_thread,NULL,"waking_thread"); 87 threads.thread[FIRST_READER_THREAD] = 88 kthread_run(reader_thread,(void *)&first_reader_number,"first_reader_thread"); 89 threads.thread[SECOND_READER_THREAD] = 90 kthread_run(reader_thread,(void *)&second_thread_number,"second_reader_thread"); 91 return 0; 92 } 93 94 static void __exit threads_exit(void) 95 { 96 kthread_stop(threads.thread[WAKING_THREAD]); 97 kthread_stop(threads.thread[WRITER_THREAD]); 98 kthread_stop(threads.thread[FIRST_READER_THREAD]); 99 kthread_stop(threads.thread[SECOND_READER_THREAD]); 100 } 101 102 module_init(threads_init); 103 module_exit(threads_exit); 104 105 MODULE_LICENSE("GPL"); 106 MODULE_DESCRIPTION("An example of using the kernel linux threads and an RCU mechanism."); 107 MODULE_AUTHOR("Arkadiusz Chrobot <[email protected]>"); Kod tego modułu jest modyfikacja kodu z listingu 5. Tym razem to wątek pisarza jest aktywowany co sekundę, a wątki czytelników co 1 4 sekundy. Do modułu, oprócz pliku nagłówkowego linux/rcupdate.h (wiersz nr 5) jest także dołączany plik nagłówkowy linux/slab.h (wiersz nr 6), ponieważ zasób współ- dzielony przez wątki będzie zmienną typu int, na którą pamięć będzie przydzielana dynamicznie przy pomocy funkcji kmalloc() i zwalniana za pomocą kfree(). W wierszu nr 16 zdefiniowano główny wskaź- nik do współdzielonego zasobu. Funkcja wątku wykonywana przez wątki-czytelników zdefiniowana jest w wierszach 19-35 kodu modułu. W wierszu nr 21 zdefiniowany jest lokalny dla tej funkcji wskaźnik, do którego zapisywany będzie uzyskany adres współdzielonego zasobu. Zanim to jednak nastąpi funkcja wywołuje rcu_read_lock() celem zasygnalizowania pisarzowi, że rozpoczyna odczyt zasobu. Następnie pozyskuje ona wskaźnik na ten zasób (wiersz nr 24) i sprawdza, czy nie jest to wskaźnik pusty. Jeśli 20 ten warunek jest prawdziwy, to umieszcza w buforze jądra odczytaną wartość zasobu, wraz z identy- fikatorem wątku, który został jej przekazany przez parametr data (wiersze nr 26 i 27), a następnie wywołuje rcu_read_unlock() celem poinformowania pisarza, że zakończyła odczyt zasobu. Funkcja writer_thread() wykonywana w ramach wątku pisarza jest bardziej skomplikowana. Wierszu nr 40 zadeklarowana jest zmienna number, której wartości będą nadawane kolejnym kopiom zasobu współdzie- lonego. W wierszu nr 39 zdefiniowany jest lokalny wskaźnik na utworzoną kopię zasobu współdzielonego. Wewnątrz pętli for w wierszu nr 43 zadeklarowany jest wskaźnik, w którym zostanie zapisany adres poprzedniej kopii zasobu współdzielonego. W wierszu nr 44 wątek pisarza tworzy nową kopię zasobu współdzielonego przydzielając na nią pamięć przy pomocy wywołania funkcji kmalloc(). Jeśli ten przy- dział się powiedzie, to temu zasobowi nadawana jest wartość zmiennej number, która następnie ulega zwiększeniu o jeden w tej zmiennej (wiersz nr 49). W wierszu nr 50 zapamiętywany jest we wskaźni- ku old_pointer adres kopii zasobu, która bieżąco jest dostępna dla czytelników. Jest on kopiowany ze wskaźnika głównego. Proszę zauważyć, że w przeciwieństwie do czytelników pisarz nie musi uzyskiwać tego adresu w szczególny sposób, po prostu kopiuje go przy pomocy instrukcji przypisania. W wierszu nr 51 pisarz publikuje adres nowej kopii zasobu przepisując go ze wskaźnika local_number_pointer do wskaźnika number_pointer (głównego wskaźnika zasobu współdzielonego) przy pomocy wywołania funkcji rcu_assign_pointer(). Następnie pisarz wywołuje funkcję synchronize_rcu() celem zaczeka- nia, aż czytelnicy przestaną korzystać ze poprzedniej kopii współdzielonego zasobu. Jeśli tak się stanie, to zwolni on pamięć na ten zasób przy pomocy wskaźnika old_pointer (wiersz nr 54), wcześniej upew- niając się, że nie był on pusty (wiersz nr 53). Pozostały kod funkcji i reszty modułu jest podobny do tych prezentowanych wcześniej w tej instrukcji. Proszę czytając kody źródłowe modułów zwrócić szczególną uwagę na kolejność urucha- miania i kończenia działania poszczególnych wątków! Jest ona bardzo ważna. Jeśli praca wątków będzie kończona w niewłaściwej kolejności, to może dojść do zawieszenia działania jądra systemu. Zdania 1. [3 punktów] Zademonstruj działanie niepodzielnych operacji na bitach. 2. [5 punkty] Rozwiąż problem producenta i konsumenta przy pomocy listy i zmiennej sygnałowej. Zauważ, że jeśli jest używana lista, to praca producenta nie musi być wstrzymywana. 3. [7 punkty] Zmodyfikuj moduł z listingu 6 tak, aby korzystał on z funkcji call_rcu() do niszczenia poprzedniej wersji zasobu współdzielonego. 4. [3 punktów] Zmień kod źródłowy modułu z listingu 1 tak, aby korzystał on z funkcji kthread_create() zamiast kthread_run(). 5. [5 punktów] Rozwiąż problem producenta i konsumenta przy pomocy kolejki fifo, operacji nie- podzielnych na zmiennej typu atomic_t i muteksa. 6. [7 punktów] Rozwiąż problem producenta i konsumenta przy pomocy dziesięcioelementowej ta- blicy, muteksa i zmiennych sygnałowych. 7. [3 punktów] Zmień moduł z listingu 4 tak, aby wątek czytelnika korzystał, zamiast z funkcji wait_for_completion(), z funkcji wait_for_completion_timeout() i umieszczał w buforze ją- dra informację ile to oczekiwanie zajmowało taktów zegara. 8. [5 punktów] Zmień moduł z listingu 4 tak, aby wątek czytelnika sygnalizował wątkowi pisarza, że zakończył już odczyt zmiennej. 9. [7 punktów] Zmodyfikuj kod modułu z listingu 6 tak, aby uruchamiane w nim były dwa wątki pisarzy. 21