Programowanie modułów jądra systemu Linux

Transkrypt

Programowanie modułów jądra systemu Linux
Programowanie modułów jądra systemu Linux
Instrukcja do laboratorium Systemów Operacyjnych
dr inż. Dariusz Bismor
marzec, 2007
1
Najprostszy moduł: „Hello, world!”
Pierwsze ćwiczenie polega na napisaniu modułu typu „Hello, World!”. Cała trudność pierwszego modułu wynika
z faktu, że jedną z podstawowych cech odróżniających moduły jądra od zwykłych programów języka C jest brak
możliwości używania standardowych funkcji do komunikacji z użytkownikiem. W zamian jądro oferuje funkcję,
której głównym przeznaczeniem jest zapisywanie komunikatów jądra w plikach z logami, funkcję: printk(). Tę
właśnie funkcję wykorzystamy do wypisania sławnego „Hello, world!”. Poniższy kod należy zapisać w pliku hello.c.
#i n c l u d e <l i n u x / module . h>
#i n c l u d e <l i n u x / k e r n e l . h>
#i n c l u d e <l i n u x / i n i t . h>
#d e f i n e MOD_AUTHOR " D a r i u s z Bismor <D a r i u s z . Bismor [ a t ] p o l s l . pl>"
#d e f i n e MOD_DESC " Przykladowy modul H e l l o , world "
// F u n k c j a i n i c j a l i z a c j i modułu , wykonywana z a r a z po insmod
s t a t i c i n t __init h e l l o 4 _ i n i t ( v o i d ) {
p r i n t k ( KERN_INFO " H e l l o , world 4 ! \ n" ) ;
/∗ Z w r ó c e n i e w a r t o ś c i r ó ż n e j od 0 o z n a c z a b ł ą d − modułu n i e d a j e s i ę wgrać ∗/
return 0;
}
// F u n k c j a wykonywana c h w i l ę p r z e d u s u n i ę c i e m modułu z j ą d r a
s t a t i c v o i d __exit h e l l o 4 _ e x i t ( v o i d ) {
p r i n t k ( KERN_INFO "Goodbye , world 4 ! \ n" ) ;
}
module_init ( h e l l o 4 _ i n i t ) ;
module_exit ( h e l l o 4 _ e x i t ) ;
MODULE_LICENSE( "GPL v2 " ) ;
//MODULE_LICENSE( " P r o p r i e t a r y " ) ;
MODULE_AUTHOR( MOD_AUTHOR ) ;
MODULE_DESCRIPTION( MOD_DESC ) ;
Pierwsze trzy linijki załączają potrzebne nagłówki: module.h konieczny dla każdego modułu jądra, kernel.h, w
którym znajdziemy definicję poziomu istotności komunikatu jądra KERN_INFO oraz init.h, zawierający makra użyte
w ostatnich linijkach. Znaczenie linijek z instrukcjami #define jest intuicyjne.
Kolejne linijki definiują funkcję hello_init(), która będzie odpowiednikiem funkcji main() — moduły jądra nie
posiadają funkcji main(). Funkcja ta jest wywoływana w momencie dogrywania modułu do jądra. Jest to
miejsce do wykonania wszystkich czynności wstępnych danego modułu jądra — można ją też porównać do funkcji
konstruktora klasy w języku C++. Funkcja ta jest typu int i powinna zwrócić 0 w przypadku powodzenia oraz
wartość różną od zero w przypadku błędu. Funkcja powinna być poprzedzona przedrostkiem __init, którego działanie polega na zezwoleniu na zwolnienie pamięci zajmowanej przez tą funkcję jedynie w przypadku wkompilowania
1
modułu na stałe do jądra. W naszym przypadku jedynym celem działania funkcji jest wypisanie komunikatu jądra
za pomocą funkcji printk().
Następne linie definiują funkcję kończącą działanie modułu hello_exit(). Celem działania tej funkcji jest „odczynienie” wszystkiego, co zrobiła funkcja inicjalizująca — można ją porównać do funkcji destruktora klasy języka
C++. Funkcja ta jest wywoływana bezpośrednio przed usunięciem modułu z jądra i nie posiada typu zwracanego
(jest typu pustego). Powinna ona być poprzedzona przedrostkiem __exit, który powoduje pominięcie kodu funkcji
wtedy, gdy moduł kompilujemy na sztywno do jądra. W naszym przykładzie funkcja również wypisuje komunikat
jądra do pliku z logami.
Linie programu wykorzystujące makra module_init i module_exit służą do powiadomienia jądra jak nazywają się
funkcja inicjalizująca i kończąca pracę modułu. Możliwość dowolnego nazywania funkcji inicjalizującej i kończenia
pracy modułu pojawiła się dopiero w jądrach z serii 2.6 — uprzednio funkcje te musiały nosić obligatoryjne nazwy
init_module() i cleanup_module().
Ostatnie linie programu definiują typ licencji, na podstawie której udostępnia się moduł, autora modułu i jego
krótki opis. Informacje te są zapisane w skompilowanej wersji modułu i można je odczytać za pomocą polecenia
modinfo nazwa_modulu.
2
Kompilacja i wczytanie modułu
W celu ułatwienia etapu kompilacji tego i następnych modułów najprościej będzie przygotować plik Makefile o
prezentowanej poniżej zawartości:
obj−m += h e l l o . o
all :
make −C / l i b / modules / $ ( s h e l l uname −r ) / b u i l d M=$ (PWD) modules
clean :
make −C / l i b / modules / $ ( s h e l l uname −r ) / b u i l d M=$ (PWD) c l e a n
Kompilacja przy tak przygotowanym pliku Makefile polega jedynie na wydaniu polecenia make.
Po udanej kompilacji należy załadować moduł do jądra za pomocą polecenia insmod hello.ko, a następnie
zajrzeć do końcowych linii pliku /var/log/messages (do obydwu tych operacji potrzebne są uprawnienia superużytkownika). Pojawienie się napisu „Hello, world!” oznacza poprawne wykonanie pierwszej części ćwiczenia.
Ponieważ nasz moduł nie jest do niczego w jądrze potrzebny, należy go usunąć za pomocą polecenia rmmod hello.
Udało się? Brawo! Zostałeś/zostaliście właśnie autorem/autorami modułu jądra!
3
Przekazywanie parametrów do modułu jądra
Każdy moduł jądra może wykorzystywać parametry podawane w linii poleceń ładujących moduł do jądra. Zmienne
przechowujące takie parametry muszą być zadeklarowane jako globalne statyczne zmienne modułu, a każda z nich
musi być przetwarzana przez makro module_param(), które jest zdefiniowane w pliku nagłówkowym moduleparam.h.
Dodatkowo, każdej zmiennej można przypisać opis, który będzie można wyświetlić za pomocą polecenia modinfo.
Ilustruje to poniższy fragment kodu, który należy połączyć z kodem pierwszego modułu. Następnie należy moduł
skompilować i załadować podając mu jeden lub kilka parametrów1 :
insmod hello2.ko mojInt=-10 lancuch=’"To juz nie test"’
i zaobserwować efekt w pliku /var/log/messages.
static
static
static
static
s h o r t i n t mojShort = 1 ;
i n t mojInt = 1 0 2 4 ;
l o n g i n t mojLong = 1 0 0 0 0 0 ;
c h a r ∗ l a n c u c h = " Test " ;
/∗ F u n k c j a do r e j e s t r a c j parametrów modułu
module_param ( nazwa , t y p , d o s t e p )
nazwa − nazwa parametru ,
1 W zapisie należy zastosować podwójne znaki cudzysłowu: pojedyncze na zewnątrz i podwójne w środku, w celu zabezpieczenia
łańcucha znakowego zawierającego „białe” znaki przed zinterpretowaniem przez powłokę jako następne parametry. Łańcuch znakowy
nie zawierający odstępów w ogóle nie musi być w cudzysłowach.
2
t y p − t y p danych
d o s t ę p − t r y b d o s t ę p u do p a r a m e t r u w s y s f s ∗/
module_param ( mojShort , s h o r t , S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP ) ;
MODULE_PARM_DESC( mojShort , " parametr typu s h o r t i n t " ) ;
module_param ( mojInt , i n t , S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP ) ;
MODULE_PARM_DESC( mojInt , " parametr typu i n t " ) ;
module_param ( mojLong , l o n g , S_IRUSR ) ;
MODULE_PARM_DESC( mojLong , " parametr typu l o n g i n t " ) ;
module_param ( lancuch , charp , 0 ) ;
MODULE_PARM_DESC( lancuch , " parametr w p o s t a c i ł a ń c u c h a znakowego " ) ;
s t a t i c i n t __init h e l l o 6 _ i n i t ( v o i d ) {
p r i n t k ( KERN_INFO " H e l l o , world ! \ n===================\n" ) ;
p r i n t k ( KERN_INFO " Parametr typu s h o r t i n t z a w i e r a : %hd\n" , mojShort ) ;
p r i n t k ( KERN_INFO " Parametr typu i n t z a w i e r a : %d\n" , mojInt ) ;
p r i n t k ( KERN_INFO " Parametr typu l o n g i n t z a w i e r a : %l d \n" , mojLong ) ;
p r i n t k ( KERN_INFO " Parametr łańcuchowy z a w i e r a : %s \n" , l a n c u c h ) ;
return 0;
}
Dla wyjaśnienia, trzeci z parametrów makra module_param() oznacza tryb dostępu do parametru w hierarchii
katalogu /sysfs. Należy także wspomnieć, że istnieje możliwość zadeklarowania tablicy elementów dla różnych
typów jako parametru linii poleceń — zagadnienie to nie mieści się jednak w ramach czasowych laboratorium.
4
Obsługa urządzeń znakowych
W większości przypadków celem pracy modułu jest zapewnienie dostępu użytkownika do konkretnej części sprzętu
lub do jakiejś funkcjonalności czy protokołu. Dlatego musi istnieć możliwość komunikacji użytkownika, którym
niekoniecznie jest superużytkownik, z modułem jądra. Komunikację taką realizuje się przez specjalne pliki, zwane
plikami urządzeń (ang. device files), znajdujące się zwykle w kartotece /dev i jej podkartotekach. Celem następnego
modułu jądra jest komunikacja z takim plikiem.
Pliki specjalne znajdujące się w kartotece /dev można podzielić na pliki urządzeń blokowych i znakowych.
Urządzenia blokowe to takie, które posiadają bufor na przekazywane dane, który z kolei umożliwia optymalizację
odczytu/zapisu urządzenia. Właściwość tą wykorzystują urządzenia przechowujące dane, jak dyski twarde czy
pamięci flash. Pliki urządzeń blokowych mają literkę "b" w pierwszym polu rozszerzonego listingu zawartości
kartoteki (ls -l). Natomiast urządzenia znakowe to urządzenia, które nie buforują przekazywanych danych —
można powiedzieć, że jest to większość urządzeń (porty szeregowe, karty muzyczne, karty sieciowe, itp). Pliki
urządzeń znakowych mają literkę "c" (od ang. character devices) w pierwszym polu rozszerzonego listingu.
Każdy plik specjalny charakteryzuje się, oprócz nazwy, swoim numerem główny (ang. major number) i numerem
pobocznym (ang. minor number) — możemy je zobaczyć w odpowiednio 5-tej i 6-tej kolumnie rozszerzonego listingu
zawartości kartoteki. Właśnie za pomocą numeru głównego jądro rozróżnia, który moduł jądra powinien otrzymać
dane zapisywane do pliku specjalnego lub odpowiedzieć na żądanie przesłania danych, czyli odczyt pliku specjalnego.
Numer poboczny, natomiast, jest ważny jedynie dla samego modułu jądra i służy mu do rozróżniania pomiędzy
różnymi funkcjonalnościami (np. ten sam moduł jądra obsługuje wiele dysków twardych i partycji dyskowych).
Należy zdawać sobie sprawę z tego, że numery główne przydzielają dla poszczególnych urządzeń twórcy jądra
Linuxa. I tak np. numer główny 2 dla urządzeń znakowych został zarezerwowany dla pseudoterminali (pty), a
numer główny 2 dla urządzeń blokowych dla napędów dyskietek (fd). Listę zarezerwowanych numerów można
znaleźć w /usr/src/linux/Documentation/devices.txt. W pliku tym określono również kilka zakresów numerów
eksperymentalnych, które będą najbardziej odpowiednie dla celów laboratorium.
Poleceniem, które służy do tworzenia plików specjalnych jest mknod o następującej składni:
mknod /dev/nazwa c major minor, gdzie nazwa to nazwa pliku specjalnego, c oznacza żądanie utworzenia pliku
urządzenia znakowego, a major i minor oznaczają numer główny i poboczny urządzenia. Oczywiście, nic nie stoi na
przeszkodzie aby umieścić plik specjalny w innej kartotece, niż /dev, jeśli nasz moduł jądra nie będzie wykorzystywany przez nikogo prócz nas.
Ładowanie do jądra modułu obsługującego urządzenia o danym numerze oznacza konieczność powiadomienia
jądra, który moduł będzie to urządzenie obsługiwał. Jest to jednoznaczne z przyporządkowaniem numeru głównego
do modułu jądra, a wykonuje się tą operację przez wywołanie funkcji:
3
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
gdzie name oznacza nazwę urządzenia przechowywaną przez system, a fops to wskaźnik do bardzo ważnej i bardzo
rozbudowanej struktury file_operations, która odpowie m.in. na pytanie jaka funkcja ma zostać wywołana w momencie zapisu/odczytu pliku urządzenia specjalnego. Struktura ta przechowuje wskaźniki do funkcji odpowiedzialnych za poszczególne funkcjonalności jądra. Na szczęście wykorzystanie tej struktury dla celów laboratorium okarze
się bardzo proste. Wartość zwracana przez funkcję to zero w przypadku powodzenia, lub wartość ujemna w przypadku wystąpienia błędu.
Jeżeli podany do funkcji register_chardev() pierwszy parametr wywołania będzie miał wartość równą zero,
jądro samo przydzieli numer urządzenia znakowego z puli numerów, które nie są aktualnie zajęte. W przypadku
takim funkcja zwróci przydzielony numer urządzenia jako rezultat. Jednakże w takim przypadku numer ten nie jest
z góry znany, i odpowiednie wywołanie funkcji mknod może zostać wykonane dopiero po załadowaniu modułu do
jądra.
Jest jeszcze jeden ważny aspekt wiążący się z usuwaniem z jądra modułu odpowiedzialnego za obsługę jakiegoś
urządzenia. Wydaje się być intuicyjnym fakt, że nie można tego zrobić w każdym momencie, np. wtedy, gdy plik
urządzenia jest otwarty przez jakiś proces. Do kontroli stanu wykorzystania modułu służą funkcje
try_module_get(THIS_MODULE) i module_put(THIS_MODULE) — pierwsza z nich inkrementuje wskaźnik użycia
modułu, a druga go dekrementuje. Modułu nie można usunąć z jądra gdy jego wskaźnik użycia jest większy od 0.
Poniżej przedstawiono fragmenty kodu przykładowego modułu posiadającego funkcje do otwarcia, odczytu,
zapisu i zamknięcia pliku urządzenia znakowego. Prezentowane fragmenty kodu należy powiązać z kodem poprzedniego programu. Przykład wykorzystuje numer główny urządzenia z puli numerów eksperymentalnych (120-127).
Po poprawnym skompilowaniu kodu i załadowaniu modułu do jądra należy utworzyć plik urządzenia za pomocą
polecenia mknod /dev/hello3 c 122 1. Wówczas możliwe będzie czytanie z pliku urządzenia - moduł jądra przy
każdym odczycie wypisze ile razy czytano z pliku urządzenia. Zapis do pliku spowoduje wypisanie do plików z
logami informacji, że sterownik nie obsługuje takiej operacji.
Najpierw zmienne globalne modułu i zapowiedzi funkcji.
#i n c l u d e
#i n c l u d e
#i n c l u d e
#i n c l u d e
<l i n u x / k e r n e l . h>
<l i n u x / module . h>
<l i n u x / f s . h>
<asm/ u a c c e s s . h>
// P r o t o t y p y f u n k c j i − powinny b y ć w p l i k u
static
static
static
static
static
static
.h
i n t __init c h a r d e v _ i n i t ( v o i d ) ;
v o i d __exit c h a r d e v _ e x i t ( v o i d ) ;
i n t device_open ( s t r u c t i n o d e ∗ , s t r u c t f i l e ∗ ) ;
i n t device_release ( s t r u c t inode ∗ , s t r u c t f i l e ∗ ) ;
s s i z e _ t device_read ( s t r u c t f i l e ∗ , char ∗ , size_t , l o f f _ t ∗ ) ;
ssize_t device_write ( s t r u c t f i l e ∗ , const char ∗ , size_t , l o f f _ t ∗ ) ;
#d e f i n e SUCCESS 0
#d e f i n e DEVICE_NAME " chardev "
#d e f i n e BUF_LEN 100
/∗ Zmienne g l o b a l n e s ą s t a t y c z n e , a p r z e z t o p o z o s t a j ą g l o b a l n e d l a p l i k u ∗/
s t a t i c i n t Major = 1 2 2 ;
// Główny numer u r z ą d z e n i a w / d e v
s t a t i c i n t Device_Open = 0 ;
// Czy u r z ą d z e n i e o t w a r t e − f l a g a
// używana do z a b e z p i e c z e n i a p r z e d
// w i e l o k r o t n y m d o s t ę p e m do p l i k u
s t a t i c c h a r msg [ BUF_LEN ] ;
s t a t i c c h a r ∗msg_Ptr ;
// B u f o r na wiadomość od modułu
// Wskaźnik do o s t a t n i o o d c z y t a n e g o m i e j s c a w b u f o r z e
/∗
Struktura file_operations j e s t zdefiniowana w linux / f s . h i zawiera wskaźniki
do r ó ż n y c h f u n k c j i , k t ó r e s t e r o w n i k u r z ą d z e n i a może z d e f i n i o w a ć , a k t ó r e b ę d ą
wywoływane w p r z e r ó ż n y c h s y t u a c j a c h .
Poniżej zdefiniowano cztery
n a j b a r d z i e j podstawowe z nich , k o r z y s t a j ą c
4
z r o z s z e r z e n i a z d e f i n i o w a n e g o w s t a n d a r d z i e C99
∗/
s t a t i c struct file_operations fops = {
. r e a d = device_read ,
. write = device_write ,
. open = device_open ,
. release = device_release
};
W powyższym kodzie zwraca uwagę struktura fops i jej elegancki sposób wykorzystania: definiujemy jedynie
te funkcje, które będziemy potrzebowali (pozostałe wskaźniki będą miały wartość NULL, gdyż jest to struktura
globalna). Dla tego też celu potrzebne były deklaracje zapowiadające funkcji.
Funkcja inicjalizująca moduł powinna mieć teraz następującą zawartość:
/∗ F u n k c j a wykonywana p r z y wgrywaniu modułu ∗/
s t a t i c i n t __init c h a r d e v _ i n i t ( v o i d ) {
int ret ;
/∗
Funkcja r e j e s t r u j ą c a s t e r o w n i k u r z ą d z e n i a znakowego w j ą d r z e .
P i e r w s z y p a r a m e t r t o g ł ó w n y numer u r z ą d z e n i a znakowego , d r u g i t o nazwa
u r z ą d z e n i a , k t ó r a p o j a w i s i ę w / p r o c / d e v i c e s , a t r z e c i t o w s k a ź n i k do
s t r u k t u r y f i l e _ o p e r a t i o n s , k t ó r a powinna b y ć s t r u k t u r ą g l o b a l n ą s t e r o w n i k a .
J e ż e l i p i e r w s z y p a r a m e t r z o s t a n i e podany j a k o z e r o , j ą d r o samo p r z y d z i e l i
g ł ó w n y numer u r z ą d z e n i a .
Wartość z w r a c a n a p r z e z f u n k c j ę t o numer z a r e j e s t r o w a n e g o u r z ą d z e n i a .
Jeżeli
wartość ta j e s t < 0 , wystąpił błąd .
∗/
r e t = r e g i s t e r _ c h r d e v ( Major , DEVICE_NAME, &f o p s ) ;
i f ( r e t < 0 ){
p r i n t k ( KERN_ALERT " Nieudana próba z a r e j e s t r o w a n i a u r z ą d z e n i a "
" w j ą d r z e − zwrócony numer %d\n" , r e t ) ;
return ret ;
}
// Ponieważ numer g ł ó w n y p r z y d z i e l o n o a u t o m a t y c z n i e , t r z e b a go w y p i s a ć
p r i n t k ( KERN_INFO " P r z y d z i e l o n o mi numer u r z ą d z e n i a %d . " , Major ) ;
p r i n t k ( KERN_INFO " Utwórz p l i k u r z ą d z e n i a \ nza pomocą "
" ’ mknod / dev/%s c %d 0 ’ , a potem" , DEVICE_NAME, Major ) ;
p r i n t k ( KERN_INFO " z i n n ą o s t a t n i ą c y f r ą \ nPróbuj c z y t a ć i p i s a ć " ) ;
p r i n t k ( KERN_INFO " do t e g o u r z ą d z a n i a . Po u s u n i ę c i u \n"
" u r z ą d z e n i a usuń i p l i k \n" ) ;
r e t u r n SUCCESS ;
}
a funkcja wyrejestrowująca moduł zawartość:
/∗ F u n k c j a wywoływana p r z y u s u w a n i u u r z ą d z e n i a ∗/
s t a t i c v o i d __exit c h a r d e v _ e x i t ( v o i d ) {
/∗ F u n k c j a w y r e j e s t r o w u j ą c a moduł u r z ą d z e n i a z j ą d r a .
J e j p a r a m e t r y muszą s i ę z g a d z a ć z tymi , k t ó r e z o s t a ł y u ż y t e
p r z y r e j e s t r o w a n i u modułu ∗/
i n t r e t = u n r e g i s t e r _ c h r d e v ( Major , DEVICE_NAME ) ;
i f ( r e t < 0 ){
p r i n t k ( KERN_ALERT " Błąd przy w y r e j e s t r o w y w a n i u u r z ą d z e n i a "
" numer błędu %d\n" , r e t ) ;
}
5
p r i n t k ( KERN_INFO " Żegnaj , ś w i e c i e ! \ n" ) ;
}
Pora na funkcje obsługujące otwarcie, zamknięcie, zapis i odczyt. Pierwsza z prezentowanych poniżej funkcji,
funkcja device_open(), jest wywoływana, kiedy dochodzi do otwarcia pliku urządzenia. Wówczas inkrementowana
jest flaga zajętości urządzenia, gdyż nasz prosty moduł może obsługiwać jednocześnie tylko jeden proces. Do bufora
kopiowana jest informacja o ilości poprzednich operacji otwarcia pliku a wskaźnik wiadomości jest ustawiany na
początku bufora. Użycie wskaźnika jest konieczne, ponieważ nie ma pewności, czy jedna operacja odczytu będzie w
stanie odczytać całą wiadomość. Ostatnia operacja to powiadomienie jądra, że moduł jest w trakcie obsługi procesu
i nie można go usuwać. Kod funkcji wygląda następująco.
/∗ F u n k c j a wywoływana g d y j a k i ś p r o c e s p r ó b u j e o t w o r z y ć p l i k
urządzenia
do o d c z y t u , np . p r z y w y w o ł a n i u c a t / d e v / c h a r d e v
Parametry f u n k c j i s ą wymuszone p r z e z p o s t a ć s t r u k t u r y f i l e _ o p e r a t i o n s ,
choć ta , p r o s t a
j e j w e r s j a , z n i c h n i e k o r z y s t a . Obydwie s t r u k t u r y ,
t j . inode i f i l e , są z d e f i n i o w a n e w l i n u x / f s . h
W s t r u k t u r z e i n o d e j e s t z a s z y t y , np . numer p o b o c z n y u r z ą d z e n i a
(w p o l u i _ r d e v ) .
S t r u k t u r y f i l e n i e n a l e ż y m y l i ć z e s t r u k t u r ą FILE , k t ó r a w y s t ę p u j e
w z w y k ł y c h programach .
∗/
s t a t i c i n t device_open ( s t r u c t i n o d e ∗ inoda , s t r u c t f i l e ∗ p l i k ) {
s t a t i c int licznik = 0;
// I n f o r m a c j a o numerze pobocznym
p r i n t k ( KERN_INFO " Otwarcie p l i k u u r z ą d z e n i a o numerze pobocznym"
" %d\n" , MINOR( inoda−>i_rdev ) ) ;
// Czy k t o ś j u ż n i e k o r z y s t a z u r z ą d z e n i a ? ( s p r a w d z e n i e
flagi )
i f ( Device_Open ) {
r e t u r n −EBUSY;
}
Device_Open++;
// Z a b l o k o w a n i e d o s t ę p u − u s t a w i e n i e f l a g i
s p r i n t f ( msg , " H e l l o , world ! mówię po r a z %d\n" , ++l i c z n i k ) ;
msg_Ptr = msg ;
/∗ Z w i ę k s z e n i e l i c z n i k a u ż y c i a modułu , u n i e m o ż l i w i j e g o
u s u n i ę c i e z j ą d r a ∗/
try_module_get ( THIS_MODULE ) ;
r e t u r n SUCCESS ;
Kolejna funkcja, device_release(), to funkcja zwalniania urządzenia, wywoływana przy zamykaniu pliku
urządzenia. Jej zadaniem jest dekrementacja flagi zajętości i poinformowanie jądra, że moduł nie jest już używany.
/∗ F u n k c j a wywoływana g d y p r o c e s zamyka p l i k
urządzenia
Parametry f u n k c j i s ą wymuszone p r z e z p o s t a ć s t r u k t u r y f i l e _ o p e r a t i o n s ,
c h o ć t a , p r o s t a j e j w e r s j a , z n i c h n i e k o r z y s t a . Obydwie s t r u k t u r y ,
t j . inode i f i l e , są z d e f i n i o w a n e w l i n u x / f s . h
∗/
s t a t i c i n t d e v i c e _ r e l e a s e ( s t r u c t i n o d e ∗ inoda , s t r u c t f i l e ∗ p l i k ) {
Device_Open−−;
// O d b l o k o w a n i e d o s t ę p u
/∗ J e ś l i n i e z m n i e j s z y s i ę l i c z n i k a
s i ę go u s u n ą ć z j ą d r a ∗/
u ż y c i a modułu n i e da
module_put ( THIS_MODULE ) ;
6
return 0;
}
O ile poprzednie dwie funkcje nie korzystają z przekazywanych im parametrów, to funkcja odczytu urządzenia
musi już z nich skorzystać (choć nie z wszystkich). Potrzebna jest bowiem informacja, do jakiego miejsca w pamięci
należy skopiować wiadomość modułu, znajdującą się w jego buforze - ten parametr nazwany został buforUz – bufor
użytkownika. Trzeba także znać jego dlugosc, której nie można przekroczyć przy zapisie. Druga linia ciała funkcji,
ta z instrukcją if, mówi, że jeśli funkcja odczytu zostanie wywołana, a miejsce wskazywane przez msg_pointer
zawiera znak końca łańcucha, przesłaliśmy już całą wiadomość. Zwrócone w tym przypadku 0 jest znakiem końca
pliku. Poniżej jest pętla while, która zajmuje się właściwą operacją przepisania wiadomości. Znamienne jest
tutaj użycie funkcji put_user(), które jest konieczne, ponieważ przepisujemy dane z chronionego obszaru pamięci
jądra systemu operacyjnego (z tzw. pierścienia wewnętrznego) do obszaru pamięci użytkownika, której mapowanie
może się w każdym momencie zmienić, np. przez „swapowanie”. (W przypadku przepisywania danych w odwrotnym
kierunku należałoby użyć funkcji get_user().) Właśnie dla tej funkcji konieczne było załączenie pliku nagłówkowego
<asm/uaccess.h>. Ostatnia linijka funkcji czyni zadość zwyczajowi zwracania liczby odczytu bajtów przez funkcję
odczytu. Na końcu zamieszczono kod trywialnej (w tym momencie) funkcji device_write().
/∗ F u n k c j a wywoływana , g d y p r o c e s , k t ó r y o t w a r ł p l i k
urządzenia
p r ó b u j e z n i e g o c z y t a ć , np c a t / d e v / c h a r d e v
Parametry f u n k c j i s ą wymuszone p r z e z p o s t a ć s t r u k t u r y
P i e r w s z y p a r a m e t r t o w s k a ź n i k do s t r u k t u r y
file_operations .
f i l e , drugi to bufor ,
k t ó r y s t e r o w n i k p o w i n i e n w y p e ł n i ć danymi , t r z e c i p a r a m e t r t o d ł u g o ś ć
t e g o b u f o r a , a c z w a r t y t o o f f s e t ( l o f f _ t t o l o n g o f f s e t t y p e , co n a j m n i e j 64 b )
∗/
s t a t i c s s i z e _ t d e v i c e _ r e a d ( s t r u c t f i l e ∗ p l i k , c h a r ∗ buforUz ,
size_t dlugosc , l o f f _ t ∗ o f f s e t ){
// b u f o r U z t o m i e j s c e , g d z i e n a l e ż y w p i s a ć o d p o w i e d ź ,
// ma on d l u g o s c m i e j s c a
// L i c z b a b a j t ó w tym razem w p i s a n y c h do b u f o r a − c a ł o ś ć
// w i a d o m o ś c i n i e k o n i e c z n i e musi b y ć p r z e c z y t a n a z a jednym razem
i n t odczytano = 0 ;
// J e ś l i o s i ą g n i ę t o k o n i e c b u f o r a , zwracamy 0
// Koniec j e s t r o z p o z n a n y , bo j e s t w nim z n a k k o ń c a ł a ń c u c h a z n a k o w e g o \0
i f ( ∗msg_Ptr == 0 ) {
return 0;
}
// P r z e p i s a n i e danych do b u f o r a u ż y t k o w n i k a
w h i l e ( d l u g o s c && ∗msg_Ptr ) {
/∗ Ponieważ b u f o r
j e s t w p r z e s t r z e n i danych u ż y t k o w n i k a
a n i e w p a m i ę c i j ą d r a , n i e można danych p r z e p i s y w a ć
b e z p o ś r e d n i o , a j e d y n i e z a pomocą f u n k c j i p u t _ u se r ,
k t ó r a s ł u ż y w ł a ś n i e do p r z e p i s a n i a danych z p a m i ę c i
j ą d r a do p a m i ę c i u ż y t k o w n i k a ∗/
put_user ( ∗ ( msg_Ptr++), buforUz++ ) ;
d l u g o s c −−;
odczytano++;
}
// W i ę k s z o ś ć f u n k c j i do o d c z y t u z w r a c a i l o ś ć
p r z e p i s a n y c h danych
r e t u r n odczytano ;
}
s t a t i c s s i z e _ t d e v i c e _ w r i t e ( s t r u c t f i l e ∗ p l i k , c o n s t c h a r ∗ buf or ,
size_t dlugosc , l o f f _ t ∗ o f f s e t ){
p r i n t k ( KERN_ALERT "To u r z ą d z e n i e n i e o b s ł u g u j e z a p i s u ! \ n" ) ;
7
r e t u r n −EINVAL ;
}
5
Konfuguracja urządzeń znakowych przez ioctl
Większość urządzeń komputerowych, oprócz przetwarzania „normalnych” danych, posiada także możliwości konfiguracji swoich własności, realizowane zwykle przez zmianę pewnych parametrów. W systemie Linux istnieją zasadniczo
dwa sposoby realizacji takich zmian przez programy użytkownika:
• parametry konfiguracyjne mogą być przesyłane wśród „normalnych” danych, jako specjalne kody sterujące –
ten sposób jest np. wykorzystywany przez terminal do zmiany koloru tekstu, przesuwania kursora, oraz innych
czynności,
• parametry konfiguracyjne mogą być zmieniane przez wywołanie specjalnych funkcji, tzw. wywołań systemowych, realizowanych przez funkcję o nazwie ioctl().
Ta ostatnia technika jest niewątpliwie najczęściej wykorzystywana, dlatego tylko ona będzie w niniejszej instrukcji
omawiana.
Kiedy program użytkownika wywołuje funkcję ioctl, musi to zrobić zgodnie z prototypem:
int ioctl ( int deskr, int polecenie , ... );
Pierwszym argumentem funkcji jest deskryptor pliku otwartego już urządzenia (np. /dev/chardev), drugim zaś
identyfikator polecenia, czy też czynności, które sterownik urządzenia (moduł jądra) powinien wykonać. Polecenie
może mieć argument w postaci np. danych konfiguracyjnych, które mogą zostać przekazane w trzecim argumencie
wywołania funkcji (najbardziej typowe argumenty to liczba typu integer oraz wskaźnik do przechowywanych w
pamięci użytkownika danych). Polecenie może też być bezargumentowe – stąd trzeci argument wywołania jest
oznaczony kropkami.
Wartością zwracaną przez funkcje ioctl() jest liczba typu integer, która jest wynikiem wykonania instrukcji
return w module jądra. Dodatnia wartość zwracana świadczy o pomyślnym zakończeniu wywołania. Wartość
ujemna jest interpretowana przez system jako błąd wywołania systemowego, i powoduje ustawienie wartości errno.
Po stronie modułu jądra, w strukturze file_operations zawarte jest pole ze wskaźnikiem do następująco zadeklarowanej funkcji:
int (∗ ioctl )( struct inode ∗ino, struct file ∗plik , unsigned int cmd, unsigned long par );
Znaczenie dwóch pierwszych argumentów powyższego prototypu jest identyczne, jak w funkcjach wywoływanych
w momencie otwierania i zamykania pliku urządzenia. Trzeci argument to przekazane przez program użytkownika polecenie, a czwarty to (opcjonalny w wywołaniu) parametr polecenia, który zawsze jest przekazywany jako
unsigned long.
Po zrozumieniu powyższego opisu jasne jest, że zarówno moduł jądra, jak i program użytkownika, muszą uzgodnić
między sobą numery poleceń. Co więcej, pożądane byłoby, gdyby numery poleceń nie pokrywały się nawet dla
różnych urządzeń, dzięki czemu możliwym byłoby uniknięcie sytuacji, w której przesyła się poprawne polecenie, lecz
omyłkowo do innego sterownika urządzenia. System Linux oferuje pewną pomoc w realizacji tego zamysłu. Stosując
się do zaleceń, numer polecenia należy budować w oparciu o następujące składniki:
• specjalnie dobrany tzw. numer magiczny – szczegółowe wskazówki dotyczące jego wyboru znajdują się w pliku
Documentation/ioctl-number.txt źródeł jądra; dopuszczalnym sposobem jego wyboru jest przyjęcie go równym
numerowi głównemu urządzenia,
• własny numer polecenia, z zakresu 0 do 255,
• kierunek przesyłu danych polecenia, jako _IOC_NONE (polecenie bezargumentowe), _IOC_READ (odczyt danych
z modułu przez program użytkownika), _IOC_WRITE (zapis danych przez program użytkownika do modułu)
lub logiczna suma tych dwu ostatnich (zapis i odczyt),
• rozmiar danych przesyłanych jako argument polecenia.
Do tworzenia zgodnych z powyższą konwencją numerów w pliku nagłówkowym <linux/ioctl.h> zostały zdefiniowane specjalne makra:
_IO(mag, nr) do tworzenia numeru polecenia bezargumentowego,
_IOR(mag, nr, typ) do tworzenia polecenia, za pomocą których program użytkowy odczytuje dane z modułu,
8
_IOW(mag, nr, typ) do tworzenia polecenia, za pomocą których program użytkowy zapisuje dane do modułu,
_IOWR(mag, nr, typ) do tworzenia polecenia, za pomocą których program użytkowy zarówno zapisuje, jak i
odczytuje dane z modułu,
W powyższym opisie parametr mag oznacza numer „magiczny”, nr to kolejny numer polecenia, a typ to typ argumentu
polecenia.
Dla przykładu, poniższe dyrektywy preprocesora definiują nazwy symboliczne, które będą w trakcie preprocesingu zamieniane na numery poleceń utworzone z zachowaniem opisanej powyżej konwencji:
#d e f i n e IOCTL_SET_MSG _IOW( MAJOR_NUM, 0 , c h a r ∗ )
#d e f i n e IOCTL_GET_MSG _IOR( MAJOR_NUM, 1 , c h a r ∗ )
#d e f i n e IOCTL_GET_NTH_BYTE _IOWR( MAJOR_NUM, 2 , i n t )
Dyrektywy te należałoby zamieścić w pliku nagłówkowym modułu urządzenia. Plik ten będzie załączany nie tylko
podczas kompilacji modułu, lecz także podczas kompilacji programów użytkownika, które będą chciały sterować
modułem (wykonywać na nim ioctl()). Dzięki temu numery poleceń zostaną uzgodnione.
Po stronie modułu implementacja funkcji odpowiedzialnej za obsługę wywołań ioctl() zasadza się zwykle
na instrukcji switch, której kolejne przypadki to zdefiniowane numery poleceń. Dla przykładu, implementacja
wywołania systemowego dla programu omawianego w punkcie 4, a polegającego na zwróceniu przez wywołanie
bajtu bufora msg o numerze określonym przez parametr wiadomości, może wyglądać następująco:
i n t i o c t l U r z a d z e n i a ( s t r u c t i n o d e ∗ inoda , s t r u c t f i l e ∗ p l i k ,
u n s i g n e d i n t ioctlNumer , u n s i g n e d l o n g i o c t l P a r a m ) {
/∗ A k c j e b ę d ą podejmowana w z a l e ż n o ś c i od numeru p o l e c e n i a
i o c t l ∗/
s w i t c h ( ioctlNumer ){
c a s e IOCTL_GET_NTH_BYTE:
/∗ To p o l e c e n i e o z n a c z a p r z e s ł a n i e n−t e g o b a j t u w i a d o m o ś c i ∗/
i f ( i o c t l P a r a m >= 0 && i o c t l P a r a m < BUF_LEN ) {
r e t u r n msg [ i o c t l P a r a m ] ;
}else{
r e t u r n −EFAULT;
}
break ;
c a s e IOCTL_SET_MSG:
...
default :
/∗ To m i e j s c e o z n a c z a w y w o ł a n i e p o l e c e n i a , k t ó r e n i e j e s t
p r z e z moduł o b s ł u g i w a n e − z a c h o w a n i e d e f i n i u j e s t a n d a r d POSIX ∗/
r e t u r n −ENOTTY;
}
return 0;
}
Po stronie programu użytkownika najważniejsze jest zachowanie zgodności numerów oraz parametrów będących
argumentami poleceń ioctl(). Przykładowo, powyżej zdefiniowane wywołanie oczekuje parametru w postaci liczby
całkowitoliczbowej z określonego przedziału, a jego rezultatem jest w istocie pewien bajt wiadomości, zatem może
ono wyglądać następująco:
char c ;
i n t nr = 3 ;
c = i o c t l ( deskr , IOCTL_GET_NTH_BYTE, nr ) ;
(parametr deskr w powyższym wywołaniu oznacza oczywiście deskryptor pliku urządzenia – /dev/chardev w tym
przypadku).
Nieco trudniejszy jest problem przekazywania danych większej liczby danych do i z modułu, jak miałoby to być w
przypadku wywołań ioctl ustawiających i odczytujących dane z modułu. Przekazując bowiem wskaźnik do adresu
w pamięci użytkownika, nie ma możliwości jednoczesnego przekazania informacji o rozmiarze zarezerwowanego
obszaru. Należałoby to zrobić wcześniej, za pomocą jeszcze jednego (innego) wywołania systemowego. Dlatego
najbardziej typową sytuacją jest przesyłanie wskaźników do struktur zdefiniowanych w pliku nagłówkowym modułu,
które będą zawierały odpowiednie dane.
9
6
Literatura
Więcej na temat pisania modułów jądra systemu Linux znaleźć można w Internecie. Spośród darmowych książek online wzmianki warte są pozycja „Linux Kernel Module Programming Guide” autorstwa Ori Pomerantza oraz „Linux
Device Drivers” autorstwa Alessandro Rubiniego i Jonathana Corbeta. Można także zamówić wydania książkowe
poniższych pozycji wymienionych pod adresem http://www.linux.org.
• Gary Nutt, „Kernel Projects for Linux”, Addison-Wesley Pub Co, 2000;
• Robert Love, „Linux Kernel Development”, SAMS, 2003;
• Harald Bohme, „Linux Kernel Internals”, Addison-Wesley Pub Co, 1997;
• Eric Dumas, „The Linux Kernel Book”, John Wiley & Sons, 1998;
• Daniel Pierre Bovet, „Understanding the Linux Kernel”, O’Reilly & Associates, 2000;
Autor tej instrukcji wykorzystał w dużym stopniu „The Linux Kernel Module Programming Guide” autorstwa Petera
Jaya Salzmana, Mechaela Buriana i Ori Pomerantza, którym dziękuje za doskonały przewodnik.
Linus Torvalds twierdzi jednak, że najlepszym sposobem nauczenia się pisania modułów jądra jest przeglądanie
kodu źródłowego już istniejących, sprawdzonych i poprawnie działających modułów.
10

Podobne dokumenty