Zajęcia 5: Urządzenia znakowe¶
Data: 27.03.2018
Contents
O urządzeniach i sterownikach w Linuksie¶
Sterownikiem urządzenia nazywamy zbiór kodu obsługujący jakieś urządzenie (zazwyczaj sprzętowe, ale zdarzają się urządzenia wirtualne) i eksportujący zbiór funkcji pozwalający na wykorzystanie tego urządzenia przez użytkownika. Sterownik prawie zawsze ma wyłączny bezpośredni dostęp do urządzenia. Często jest odpowiedzialny za pogodzenie ze sobą potrzeb różnych użytkowników (choć równie często jest to obsługiwane przez wyższe warstwy – w przypadku dysku twardego za taką warstwę można uznać system plików).
Sterowniki urządzeń zazwyczaj są modułami jądra. Jeśli urządzenie nie jest zbyt “blisko” wpięte w procesor, da się również napisać sterownik w przestrzeni użytkownika (urządzenia USB się do tego świetnie nadają), ale nie będziemy się tym zajmować na zajęciach.
Aby udostępnić funkcjonalność sterownika dla programów użytkownika, należy użyć jednego z wielu możliwych mechanizmów komunikacji z jądrem. Najczęściej spotykane to:
- plik urządzenie blokowego – używane do dysków twardych i dostatecznie podobnych tworów (dyskietka, CD-ROM, SSD, …). Sterownik udostępnia funkcje zapisu i odczytu bloków, a warstwa blokowa zajmuje się obsługą żądań od użytkownika (buforowanie, kolejkowanie, itp).
- plik urządzenia znakowego – używany do większości rodzajów urządzeń. Sterownik po prostu dostarcza funkcje odpowiadające wywołaniom systemowym działającym na plikach – jądro przekazuje takie wywołania prosto do sterownika, pozwalając na implementację dowolnego interfejsu.
- interfejs sieciowy – sterownik udostępnia funkcje wysłania i odebrania
pakietu, do których wpina się podsystem sieci. Użytkownik może korzystać
z niego przez wywołania
socket
. - plik w
proc
– używane w przypadku sterowników z trywialnym interfejsem (np. cała funkcjonalność sterownika to odczyt/zapis jednego parametru). - plik w
sysfs
– jak wyżej, ale nowszy (i prostszy) interfejs.
Reprezentacja plikowa¶
Użycie urządzeń znakowych i blokowych odbywa się przez stworzenie
odpowiadającego im pliku specjalnego gdzieś w systemie plików (prawie zawsze
/dev
) i otwarcie go. Taki plik specjalny jest tylko “wrotami do jądra”
i jedyne informacje o nim przechowywane w systemie plików to:
- flagę określająca typ urządzenia (
b
– blokowe,c
– znakowe) - numer główny (major device number)
- numer drugorzędny (minor device number)
Zazwyczaj numer główny wybiera sterownik urządzenia, a numer drugorzędny wybiera konkretny egzemplarz urządzenia obsługiwany przez ten sterownik. Zdarzają się jednak przypadki, że jeden numer główny jest współdzielony przez wiele sterowników, jeśli eksportują one tylko po jednym urządzeniu.
Na poziomie języka C oba numery są pakowane w jedną liczbę typu dev_t
.
Przydatne są następujące makra (linux/kdev_t.h
):
int MAJOR(dev_t numer)
- zwraca numer główny urządzenia.
int MINOR(dev_t numer)
- zwraca numer drugorzędny.
dev_t MKDEV(int major, int minor)
- Skleja numery w typ
dev_t
.
Do utworzenia pliku urządzenia ręcznie można użyć następującego polecenia:
mknod mknod /dev/nazwa_pliku typ major minor
Wpisując ls -l
zobaczymy numery urządzenia tam, gdzie zazwyczaj znajduje
się rozmiar pliku.
W dawnych czasach, numery były przydzielane odgórnie i zsyłane przez
programistów jądra na ziemię na kamiennych tablicach
(Documentation/devices.txt
). Dystrybucje Linuxa tworzyły na podstawie
tych tablic gotowy katalog /dev
z wszystkimi możliwymi urządzeniami
(na zapas). Nie było to jednak najlepsze rozwiązanie.
We współczesnych czasach, jądro eksportuje informacje o dostępnych
sterownikach urządzeń na żywo w systemie plików sysfs
, a program udevd
(lub systemd-udevd
) na bieżąco monitoruje te informacje i tworzy
odpowiednie pliki urządzeń. Ponieważ nie ma potrzeby wcześniejszego
ustalenia użytych numerów, są one alokowane dynamicznie. Aby ten mechanizm
działał, sterownik musi zarejestrować swoje urządzenie w hierarchii sysfs
.
Sterowniki urządzeń znakowych w Linuksie¶
Rejestracja sterowników urządzeń¶
Rejestracja sterownika odbywa się w kilku etapach:
Alokujemy zakres numerów urządzeń:
int alloc_chrdev_region(dev_t *first, unsigned int count, const char *name); void unregister_chrdev_region(dev_t first, unsigned int count);
Tą alokację zazwyczaj wywołuje się tylko raz, w funkcji inicjalizacyjnej, alokując cały zakres numerów na zapas.
Przygotowujemy strukturę
file_operations
opisującę operacje na naszym urządzeniu. Takie struktury są zazwyczaj globalne (nie ma po co ich alokować dynamicznie).Tworzymy i wypełniamy strukturę
cdev
:void cdev_init(struct cdev *cdev, const struct file_operations *fops);
Możemy również poprosić o dynamiczne zaalokowanie struktury:
struct cdev *cdev_alloc(void);
W tym wypadku należy ręcznie wypełnić pole
ops
wskaźnikiem na naszą strukturę (nie należy mieszać tego wywołania zcdev_init
).Rejestrujemy naszą strukturę
cdev
:int cdev_add(struct cdev *p, dev_t dev, unsigned count); void cdev_del(struct cdev *p);
W tym momencie nasze urządzenie staje się dostępne dla przestrzeni użytkownika, gdy tylko otworzy on odpowiedni plik specjalny.
Możemy podpinać urządzenia pojedynczo, bądź ciągłymi zakresami (parametr
count
).Jeśli struktura
cdev
była stworzona przezcdev_alloc
, struktura zostanie automatycznie zwolniona przezcdev_del
. Jeśli natomiast była inicjowana przezcdev_init
, zwolnienie jej jest sprawą sterownika.Należy zauważyć, że
cdev_del
tylko odpina urządzenie z tablicy urządzeń, ale nie gwarantuje, że nikt już go nie używa - wcześniej otwarte deskryptory plików dalej będą działać (choć jeśli jesteśmy wmodule_exit
, mamy gwarancję że takich deskryptorów nie ma). W przypadku implementacji np. urządzenia, które powinno obsługiwać hot-unplug trzeba samemu zapewnić np. zliczanie referencji.Rejestrujemy klasę urządzeń w
sysfs
(bądź używamy istniejącej, jeśli pasuje):struct class moja_klasa = { .name = "abc", .owner = THIS_MODULE, }; int class_register(struct class *class); void class_unregister(struct class *class);
To wykonujemy tylko raz na wszystkie nasze urządzenia (bądź na typ urządzeń, jeśli mamy wiele).
Rejestrujemy nasze urządzenie w
sysfs
:struct device *device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...); void device_destroy(struct class *cls, dev_t devt);
parent
wskazuje na urządzenie, do którego nasze urzadzenie jest podłączone – katalog w sysfs odpowiadający naszemu urządzeniu będzie podkatalogiem katalogu podanego urządzenia. W przypadku sterowników urządzeń znakowych odpowiadających np. urządzeniom PCI, parent będzie ustawiony na poledev
strukturypci_device
. Można ustawić ten parametr naNULL
, aby otrzymać urządzenie najwyższego poziomu.drvdata
może służyć do przechowywania dodatkowych informacji prywatnych dla naszego sterownika (przydatne jeśli np. chcemy stworzyć pliki w sysfs do kontroli naszego urządzenia).fmt
i dalsze parametry przekazywane są dosprintf
w celu stworzenia nazwy urządzenia, która pojawi się w/dev
.W tym momencie,
udevd
dostanie powiadomienie o powstaniu nowego urządzenia i (w skończonym czasie) stworzy odpowiedni plik w/dev
.
Struktura file_operations
¶
Struktura file_operations
(zdefiniowana w linux/fs.h
) opisuje jak
wykonywać operacje na danym pliku. Każdy plik (i w ogólności wszystko, co może
być otwartym deskryptorem) w Linuksie ma taką strukturę – w przypadku zwykłych
plików, jest dostarczana przez sterownik systemu plików. W przypadku urządzeń
znakowych, dostarcza ją sterownik urządzenia. Ma bardzo wiele pól
(odpowiadających operacjom), z których najważniejsze to:
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t,
loff_t *);
int (*unlocked_ioctl) (struct file *, unsigned int,
unsigned long);
int (*compat_ioctl) (struct file *, unsigned int,
unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
/* ... */
};
Pole owner
musimy wypełnić wskaźnikiem THIS_MODULE
–
umożliwia to jądru automatyczne zarządzanie licznikiem odwołań do modułu.
Pola struktury file
¶
Struktura file
(zdefiniowana w linux/fs.h
) reprezentuje w jądrze otwarty
plik. Jest tworzona przez jądro w momencie wywołania open
i przekazywana do
wszystkich operacji na pliku, aż do ostatniego wywołania close
(czyli
momentu, kiedy wywołane zostanie release
). Warto zauważyć, że otwarty plik
(struktura file
) to co innego, niż plik na dysku (reprezentowany przez
strukturę inode
). Najważniejsze pola tej struktury to:
struct file {
mode_t f_mode;
loff_t f_pos;
unsigned int f_flags;
struct file_operations *f_op;
void *private_data;
/* ... */
};
Pole f_mode
pozwala określić, czy plik jest otwarty do odczytu
(FMODE_READ
), zapisu (FMODE_WRITE
) lub obu. Pola tego nie trzeba
sprawdzać w funkcjach read
i write
, bo jądro wykonuje taki test przed
wywołaniem odpowiedniej funkcji sterownika.
Pole f_pos
określa pozycję do pisania lub odczytu (używana przez read
,
write
, lseek
itp).
Flagi f_flags
wykorzystuje się głównie do sprawdzenia, czy operacja ma
być blokująca, czy nie (O_NONBLOCK
), choć zawiera dużo więcej flag.
Pole f_op
określa zestaw funkcji implementujących operacje na pliku.
Pole to jest ustawiane (na operacje ze struktury cdev
) przez jądro
przy wywołaniu open
, a następnie jest używane do wszystkich następnych
operacji (sterownik może podmienić wartość tego pola w open
, aby wybrać
alternatywny zestaw funkcji).
Wskaźnik private_data
jest ustawiany na NULL
przy otwieraniu pliku.
Sterownik może wykorzystać ten wskaźnik dla własnych celów (wtedy jest
odpowiedzialny za zwolnienie pamięci przydzielonej na rzecz tego pola).
Operacja open
– otwarcie pliku¶
Prototyp tej funkcji wyglada następująco:
int open(struct inode *inode, struct file *filp)
Operacja open
umożliwia sterownikowi przeprowadzenie czynności
przygotowawczych przed innymi operacjami. Zazwyczaj wykonuje się
nastepujące kroki:
- sprawdzenie błędów związanych z urządzeniem (np. sprawdzenie, czy urządzenie jest gotowe);
- inicjalizacja urządzenia jeślijest otwierane po raz pierwszy i stosujemy leniwą inicjalizację;
- identyfikacja numeru drugorzędnego (
MINOR(inode->i_rdev)
) i, jeśli jest to konieczne, podmiana zestawu operacji wskazywanej przezf_op
; - przydzielenie pamięci na dane związane z urządzeniem, inicjalizacja
struktur danych oraz przypisanie wskaźnika
private_data
;
Operacja release
– zamknięcie pliku¶
Jądro trzyma licznik referencji dla każdej istniejącej struktury file
.
Może być zwiększony np. przez wywołanie dup
bądź dziedziczenie otwartego
pliku przez fork
. Gdy ten licznik w końcu spadnie do 0 (zostanie wywołany
close
na ostatnim deskryptorze, bądź proces trzymający ten deskryptor
wywoła exit
), zostanie wywołana funkcja release
, służąca za destruktor
pliku:
int release(struct inode *inode, struct file *filp)
Jej zadaniem jest zwolnienie zasobów alokowanych w operacji open
i wykonanie podobnych czynności sprzątających:
- zwolnienie pamięci
private_data
; - wyłączenie urządzenia, gdy jest to ostatnie wywołanie
release
;
Operacje read
i write
– transfer danych¶
Prototyp tej funkcji wyglada następująco:
ssize_t read(struct file *filp, char __user *buff, size_t count, loff_t *offp)
ssize_t write(struct file *filp, const char __user *buff, size_t count,
loff_t *offp)
Zadaniem operacji read
jest przepisanie pewnej porcji danych z przestrzeni
adresowej jądra pod wskazany adres (buff
) w przestrzeni adresowej
użytkownika. Operacja write
działa w odwrotnym kierunku. Funkcje
te są używane do implementacji wielu wywołań systemowych (read
, pread
,
readv
, …).
offp
jest wskaźnikiem na bieżącą pozycję w pliku. Jeśli w przypadku naszego
pliku taka pozycja ma sens, bierzemy ją z tego miejsca i wpisujemy tu zaktualizowaną
wartośc pozycji. W przypadku zwykłego read
i write
będzie to wskaźnik
na filp->f_pos
, a w przypadku pread
i pwrite
będzie to wskaźnik na
jakąś zmienną na stosie zawierającą parametr wywołania systemowego.
Wartość zwracana przez tę funkcję będzie interpretowana następująco:
- wartość większa od zera oznacza liczbę przepisanych bajtów; jeśli jest
równa wartości argumentu przekazanego do wywołania systemowego, to
oznacza pełen sukces, jeśli zaś mniejsza, to oznacza, że tylko część
danych została przekazana - należy się wtedy spodziewać, że program
powtórzy wywołanie systemowe (takie jest np. standardowe zachowanie
funkcji bibliotecznej
fread
/fwrite
) - jeśli wartość jest równa
0
to został osiągnięty koniec pliku (używane tylko wread
) - wartość ujemna oznacza błąd
Implementując te operacje, należy pamiętać o zachowaniu właściwej semantyki – zwrócenie błędu mówi, że żadne bajty nie zostały odczytane/zapisane. Jeśli nasz sterownik zauważy błąd dopiero po pewnej ilości przetransferowanych bajtów (i nie ma łatwego sposobu, by ten transfer cofnąć), należy zamiast błędu zwrócić liczbę przetransferowanych bajtów – kod błędu będzie zwrócony gdy użytkownik ponowi operację dla pozostałych bajtów.
Operacja llseek
– zmiana pozycji pliku¶
Prototyp tej funkcji wyglada następująco:
loff_t llseek(struct file *filp, loff_t off, int whence)
Operacja llseek
implementuje wywołania systemowe lseek
i llseek
.
Domyślnym działaniem jądra, gdy operacja llseek
nie jest
wyszczególniona w operacjach sterownika, jest zmiana pola f_pos
struktury file. W przypadku, gdy pojęcie zmiany pozycji pliku nie ma sensu
dla urządzenia, należy wpisać tu funkcję zwracającą błąd. W jądrze jest
w tym celu dostępna gotowa funkcja no_llseek
, zawsze zwracająca -ESPIPE
.
Operacja ioctl
– wywołanie komend specyficznych dla urządzenia¶
Prototypy funkcji wygladają następująco:
long (*unlocked_ioctl) (struct file *filp, unsigned int cmd,
unsigned long arg);
long (*compat_ioctl) (struct file *filp, unsigned int cmd,
unsigned long arg);
Funkcja unlocked_ioctl
odpowiada wywołaniom ioctl
przez “główną”
architekturę jądra. Nazwa jest zaszłością historyczną z czasów big
kernel locka – niegdyś sterowniki urządzeń wymagające big kernel locka
wypełniały pole ioctl
, podczas gdy nowsze lub skonwertowane sterowniki
używające własnych blokad używały unlocked_ioctl
. W obecnych wersjach
jądra big kernel lock i pole ioctl
już nie istnieją.
Funkcja compat_ioctl
odpowiada wywołaniom ioctl
przez programy
użytkownika w trybie zgodności z 32-bitową wersją architektury –
np. programy na architekturę i386 pod jądrem na architekturę x86_64.
W przypadku, gdy struktury przekazywane przez ioctl
nie zawierają
pól o rozmiarze zależnym od architektury, można ustawić oba pola
na tą samą funkcję.
Pierwszy argument odpowiada deskryptorowi pliku przekazanemu
przez wywołanie systemowe. Argument cmd
jest dokładnie taki, jak w
wywołaniu systemowym. Opcjonalny argument arg
jest przekazywany w
postaci liczby typu unsigned long
bez względu na typ użyty przy
wywołaniu systemowym.
Zazwyczaj implementacja operacji ioctl
zawiera po prostu konstrukcję
switch wybierającą odpowiednie zachowanie w zależności od wartości
argumentu cmd
. Różne komendy są reprezentowane różnymi numerami, którym
zazwyczaj nadaje się nazwy korzystając z definicji preprocesora. Program
użytkownika powinien mieć możliwość włączenia pliku nagłówkowego z
deklaracjami (zazwyczaj tego samego, który jest używany przy kompilacji
modułu sterownika).
Do twórcy interfejsu sterownika należy ustalenie wartości liczbowych
odpowiadających komendom interpretowanym przez sterownik. Najprostszy
wybór, przypisujący kolejne małe wartości poszczególnym komendom,
niestety ogólnie nie jest dobrym rozwiązaniem. Komendy powinny być
unikalne w skali systemu, żeby uniknąć błędów, gdy poprawną komendę
wysyłamy do niepoprawnego urządzenia. Taka sytuacja może nie występować
zbyt często, ale jej konsekwencje mogą by poważne. Przy różnych
komendach dla wszystkich ioctl
, w przypadku pomyłki, zostanie zwrócone
-ENOTTY
zamiast wykonania niezamierzonej akcji.
W ustalaniu wartości liczbowych dla komend powinno się używać następujących
makr (zdefiniowanych w linux/ioctl.h
):
_IO(type, nr)
- komenda ogólnego przeznaczenia (bez argumentu)
_IOR(type, nr, dataitem)
- komenda z zapisem w przestrzeni użytkownika
_IOW(type, nr, dataitem)
- komenda z odczytem z przestrzeni użytkownika
_IOWR(type, nr, dataitem)
- komenda z zapisem i odczytem
Oznaczenia:
type
- unikatowy numer dla sterownika (8 bitów, wybrany po przejrzeniu
Documentation/ioctl-number.txt
) – numer magiczny nr
- kolejny numer komendy (8 bitów)
dataitem
- struktura związana z komendą; rozmiar podanej struktury zazwyczaj
nie może byc większa niż 16kb-1 (zależy to od wartości
_IOC_SIZEBITS
). Kodowanie rozmiaru struktury zapisywanej/odczytywanej jako parametr może się przydać do wyłapywania programów skompilowanych z nieaktualnymi wersjami sterownika i pozwala uniknąć np. pisania poza buforem.
Przykład:
#define DN_SETCOUNT _IOR(0, 3, int)
Funkcje przydatne przy programowaniu w trybie jądra¶
Informacje o procesie¶
Podczas pisania kodu, który będzie się wykonywał w przestrzeni
adresowej jądra należy cały czas pamiętać, że wątek wykonania jest
związany z pewnym procesem użytkownika, w imieniu którego jądro
wykonuje określone operacje. Korzystając z makra current
(asm/current.h
) możemy łatwo (i szybko) dotrzeć do wszystkich
informacji jakie jądro przechowuje o bieżącym procesie w strukturze
task_struct (linux/sched.h
).
Wyjątkiem jest obsługa przerwań - o ile w trakcie wykonywania funkcji
obsługi przerwania sprzętowego, makro current
może wskazywać na jakiś
proces, to nie należy się do niego odwoływać (nie ma on związku z tym
przerwaniem) jak również nie wolno przełączać się na inne procesy
(czyli nie można też wykonywać operacji blokujących).
Ćwiczenie:
Proszę zapoznać się z definicją struktury task_struct i spróbować wyjaśnić
znaczenie innych jej pól (linux/sched.h
).
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 nie przyjmuje żadnych sygnałów
w czasie ewentualnego oczekiwania na zwolnienie blokady. Druga pozwala
na odebranie sygnału (zwraca -EINTR
w przypadku pobudki wywołanej
sygnałem, 0
wpp.). Trzecia działa jak druga, ale jest przerywalna tylko
przez sygnały, które spowodują zabicie procesu. Trzecia 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.
Wymiana danych między przestrzenią adresową użytkownika i jądra¶
Aby z poziomu jądra odczytać/zapisać coś z/do przestrzeni pamięci programów użytkownika należy posłużyć się następującymi funkcjami (właściwie makrami):
put_user(kptr, ptr)
- wpisanie bajtu/słowa/długiego słowa do pamięci programów użytkownika
(spod adresu
ptr
); makrodefinicja działa automagicznie - rozmiar określony jest na podstawie typu, na który wskazujekptr
get_user(kptr, ptr)
- j.w., ale odczytanie
Do kopiowania większych obszarów pamięci służą funkcje:
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
Pierwsza umożliwia kopiowanie danych z przestrzeni adresowej
użytkownika do przestrzeni adresowej jądra, druga odwrotnie. Ogólnie
zachowują się jak memcpy
, jednak trzeba pamiętać, że w przypadku błędu
braku strony adresu w przestrzeni użytkownika mogą spowodować uśpienie
procesu aż do momentu ściągnięcia strony z pliku wymiany. Przed
kopiowaniem sprawdzana jest poprawność adresu w przestrzeni
użytkownika. Jeśli początek obszaru jest poprawny, ale dalsza część
nie, to kopiowany jest najdłuższy możliwy fragment.
Wartością zwracaną przez obie funkcje jest liczba NIE skopiowanych bajtów - niezerowa oznacza wystąpienie błędu przy kopiowaniu.
Funkcje i odpowiadające im makrodefinicje zdefiniowane są w pliku
asm/uaccess.h
. Należy zwrócić uwagę, iż funkcje dla bloków o
rozmiarze potęg dwójki są zoptymalizowane.
W przypadku błedu przy kopiowaniu z/do przestrzeni użytkownika, syscalle
powinny zwracać -EFAULT
.
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 listy list_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
Po szczegóły odsyłam do kodu źródłowego.
Literatura¶
- A. Rubini, J. Corbet, G. Kroah-Hartman, Linux Device Drivers, 3rd edition, O’Reilly, 2005. (http://lwn.net/Kernel/LDD3/)
- Książki podane na stronie przedmiotu: http://students.mimuw.edu.pl/ZSO/