Zajęcia 4: Interfejsy wewnętrzne jądra, część 1¶
Data: 21.03.2022
Materiały dodatkowe¶
Wprowadzenie¶
Na poprzednich zajęciach omówiliśmy procedurę kompilacji źródeł jądra. Dzisiaj pokażemy najczęściej używane interfejsy wewnętrzne w jądrze.
Jądro jest napisane w języku C, ale nie można w nim korzystać ze standardowej biblioteki języka C (ani żadnej innej biblioteki znanej nam z przestrzeni użytkownika). Ma jednak swoją własną biblioteczkę przydatnych funkcji. Część z nich jest identyczna z tymi ze standardowej biblioteki (lub bardzo podobna), lecz wiele z nich ma znaczące różnice, bądź jest kompletnie specyficzna dla Linuksa.
Przykładowe funkcje dostępne w jądrze:
większość funkcji znanych ze
string.h
(memcpy
,strcmp
,strcpy
, …)kstrto[u](int|l|ll)
: funkcje konwersji ze stringów w liczby, podobne do standardowychstrto*
, lecz z innym interfejsemmalloc
/free
/calloc
: nie istnieją, zastąpione przezkmalloc
,vmalloc
, i kilka innych alokatorów pamięci w zależności od potrzebsnprintf
/sscanf
: działają podobnie do zwykłych, ale posiadają inny zestaw formatów (np.%pI4
drukuje adres IPv4)bsearch
: jak w standardzie Csort
: jak standardowyqsort
, ale trzeba mu jeszcze przekazać funkcję swapującą dwa elementy
Funkcje te są zawarte w innych nagłówkach niż zwykle: musimy użyć np.
linux/string.h
, linux/bsearch.h
, itd.
printk¶
Do celów debugowania oraz informowania o ważnych zdarzeniach można użyć
funkcji printk
, działającej podobnie do printf
:
printk(KERN_WARNING "Popsuło się, kod błędu: %d\n", err);
Przed wiadomością należy załączyć jej priorytet (należy zauważyć brak przecinka), którym może być (w rosnącej kolejności):
KERN_DEBUG
KERN_INFO
KERN_NOTICE
KERN_WARNING
KERN_ERR
KERN_CRIT
KERN_ALERT
KERN_EMERG
Wiadomości wypisane
przez printk
znajdą się w logu systemowym, który można podejrzeć za
pomocą polecenia dmesg
. Jeśli mają odpowiednio wysoki priorytet, będą
również natychmiast wypisane na konsolę. Aby zmienić priorytet, od którego
wiadomości są wypisywane bezpośrednio na konsolę, używamy polecenia
dmesg -n <poziom>
, gdzie 8 powoduje wypisanie wszystkich wiadomości,
a 1 tylko tych krytycznych.
Dynamiczny przydział pamięci dla jądra¶
W jądrze istnieje wiele funkcji pozwalających na dynamiczną alokację
pamięci. Najważniejsza i najczęściej używana to kmalloc
(linux/slab.h
):
void *kmalloc(size_t size, gfp_t flags);
void kfree(void *obj);
Funkcja kmalloc
pozwala na przydział spójnego obszaru pamięci fizycznej
wielkości maks. 32 stron pamięci (daje to dla x86 niecałe 128kb pamięci;
cześć pamięci jądro rezerwuje na nagłówek bloku). Przydział pamięci
realizowany jest szybko (algorytm Buddy). Parametr flags
określa
rodzaj pamięci (stałe GFP_*
zdefiniowane w pliku linux/gfp.h
) – najważniejsze to:
GFP_KERNEL
- najczęściej używana, może być blokująca, więc wołać ją można tylko z kontekstu procesu lub we własnym wątku.GFP_ATOMIC
- nie blokuje, może być wołana z procedur obsługi przerwań (choć zazwyczaj i tak jest to zły pomysł).
void *vmalloc(size_t size);
void vfree(void *addr);
Przy pomocy vmalloc
można przydzielić obszar dowolnie duży (pod
warunkiem, ze jest odp. dużo wolnej pamieci fizycznej), ale już
niespójny fizycznie (pamięć ta przechodzi przez translację adresów).
Użycie tej funkcji bez dobrego powodu nie jest zalecane.
struct page *alloc_pages(gfp_t flags, unsigned long order)
void __free_pages(struct page *page, unsigned long order)
Przydziela 2**order
całych stron, parametr flags
określa, jak
przydzielać strony (jak w kmalloc
).
Prywatna sterta¶
Gdy mamy dużo obiektów identycznych długości, przydatne może być stworzenie własnej sterty przeznaczonej specjalnie na dany typ obiektów. Służą do tego następujące funkcje:
struct kmem_cache * kmem_cache_create(
char *name, size_t size, size_t align,
unsigned long flags,
void (*ctor)(void*));
int kmem_cache_destroy (struct kmem_cache * cachep);
Jako parametr flags
zazwyczaj podaje się 0 (większość flag służy tylko
do debugowania).
struct kmem_cache
jest naszą prywatną stertą – składa się z dynamicznie
alokowanych stron pociętych na fragmenty o dokładnie zadanej długości
z minimalnym narzutem, dodatkowo ułożonych tak, aby maksymalnie wykorzystać
cache procesora. Możemy zaalokować na niej nasz obiekt za pomocą
następujących funkcji:
void *kmem_cache_alloc(struct kmem_cache *cachep, int flags);
void kmem_cache_free(struct kmem_cache *cachep, void* objp);
Pamięć na nowy obiekt jest inicjalizowana za pomocą konstruktora podanego
przy tworzeniu cache. Dla wygody dla prostych przypadków (jeśli konstruktor
nie jest potrzebny) jest zdefiniowane makro KMEM_CACHE
opakowujące
kmem_cache_create
.
Obsługa błędów w jądrze¶
Pisząc w trybie jądra należy pamiętać, że od poprawnego działania naszego kodu zależy stabilność całego systemu – absolutnie konieczna jest obsługa wszelkich możliwych błędów w naszym module w sposób nie zakłócający pracy reszty jądra oraz ścisła kontrola nad czasem życia zaalokowanych zasobów (wyciek pamięci w jądrze powoduje konieczność okresowego restartu całego systemu).
Większość nietrywialnych funkcji w jądrze może się nie udać i zwrócić jako
wynik kod błędu. Do opisu napotkanego błędu używane są liczbowe kody błędów,
takie same jak errno
w kodzie użytkownika, ale zanegowane (czyli np.
funkcja wykrywająca błąd uprawnień wykonuje return -EPERM;
). Zakres
liczb przeznaczony na takie kody błędów to -4096 .. -1.
Istnieją 4 konwencje zwracania kodów błędów z funkcji w jądrze (i przed użyciem funkcji należy zawsze sprawdzić, której konwencji używa):
funkcja nie zwraca wyniku poza kodem błędu – typem zwracanym jest
int
, wartością zwracaną jest kod błędu, bądź 0 w przypadku sukcesu.funkcja zwraca typ liczbowy (
int
,long
,off_t
, …) – wartości z przedziału -4096 .. -1 oznaczają kod błędu, pozostałe wartości oznaczają “normalny” wynik.funkcja zwraca wskaźnik – w razie błędu, ujemny kod błędu jest rzutowany na wskaźnik i zwracany. Przy użyciu, trzeba sprawdzić, czy zwrócony wskaźnik nie jest przypadkiem przerzutowanym błędem.
funkcja zwraca wskaźnik i nie używa kodów błędów – w razie błędu zwracany jest
NULL
, a użytkownik sam musi wywnioskować odpowiedni kod błędu (przykładem takiej funkcji jestkmalloc
– w razie zwróceniaNULL
, należy przekazać wyżej kod-ENOMEM
).
Do obsługi kodów błędów przydatne są następujące makra (linux/err.h
):
IS_ERR_VALUE(x)
prawda jeśli
x
(liczba całkowita) jest kodem błędu (czyli ma wartość -4096..-1)void *ERR_PTR(long error)
konwertuje kod błędu z liczby na wskaźnik
long PTR_ERR(const void *ptr)
konwertuje w odwrotnym kierunku
long IS_ERR(const void *ptr)
prawda jeśli wskaźnik jest kodem błędu
long IS_ERR_OR_NULL(const void *ptr)
prawda jeśli wskaźnik jest kodem błędu lub NULLem
void *ERR_CAST(const void *ptr)
konwertuje kod błędu ze wskaźnika na wskaźnik (przydatne w wypadku różnych typów wskaźników)
W przypadku, gdy używana przez nas funkcja zwróci błąd, należy pamiętać (o ile
nie mamy zaplanowanej specjalnej obsługi danego błędu) o posprzątaniu wszystkich
zasobów zaalokowanych w obecnej funkcji i przekazaniu niezmodyfikowanego kodu
błędu wyżej. Dość często spotykanym (i zalecanym) w jądrze idiomem jest użycie
goto
do użycia wspólnej ścieżki obsługi błędów:
zasob_c *daj_c() {
int res;
mutex_lock(&lock);
zasob_a *a = daj_a();
if (IS_ERR(a)) {
res = PTR_ERR(a);
goto err_a;
}
int b = daj_b();
if (IS_ERR_VALUE(b)) {
res = b;
goto err_b;
}
zasob_c *c = kmalloc(sizeof *c, GFP_KERNEL);
if (c == NULL) {
res = -ENOMEM;
goto err_c;
}
c->a = a;
c->b; = b;
mutex_unlock(&lock);
return c;
/* Wspólna obsługa błędów */
err_c:
oddaj_b(b);
err_b:
oddaj_a(a);
err_a:
mutex_unlock(&lock);
return ERR_PTR(res);
}
Spis kodów błędów można znaleźć w asm-generic/errno-base.h
i asm-generic/errno.h
.
Należy pamiętać, że wiele z tych błędów ma ściśle zdefiniowaną semantykę,
czasem luźno związaną z opisem, i powinno się używać ich tylko w określonych
sytuacjach. Z ważniejszych kodów należy wymienić:
-EFAULT
Błąd przy kopiowaniu z/do pamięci użytkownika (dowolne inne zastosowanie jest niepoprawne).
-ENOMEM
Wyczerpanie pamięci operacyjnej (ale nie innych rodzajów zasobów).
-ENOSPC
Wyczerpanie miejsca na dysku bądź innym dostatecznie podobnym urządzeniu.
-ENOENT
Nie znaleziono podanego pliku (bądź innego dostatecznie podobnego zasobu).
-ESRCH
Nie znaleziono podanego procesu.
-EPERM
Brak bliżej nieokreślonych uprawnień do wykonania operacji.
-EACCES
Operacja zabroniona przez uprawnienia w systemie plików.
-EEXISTS
Operacja się nie udała, ponieważ plik (lub inny zasób) już istnieje (używane np. do operacji tworzących pliki).
-EIO
Urządzenie popsuło się w bliżej nieokreślony sposób nie z winy wołającej funkcji (zarysowana płyta CD itp).
-EINVAL
Użytkownik podał niepoprawne parametry (sprzeczne, nieobsługiwane przez urządzenie, itp).
-ENOTTY
Próba wykonania operacji na niezgodnym typie urządzenia (np. próba zmiana ustawień terminala na zwykłym pliku). Używane przede wszystkim do odrzucania nieznanych
ioctl
.-ERESTARTSYS
Używane do przerywania oczekiwania, gdy trzeba wyjść z jądra aby dostarczyć sygnał do procesu użytkownika – w odpowiednim miejscu zostanie przekonwertowany na
-EINTR
bądź restart wywołania systemowego.-EINTR
Wywołanie systemowe przerwane przez sygnał – nie należy używać bezpośrednio (zamiast tego zwrócić
-ERESTARTSYS
).-ESPIPE
Próba zmiany pozycji pliku na obiekcie, w którym takie pojęcie nie ma sensu (pipe, socket, terminal…).
Zwrócenie -1
zamiast kodu błędu, użycie w oczywisty sposób niepoprawnego
kodu błędu, bądź bezsensowne wyrzucenie kodu błędu zwróconego przez wywołaną
funkcję będzie warte ujemne punkty w zadaniach zaliczeniowych.
Sen i oczekiwanie w jądrze¶
Gdy piszemy kod działający w trybie jądra, często zdarza się, że musimy czekać na zajście jakiegoś zdarzenia (zwolnienie mutexa, gotowość urządzenia, dostępność danych, itp). W takich sytuacjach musimy wybrać, jakiego rodzaju oczekiwania chcemy użyć:
Oczekiwanie aktywne: nasz wątek jądra (bądź handler prerwań) zajmuje cały czas procesor i bez przerwy sprawdza, czy zdarzenie już zaszło. Wolno go używać tylko do oczekiwania na zdarzenia, które muszą zajść w skończonym i bardzo małym czasie (np. zwolnienie spinlocka). Nie wolno go używać do oczekiwania na zdarzenia, które są wyzwalane z innych wątków jądra (chyba, że mamy gwarancję, że dany wątek jest akurat aktywny i nie zostanie wywłaszczony, jak w przypadku oczekiwania na zwolnienie spinlocka). Jest to jedyny typ oczekiwania, jakiego wolno użyć w kontekście obsługi przerwania, bądź gdy już trzymamy spinlocka.
Sen nieprzerywalny: nasz wątek jądra dopisuje się do kolejki oczekiwania i idzie spać, prosząc o obudzenie go, gdy zdarzenie zajdzie. Procesor zostaje zwolniony i zaczyna przetwarzać inne wątki jądra. Jedynym sposobem na obudzenie wątku jest zajście zdarzenia — proces w takim stanie nie będzie reagował na sygnały. Powinno się go używać tylko do czekania na zdarzenia, które zdarzą się w skończonym i małym czasie (w przeciwnym wypadku możemy skończyć z niezabijalnym zawieszonym procesem).
Sen przerywalny przez sygnały (interruptible sleep) — jak wyżej, ale oczekiwanie może zostać przerwane przez nadejście sygnału, który wymaga obsługi (czyli wywołania funkcji obsługi sygnału bądź zabicia procesu). W takim wypadku, funkcja oczekująca zwraca wynik
-EINTR
, a kod oczekujący powinien porzucić to, co robi, w miarę możliwości cofnąć wszystkie zmiany, których już dokonał, po czym powrócić do przestrzeni użytkownika, zwracając kod błędu-ERESTARTSYS
. Należy go używać do czekania na zdarzenia, które nie mają ograniczonych ram czasowych (np. wpisanie danych przez użytkownika, otrzymanie danych na sockecie). Nie wolno go używać w sytuacji, gdy nie wolno nam odmówić wykonania obecnego zadania (np. w funkcjirelease
).Sen przerywalny przez śmierć (killable sleep) — jak wyżej, ale oczekiwanie może zostać przerwane tylko przez sygnały, które spowodują śmierć procesu. Jest swego rodzaju kompromis, pozwalający na zabicie zawieszonego procesu w przypadku, gdy cofnięcie częściowych zmian jest zbyt trudne. W miarę możliwości nie powinno się go używać.
Proste blokady¶
Ze względu na częstą w jądrze konieczność używania danych wspólnych dla wielu procesów, jedną z najważniejszych klas funkcji w jądrze stanowią funkcję synchronizujące wywołanie procesów, m.in. blokady.
Najprostszym rodzaj blokad są zwykłe blokady (nazywane też mutexami).
Ten rozaj blokad jest zdefiniowany w linux/mutex.h
. Taka blokada
nie jest rekurencyjna i musi zostać zwolniona przez proces, którą ją
założył. Proces próbujący zablokować już zablokowaną blokadę będzie
spał (przechodząc w stan S lub D) aż blokada zostanie zwolniona.
Blokady tworzy się w jeden z następujących sposobów:
/* dla blokad będących zmiennymi globalnymi */
static DEFINE_MUTEX(blokada);
/* dla blokad w strukturach alokowanych dynamicznie */
struct mutex blokada2;
/* ... */
mutex_init(&blokada2);
Dostępne są między innymi następujące operacje:
void mutex_lock(struct mutex *lock);
int mutex_lock_interruptible(struct mutex *lock);
int mutex_lock_killable(struct mutex *lock);
int mutex_trylock(struct mutex *lock);
Funkcje zakładające blokadę. Pierwsza używa snu nieprzerywalnago, druga
używa snu przerywalnego przez sygnały (zwraca -EINTR
w przypadku pobudki
wywołanej sygnałem, 0
wpp.), a trzecia używa snu przerywalnego przez
śmierć. Czwarta funkcja nie
oczekuje na zwolnienie blokady - w przypadku gdy blokada jest już
zablokowana, zwraca -EAGAIN
i nic nie robi, w przypadku udanego
zablokowania zwraca 0
.
void mutex_unlock(struct mutex *lock);
Zwalnia blokadę i budzi jeden z ewentualnych procesów oczekujących.
Pozostałe typy blokad i inne funkcje synchronizujące będą omówione na następnych zajęciach.
Makro container_of
¶
Dość ciekawym mechanizmem charakterystycznym dla jądra Linuksa jest makro
container_of
. Służy ono do otrzymania adresu struktury zawierającej
dane pole na podstawie adresu pola, typu struktury, oraz nazwy pola:
struct a {
int x;
int y;
int z;
};
struct a *ptr_a = ....;
/* Mając wskaźnik do pola y jakiejś instancji struktury a... */
int *ptr_y = &ptr_a->y;
/* ... możemy z niego odtworzyć wskaźnik do całej struktury */
struct a *ptr_a_recovered = container_of(ptr_y, struct a, y);
/* ptr_a == ptr_a_recovered */
Jest to mechanizm używany w wielu strukturach danych w bilbiotece jądra
(choćby listy i kref
) — np. w implementacji list, zamiast mieć osobną
strukturę listy (w osobnej alokacji), która wskazuje na właściwą
strukturę elementu listy, struktura list_head
jest po prostu osadzona
w elemencie listy jako pole, a makro container_of
jest używane do
otrzymania wskaźnika na zawierającą strukturę.
Wykorzystanie standardowej implementacji list¶
Jądro zawiera efektywną implementację list dwukierunkowych. Lista
składa się z cyklicznie połączonych struktur list_head
, najczęściej
będących składowymi jakiejś większej struktury. Należy zauważyć,
że zarówno głowa listy jak i jej elementy są strukturami list_head
–
wykrywanie końca listy następuje przez porównanie z adresem głowy.
Wszystkie operacje na listach dostępne są poprzez plik linux/list.h
.
Oto garść pojęć związanych z listami:
list_head
struktura reprezentująca głowę (cząstkę) listy
LIST_HEAD(lista)
makro definiujące i inicjalizujące zmienną z głową listy
INIT_LIST_HEAD(lista)
makro inicjalizujące głowę listy (dla list tworzonych dynamicznie)
list_add(co, do_czego)
dodanie
co
na początekdo_czego
list_add_tail(co, do_czego)
dodanie
co
na koniecdo_czego
list_del(co)
usunięcie
co
z listylist_empty(lista)
sprawdzenie, czy lista jest pusta
list_splice(co, przed_co)
sklejenie listy
co
orazprzed_co
list_for_each(kazdy, po_liscie)
iteracja zmiennej
kazdy
po każdym elemenciepo_liscie
list_for_each_safe(kazdy, tymczasem, po_liscie)
iteracja zmiennej
kazdy
po każdym elemenciepo_liscie
w sposób bezpieczny ze względu na usuwanie elementów listy (w tym celu wykorzystuje się zmiennątymczasem
)list_entry(moja_lista, struktura, pole)
wyliczenie wskaźnika na początek struktury, ktorej pole typu
list_head
jestmoja_lista
(jest to po prostucontainer_of
z inną nazwą)
Po szczegóły odsyłam do kodu źródłowego.