R-26-07.doc

(253 KB) Pobierz
PLP_Rozdział_26

Rozdział 26. Sterowniki urządzeń

W tym rozdziale zapoznamy się programowaniem jądra. Jest to zagadnienie, które z łatwością zapełniłoby samo całą książkę, a więc nie należy tego rozdziału uważać za kompletny podręcznik. Chcemy tu tylko pokazać, w jaki sposób można utworzyć sterownik urządzenia (ang. device driver). Większość użytkowników nie musi „brudzić” sobie rąk tymi sprawami, ale jeżeli ktoś ma nietypowy sprzęt, który nie jest obsługiwany przez jądro Linuksa, może próbować napisać sam odpowiedni sterownik.

Chcemy zająć się tutaj zagadnieniami podstawowymi. W jaki sposób upewnić się, czy kod inicjujący jest wywoływany w odpowiednim czasie? Jak wykrywać i konfigurować urządzenia na magistrali PCI? Jak dołączać swój sterownik do działającego systemu? Wskażemy także na kilka bardziej ulotnych aspektów oprogramowania jądra, które są przez użytkowników albo błędnie interpretowane, albo trudne do zrozumienia. W szczególności omówimy różne funkcje blokujące, stosowane w zabezpieczaniu struktur danych i kodu przy równoczesnym dostępie do nich oraz w sytuacjach, gdy każdy z tych elementów jest używany. Pokażemy także kilka z najczęściej spotykanych sytuacji prowadzących do „wyścigu” (czyli błędy powodowane przez nienormalny rozkład czasowy zdarzeń prowadzący do nieregularnego działania) i sposoby ich unikania. Omówimy tu również zasady obowiązujące przy dostępie do danych zawartych na stronach pamięci, używanych przez normalne procesy Linuksa (w tzw. przestrzeni adresowej użytkownika) zamiast w sposób bezpieczny na stronach użytkowanych przez jądro (w tzw. przestrzeni adresowej jądra), których nie można przesłać na dysk.

