Realizacja sterowników urządzeń w systemie Linux

Marcin Owsiany

<porridge@student.uci.agh.edu.pl>


Spis treści
1. Czym są sterowniki?
2. Sposoby komunikacji z urządzeniami zewnętrznymi.
Polling.
Przerwania.
DMA, czyli bezpośredni dostęp do pamięci.
Porty wejścia/wyjścia.
3. Pamięć.
4. Wykorzystywanie mechanizmów jądra.
Usypianie (blokowanie) procesów.
Szeregowanie procesów.
5. Ważne struktury danych.
6. Komunikacja z procesami w trybie użytkownika.
Specjalne pliki urządzeń "special device files".
Urządzenia znakowe - najprostsze z urządzeń Linuksa.
Urządzenia blokowe.
ioctl()
System plików /proc.
Wywołania systemowe.
Brak.
7. Moduły jądra.
8. Bibliografia

Rozdział 1. Czym są sterowniki?

Wprowadzenie.

Sterowniki urządzeń mają jedno podstawowe zadanie: oddzielenie kodu zajmującego się sprzętem od reszty oprogramowania. Działają w trybie jądra, korzystają z jego mechanizmow i algorytmów.

W Linuksie można je dynamicznie ładować i odładowywać. Sterowniki muszą więc w czasie inicjalizacji "meldować" się jądru, aby mogło ono je zarejestrować. Aby mogły zostać użyte, jądro musi o nich wiedzieć. W tym celu zawiera ono tablice przechowujące informacje na temat dostępnych sterowników, udostępnianych przez nie funkcji i dotyczących ich struktur danych.

Są one konfigurowalne i dynamiczne - można zdecydować, czy chce się je wkompilować do jądra, czy nie. Można też mieć w jądrze sterownik do urzadzenia, ktorego nie ma w systemie.

Sterownik musi udostępnić jądru (lub podsystemowi, do którego należy) pewien standardowy interfejs, dzięki czemu jądro może w taki sam sposób traktować różne urządzenia. Można w ten sposób zamknąć i ukryć w sterownikach różnice pomiędzy urządzeniami.


Rozdział 2. Sposoby komunikacji z urządzeniami zewnętrznymi.

Podstawowe problemy to sposób, w jaki jądro dowiaduje się o zakończeniu przez urządzenie peryferyjne operacji, a także transfer danych do i z urządzenia. Istnieje kilka rozwiązań.


Polling.

W tej technice co pewien czas jest sprawdzany stan urządzenia - jeśli się zmieni, to znaczy, że urządzenie zakończyło operację. Jest to mało wydajny sposób, ponieważ czas procesora jest zużywany na sprawdzanie stanu urządzenia - w ten sposób procesor wiele razy komunikuje się z urządzeniem niepotrzebnie, zanim rzeczywiście wykryje zmianę stanu.

Poza tym nie wiadomo dokładnie kiedy rzeczywiście należy sprawdzić stan urządzenia, a takie zgadywanie nie sprzyja wydajności. Najlepiej byłoby sprawdzać tylko wtedy, gdy operacja właśnie się zakończyła.


Przerwania.

Wydajniejszy sposób obsługi urządzeń.

Kiedy urządzenie wymaga obsługi, spowoduje podniesienie stanu linii przerwania systemowego. Sposób dostarczenia przerwania do procesora zależy od architektury komputera, ale najczesciej dzieje się tak, że procesor po wykonaniu każdej instrukcji sprawdza, czy nie nastąpiło przerwanie.

W przypadku architektury i386 istnieją dwa główne typy przerwań. Przerwania krótkie, jak sama nazwa wskazuje, trwają stosunkowo krótko. Podczas ich obsługi wszystkie inne przerwania są blokowane. Przerwania długie mogą trwać dłużej (na przykład ze względu na transmisję danych), a podczas ich obsługi mogą wystąpić inne przerwania (oprócz przerwań od tego samego urządzenia). Jeśli to tylko możliwe, to powinno się deklarować przerwanie jako długie, gdyż to nie powoduje blokowania dostępności systemu.

Najczęściej w czasie obsługi przerwania zablokowane są inne przerwania i z tego względu procedura jego obsługi powinna działać jak najkrócej. Jeśli ma dużo rzeczy do zrobienia, to powinna odłożyć zadanie w odpowiedniej kolejce i wykonać je po powrocie z procedury obsługi przerwania.

