Zajęcia 4: Moduły jądra¶
Data: 20.03.2018
Contents
Materiały dodatkowe¶
examples.tar
– przykładowe moduły
Co to jest moduł?¶
Moduł to relokowalny kod/dane, które mogą być wstawiane i usuwane z jądra w czasie działania systemu. Moduł może odwoływać się do (eksportowanych) symboli jądra tak, jakby był skompilowany jako cześć jądra oraz sam może udostepniać (eksportować) symbole, z ktorych mogą korzystać inne moduły. Moduł odpowiada za pewną określoną usługę w jądrze – np. modułami są sterowniki urządzeń i systemów plików, filtry sieciowe, algorytmy kryptograficzne, itp.
Moduły są kompilowane pod konkretną wersję i konfigurację jądra – użycie modułów z innych wersji jądra (bądź tej samej wersji ze znacząco różniącą się konfiguracją) prawdopodobnie się nie uda. System ładowania modułów spróbuje wykryć i uniemożliwić taką sytuację.
Programy i pliki związane z zarządzaniem modułami w Linuksie¶
insmod nazwa_modulu.ko [parametry]
Ładuje podany plik modułu do jądra. Jeśli są podane parametry, to przekazuje je modułowi. Należy podać pełną ścieżkę do modułu –
insmod
nie próbuje samemu szukać potrzebnego pliku.Parametry maja postać zmienna=wartość np.:
insmod ne.ko io=0x300 irq=7
modprobe nazwa_modulu [parametry]
- Przyjazna użytkownikowi nakładka na
insmod
- ładuje moduł, samemu znajdując go w/lib/modules
i ładując wszystkie potrzebne mu do dzialania zależności. W tym celu wykorzystywana jest baza danych o zależnosciach między modułami utworzona za pomocądepmod
(patrz poniżej). Moduły szukane są standardowo w katalogu/lib/modules/<wersja>
. depmod -a
- Tworzy bazę danych zależności pomiędzy modułami dla aktualnego jądra.
Zależności zostaną wpisane do pliku
/lib/modules/<wersja>/modules.dep
. /etc/modprobe.conf
i/lub/etc/modprobe.d/*
Pliki sterujące zachowaniem
modprobe
idepmod
. Tradycyjnie był jeden plik konfiguracyjny. Obecnie ze względu na łatwość modyfikacji używa się katalogu/etc/modprobe.d/
, w którym umieszcza się pliki zawierające opcje. W ten sposób można łatwo dodać opcje, np. gdy instalujemy jakieś urządzenie, bez konieczności modyfikacji pliku.Najważniejsze polecenia:
alias nazwa nazwa_modulu
Definiuje, że moduł nazwa_modulu ma byc załadowany, gdy zażąda się załadowania modułu nazwa, np.
alias eth0 ne2k-pci
powoduje załadowanie odpowiedniego modułu karty sieciowej gdy zażąda się załadowania modułu
eth0
.options nazwa_modulu opcje
Powoduje ustawienie podanych opcji przy każdym żądaniu załadowania danego modułu, np.
options ne io=0x300 irq=10
spowoduje użycie opcji
io=0x300 irq=10
przy każdym ładowaniu modułone
.
install nazwa_modulu polecenia...
Powoduje wykonanie polecenia powłoki zamiast ładowania danego modułu. Możliwe jest również załadowanie modułu lub kilku modułów przez polecenie, np.
install foo /sbin/modprobe bar; /sbin/modprobe --ignore-install foo $CMDLINE_OPTS
Opcja
--ignore-install
jest konieczna, by zapobiec zapętleniu przy ładowaniu modułu foo, powoduje zignorowanie opcji install. Parametr$CMDLINE_OPTS
zostanie zastąpiony opcjami podanymi w wywołaniu modprobe lub dołączonymi za pomocą poleceń options. Polecenie install przydaje się również do innych sztuczek, np. ładowania firmware po załadowaniu modułu. Możliwe jest też załadowanie pierwszego pasującego modułu za pomocą konstrukcji:install probe-ethernet /sbin/modprobe e100 || /sbin/modprobe eepro100
Pierwszy moduł, który się pomyślnie załaduje powoduje zaprzestanie dalszego sprawdzania. W tym wypadku jest to pierwszy pasujący moduł do karty sieciowej.
blacklist nazwa_modulu
- Powoduje, że moduł nie będzie automatycznie ładowany (np. przez
udev), przydaje się w przypadku zabugowanych nieużywanych przez
nas sterowników lub modułów do debugowania (np.
evbug
)
rmmod nazwa_modulu
- Usuwa podany moduł z jądra (
nazwa_modulu
to nazwa modułu, a nie nazwa pliku.ko
). Jądro automatycznie śledzi, które moduły są obecnie aktywnie używane (np. są zależnościami innych modułów, kontrolują zamontowany system plików, obsługują urządzenie otwarte przez jakiś proces) i odmawia usunięcia ich. Jeżeli bardzo chcemy usunąć używany moduł, można użyć opcji-f
, ale to zazwyczaj bardzo źle się kończy. lsmod
- Wypisuje wszystkie załadowane moduły wraz z informacją o ich zależnościach
(ten sam wynik daje
cat /proc/modules
). modinfo nazwa_modulu_lub_nazwa_pliku
- Wypisuje opis modułu wraz z listą parametrów.
Tworzenie modułów¶
Moduły jądra (jak i główny kod jądra) są pisane w języku C (użycie innych języków nie jest możliwe). Środowisko wewnątrz jądra jest jednak dość charakterystyczne i różni się poważnie od pisania zwykłego programu w przestrzeni użytkownika.
W jądrze przyjęło się pisać zgodnie z oficjalnym stylem kodowania – https://www.kernel.org/doc/html/v4.15/process/coding-style.html .
Kompilacja modułów¶
Do komplacji modułów potrzebny jest katalog ze skonfigurowanymi i skompilowanymi źródłami jądra. W zasadzie wystarczą same pliki nagłówkowe i konfiguracja, ale oddzielenie odpowiednich plików od reszty jest bardzo skomplikowanym procesem i tylko dystrybucje Linuxa z dużą ilością własnych skryptów są w stanie to zrobić. Za kompilację modułów (jak i samego jądra) odpowiedzialny jest system Kbuild, będący dośc skomplikowaną nakładką na Makefile.
Aby skompilować nasz moduł, musimy stworzyć plik Kbuild
opisujący nasz kod,
na przykład:
obj-m := modul.o inny_modul.o
skompiluje plik modul.c
do modułu modul.ko
, a plik inny_modul.c
do pliku inny_modul.ko
.
Jeśli chcemy połączyć kilka plików źródłowych w jeden moduł, możemy to zrobić następująco:
obj-m := modul.o
modul-objs := modul_p1.o modul_p2.o
Taki plik Kbuild skompiluje pliki modul_p1.c
i modul_p2.c
i połączy
je w moduł modul.ko
.
Aby wywołać kompilację modułu, należy wywołać make
w katalogu ze źródłami
jądra, wskazując mu nasz katalog z zewnętrznymi modułami:
make -C /usr/src/linux-<wersja> M=/home/<uzytkownik>/moje_moduly
Dla ułatwienia, można napisać własny Makefile
wywołujący odpowiednie
polecenie (patrz przykład).
Metadane modułu¶
Każdy moduł może (ale nie musi) definiować metadane za pomocą makr (zdefiniowanych
w linux/module.h
):
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Koń Fred");
MODULE_DESCRIPTION("Sterownik do mojego urządzenia");
Tak zdefiniowane metadane przechowywane są (wraz z wieloma innymi danymi)
w sekcji .modinfo
gotowego modułu i można je wypisać poleceniem
modinfo
.
Wybór licencji ma ważny i nieoczywisty efekt – użycie licencji zgodnej z GPL pozwoli nam używać symboli jądra oznaczonych jako dostępne tylko dla modułów na licencji GPL. Jako zgodne licencje rozpoznawane są:
"GPL"
– GNU Public License v2 lub późniejsza,"GPL v2"
– GNU Public License v2,"GPL and additional rights"
– prawa GNU Public License v2 + dodatkowe,"Dual BSD/GPL"
– GNU Public License v2 lub licencja BSD do wyboru,"Dual MPL/GPL"
– GNU Public License v2 lub Mozilla do wyboru,"Dual MIT/GPL"
– GNU Public License v2 lub MIT do wyboru.
Konstruktor i destruktor modułu¶
Moduły nie mają funkcji main
ani własnego procesu/wątku (chyba, że sobie go
stworzą, ale to dość rzadkie). Zamiast tego, kod modułu jest wywoływany przez
różne podsystemy jądra, gdy jest dla niego coś do zrobienia.
Każdy moduł może definiować funkcję inicjującą moduł (konstruktor) i zwalniająca moduł (destruktor). Standardowo funkcje te muszą być zdefiniowane w następujący sposób:
int funkcja_inicjujaca(void) {
/* ... */
}
void funkcja_zwalniajaca(void) {
/* ... */
}
module_init(funkcja_inicjujaca);
module_exit(funkcja_zwalniajaca);
Funkcja inicjująca jest wywoływana przy ładowaniu modułu. Jeśli wszystko się
udało, powinna zwrócić 0. Jeśli nie udało się zainicjować modułu, powinna
zwrócić kod błędu (zanegowany kod z errno*.h
) – moduł zostanie wtedy
natychmiast usunięty przez jądro.
Funkcja zwalniająca jest wywoływana przy usuwaniu modułu (ale nie jest wywoływana, gdy funkcja inicjująca zwróciła błąd).
Zadaniem funkcji inicjującej jest “wpięcie” funkcjonalności dostarczanej przez moduł w struktury jądra – na przykłąd sterownik urządzenia PCI będzie w tej funkcji informował podsystem PCI o obsługiwanych urządzeniach i funkcjach, które powinien wywołać w razie wykrycia pasującego urządzenia. Bez takiej rejestracji, jądro nigdy nie wywoła kodu naszego modułu, więc moduły bez funkcji inicjującej są użyteczne w zasadzie jedynie jako biblioteczki funkcji dla innych modułów.
Zadaniem funkcji zwalniającej jest odwrócenie wszystkiego, co zrobiła funkcja
inicjująca i posprzątanie po całej działalności modułu. Jeżeli moduł ma funkcję
inicjującą, zawsze należy dostarczyć też funkcję zwalniającą (choćby miała być
pusta) – w przeciwnym wypadku, jądro uzna, że nasz moduł nie obsługuje usuwania
i nie pozwoli wykonać na nim rmmod
.
Czasami można spotkać starsze moduły używające funkcji o domyślnych nazwach
init_module()
i cleanup_module()
, bez deklarowania ich przez
module_init()
i module_exit()
. Nie jest to zalecane w obecnych wersjach
jądra.
Moduł powinien mieć tylko jeden konstruktor i tylko jeden destruktor.
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ę.
Pierwszy przykładowy moduł pokazuje użycie printk
oraz konstruktora
i destruktora.
Korzystanie z symboli zewnętrznych¶
W modułach można dowolnie używać symboli zdefiniowanych i wyeksportowanych
przez główny kod jądra oraz przez inne moduły (można je obejrzeć w pliku
/proc/kallsyms
).
Aby symbol naszego modułu był widoczny z zewnątrz, należy go wyeksprotować
makrem EXPORT_SYMBOL
:
EXPORT_SYMBOL(moja_funkcja);
int moja_funkcja(int x) {
...
}
Istnieje również analogiczne makro EXPORT_SYMBOL_GPL
, eksportujące
symbol tylko dla modułów na licencji GPL (bądź kompatybilnej).
Program depmod
automatycznie zbiera informacje o zależnościach
między modułami wynikających z użycia wyeksportowanych symboli
i zapewni, żeby były ładowane w odpowiedniej kolejnośći.
Drugi przykładowy moduł pokazuje eksportowanie symboli oraz użycie wyeksportowanych symboli.
Parametryzacja modułów¶
Można zadeklarować, że określona zmienna będzie zawierała parametr, ktory może zostać zmieniony przy ładowaniu modułu. Nazwa parametru jest taka sama jak nazwa zmiennej.
W czasie ładowania modułu w miejsce podanych zmiennych zostaną wstawione wartości podane przez użytkownika (jeśli je poda), np.
insmod modul.ko irq=5
podstawi w miejsce zmiennej irq
wartosc 5.
Do deklaracji, że pewna zmienna ma być wykorzysta jako parametr modułu służy makro:
module_param(zmienna, typ, uprawnienia);
Typami moga być: byte
, short
, ushort
, int
, uint
, long
,
ulong
, charp
, bool
, invbool
. Typ charp
jest używany
do przekazywania napisów (char *
). Typ invbool
oznacza parametr
bool
, który jest zaprzeczeniem wartości.
Można definiować własne typy parametrów, trzeba wówczas zdefiniować również
funkcje param_get_XXX
, param_set_XXX
i param_check_XXX
.
Uprawnienia oznaczają uprawnienia, które zostaną nadane parametrowi w sysfs
.
Każdy parametr powinien posiadać opis. Opis parametru można potem odczytać wraz
z opisem całego modułu za pomocą programu modinfo
, dzięki czemu moduł niesie
ze sobą opis użycia. Opis nadaje się za pomocą makra MODULE_PARM_DESC
:
MODULE_PARM_DESC(zmienna, opis)
Przykłady:
int irq = 7;
module_param(irq, int, 0);
MODULE_PARM_DESC(irq, "Irq used for device");
char *path="/sbin/modprobe";
module_param(path, charp, 0);
MODULE_PARM_DESC(path, "Path to modprobe");
Użycie:
printk(KERN_INFO "Using irq: %d", irq);
printk(KERN_INFO "Will use path: %s", path);
Aby zadeklarować tablicę parametrów trzeba użyć innego makra:
module_param_array(zmienna, typ, wskaznik_na_licznik, uprawnienia)
Wszystkie pola poza wskaznik_na_licznik
mają takie same znaczenie
jak w module_param()
. wskaznik_na_licznik
zawiera wskaźnik do zmiennej
do której wpisana zostanie liczba elementów tablicy. Jeśli nie interesuje nas
liczba argumentów, można podać NULL
, ale wtedy trzeba rozpoznawać, czy
argument jest czy, nie na podstawie jego zawartości, co nie jest wskazane.
Maksymalna liczba elementów tablicy jest określona przez deklarację tablicy,
np. jeśli zadeklarujemy jej rozmiar na 4, to użytkownik będzie mógł przekazać
maksymalnie 4 elementy. W opisie parametru tablicowego zwyczajowo umieszcza się
w nawiasach kwadratowych maksymalną liczbę parametrów.
Przykład:
int num_paths = 2;
char *paths[4] = {"/bin", "/sbin", NULL , NULL};
module_param_array(paths, charp, &num_paths, 0);
MODULE_PARM_DESC(paths, "Search paths [4]");
Użycie:
int i;
for (i=0; i<num_paths; ++i)
printk(KERN_INFO "Path[%d]: %s\n", i, paths[i]);
Trzeci przykładowy moduł pokazuje użycie parametrów.
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.
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_*
zdef. 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).
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
).
Użycie funkcji kmalloc
pokazuje czwarty przykład.
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:
kmem_cache_t * kmem_cache_create(
char *name, size_t size, size_t align,
unsigned long flags,
void (*ctor)(void*));
int kmem_cache_destroy (kmem_cache_t * cachep);
Jako parametr flags
zazwyczaj podaje się 0 (większość flag służy tylko
do debugowania).
kmem_cache_t
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(kmem_cache_t *cachep, int flags);
void kmem_cache_free(kmem_cache_t *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
.
Automatyczne ładowanie potrzebnych modułów – kmod¶
Kmod to podsystem jądra zajmujący się ładowaniem modułów “na żądanie”, tzn. gdy wystąpi odwołanie do usługi związanej z danym modułem.
Gdy użytkownik zażąda dostępu do urządzenia, które jest obsługiwane przez moduł,
który nie jest załadowany, jądro zawiesza wykonanie programu i wykonuje funkcję
request_module()
żądając załadowania odpowiedniego modułu. Funkcja ta jest
obsługiwana przez kmod i polega na wykonaniu programu (domyślnie
/sbin/modprobe
, ale można to zmienić za pomocą /proc
) dla żądanego
modułu.
Jeśli w module ma być wykorzystywane ładowanie modułów na żądanie to należy dołączyć:
#include <linux/kmod.h>
Doładowanie modułu jest możliwe dzięki funkcji:
int request_module(const char *module_name)
Licznik odwołań¶
Każdy moduł ma swój licznik odwołań – dopóki jest on dodatni, jądro nie
pozwoli na usunięcie modułu. Powinien być on zwiększany, gdy nasz moduł
jest w aktywnym użyciu (np. obsługuje otwarte urządzenie czy zamontowany
system plików). Zarządzaniem takim licznikiem zazwyczaj zajmują się
inne podsystemy jądra, lecz trzeba im w tym pomóc, przekazując wskaźnik
na nasz moduł (makro THIS_MODULE
). Na przykład w przypadku sterownika
urządzenia znakowego, trzeba wypełnić pole owner
struktury file_operations
tym wskaźnikiem.
Biblioteki¶
Wewnątrz jądra nie można korzystać z żadnych bibliotek znanych z przestrzeni użytkownika, nawet standardowej biblioteki C. Jądro posiada jednak własną biblioteczkę podstawowych funkcji, zawierającą wiele funkcji znanych ze standardowej biblioteki C lub bardzo do nich podobnych, między innymi:
- 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.
Ćwiczenia wprawkowe¶
- Skompilować i uruchomić przykładowe moduły.
- Zbadać doświadczalnie maksymalny rozmiar, który można zaalokować za
pomocą
kmalloc
. - Przerobić przykład 4 tak, by działał dla większych buforów (za pomocą
vmalloc
). - Znaleźć i wyjaśnić dziurę bezpieczeństwa w jednym z kodów przykładowych. Zastanowić się nad konsekwencjami tego typu błędów w kodzie jądra.
Literatura¶
man insmod
,rmmod
,lsmod
,modprobe
,depmod
,modinfo
- A. Rubini, J. Corbet “Linux Device Drivers” 2nd Edition, O’Reilly 2001, rozdział II i XI - http://www.xml.com/ldd/chapter/book
- Peter Salzman, Ori Pomerantz “The Linux Kernel Module Programming Guide”, 2001 - http://www.faqs.org/docs/kernel
- http://tldp.org/HOWTO/Module-HOWTO/
- http://tldp.org/LDP/lkmpg/2.6/html/index.html
Documentation/kbuild/makefiles.txt
,modules.txt