Sterownik, który chcemy tu zaprezentować jako przykład, będzie sterownikiem urządzenia znakowego, jakim jest inteligentny kontroler magistrali stosowanej w sieci przemysłowej (ang. fieldbus). Będziemy aktualizować kod występujący w jądrach z rozwojowej serii 2.3. O samym urządzeniu wiemy jedynie to, że jest ono wyposażone w bufor dostępny z magistrali PCI, przez który zachodzi wymiana danych z bibliotekami rezydującymi w przestrzeni użytkownika. Co dziwne, karty tego rodzaju są produkowane przez firmę Applicom International S.A. (http://www.applicom-int.com/) i są używane jako inteligentne urządzenia komunikujące się z większością spotykanych sieci i magistral przemysłowych.

Kontekst działania

Każdy proces dysponuje mapą pamięci wirtualnej, odwzorowującą każdą stronę z jego wirtualnej przestrzeni adresowej na strony fizyczne utrzymywane albo w RAM, albo w dyskowym buforze wymiany (ang. swap). Każdy proces ma także odwzorowane strony jądra, ale może z nich korzystać tylko wówczas, gdy procesor nie działa w trybie uprzywilejowanym.

Większa część kodu jądra działa kontekstowo w odniesieniu do procesu użytkownika, co oznacza, że gdy proces wywołuje funkcję systemową, to procesor przełącza się w tryb uprzywilejowany i dalej działa na tej samej mapie pamięci wirtualnej co proces wywołujący. Jeżeli tylko kod nie wykona jakiejś sztuczki z zarządzaniem pamięcią, to procesor ma dostęp tylko do obszaru pamięci zarezerwowanego dla jądra lub obszaru zarezerwowanego dla użytkownika, w imieniu którego działa.

Każdy kod jądra uruchomiony w taki sposób może korzystać z pamięci procesu za pomocą funkcji copy_to_user i copy_from_user, które opiszemy w dalszych częściach tego rozdziału. Kod ten może także wywoływać funkcje, które można uśpić (dając procesorowi chwilowe uprawnienia do uruchamiania innych procesów podczas oczekiwania na jakieś zdarzenie lub koniec cyklu wyczekiwania).

Niektóre procedury powinny jednak zakończyć się szybko bez podejmowania prób usypiania. Zalicza się do nich kod, który może obsługiwać przerwania (albo wywłaszczać) dowolny proces w dowolnym czasie, czyli np. programy obsługi przerwań wywoływane natychmiast po wystąpieniu sygnału przerwania sprzętowego i funkcje ustawiane przez liczniki czasu, wywoływane przez jądro po upłynięciu określonego interwału czasowego. Taki kod powinien więc działać w kontekście procesu, który ma zostać uruchomiony na tej samej maszynie po spełnieniu określonych warunków i nie powinien powodować usypiania tego procesu.

Dowolny kod może utrzymywać blokadę, która jest potrzebna przy obsłudze przerwań do poprawnego zakończenia zadania. Każdy kod, który utrzymuje taką blokadę, powinien również bez usypiania umożliwiać jej zwolnienie tak szybko, jak to jest możliwe.

Normalnie do przydzielania pamięci w jądrze Linuksa wykorzystuje się funkcję kmalloc, która oprócz żądanego rozmiaru przydzielanej pamięci mierzonego w bajtach wymaga podania dodatkowego argumentu. Najczęściej ten dodatkowy argument ma wartość GFP_KERNEL, co oznacza, że proces wywołujący życzy sobie uśpienia podczas oczekiwania na przydział obszaru pamięci.

Najlepiej, jeśli funkcja kmalloc jest wywoływana w kontekście procesu, dla którego uśpienie jest dozwolone. Na przykład, sterownik karty sieciowej może utrzymywać puste bufory w kolejce, czekając na odbiór pakietów, aby program obsługujący przerwania nie musiał przydzielać nowego bufora w odpowiedzi na sygnał IRQ wygenerowany przez kartę po nadejściu nowego pakietu danych.

Jeżeli trzeba przydzielać pamięć w kontekście procesu, dla którego uśpienie nie jest dozwolone, to używany jest znacznik GFP_ATOMIC. Oznacza to, że funkcja kmalloc będzie zwracać sygnał niepowodzenia, chyba że żądanie przydziału pamięci zostanie bezzwłocznie spełnione.

Moduł i kod inicjujący

Prawie w każdym sterowniku istnieje funkcja inicjująca, która sprawdza obecność obsługiwanych urządzeń oraz rejestruje ich dostępne właściwości funkcjonalne. Trzeba być pewnym, że funkcja inicjująca jest wywoływana w odpowiednim momencie, czyli podczas rozruchu jądra, jeśli sterownik jest w nie wbudowany, albo podczas ładowania do jądra modułu zawierającego ten sterownik.

W jądrach Linuksa z serii 2.2 i we wcześniejszych można było znaleźć długą listę wywołań funkcji inicjujących pracę różnych podsystemów i sterowników (lista była umieszczona w pliku init/main.c). Po wkompilowaniu jakiegoś sterownika w jądro należało dodać do tej listy wywołanie funkcji inicjującej ten sterownik. Mogło to być albo wywołanie bezpośrednie, albo pośrednie (za pomocą innej funkcji wywoływanej z tej listy głównej).

Jeżeli sterownik był skompilowany jako moduł ładowany do jądra, to należało wywołać procedurę inicjującą init_module. Funkcja obsługująca ładowanie modułów do jądra posługiwała się tą specjalną nazwą przy identyfikacji procedury, która miała być wywołana przy pierwszym załadowaniu modułu.

W jądrach z serii 2.4 wszystko zostało uproszczone i można używać tego samego kodu zarówno dla sterowników wkompilowanych do jądra, jak i dla sterowników w postaci modułów. Trzeba tu tylko używać jednego prostego polecenia makroprocesora do identyfikacji funkcji, która ma być wywoływana podczas inicjacji, oraz drugiego do identyfikacji funkcji wywoływanej przy usuwaniu sterownika z jądra (jeśli występuje on jako moduł).

Do identyfikacji funkcji inicjującej stosuje się więc makropolecenie module_init, zaś do identyfikacji procedury zamykającej — makropolecenie module_exit. Każde z nich wymaga podania nazwy wywoływanej funkcji jako argumentu. Przykład użycia tych makropoleceń pokazujemy w następnym podrozdziale.

Ani procedura inicjująca, ani procedura zamykająca nie wymagają żadnych argumentów. Procedura inicjująca zwraca wartość typu int oznaczającą powodzenie lub niepowodzenie (wartość niezerowa oznacza nieudaną próbę wykrycia lub inicjacji urządzenia). Jeżeli sterownik został skompilowany jako moduł i procedura inicjująca init_module zwróci niezerową wartość, to system automatycznie usunie ten moduł bez wywoływania procedury zamykającej. W jądrach z serii 2.4 kod zwracany przez init_module może być następnie zwrócony w postaci kodu błędu do procesu próbującego załadować moduł (zazwyczaj jest to insmod lub modprobe).

Funkcja zamykająca jest wywoływana tuż przed usunięciem modułu z jądra, ale tylko wówczas, gdy sterownik został wcześnie załadowany jako moduł. Podczas wywołania funkcji zamykającej jest już za późno na zabezpieczenie modułu przed usunięciem, należy więc tylko oczyścić pamięć najlepiej, jak to jest możliwe. Istnieją wprawdzie sposoby zabezpieczania pracującego modułu przed usunięciem, lecz nimi zajmiemy się w dalszej części rozdziału.

Sekcje konsolidatora

Jeżeli sterownik został skonsolidowany z jądrem, to jego procedura inicjująca będzie wywoływana tylko raz podczas rozruchu systemu. W takim wypadku procedura zamykająca nie będzie wcale wywoływana, ponieważ jądro musi pozostawać nienaruszone nawet wówczas, gdy cała przestrzeń użytkownika została zamknięta, czyli aż do usunięcia modułów. Pozostawianie w pamięci całego kodu i danych wymaganych przez procedury inicjujące i zamykające można traktować jako dużą rozrzutność. Jądro Linuksa nie może być przechowywane na dysku, a więc taki nieużywany kod zajmuje cenną pamięć RAM.

Aby temu zapobiec, programista może podczas budowy jądra zaznaczyć niektóre dane i funkcje, które można usunąć, jeśli nie będą już potrzebne.

Najczęściej do tego celu bywa używane makropolecenie __init, które służy do oznaczania funkcji inicjujących. Istnieje także makropolecenie __exit dotyczące funkcji usuwających moduły oraz polecenia __initdata i __exitdata dotyczące danych, które mogą być usunięte. Działają one na zasadzie umieszczania obsługiwanych przez nie elementów w innej sekcji ELF niż normalny kod i dane. W następnym podrozdziale pokazany jest przykład zastosowania tych makropoleceń.

Wnikliwy obserwator komunikatów wytwarzanych przez jądro podczas rozruchu systemu (dostępnych także za pomocą polecenia dmesg) zauważy, że natychmiast po zamontowaniu głównego systemu plików pojawia się komunikat podobny do pokazanego niżej:

 

Freeing unused kernel memory: 108k freed

Oznacza to, że zwolniono 108 kB pamięci jądra zawierającej dane, o których wiadomo, że nie będą już potrzebne. Takie fragmenty pamięci zwalniane podczas działania systemu stanowią właśnie zawartość sekcji __init i __initdata. Jeżeli wiadomo już podczas kompilacji, że nawet fragmenty oznaczone jako __exit i __exitdata będą używane tylko przez moduły, to są one po prostu pomijane podczas końcowego przebiegu konsolidatora przy tworzeniu ostatecznej, dającej się uruchomić kopii jądra.

Przykładowy kod modułu

Poniżej podano szkieletową postać sterownika, który po inicjacji wypisuje stosowny komunikat i kończy działanie. Wykorzystano w nim również w odpowiedni sposób makropolecenia __init, __exit, __initdata oraz __exitdata. Sterownik współpracuje z jądrami od serii 2.4 i nowszymi. Przy założeniu, że pliki źródłowe jądra znajdują się na swoim zwykłym miejscu (/usr/src/linux) i że podany niżej kod jest zawarty w pliku o nazwie example.c, można go skompilować w następujący sposób:

 

$ gcc -DMODULE -D__KERNEL__ -I/usr/src/linux/include -c example.c

Wszystkie kody wchodzące w skład jądra kompiluje się przy włączonej definicji __KERNEL__, dzięki czemu pliki dołączane współdzielone przez jądro i bibliotekę C (libc) mogą zawierać części wykorzystywane wyłącznie przez jądro. Ładowalne moduły mają także włączoną definicję MODULES. Więcej szczegółów na temat dołączania sterowników do plików Makefile i konfiguracji systemu można znaleźć pod koniec tego rozdziału.

 

#include <linux/kernel.h>

#include <linux/module.h>

#include <linux/init.h>

static char __initdata hellomessage[] = KERN_NOTICE "Hello, world!\n";

static char __exitdata byemessage[] = KERN_NOTICE "Goodbye, cruel world.\n";

 

static int __init start_hello_world(void)

{

   printk(hellomessage);

   return 0'

}

 

static void __exit go_away(void)

{

   printk(byemessage);

}

 

module_init(start_hello_world);

module_exit(go_away);

Po kompilacji powinien powstać plik example.o, który będzie można załadować do jądra za pomocą polecenia insmod, a następnie usunąć go za pomocą polecenia rmmod:

 

$ /sbin/insmod example.o

$ /sbin/rmmod example

Jeżeli użyje się tych poleceń z wirtualnej konsoli, to będzie można zaobserwować komunikaty wysyłane przy zadziałaniu każdej funkcji inicjującej i zamykającej. Przy korzystaniu ze zdalnego terminala lub z X Window do obejrzenia tych komunikatów należy użyć polecenia dmesg.

Urządzenia i sterowniki magistrali PCI

Po omówieniu sposobu włączania kodu do jądra pokażemy teraz sposób, w jaki jądro Linuksa obsługuje urządzenia na magistrali PCI.

Struktura pci_dev

Struktura pci_dev jest głównym miejscem do przechowywania informacji o fizycznym urządzeniu PCI wykorzystywanym przez system Linux. Jej pełną postać można obejrzeć w pliku /include/linux/pci.h (położenie pliku zależy od konfiguracji) i zawiera ona o wiele więcej elementów, niż będziemy tu omawiać. Istnieje w niej kilka pól, które bezpośrednio dotyczą omawianego zagadnienia. Najpierw zajmiemy się polami pomagającymi rozpoznać dane urządzenie.

Pola liczbowe stanowią odwzorowanie podstawowej części specyfikacji PCI, zaś tabela zawierająca przyporządkowane sobie identyfikatory, nazwy producentów oraz urządzeń znajduje się w pliku linux/drivers/pci/pci.ids (przy jądrach z serii 2.4) albo w pakiecie pciutils:

 

unsigned short vendor            ID producenta PCI

unsigned short device            ID urządzenia PCI

unsigned short subsystem_vendor  ID producenta podsystemu PCI

unsigned short subsystem_device  ID podsystemu urządzenia PCI

unsigned int class               Kombinacja of klasy podstawowej,

                                 podklasy i interfejsu programowego

Następnie umieszczone są pola umożliwiające wyszukanie zasobów pamięci, adresów wejść i wyjść oraz przerwań używanych przez urządzenie PCI. Zasoby te są w zasadzie przydzielane w komputerze PC w konfiguracji BIOS, ale można je inaczej odwzorować w jądrze albo nawet przydzielić je od nowa. Gdy Linux przydziela zasoby, może nie zmieniać wartości w pamięci konfiguracyjnej urządzeń PCI, a więc ważne jest, aby programista nie odczytywał ich z tych urządzeń, lecz korzystał z wartości przechowywanych w strukturze pci_dev powiązanej z danym urządzeniem:

 

unsigned int irq              Linia przerwań (IRQ)

struct resources resource[]   porty I/O i obszary pamięci

Adresy wejść i wyjść (porty I/O) oraz adresy pamięci wykorzystywane przez urządzenie są opisane w strukturze zdefiniowanej w pliku include/linux/ioport.h. Część tej struktury może być na tym etapie istotna dla programisty:

 

unsigned long start, end

unsigned long flags

Pola start i end określają zakres adresów pamięci zajmowanej przez urządzenie, zaś pole flags zawiera znaczniki zdefiniowane także w inlude/linux/ioport.h. W tym przypadku każdy zasób powinien mieć ustawiony albo bit IORESOURCE_IO (dla portów I/O), albo bit IORESOURCE_MEM (dla obszarów pamięci wykorzystywanych do komunikacji z urządzeniem, tzw. MMIO). Zależy to od rodzaju dostępu do urządzenia. W celu zachowania zgodności z przyszłymi modyfikacjami struktury, przy dostępie do tej informacji najlepiej skorzystać z makropoleceń pci_resource_start, pci_resource_end i pci_resource_flags. Polecenia te wymagają podania dwóch argumentów: struktury urządzenia PCI i numeru zasobu w postaci przesunięcia względem początku podanej wyżej tablicy zasobów. Obecnie makropolecenia te są zdefiniowane w pliku include/linux/pci.h w następujący sposób:

 

#define pci_resource_start(dev,bar)   ((dev)->resource[(bar)].start)

#define pci_resource_end(dev,bar)     ((dev)->resource[(bar)].end)

#define pci_resource_flags(dev,bar)   ((dev)->resource[(bar)].flags)

Istnieje także makropolecenie o nazwie pci_resource_len obliczające rozmiary obszaru zajmowanego od adresu początkowego do adresu końcowego.

Na zakończenie tych informacji należy jeszcze wspomnieć o polu identyfikującym sterownik PCI aktualnie kontrolujący urządzenie (jeżeli takie istnieje) oraz o obszarze pamięci zarezerwowanym na prywatne dane wymagane np. do śledzenia stanu urządzenia:

 

struct pci_driver *driver    Struktura sterownika PCI (opisana dalej)

void *driver_data            Prywatne dane dla sterownika PCI

Wyszukiwanie urządzeń PCI

Istnieje kilka sposobów wykrywania urządzeń PCI przez sterownik działający w systemie umożliwiającym sterowanie. Można przeprowadzić ręczne przeszukiwanie dostępnych magistrali w czasie inicjacji, uruchamiając natychmiast wykryte urządzenia. Można także zarejestrować się w podsystemie PCI jądra, podając strukturę zawierającą wywołania zwrotne i zestaw kryteriów dla urządzeń, którymi jesteśmy zainteresowani, a następnie czekać bezczynnie aż do wezwania funkcji wywołań zwrotnych (występującego wówczas, gdy urządzenie spełniające podane kryteria zostanie dołączone do systemu lub z niego usunięte).

Pierwsza metoda, czyli przeszukiwanie ręczne (ang. manual scanning) jest stosowana w jądrach Linuksa z serii 2.2 i wcześniejszych. Nie umożliwia ona obsługi kart PCI wymienianych podczas pracy systemu (np. CompactPCI, CardBus itp.). W jądrach z serii 2.4 można tę metodę zastosować, lecz traktowana jest jako przestarzała w porównaniu do systemu wywołań zwrotnych dla PCI.

Przeszukiwanie ręczne

Pomimo że przeszukiwanie ręczne jest nazywane przestarzałym w jądrach z serii 2.4, warto wyjaśnić w skrócie na czym ono polega, ponieważ nadal jest ono potrzebne w kodzie, który ma działać na jądrach z serii 2.2.

W najprostszej postaci przeszukiwania wykorzystuje się funkcję pci_find_device, która wymaga podania trzech argumentów: identyfikatora producenta, identyfikatora urządzenia i wskaźnika do struktury pci_dev * określającego miejsce na liście urządzeń PCI, od którego należy rozpocząć przeszukiwanie. Taka postać trzeciego argumentu jest potrzebna po to, aby można było znaleźć kilka urządzeń spełniających kryteria, a nie tylko jedno.

Aby rozpocząć przeszukiwanie od początku listy, należy podać NULL jako wartość trzeciego argumentu. Kontynuacja przeszukiwania, począwszy od ostatnio znalezionego urządzenia, odbywa się po podaniu adresu tego właśnie urządzenia. Dozwolone jest tu użycie stałej PCI_ANY_ID jako identyfikatora wieloznacznego, czyli np. szukając dowolnego urządzenia wytwarzanego przez producenta o identyfikatorze PCI_VENDOR_ID_MYVENDOR podanym w pliku include/linux/pci_ids.h, można użyć następującego kodu:

 

struct pci_dev *dev = NULL;

 

while ((dev=pci_find_device(PCI_VENDOR_ID_MYVENDOR,

      PCI_ANY_ID, dev)))

    setup_device(dev);

Istnieje także kilka innych funkcji, dzięki którym sterownik może wyszukiwać urządzenia spełniające inne kryteria, np. pci_find_class, pci_find_subsys lub pci_find_slot. Wszystkie te funkcje są zdefiniowane w pliku include/linux/pci.h:

 

struct pci_dev *pci_find_device (unsigned int vendor, unsigned int device,

                                const struct pci_dev *from);

struct pci_dev *pci_find_subsys (unsigned int vendor, unsigned int device,

                                unsigned int ss_vendor, unsigned int ss_device,

                                const struct pci_dev *from);

struct pci_dev *pci_find_class  (unsigned int class, const struct pci_dev *from);

struct pci_dev *pci_find_slot   (unsigned int bus, unsigned int devfn);

Sterowniki PCI

Niezależnie od tego, że opisana wyżej metoda wyszukiwania urządzeń działa także w jądrach z serii 2.4, to zalecaną dla tych jąder metodą tworzenia sterownika PCI jest rejestracja w podsystemie PCI obecności procedury wykrywającej i kilku danych o wykrywanych urządzeniach. Podsystem PCI jest specyficznym rozwiązaniem zastosowanym w jądrach z serii 2.4 i nie ma go w jądrach z serii 2.2. Wszystkie istotne informacje o sterowniku są przechowywane w strukturze pci_driver, która powinna zostać wypełniona przez funkcję inicjującą, a następnie zarejestrowana za pomocą funkcji register_pci_driver. Ta struktura i funkcje są zdefiniowane w pliku include/linux/pci.h:

 

int pci_register_driver(struct pci_driver *)

void pci_unregister_driver(struct pci_driver *);

Pola w strukturze pci_driver, które muszą być wypełnione danymi, są następujące:

Zgłoś jeśli naruszono regulamin