Jądro sprawdza, które urządzenie wywołało przerwanie i uruchamia kod w odpowiednim sterowniku. Udaje się to zrobic dzieki temu, że sterowniki w czasie inicjalizacji rejestrują w jądrze fakt, że będą obsługiwać przerwanie (patrz /proc/interrupts).

Aby zarejestrować w sterowniku procedurę obsługi przerwania należy użyć funkcji request_irq, która przyjmuje jako parametry numer przerwania (w skrócie IRQ, od Interrupt ReQuest), nazwę funkcji, flagi, nazwę, która zostanie umieszczona w /proc/interrupts, oraz parametr, który zostanie przekazany procedurze obsługi przerwania. Flagi mogą zawierać SA_SHIRQ, co oznacza, że sterownik zgadza się współdzielić przerwanie, (zazwyczaj dlatego, że kilka urządzeń zewnętrznych korzysta z tego samego przerwania) oraz SA_INTERRUPT, co oznacza, że jest to przerwanie krótkie. Wywołanie funkcji request_irq powiedzie się tylko, jeśli jeszcze nie ma zarejestrowanej dla tego urządzenia procedury obsługi, lub jeśli obie procedury zgadzają się współdzielić przerwanie.

W samej procedurze obsługi przerwania (interrupt handler) sterownik komunikuje się ze sprzętem - na przykład przy pomocy instrukcji inb() lub outb(), a następnie używa funkcji queue_task_irq z parametrem tq_immediate oraz funkcji mark_bh z parametrem BH_IMMEDIATE, aby kazać schedulerowi wywołać odpowiednie zadanie zaraz po powrocie z procedury obsługi przerwania.

Do wyrejestrowania procedury obsługi przerwania służy funkcja free_irq, która przyjmuje dwa argumenty: numer przerwania, oraz wskaźnik do identyfikatora urządzenia.

Do zablokowania i odblokowania przerwań służą odpowiednio funkcje cli() oraz sti().


DMA, czyli bezpośredni dostęp do pamięci.

DMA to sposób rozwiązania problemów z wydajnością, które pojawiają się w przypadku przekazywania przez/do urządzenia dużych ilości danych.

Kontroler DMA zarządza ośmioma kanałami DMA, z których 7 można wykorzystywać w sterownikach urządzeń. Z każdym z kanałów DMA jest skojarzony 16 bitowy rejestr adresowy i 16 bitowy rejestr licznikowy.

Aby rozpocząć transfer danych, sterownik ustawia zawartość obu rejestrów (przy pomocy funkcji set_dma_addr(), pobierającej jako argument numer kanału oraz adres bufora, i set_dma_count(), pobierającej jako argument numer kanału oraz rozmiar przesyłanych danych), kierunek przesyłania danych (czytanie lub pisanie, przy pomocy funkcji set_dma_mode(), która pobiera jako argumenty numer kanału i tryb przesyłu - DMA_MODE_READ lub DMA_MODE_WRITE), a następnie daje urządzeniu przy pomocy funkcji enable_dma() znać, że może rozpocząć transfer DMA. Należy pamiętać, że funkcji disable_dma(), enable_dma(), request_dma(), oraz free_dma() powinno się używać z wyłączonymi przerwaniami.

Urządzenia nie wiedzą nic o pamięci wirtualnej, bloki pamięci muszą być ciągłe. Jeśli transfer ma być przeprowadzany bezpośrednio do/z pamięci użytkownika, to strony trzeba zablokować w pamięci.

Nie da się pisać do/z całej pamięci fizycznej, tylko do/z 16 MB, ponieważ rejestry DMA są za małe - wykorzystywane jest 16 bitów rejestru adresowego i 8 bitów licznikowego.

Jądro pamięta czy dany kanał DMA jest zajęty, czy wolny zapisując informacje w tablicy dma_chan_busy struktur dma_chan. Sterowniki powinny rezerwować i zwalniać kanały DMA używając funkcji request_dma() i free_dma(). Funkcja rezerwująca kanał pobiera dwa argumenty: numer kanału, oraz nazwę sterownika (która zostanie umieszczona w /proc/dma).


Porty wejścia/wyjścia.

Dane można przesyłać z/do urządzeń zewnętrznych przez porty wejścia/wyjścia. Służą do tego instrukcje zdefiniowane w <asm/io.h>. Funkcje czytające dane pobierają jeden argument - numer portu, a funkcje piszące - dwa: dane do wysłania i numer portu. Oto te funkcje, dla architektury i386: inb(), inb_p(), outb(), outb_p(), inw(), inw_p(), outw(), outw_p(), inl(), inl_p(), outl(), outl_p(), oraz inb(). Różnią się one wielkością przesyłanych danych (b - bajt, w - word, l - long). Funkcje, których nazwa zawiera '_p' czekają przez krótki czas po wykonaniu operacji, ponieważ niektóre urządzenia nie nadążają za procesorem.


Rozdział 3. Pamięć.

Sterowniki nie mogą używać pamięci wirtualnej (użytkownika), ponieważ nigdy nie wiadomo, w kontekście którego procesu znajdzie się system podczas gdy działa dany sterownik.

Mogą one używać pamięci alokowanej statycznie, ale jeśli potrzebują większych ilości pamięci przez krótki czas, byłoby to marnotrastwo.

Mogą też używać alokowanej dynamicznie, niestronicowanej pamięci jądra.

Jądro udostępnia funkcje przydzielające i zwalniające pamięć, które działają na blokach pamięci, których wielkościami są potęgi dwójki. Wielkość każdego bloku jest zaokrąglana w górę do najmniejszej wartości będącej potęgą dwójki - to w pewnych okolicznościach powoduje marnowanie pamięci, ale jednocześnie bardzo ułatwia jądru jej zarządzanie, gdyż w łatwy sposób można łaczyć małe bloki w większe.

Jeśli w systemie nie ma wystarczającej ilości pamięci i niektóre strony mogą wymagać wymiany. Jądro zazwyczaj odkłada proszący o pamięć proces na pewną kolejkę i przydziela pamięć, gdy już jest wolna.

Sterownik może przy przydzieleniu określić, że nie chce czekać na przydzielenie pamięci (gdy nie ma od razu wystarczającej ilości), ale woli, żeby operacja przydzielenia pamięci nie powiodła się.

Sterownik może przy przydzieleniu określić, że chce, aby do/z danej pamięci można było przeprowadzić transfer przy pomocy kanału DMA. Dzięki temu to jądro musi wiedzieć, jakie warunki muszą być spełnione, aby było to możliwe - sterownik nie musi się o to martwić.


Rozdział 4. Wykorzystywanie mechanizmów jądra.

Usypianie (blokowanie) procesów.

Jeśli moduł nie chce, aby proces uzyskał natychmiast zasób o który prosi (z różnych powodów, ale najczęściej dlatego, że dany zasób jest obecnie używany), może spowodować, że dalsze wykonanie tego procesu będzie blokowane do czasu zwolnienia się danego zasobu.

Należy przy tym wziąć pod uwagę flagę O_NONBLOCK, która jest dopuszczalna przy niektórych wywołaniach systemowych, a która oznacza, że proces woli raczej, żeby wywołanie powróciło z błędem (EAGAIN, czyli "spróbuj jeszcze raz"), jeśli nie może być zrealizowane natychmiast, niż aby wykonanie procesu było blokowane.

W przypadku modułów blokowanie zazwyczaj realizuje się w następujący sposób przy pomocy funkcji interruptible_sleep_on (lub analogicznej), która ustawia dany proces (task) w tryb TASK_INTERRUPTIBLE (co oznacza, że proces będzie spał, dopóki nie zostanie obudzony przez zwolnienie zasobu lub otrzymanie nieobsługiwanego sygnału), dodaje proces do kolejki procesów oczekujących na dostęp do pliku, a następnie uruchamia procedurę szeregującą (scheduler), aby zmieniła kontekst na inny proces, który może w tym czasie wykonywać jakieś zadanie.


struct wait_queue *queue = NULL;

if ((file->flags & O_NONBLOCK) && zasób_zajęty)
	return -EAGAIN;

MOD_INC_USE_COUNT;

while (zasób_zajęty) {
	module_interruptible_sleep_on(&queue);
	for(i = 0; i < _NSIG_WORDS && ! is_sig; i++)
		is_sig = current->signal.sig[i] & ~current->blocked.sig[i];
	if (is_sig) {
		MOD_DEC_USE_COUNT;
		return -EINTR;
	}
}
zajmij_zasób();

Szeregowanie procesów.

Jeśli chcemy, aby sterownik wykonywał co pewien czas jakieś zadanie, należy utworzyć nowe zadanie (struct tq_task), zawierające wskaźnik do danej funkcji i użyć funkcji queue_task aby umieścić to zadanie w kolejce o nazwie tq_timer, która jest listą zadań, które mają zostać wywołane przy następnym przerwaniu zegara. Jeśli chcemy, aby dana funkcja została wywołana więcej niż jeden raz, powinniśmy w niej samej powtarzać procedurę szeregowania zadania. Należy pamiętać, że jeśli zostanie uruchomiony program rmmod, jądro nie sprawdzi, że przy następnym przerwaniu zegara zostanie wywołana funkcja z modułu, który właśnie ma zostać usunięty. Z kolei funkcja cleanup_module nie może zwrócić błędu, bo jest typu void. Rozwiązaniem jest uśpienie procesu rmmod w funkcji cleanup_module i ustawienie jakiegoś znacznika, dzięki któremu funkcja wywoływana przez zegar systemowy nie uszereguje się ponownie. Później możemy już spokojnie zakończyć działanie funkcji cleanup_module.


Rozdział 5. Ważne struktury danych.

Oto dwie struktury, które mają duże znaczenie przy używaniu funkcji opisywanych w następnym rozdziale:


struct inode_operations {
        struct file_operations * default_file_ops;
        int (*create) (struct inode *,struct dentry *,int);
        struct dentry * (*lookup) (struct inode *,struct dentry *);
        int (*link) (struct dentry *,struct inode *,struct dentry *);
        int (*unlink) (struct inode *,struct dentry *);
        int (*symlink) (struct inode *,struct dentry *,const char *);
        int (*mkdir) (struct inode *,struct dentry *,int);
        int (*rmdir) (struct inode *,struct dentry *);
        int (*mknod) (struct inode *,struct dentry *,int,int);
        int (*rename) (struct inode *, struct dentry *, struct inode *, struct dentry *);
        int (*readlink) (struct dentry *, char *,int);
        struct dentry * (*follow_link) (struct dentry *, struct dentry *, unsigned int);
        int (*readpage) (struct file *, struct page *);
        int (*writepage) (struct file *, struct page *);
        int (*bmap) (struct inode *,int);
        void (*truncate) (struct inode *);
        int (*permission) (struct inode *, int);
        int (*smap) (struct inode *,int);
        int (*updatepage) (struct file *, struct page *, unsigned long, unsigned int, int);
        int (*revalidate) (struct dentry *);
};

Są to wskaźniki do funkcji operujących na i-węźle pliku.

struct file_operations {
	int (*lseek) (struct inode *, struct file *, off_t, int);
	int (*read) (struct inode *, struct file *, char *, int);
	int (*write) (struct inode *, struct file *, char *, int);
	int (*readdir) (struct inode *, struct file *, struct dirent *, int);
	int (*select) (struct inode *, struct file *, int, select_table *);
	int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
	int (*mmap) (struct inode *, struct file *, struct vm_area_struct *);
	int (*open) (struct inode *, struct file *);
	void (*release) (struct inode *, struct file *);
	int (*fsync) (struct inode *, struct file *);
	int (*fasync) (struct inode *, struct file *, int);
	int (*check_media_change) (dev_t dev);
	int (*revalidate) (dev_t dev);
};

Są to wskaźniki do funkcji operujących na pliku.


Rozdział 6. Komunikacja z procesami w trybie użytkownika.

Specjalne pliki urządzeń "special device files".

Dzięki przyjęciu takiego interfejsu, można odwoływać się do urządzeń jak do plików - procesy mogą je otwierać, czytać je, pisać do nich, zamykać itp.

Każdy specjalny plik urządzenia jest reprezentowany przez i-węzeł VFS, podobnie jak każdy inny plik. Plik urządzenia rózni się jednak tym, że jego i-węzeł zawiera dwie liczby: główny oraz poboczny numer urządzenia. Każdy rodzaj urządzenia otrzymuje numer główny (poza kilkoma wyjątkami, na przykład niektórym urządzeniom są przyporządkowane dwa numery główne). Numer poboczny natomiast identyfikuje dane urządzenie fizyczne w systemie. Na przykład wszystkie urządzenia SCSI mają numer główny 8, a poszczególne urządzenia SCSI rozróżniane są przez różne numery poboczne.


Urządzenia znakowe - najprostsze z urządzeń Linuksa.

W czasie inicjalizacji urządzenia znakowego, jego sterownik rejestruje się w jądrze (w przypadku modułu przy pomocy funkcji module_register_chrdev), która dodaje informację o danym urządzeniu do specjalnej tablicy o nazwie chrdevs. Identyfikator główny urządzenia (device major number) jest używany jako indeks w tej tablicy. Każdy element tej tablicy jest strukturą device_struct, która zawiera dwa pola: wskaźnik do nazwy zarejestrowanego sterownika, oraz wskaźnik do bloku operacji plikowych, czyli bloku adresów funkcji znajdujących się w sterowniku danego urządzenia, służących do przeprowadzania różnych operacji plikowych, takich jak open(), read(), lseek() itp. Zawartość pliku /proc/devices jest pobierana właśnie z tablicy chardevs (i blkdevs).

Z każdym i-węzłem VFS pliku urządzenia znakowego jest skojarzony zbiór operacji plikowych - zależą one od tego, jaki obiekt w systemie plików identyfikuje dany i-węzeł. Po utworzeniu nowego i-węzła VFS związanego z plikiem urządzenia znakowego zostają skojarzone z nim domyślne operacje dla urządzeń znakowych. W zasadzie jest to tylko jedna operacja - otwarcia pliku. Po otwarciu takiego pliku przez proces, standardowa operacja otwarcia wykorzystuje główny numer urządzenia jako indeks w tablicy chrdevs i pobiera za jej pośrednictwem skojarzony z danym urządzeniem zbiór operacji plikowych. Inicjalizuje ona także odpowiednią strukturę "file", ustawiając jej wskaźnik do bloku operacji plikowych na blok operacji plikowych danego sterownika. Dzięki temu operacje na danym pliku są odwzorowane na operacje danego sterownika urządzenia.


Urządzenia blokowe.

Wykorzystanie wygląda podobnie jak w przypadku urządzeń znakowych, z tym że tablica urządzeń nosi nazwę blkdevs.

W przeciwieństwie do urządzeń znakowych, istnieją klasy urządzeń blokowych (np klasa urządzeń SCSI lub IDE). To cała klasa urządzeń rejestruje się w jądrze i udostępnia mu swoje usługi. Sterowniki należące do takiej klasy udostępniają interfejs właśnie jej.

Każdy sterownik urządzenia blokowego oprócz standardowych operacji plikowych musi odostępnić pewien interfejs podsystemowi buforów podręcznych (buffer cache). Robi to wypełniając strukturą blk_dev_struct odpowiadającą jego numerowi głównemu pozycję tablicy blk_dev. Struktura ta zawiera wskaźnik do funkcji żądania (request routine) a także wskaźnik do listy struktur "request", z których każda reprezentuje żądanie odczytania lub zapisania bloku danych od podsystemu buforów podręcznych. Za każdym razem, gdy podsystem buforowania chce odczytać lub zapisać blok z/do urządzenia, dodaje do struktury blk_dev_struct tego urządzenia strukturę "request". Każda z tych struktur zawiera wskaźnik do struktur buffer_head, które są zablokowane w pamięci przez podsystem buforowania. Kiedy podsystem ten dodaje żądanie do pustej listy, wywołuje funkcję żądania, aby sterownik urządzenia zaczął przetwarzać listę żądań. Po przeczytaniu danych do bufora sterownik musi odłączyć struktury buffer_head od struktury request, zaznaczyć je jako aktualne i odblokować. Odblokowanie tej struktury spowoduje obudzenie śpiącego w oczekiwaniu na zakończenie operacji wejścia/wyjścia procesu.

Czytanie i pisanie z/do urządzenia przy pomocy plików urządzeń jest obsługiwane przez sterownik poprzez zarejestrowanie w strukturze file_operations funkcji read i write. Funkcja read przyjmuje jako parametry wskaźnik do struktury file, wskaźnik do bufora, do którego sterownik powinien zapisać dane, ilość bajtów do odczytania oraz offset do pliku. Powinna ona zwracać ilość przekazanych procesowi bajtów. Funkcja write przyjmuje jako parametry wskaźnik do struktury file, wskaźnik do bufora, z którego sterownik powinien odczytać dane, ilość bajtów do zapisania do urządzenia oraz offset. Funkcja ta powinna zwrócić ilość przekazanych bajtów.


ioctl()

Pisanie i czytanie danych do/z urządzeń zewnętrznych nie zawsze wystarcza. Czasem trzeba nawiązać z urządzeniem bardziej bezpośrednią komunikację na niższym poziomie abstrakcji. Jest to konieczne w takich przypadkach, jak na przykład ustawianie parametrów transmisji modemu, parametów pracy dysku twardego, itp.

Linux umożliwia procesom takie działania przy pomocy funkcji ioctl() (od I/O control - kontrola wejścia/wyjścia). Znaczenie parametrów tej funkcji zależy od rodzaju urządzenia, którym się manipuluje. Można przy jej pomocy zarówno wysyłać, jak i otrzymywać informacje od urządzenia. Funkcja ta przyjmuje trzy parametry: deskryptor, numer ioctl-a, oraz parametr typu long, który można rzutować na cokolwiek. Numer ioctl-a zawiera zakodowany główny numer urządzenia, typ ioctl-a, komendę oraz typ parametru. Jeśli zamierza się używać tej funkcji, należy upewnić się, że wykorzystuje się unikatowy numer ioctl - znajdują się one w /usr/src/linux/Documentation/ioctl-number.txt.

Sterownik obsługuje wywołanie przez użytkownika funkcji ioctl poprzez funkcję ioctl zarejestrowaną w strukturze file_operations. Funkcja ta powinna przyjmować jako parametry oprócz wskaźników do struktur inode i file także numer ioctl i parametr.


System plików /proc.

W założeniu miał służyć temu, aby w łatwy sposób można było otrzymywać informacje na temat działających procesów. Obecnie znajdują się tam również takie pseudo-pliki, przez które różne części jądra udostępniają różne interesujące informacje.

Sterownik może utworzyć w systemi plików /proc plik, który będzie służyć do wymiany informacji z procesami. Do tworzenia i usuwania takiego pliku służą funkcje proc_register i proc_unregister.

Należy pamiętać, że w jądrze znaczenia czytania i pisania do pliku są odwrócone, bo to, co czyta proces w trybie użytkownika, jądro musi pisać, i odwrotnie.

Funkcja tworząca plik w systemie proc proc_register przyjmuje dwa argumenty - wskaźniki do struktur proc_dir_entry, z których pierwsza oznacza katalog, w którym ma zostać utworzony plik, a druga zawiera wszystkie potrzebne informacje na temat tworzonego pliku.

Funkcja usuwająca plik proc_unregister również przyjmuje dwa argumenty - wskaźnik do struktury proc_dir_entry oznaczający katalog, z którego chcemy usunąć pli, oraz numer i-węzła pliku, który chcemy usunąć.

Struktura proc_dir_entry zawiera wszystkie niezbędne informacje na temat danego pliku w systemie plików proc. Oto jej składniki:

struct proc_dir_entry {
	/*
	 * Numer i-węzła. Jeśli równy zero, to zostanie automatycznie
	 * ustawiony przez funkcję rejestrującą proc_register.
	 */
		unsigned short low_ino;

	/* Długość nazwy pliku.
	 */
		unsigned short namelen;

	/* Nazwa pliku.
	 */
		const char *name;

	/* Tryb dostępu do pliku.
	 */
		mode_t mode;

	/* Ilość dowiązań do pliku
	 */
		nlink_t nlink;

	/* Właściciel pliku - UID i GID.
	 */
		uid_t uid;
		gid_t gid;

	/* Rozmiar pliku.
	 */
		unsigned long size;

	/* Wskaźnik do struktury opisującej możliwe operacje na i-węźle.
	 * Jeśli mamy zamiar umożliwić jedynie czytanie z tego pliku, to
	 * lepiej jest użyć funkcji get_info.
	 */
		struct inode_operations * ops;

	/* Funkcja, która zostanie wywołana, jeśli proces spróbuje coś
	 * czytać z tego pliku, umożliwia ona przekazywanie informacji z
	 * jądra do procesów.
	 */
		int (*get_info)(char *, char **, off_t, int, int);

	/* Funkcja wypełniająca i-węzeł - ustawia właściciela, prawa
	 * dostępu itp. dla konkretnego pliku.
	 */
		void (*fill_inode)(struct inode *, int);
		
	/* Wskaźniki do podobnych struktur, które tworzą drzewo katalogów
	 * i plików. Pierwszy z nich to wskaźnik do następnego elementu w
	 * liście pozycji w danym katalogu. Drugi to wskaźnik do katalogu
	 * nadrzędnego, a trzeci, w przypadku i-węzła reprezentującego
	 * katalog, to wskaźnik do pierwszego elementu w tym katalogu.
	 */
		struct proc_dir_entry *next, *parent, *subdir;

	/* Inne funkcje do czytania i pisania danych.
	 */
		void *data;
		int (*read_proc)(char *page, char **start, off_t off,
					int count, int *eof, void *data);
		int (*write_proc)(struct file *file, const char *buffer,
					unsigned long count, void *data);
		int (*readlink_proc)(struct proc_dir_entry *de, char *page);

	/* Licznik użycia
	 */
		unsigned int count;

	/* Flaga skasowania
	 */
		int deleted;
};

Funkcja get_info przyjmuje następujące argumenty: wskaźnik do bufora zaalokowanego przez jądro, do którego mają zostać przesłane dane, wskaźnik do wskaźnika do bufora, który można samodzielnie zaalokować i przesłać dane do niego, zamiast do bufora zaalokowanego przez jądro (wtedy należy zwrócić adres utworzonego bufora w komórce wskazywanej przez ten wskaźnik), obecną pozycję w pliku, rozmiar bufora, na który wskazuje wskaźnik w pierwszym argumencie, zero - zarezerwowane do użytku w przyszłości. Funkcja ta powinna zwracać ilość przepisanych bajtów, lub zero, jeśli nie ma już nic do przekazania (koniec pliku). Ujemna wartość oznacza wystąpienie błędu.

Ze względu na segmentację pamięci w przypadku niektórych architektur należy przedwsięwziąc specjalne działania jeśli ma się zamiar modyfikować lub czytać pamięć procesu trybu użytkownika. W wygodny sposób umożliwiają to makra put_user i get_user.


Wywołania systemowe.

Procesy w trybie użytkownika mogą zlecać jądru wykonanie tak zwanych "wywołań systemowych" (system calls). Choć w językach programowania takich jak C wygląda to jak zwykłe wywołanie funkcji, to jednak działanie jest inne. Proces umieszcza parametry wywołania w odpowiednich rejestrach, a następnie uruchamia specjalną instrukcję (zależną od architektury - na niektórych maszynach istnieją specjalne instrukcje, w przypadku architektury zgodnej z procesorami i386 jest to wywołanie przerwania nr 80h), które nakazuje sprzętowi zmianę trybu wykonywania na tryb jądra - procesor zaczyna wykonywać kod jądra od adresu ``system_call''. W zależności od numeru wywołania systemowego, jądro uruchamia odpowiednią funkcję (na podstawie tablicy ``sys_call_table'').

Dlatego, jeśli chcemy zmienić sposób, w jaki działa wywołanie systemowe, musimy napisać własną funkcję (która najczęściej będzie wykonywać jakieś działanie, a następnie wywoływać oryginalną funkcję), i podmienić jej adres w tablicy ``sys_call_table''. Oczywiście jeśli sterownik ma postać modułu, to w funkcji cleanup_module należy przywrócić poprzedni stan tablicy wywołań systemowych jeśli system nadal ma działać stabilnie. Wygodnie jest indeksować tablicę wywołań systemowych przy pomocy zdefiniowanych stałych - ich nazwy to __NRsyscall, gdzie syscall to nazwa wywołania systemowego.

Manipulując w ten sposób adresami wywołań systemowych należy zachować szczególną ostrożność, jeśli robi się to przy pomocy modułów jądra. Może się bowiem zdażyć, że jeśli załadujemy jeden moduł, później załadujemy drugi, a następnie najpierw odładujemy pierwszy, a później drugi, to w tablicy wywołań systemowych pojawi się adres nieistniejącej już funkcji z modułu pierwszego.

Uzyskiwać informacje na temat procesu, na rzecz którego pracuje sterownik można dzięki wskaźnikowi ``current'', który wskazuje na strukturę zawierającą informacje na temat obecnie działającego zadania (task). Struktura ta zawiera mnóstwo cennych informacji, takich jak stan, flagi procesu, otwarte pliki, terminal kontrolny itd...


Brak.

Są to na przykład sterowniki protokołów sieciowych. Do ich działania nie są potrzebne ani pliki urządzeń, ani wpisy w systemie plików /proc. Sterowniki takie jedynie rejestrują odpowiednie funkcje w jądrze.


Rozdział 7. Moduły jądra.

Umożliwiają dynamiczne, automatyczne ładowanie i odładowywanie kodu do jądra. Jako moduł taki może działać na przykład sterownik urządzenia. Dzięki temu można zminimalizować wielkość jądra - zmniejszenie zajmowania pamięci. Ma to swój (jednak mały) koszt. Z powodu dodatkowego poziomu pośredniczenia moduły muszą dostarczyć pewnych dodatkowych funkcji. Oprócz tego dodatkowe struktury danych zabierają pewną ilość pamięci.

Moduły są skompilowane jako pliki obiektowe z możliwością relokacji, to znaczy mogące działać z dowolnego adresu.

Przy ładowaniu modułu program insmod prosi jądro o pamięć na załadowanie modułu. Jądro alokuje żądany obszar pamięci i mapuje go w obszar przydzielony programowi insmod oraz dołącza go na koniec tablicy modułów i oznacza jako ``UNINITIALIZED''. Następnie insmod ładuje do tego obszaru moduł i dostosowuje adresy symboli, do których odwołuje się kod modułu. Adresy te są utrzymywane przez jądro w tablicy symboli. Jądro importuje także do tej tablicy eksportowane przez moduł symbole. Następnie wywołuje funkcję inicjalizacji modułu, po czym jego stan jest ustawiany na ``RUNNING''.

Jeśli moduł jest używany, fakt ten zostaje zapamiętany przez umieszczenie w tak zwanym liczniku użycia pewnej wartości. Naa przykład na każdy zamontowany system plików, które sterownik jest załadowany w postaci modułu przypada jedynka dodana do licznika. Jądro odmawia odładowania modułu jeśli licznik użycia nie jest równy zero. Licznik ten jest dostępny przez zmienną mod_use_count_, jednak do manipulacji nim zazwyczaj używa się makr MOD_INC_USE_COUNT oraz MOD_DEC_USE_COUNT.

Przed odładowaniem modułu jądro powiadamia o tym zamiarze moduł, dzięki czemu może on uwolnić zajmowane przez siebie zasoby. Następuje też usunięcie z tablicy symboli eksportowanych przez moduł. Następnie stan modułu jest oznaczany jako ``DELETED'', a od liczników wykorzystania modułów, których wymagał dany moduł jest odejmowane jeden. Pamięć poświęcona na przechowanie modułu jest zwalniana.

Jak widać z opisu ładowania i usuwania modułu, każdy moduł musi dostarczyć dwie funkcje: int init_module(void) i void cleanup_module(void) - nie są one eksportowane przez moduły, ale ich adresy podawane są jądru przez program insmod, a ono zapisuje je w tablicy symboli danego modułu.

Funkcja init_module() zazwyczaj rejestruje moduł w jądrze albo podmienia jakąś jego funkcję funkcją modułu (która zazwyczaj wykonuje jakieś zadanie, po czym wywołuje starą funkcję). Funkcja ta powinna zwracać zero, jeśli inicjalizacja modułu się powiodła. Jeśli zwróci inną wartość, oznacza to, że moduł nie może zostać załadowany.

Funkcja cleanup_module() służy do wyrejestrowania usuług modułu przed jego odładowaniem. Jest ona typu void, więc nie może ona zwrócić błędu (choć może go zgłosić na przykład przy pomocy funkcji printk). Po jej wykonaniu moduł znika z jądra.

Istnieją dwa zasadnicze sposoby ładowania modułów. Pierwszy to ręczne ładowanie przy pomocy programu insmod. Drugi to automatyczne ładowanie modułu na żądanie przez wątek jądra ``kmod''.

Możliwe jest podanie modułowi parametrów. Oczywiście nie są dostępne zmienne argv i argc, zamiast tego można utworzyć w module zmienne globalne i poprosić insmod-a o wypełnienie ich zgodnie z parametrami linii poleceń. Dostępne jest makro MACRO_PARM (zdefiniowane w <linux/modules.h>), dzięki któremu można dać insmod-owi znać, że moduł oczekuje parametru o danej nazwie i danym typie. Należy więc zadeklarować globalny wskaźnik do zmiennej i podać jego nazwę makru jako pierwszy argument. Drugim argumentem powinien być jednoliterowy łańcuch oznaczający typ argumentu ("i" oznacza int, a "s" oznacza string). W czasie wywołania funkcji init_module wskaźnik będzie już wskazwał na wartość argumentu (o ile oczywiście został podany przez użytkownika).


Rozdział 8. Bibliografia

Linux Kernel Module Programming Guide

The Linux Kernel

Writing Linux Device Drivers

The Linux Kernel Hacker's Guide