R-26-07(1), Informacje dot. kompa


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:

char *name

Nazwa sterownika urządzenia.

const struct pci_device_id *id_table

Lista identyfikatorów obsługiwanych urządzeń.

int (*probe) (struct pci_dev *dev, const struct pci_device_id *id)

Funkcja sondująca, wywoływana przez podsystem PCI jądra wtedy, gdy zostało znalezione urządzenie pasujące do jednego z wpisów w id_table.

void (*remove)

Wywoływane przez podsystem PCI jądra wtedy, gdy urządzenie zostanie usunięte, lub po wyrejestrowaniu pci_driver.

void (*suspend) (struct pci_dev *dev)

Wywoływane przez kod obsługujący oszczędzanie energii w celu poinformowania sterownika, że urządzenie zostało zatrzymane.

void (*resume) (struct pci_dev *dev)

Wywoływane przez kod obsługujący oszczędzanie energii w celu powiadomienia sterownika, że urządzenie zostało obudzone i może wymagać ponownej inicjacji.

Struktura pci_device_id zdefiniowana także w pliku include/linux/pci.h zawiera informacje podobne do tych, które były przekazywane funkcjom pci_find_. Są to:

unsigned int vendor, device

Wymagane identyfikatory producenta i urządzenia albo PCI_ANY_ID, gdy identyfikacja nie jest ważna.

unsigned int subvendor, subdevice

Wymagane numery identyfikacyjne podsystemu lub PCI_ANY_ID.

unsigned int class, class_mask

Kombinacja po jednym bajcie dla każdego elementu (klasy, podklasy, interfejsu programowego) z maską bitową. Bit maski o wartości 1 oznacza poszukiwanie odpowiadającego mu bitu w bajcie elementu. Bity w polu class, dla których odpowiadający im bit w polu class_mask nie jest ustawiony, nie muszą być dopasowane.

unsigned long driver_data

Dane prywatne wykorzystywane przez sterownik.

Po tym, jak pasujący identyfikator pci_device_id zostanie przekazany do funkcji sondującej sterownika, pole driver_data może być użyte do przechowywania specyficznych informacji o urządzeniu (informacje te mogą być różne dla różnych urządzeń obsługiwanych przez dany sterownik). Przykładowo: sterownik obsługujący różne wersje kart lub zestawów układów scalonych może w polu device_data dla każdej dopasowanej struktury pci_device_id umieszczać zestaw znaczników charakteryzujących właściwości urządzenia. W takim przypadku funkcja sondująca sterownika nie musi ponownie sprawdzać dokładnych numerów urządzeń.

Pole id_table struktury pci_driver powinno wskazywać na tablicę zawierającą dopasowane struktury pci_device_id, zakończoną wpisem wypełnionym zerowymi wartościami.

Podczas pierwszej rejestracji sterownika jego funkcja sondująca jest wywoływana dla każdego urządzenia PCI w systemie, które pasuje do wpisu na liście pci_device_id i które nie zostało jeszcze przydzielone innemu sterownikowi. Jeżeli później będą dodawane nowe pasujące urządzenia, które mogą być włączane podczas pracy systemu, to podsystem PCI jądra będzie wywoływał funkcje sondujące każdego zarejestrowanego sterownika pasującego do nowego urządzenia aż do momentu, gdy któraś z nich zwróci wartość zerową (co oznacza, że dany sterownik zaakceptował urządzenie).

Mamy tu gwarancję, że funkcja sondująca naszego sterownika będzie wywoływana w kontekście procesu (patrz wcześniejszy podrozdział na temat uruchamiania kontekstowego). Oznacza to, że ta funkcja może być w razie potrzeby uśpiona. Funkcja powinna zwracać wartość zerową, jeżeli urządzenie zostanie zaakceptowane i sterownik może je obsłużyć, w przeciwnym wypadku powinien zostać zwrócony niezerowy kod błędu. Pozwala to podsystemowi PCI przejść do innych sterowników, do których pasuje identyfikator występujący na liście. Kody błędów są zdefiniowane w plikach include/linux/errno.h oraz include/asm/errno.h — i tak jak dla wszystkich funkcji występujących w jądrze Linuksa — normalne jest zwracanie ujemnych wartości sygnalizujących błąd, na przykład:

return -EIO; /* I/O error encountered */

Funkcja usuwająca sterownika będzie wywoływana tylko dla tych urządzeń, które zostały zaakceptowane przez funkcję sondującą. Będzie ona wywoływana automatycznie przez podsystem PCI jądra podczas usuwania urządzeń, które mogą być wymieniane w czasie pracy systemu, albo przy wyrejestrowaniu sterownika za pomocą procedury pci_unregister_driver. W tym wypadku funkcja usuwająca będzie wywoływana tyle razy, ile urządzeń było obsługiwanych przez sterownik.

Czasami w jądrze nie jest uaktywniony system oszczędzania energii i wtedy funkcje suspend i resume nie będą nigdy wywoływane. Odpowiednie pola w strukturze są jednak obecne przez cały czas, ale ich wartości będą równe NULL. Oznacza to, że tego rodzaju właściwości nie będą obsługiwane.

Funkcje dostępu do urządzeń PCI

Przed próbą dostania się do portów I/O lub dzielonej pamięci urządzenia sterownik powinien sprawdzić, czy urządzenie jest aktywne. Służy do tego funkcja pci_enable_device, która próbuje przydzielić porty I/O oraz wymagane obszary pamięci, a także sprawdza, czy urządzenie jest poprawnie zasilane. Należy być przygotowanym na obsługę takiej sytuacji, że wywołanie pci_enable_device nie powiedzie się i trzeba będzie wyświetlić komunikat ostrzegawczy oraz zwrócić niezerowy wynik z procedury inicjującej. Funkcja ta zwraca niezerową wartość sygnalizującą wystąpienie błędu lub zero po pomyślnym zakończeniu:

int pci_enable_device(struct pci_dev *dev);

Oprócz tego, jeśli będą potrzebne funkcje zarządzania magistralą, to należy je oddzielnie uaktywnić za pomocą funkcji pci_set_master. Wywołanie tej funkcji zawsze musi się udać:

void pci_set_master(struct pci_dev *dev);

Po uaktywnieniu urządzenia można korzystać z obszaru pamięci konfiguracyjnej PCI, posługując się funkcją pci_read_config_byte i związanymi z nią procedurami. Dozwolone są tu wszelkie kombinacje odczytu i zapisu bajtów, słów i słów podwójnych. Należy przy tym pamiętać, że wszystkie procedury pci_read_config_* nie zwracają odczytanej wartości, lecz wskaźnik do miejsca ich przechowywania, oraz że mogą zwracać kod błędu (albo zero w wypadku udanej operacji):

int pci_read_config_byte(struct pci_dev *dev, int where, u8 *val);

int pci_read_config_word(struct pci_dev *dev, int where, u16 *val);

int pci_read_config_dword(struct pci_dev *dev, int where, u32 *val);

int pci_write_config_byte(struct pci_dev *dev, int where, u8 *val);

int pci_write_config_word(struct pci_dev *dev, int where, u16 *val);

int pci_write_config_dword(struct pci_dev *dev, int where, u32 *val);

Przydział zasobów

Zanim będzie można skorzystać z portów I/O lub pamięci, muszą one zostać poprawnie przydzielone. W przypadku obszarów pamięci rezerwowanych dla urządzenia PCI ich fizyczne adresy muszą być odwzorowane w wirtualnej przestrzeni adresowej procesora — w taki sam sposób, jak wszystkie inne strony fizyczne pamięci są odwzorowywane w adresach wirtualnych.

Do przydziału portów I/O lub obszaru pamięci używa się odpowiednio funkcji request_region lub request_memory_region. Każda z nich wymaga podania adresu początkowego, rozmiaru rezerwowanego obszaru oraz nazwy, która będzie używana przy wyświetlaniu mapy przydziału zasobów w pliku /proc/ioports lub /proc/iomem (przy założeniu, że w systemie został zamontowany specjalny system plików /proc).

Funkcje te mogą zwracać wartości NULL w wypadku nieudanej próby przydziału zasobów lub wskaźniki do struktury przydzielonego obszaru, jeżeli wszystko przebiegnie pomyślnie. W rzeczywistości w jądrach z serii 2.4 funkcje te są makropoleceniami korzystającymi z tej samej rodzimej funkcji __request_region, ale nie jest to widoczne dla kodu używającego tych makropoleceń. Wywołania te mają następującą postać:

struct resource *request_region(unsigned long start,

unsigned long n, const char * name);

struct resource *request_mem_region(unsigned long start,

unsigned long n, const char * name);

Nie ma potrzeby przechowywania zwróconych adresów nowego zasobu, ponieważ przydzielony obszar może być zwolniony za pomocą funkcji release_region lub release_mem_region. Funkcje te wymagają podania takich samych argumentów, jak odpowiadające im funkcje do przydziału zasobów. Obydwie funkcje są zdefiniowane w pliku include/ioport.h i podobnie jak poprzednie, również są makropoleceniami korzystającymi z pewnych rodzimych procedur obsługi zasobów. Ich wywołania mają następującą postać:

void release_region(unsigned long start, unsigned long n);

void release_mem_region(unsigned long start, unsigned long n);

Po przydzieleniu zasobów można natychmiast korzystać z portów I/O, lecz dostęp do obszarów pamięci będzie możliwy dopiero po ich odwzorowaniu w wirtualnej przestrzeni adresowej jądra. Do tego odwzorowania służy funkcja ioremap, przekazująca fizyczny adres znaleziony w strukturze zasobów danego urządzenia oraz rozmiar obszaru, który ma być odwzorowany. Zazwyczaj wartości tych argumentów pokrywają się z wartościami start i length używanymi przez wcześniej opisywane makropolecenia pci_resource_start i pci_resource_length.

Odwzorowanie skonfigurowane za pomocą ioremap można później usunąć, przekazując zwrócony przez tę funkcję adres do funkcji iounmap wykonującej operację odwrotną:

void *ioremap(unsigned long offset, unsigned long size);

void iounmap(void * addr);

Funkcja ioremap zwraca adres należący do wirtualnej przestrzeni adresowej procesora. Tego adresu nie można użyć bezpośrednio jako wskaźnika, ale tylko poprzez makropolecenia readb, readw, readl, writeb, writew i writel. Pomimo tego, że bezpośredni dostęp do odwzorowanego obszaru jest obecnie możliwy w 32-bitowych maszynach z procesorami firmy Intel, to kod korzystający z tej właściwości nie będzie przenośny, czyli należy go traktować jako błędny. Procesory Alpha 21064 nie umożliwiają np. adresowania pojedynczych bajtów i muszą korzystać z różnych wirtualnych adresów przy dostępie do magistral o różnej szerokości, pozostawiając całą obsługę tego problemu układom PCI. W takim przypadku użycie wyżej wymienionych makropoleceń jest bezwzględną koniecznością.

Obsługa przerwań

Oprócz konfiguracji portów I/O oraz adresów pamięci urządzenia trzeba także zadbać o odpowiednią obsługę przerwań. Zajmuje się tym fragment kodu wywoływany za każdym razem, gdy urządzenie wykryje obecność sygnału na linii IRQ w magistrali PCI.

Na temat programów do obsługi przerwań wspomniano na początku rozdziału. Wiadomo, że przerwanie może wystąpić w dowolnym momencie i że program obsługujący je musi działać bardzo szybko, bez usypiania oraz bez prób dostępu obszarów pamięci zarezerwowanych dla użytkownika.

Funkcja obsługująca przerwanie (ang. interrupt handler) ma następujący prototyp:

void my_irqhandler(int irq, void *dev_id, struct pt_regs *regs);

Argument irq jest numerem linii IRQ, na której wystąpił sygnał przerwania. Program może skorzystać z tej wartości, jeżeli został zarejestrowany do obsługi więcej niż jednego poziomu przerwań (czyli gdy np. urządzenie wykorzystuje więcej niż jedną linię przerwań) lub gdy ten sam program został wielokrotnie zarejestrowany do obsługi różnych urządzeń. Drugim argumentem jest nieprzezroczysty wskaźnik (ang. opaque pointer) *dev_id (jądro nigdy się do niego nie odwołuje) przekazywany przez sterownik podczas rejestracji programu obsługi przerwania. Ostatni argument (*regs) jest wskaźnikiem do obszaru pamięci, w którym podczas obsługi przerwania są przechowywane zawartości rejestrów procesora. Normalnie nie jest potrzebny dostęp do tych wartości, ale gdy np. w procesorach Intel 386 operacja zmiennoprzecinkowa jest sygnalizowana za pomocą przerwania, to program obsługujący takie przerwanie musi mieć możliwość odczytu i modyfikacji zawartości rejestrów jeszcze przed zakończeniem obsługi.

Aby zarejestrować program obsługi przerwań, należy użyć funkcji request_irq zdefiniowanej w pliku include/linux/sched.h:

int request_irq(unsigned int irq,

void (*handler)(int, void *, struct pt_regs *),

unsigned long irqflags, const char *devname,

void *dev_id);

Mamy tu kilka argumentów. Pierwszym z nich jest numer żądanej linii przerwań (irq), a drugim jest wskaźnik do faktycznego programu obsługi (handler), który ma być wzywany przy każdym sprzętowym wyzwoleniu przerwania. Argument devname jest używany przy wpisywaniu przydzielonych przerwań do specjalnego pliku /proc/interrupts, zaś dev_id jest nieprzezroczystym wskaźnikiem (był omawiany wcześniej) przekazywanym do programu obsługi przy każdym wezwaniu. Argument irqflags może zawierać dowolne znaczniki zdefiniowane w pliku include/asm/signal.h. Wiele z nich to znaczniki przestarzałe lub nieobsługiwane, zaś najważniejsze z nich są objaśnione niżej:

SA_SHIRQ

Akceptacja współdzielonych przerwań. Do momentu ustawienia tego znacznika na wszystkich programach obsługi tylko jeden z nich może być zarejestrowany dla danego poziomu IRQ. W poprawnie zaprojektowanych urządzeniach PCI nigdy nie powinno być konieczne rejestrowanie obsługi przerwania bez włączonego znacznika SA_SHIRQ.

SA_INTERRUPT

Znacznik ten umożliwia zablokowanie systemu przerwań procesora po wezwaniu programu obsługi przerwań. Nie powinien on być ustawiany przez sterowniki normalnych urządzeń.

SA_SAMPLE_RANDOM

Włączenie tego znacznika powoduje wykorzystanie kroku czasowego danego przerwania przy generacji danych dla urządzenia /dev/random.

Na zakończenie program obsługi jest wyrejestrowywany za pomocą funkcji free_irq, do której powinny zostać przekazane takie same argumenty, jakie przekazano wcześniej do funkcji request_irq.

void free_irq(unsigned int irq, void *dev_id);

Zwróćmy uwagę na to, że pomimo iż jądro nigdy nie odwołuje się do wskaźnika dev_pci, to korzysta z niego przy określaniu programu obsługi, który ma być zwolniony w przypadku, gdy do obsługi tego samego przerwania zarejestrowano więcej programów. Z tego właśnie powodu sterownik nie powinien nadawać temu wskaźnikowi wartości NULL, nawet gdy nie będzie z niego korzystał. Należy wówczas wstawić tam jakąś wartość specyficzną dla danego sterownika.

Kod sterownika PCI modułu Applicom

Nadszedł teraz czas na sprawdzenie swoich umiejętności, bowiem w tym podrozdziale pokażemy rzeczywisty kod. Opiszemy tu rejestracje struktury pci_driver sterownika zawierającego prosty kod próbkujący kartę przemysłową firmy Applicom (wspomnianą na początku rozdziału).

Rozpoczniemy od deklaracji funkcji:

static int apdrv_probe(struct pci_dev *dev,
const st
ruct pci_device_id *devid)

{

Musimy zadeklarować także dwie zmienne lokalne. Pierwsza z nich będzie przechowywała wirtualny adres, pod którym jest odwzorowany obszar pamięci obsługujący wejścia i wyjścia urządzenia (MMIO). Druga będzie przechowywała numer karty, konfigurowany za pomocą zworek:

void *VirtIO;

int boardno;

Po inicjacji próbujemy najpierw uaktywnić urządzenie. Jeżeli funkcja pci_enable_device zwróci niezerowy wynik świadczący o niepowodzeniu w przydziale zasobów dla urządzenia, to sterownik musi zakończyć działanie, zwracając odpowiedni kod błędu:

if (pci_enable_device(dev))

return -EIO;

Przy założeniu, że udało się uaktywnić urządzenie, próbujemy odwzorować jego przestrzeń adresową w wirtualnej pamięci jądra. Ponownie musimy sprawdzić, czy to się udało i zwrócić odpowiedni kod błędu w wypadku niepowodzenia. Zwróćmy uwagę na to, że ani w tym wypadku, ani w poprzednim nie wyświetlamy komunikatu o błędzie, ponieważ mogłoby to prowadzić do szybkiego wyczerpania zasobów. Gdyby każdy sterownik wyświetlał komunikaty o błędach, do wyczerpania zasobów mogłoby dojść jeszcze szybciej.

VirtIO = ioremap(pci_resource_start(dev, 0), pci_resource_len(dev, 0));

if (!VirtIO)

return -EIO;

Teraz wywołujemy funkcję sondującą urządzenie, która sprawdza, czy zachowuje się ono tak, jak powinna zachowywać się karta firmy Applicom. Zazwyczaj bywa to jakiś test specyficzny dla danego urządzenia. W naszym przypadku dokładna znajomość programu testującego nie ma zbyt wielkiego znaczenia, a więc wstawiliśmy go jako oddzielną funkcję ac_probe_board.

Jeżeli wydaje się, że urządzenie nie działa poprawnie, to nie chcemy brać za to odpowiedzialności i ponownie zwracamy kod błędu, wywołując przedtem funkcję iounmap w celu zwolnienia pamięci odwzorowanej poprzednio przez ioremap.

Ponieważ jest to nieoczekiwany błąd (nie oznacza to jednak, że nie należy go przewidywać), to w tym wypadku wyświetlimy stosowny komunikat. Używana do tego celu funkcja printk jest bardzo podobna do połączenia printf z makropolecenia KERN_INFO. W celu zwiększenia widoczności naszego komunikatu na ekranie dodaje ona do niego fragment tekstu na początku wiersza. Definicja tej funkcji znajduje się w pliku include/linux/kernel.h.

if ((boardno = ac_probe_board(dev->resource[0].start,

(unsigned long)VirtIO)) == -1) {

printk(KERN_INFO "ac.o: PCI Applicom device doesn't have"

" correct signature.\n");

iounmap(VirtIO);

return -EIO;

}

Jeżeli wydaje się, że wszystko działa poprawnie, musimy zarejestrować program obsługujący przerwania dla numeru znalezionego w strukturze pci_dev naszego urządzenia. Zastosujemy tu znacznik SA_SHIRQ dla podkreślenia, że korzystamy z dzielonych przerwań. Jeśli rejestracja się nie uda, to tak jak poprzednio sygnalizujemy błąd, wywołujemy iounmap i przerywamy działanie:

if (request_irq(dev->irq, &ac_interrupt, SA_SHIRQ,

"Applicom PCI", dev)) {

printk(KERN_INFO "Could not allocate IRQ %d for "

"PCI Applicom device.\n", dev->irq);

iounmap(VirtIO);

return -EIO;

}

Jeżeli do tego momentu nie został zwrócony żaden kod błędu, to wszystko działa poprawnie i można zwrócić kod sygnalizujący taki stan. Oznacza to, że kontrolujemy urządzenie i że nie powinno być ono udostępniane innym sterownikom, nawet jeżeli będzie mogło być przez nie obsługiwane:

return 0;

}

Dostęp do pamięci w obszarze użytkownika

Po wykryciu urządzenia i stwierdzeniu, że można się z nim komunikować, trzeba znaleźć sposób wymiany pakietów danych miedzy tym urządzeniem i korzystającymi z niego programami działającymi w przestrzeni adresowej użytkownika.

Jak już wspomniano wcześniej, obszar danych obsługiwany przez użytkownika wymaga specjalnego traktowania podczas dostępu z jądra. Jeżeli użytkownik zapewnia bufor danych, mogą tu się pojawić trzy problemy:

Fakt, że dostęp do wskaźników w przestrzeni adresowej użytkownika może powodować błędy stronicowania, oznacza, że nie można korzystać z tej przestrzeni w sytuacjach, gdy nasz kod nigdy nie może być uśpiony, czyli gdy procesor wykonujący ten kod ma zablokowane przerwania albo gdy np. kod podtrzymuje blokadę pętlową (ten rodzaj blokady i inne omówione są dalej).

Program obsługujący przerwania również nie może skorzystać z obszaru pamięci użytkownika — nie tylko z tego powodu, że mogłoby to prowadzić do jego uśpienia w oczekiwaniu na pobranie strony pamięci z obszaru wymiany, ale przede wszystkim dlatego, że nie ma możliwości określenia procesu, w kontekście którego program miałby działać.

Aby umożliwić ominięcie tych wszystkich pułapek, w Linuksie zdefiniowano makropolecenia copy_to_user i copy_from_user zapewniające dostęp do danych w obszarze użytkownika. Te makropolecenia sprawdzają odpowiednie uprawnienia do dostępu oraz zachowują się poprawnie także przy błędzie stronicowania, jeżeli pojawi się on z jakichkolwiek powodów.

Są dwie główne przyczyny błędów stronicowania pamięci (ang. page fault). Oczekuje się, że najczęściej występuje sytuacja, gdy strona istnieje, ale nie jest poprawnie odwzorowana w pamięci fizycznej. Może się to zdarzyć wówczas, gdy strona danych została przeniesiona do obszaru wymiany w celu zwolnienia miejsca w fizycznej pamięci RAM albo gdy strony rezydują w pliku wykonywalnym w systemie plików i muszą być ładowane na żądanie (Linux nie ładuje programów bezpośrednio do pamięci podczas ich uruchamiania, lecz czeka przed ich załadowaniem na udostępnienie każdej strony).

W takich okolicznościach program obsługujący błędy stronicowania jest uśpiony, oczekując na pojawienie się żądanej strony w pamięci. Dopiero wtedy podejmie on działanie na kopii — tak, jakby nic się nie stało.

Inna klasa błędów stronicowania pojawia się wówczas, gdy żądanie dostępu nie jest poprawne. Może tak się zdarzyć np. z powodu błędnego odwołania do wskaźnika lub dokonania próby zapisu w obszarze dostępnym tylko do odczytu. Jeżeli wystąpi błąd stronicowania takiego rodzaju, makropolecenie zwróci niezerowy wynik sygnalizujący ten fakt.

Jak wspomniano poprzednio, ze względu na możliwość usypiania programu obsługującego błędy stronicowania i możliwość wystąpienia przerwania w kontekście dowolnego procesu (nie tylko tego, który utworzył bufor), program obsługujący przerwania nie może korzystać z przestrzeni użytkownika.

copy_to_user(to, from, n)

copy_from_user(to, from, n)

Powyższe procedury przekazują dane jednokierunkowo między buforem umieszczonym w przestrzeni użytkownika a jądrem, oczekując w razie potrzeby na udostępnienie stron pamięci. Trzeci argument w wywołaniach tych funkcji oznacza liczbę bajtów do skopiowania. Po udanej operacji zwracana jest wartość zerowa, a w przeciwnym wypadku — liczba bajtów, które jeszcze nie zostały skopiowane w momencie pojawienia się błędu. Powszechnie używa się tych wywołań w następujący sposób:

if (copy_to_user(buf, result, sizeof(result)))

return -EFAULT; /* Nieprawidłowy adres */

Procedury są używane w jądrze Linuksa i prawdopodobnie nie ma potrzeby szczegółowego ich rozpatrywania. Należy tylko pamiętać o wymienionych wyżej ograniczeniach ich stosowania i o tym, aby nie używać ich bez potrzeby.

Architektura kiobuf

W jądrach z serii 2.2 programy obsługi przerwań lub sprzęt korzystający z kanałów DMA nie mogły uzyskać bezpośredniego dostępu do buforów umieszczonych w przestrzeni adresowej użytkownika. Należało kopiować dane za pomocą bufora umieszczonego w przestrzeni jądra, co w niektórych przypadkach prowadziło do zmniejszenia wydajności (szczególnie wtedy, gdy sterowniki musiały kopiować dużo danych między urządzeniem i procesem działającym w przestrzeni użytkownika — np. w kartach buforujących obraz lub w kartach dźwiękowych).

W czasie opracowywania jąder z serii 2.3 znaleziono metodę umożliwiającą sterownikom blokowanie stron w przestrzeni użytkownika, dzięki czemu można z nich korzystać przy bezpośrednim dostępie bez opisywanych wyżej ograniczeń. Metoda ta została nazwana kiobuf.

Działa to w taki sposób, że najpierw sprawdza się obecność żądanych stron w pamięci fizycznej i w razie potrzeby pobiera je z obszaru wymiany, a następnie blokuje się je — wtedy nie mogą one być ponownie przeniesione do obszaru wymiany lub przesunięte na inne miejsce. Po wykonaniu takiej operacji dowolny program może z nich bezpiecznie korzystać aż do momentu ich odblokowania.

Aby użyć właściwości kiobuf, trzeba najpierw przydzielić tablicę zawierającą struktury kiobuf, w których system będzie przechowywał dane o odwzorowaniu adresów. Służy do tego funkcja alloc_kiovec:

int alloc_kiovec(int nr, struct kiobuf **bufp);

void free_kiovec(int nr, struct kiobuf **bufp);

Powyższe funkcje przydzielają i zwalniają tablicę struktur kiobuf używaną przez „prawdziwe” operacje kiobuf. Przyczyną posługiwania się strukturami kiobuf w postaci tablicy, a nie pojedynczo, jest umożliwienie łatwiejszej obsługi operacji rozsyłania (ang. scatter) lub pobierania danych (ang. gather). Każda struktura kiobuf może reprezentować tylko jeden ciągły zakres adresów, a więc aby posługiwać się różnymi obszarami adresowymi pamięci w pojedynczej operacji przekazu danych, trzeba te struktury pogrupować w tablicę (kiovec).

Operacja rozsyłania lub gromadzenia danych jest pewną postacią bezpośredniego dostępu do pamięci (DMA), podczas którego urządzenie otrzymuje uporządkowaną listę stron, do których mają być skopiowane dane, a nie pojedynczy adres fizyczny i rozmiar kopiowanego obszaru, jakim posługiwały się starsze urządzenia korzystające z DMA. Oznacza to, że jądro nie musi już zajmować się przydziałem ciągłych obszarów fizycznej pamięci o dużych rozmiarach i dbać o utrzymanie tych obszarów w stanie bez fragmentacji.

Po przydzieleniu miejsca na tablicę struktur kiobuf każda z tych struktur musi zostać odpowiednio skonfigurowana. Należy podać adres wirtualny i rozmiar obszaru pamięci, który ma ona reprezentować, pozwalając procedurom zarządzania pamięcią na weryfikację istnienia każdej żądanej strony oraz sprawdzenie uprawnień do dostępu. Konfiguracja odbywa się za pomocą procedury map_user_kiobuf z odpowiednimi parametrami:

int map_user_kiobuf(int rw, struct kiobuf *iobuf,

unsigned long va, size_t len);

Argument rw wskazuje, czy dana struktura będzie wykorzystywana tylko do odczytu, czy także do zapisu. Wartość zerowa tego argumentu oznacza tryb tylko do odczytu, zaś wartość równa 1 oznacza także możliwość zapisu. Próba uaktywnienia możliwości zapisu na stronach, w stosunku do których bieżący proces nie ma wystarczających uprawnień, spowoduje, że nie powiedzie się operacja odwzorowania. Inne, mniej oczywiste ograniczenie działania funkcji map_user_kiobuf polega na tym, że wartość argumentu va (skrót od virtual address) musi być dopasowana do rozmiaru strony (czyli musi być wielokrotnością rozmiaru strony w danym systemie, zdefiniowanego jako PAGE_SIZE w pliku include/asm/page.h), zaś rozmiar obszaru adresowanego nie może przekraczać 64 kB. Przy korzystaniu z większych obszarów należy posługiwać się wieloma strukturami kiobuf.

Po poprawnej konfiguracji struktur kiobuf wskazujących na wymagany obszar pamięci trzeba jeszcze zablokować cały zakres stron w pamięci fizycznej. Służy do tego funkcja lock_kiovec:

int lock_kiovec(int nr, struct kiobuf *iovec[], int wait);

int unlock_kiovec(int nr, struct kiobuf *iovec[]);

Argument wait podawany dla funkcji lock_kiovec kontroluje jej zachowanie przy braku jakiejś strony, wymagającego pobrania jej z obszaru wymiany. Jeżeli wait ma wartość zerową, to funkcja może zwrócić kod błędu -EAGAIN informujący, że brak wymaganych stron. W przeciwnym wypadku funkcja będzie oczekiwać na udostępnienie wszystkich stron, a następnie ich zablokowanie.

Po odwzorowaniu stron pamięci adres każdej z nich zawarty w strukturze kiobuf staje się dostępny poprzez listę odwzorowanych pól (ang. maplist field), wskazującą na tablicę struktur tych odwzorowanych stron. Kolejną komplikację powoduje użycie w najnowszych procesorach firmy Intel tzw. rozszerzenia adresu fizycznego (Physical Address Extension, w skrócie PAE). W takim wypadku strony fizyczne mogą mieć adresy mieszczące się poza zakresem dostępnym bezpośrednio z jądra (czyli powyżej ok. 4 GB) i pomimo ich faktycznego zablokowania w tym obszarze należy sprawdzać, czy są one odwzorowane w bieżącym obszarze wirtualnym. Do tego celu służy funkcja kmap zwracająca rzeczywisty adres wirtualny, który możne być używany podczas dostępu do zablokowanej strony. Po zakończeniu operacji wymagającej dostępu należy usunąć odwzorowanie wirtualne za pomocą funkcji kunmap. Ta para funkcji jest zdefiniowana w pliku include/linux/highmem.h, który może także dołączać include/asm/highmem.h:

unsigned long kmap(struct page *page);

void kunmap(struct page *page);

Ponieważ funkcja kmap w celu zmniejszenia liczby kosztownego przesyłania stron do obszaru wymiany korzysta z bardzo wymyślnych algorytmów dostępu do obszarów pamięci wirtualnej (wielokrotnie wykorzystuje wstępnie przydzielone zakresy adresów pamięci wirtualnej), może być zmuszana do przejścia w stan uśpienia w oczekiwaniu na zwolnienie wirtualnego adresu. Nieważne, że nie są niezrozumiałe przyczyny takiego działania tej funkcji — należy tylko zapamiętać, że może on być uśpiona.

Kod obsługi kiobuf dla karty firmy Applicom

Powróćmy teraz do sterownika dla karty firmy Applicom. Aby zabezpieczyć się przed skutkami dostępu programu obsługi przerwań do karty podczas transmisji danych, w czasie tych operacji trzeba zablokować przerwania. Oznacza to, że podczas transmisji danych nie można bezpośrednio korzystać z dostępu do bufora użytkownika, czyli nie można po prostu skopiować pakietu danych z obszaru użytkownika do karty i odwrotnie.

Trzeba więc kopiować cały pakiet albo do bufora pośredniego umieszczonego w przestrzeni jądra (zwanego „buforem odrzucającym” ze względu na sposób wykorzystania go przez dane z niego wchodzące i wychodzące) i następnie blokować przerwania podczas transmisji danych z tego bufora do karty, albo wykorzystać strukturę kiobuf do blokowania bufora użytkownika przed rozpoczęciem transmisji. „Bufory odrzucające” (ang. bounce buffers) powodują znaczny spadek wydajności, a więc w podanym tu fragmencie kodu funkcji ac_write użyjemy struktur kiobuf.

Najpierw przydzielamy pojedynczą strukturę, ponieważ chcemy zablokować tylko jeden obszar:

struct kiobuf *iobuf;

ret = alloc_kiovec(1, &iobuf);

if (ret)

return ret;

Jeżeli przydział pamięci się nie uda, to zwracamy kod błędu; w przeciwnym wypadku konfigurujemy odwzorowanie naszej pojedynczej struktury. Jest to nieco sztuczne, ponieważ takie odwzorowania zawsze muszą pokrywać się z granicami stron. Obszar faktycznie odwzorowany w kiobuf rozciąga się więc od początku pierwszej strony aż do końca ostatniej strony w buforze użytkownika. (Użyta niżej struktura mailbox jest przekazywana do urządzenia i pobierana z niego. Wartość sizeof(struct mailbox) określa więc rozmiar bufora kopiowanego podczas transmisji danych).

bufadr=((unsigned long)buf) & PAGE_MASK;

bufofs=((unsigned long)buf) & ~PAGE_MASK;

ret = map_user_kiobuf(READ, iobuf, bufadr,

sizeof(struct mailbox) + bufofs);

Tutaj w wypadku niepowodzenia także musimy zwolnić poprzednio przydzieloną tablicę kiovec i przekazać odpowiedni kod błędu do programu wywołującego:

if (ret) {

free_kiovec(1, &iobuf);

return ret;

}

Jeżeli wszystko przebiegnie pomyślnie, to natychmiast blokujemy bufor. Wcześniejsze wersje poprawek do jądra z serii 2.2 wprowadzających strukturę kiobuf nie wymagały tej czynności, ponieważ odwzorowanie równocześnie blokowało bufor. W ostatecznej wersji kodu kiobuf w jądrach z serii 2.4 blokowanie jest wykonywane oddzielnie.

ret = lock_kiovec(1, &iobuf, 1);

if (ret) {

unmap_kiobuf(iobuf);

free_kiovec(1, &iobuf);

return ret;

}

Następnie musimy pobrać rzeczywiste adresy, pod którymi znajdują się odwzorowane i zablokowane strony pamięci. Na szczęście wiemy, że potrzebny będzie dostęp tylko do dwóch (i nie więcej) stron, ponieważ nasz pakiet danych nie przekracza rozmiaru strony (w najgorszym przypadku może on więc zajmować pamięć pod koniec pierwszej strony i na początku następnej). Pole nr_pages w iobuf zawiera liczbę odwzorowanych stron.

Jak już wspomniano wcześniej, przed zablokowaniem przerwań trzeba użyć funkcji kmap do sprawdzenia, czy każda strona została odwzorowana w wirtualnym obszarze jądra (ze względu na możliwość uśpienia):

pageadr[0] = kmap(iobuf->maplist[0]);

if (iobuf->nr_pages > 1)

pageadr[1] = kmap(iobuf->maplist[1]);

Po zablokowaniu buforów można zablokować przerwania i skopiować pakiet danych do karty. Funkcja spin_lock_irq blokująca przerwania zostanie omówiona dalej. Mówiąc dokładnie: należy sprawdzić, czy urządzenie jest gotowe do przyjęcia danych i jeżeli trzeba, odczekać na osiągnięcie jego gotowości. Kod realizujący tę operację został tu pominięty w celu uproszczenia, ale będzie jeszcze omawiany w przykładzie obsługi kolejki.

spin_lock_irq(&apbs[IndexCard].mutex);

Adres źródłowy jest adresem, pod którym jest odwzorowana pierwsza strona plus przesunięcie na tej stronie, pod którym znajduje się pakiet danych. Wartość tego przesunięcia (offset) jest obliczana wcześniej, tuż przed wywołaniem funkcji map_user_kiobuf.

from = (char *)pageadr[0] + bufofs;

Adres przeznaczenia ma stałe przesunięcie względem adresu, pod którym został odwzorowany obszar pamięci na karcie PCI. Był on określony i zarejestrowany przez omawianą wcześniej funkcję apdrv_probe.

to = (unsigned long) apbs[IndexCard].VirtIO + RAM_FROM_PC;

Po ustawieniu adresów rozpoczynamy kopiowanie:

for (i = 0; i < sizeof(struct mailbox); i++) {

writeb(*(from++), to++);

Nie ma gwarancji, że druga strona jest umieszczona tuż za pierwszą, a więc po obsłużeniu ostatniego bajtu na pierwszej stronie musimy zmienić adres źródłowy tak, aby był on adresem pierwszego bajtu na drugiej stronie.

if (!((unsigned long)from) & PAGE_MASK))

from = (char *) pageadr[1];

}

Po zakończeniu operacji kopiowania należy zwolnić blokadę i ponownie uaktywnić przerwania...

spin_unlock_irq(&apbs[IndexCard].mutex);

... a na zakończenie usunąć odwzorowanie każdej strony, odblokować i usunąć odwzorowanie oraz zwolnić używaną strukturę kiobuf:

kunmap(iobuf->maplist[0]);

if (iobuf->nr_pages > 1)

kunmap(iobuf->maplist[1]);

unlock_kiovec(1, &iobuf);

unmap_kiobuf(iobuf);

free_kiovec(1, &iobuf);

Podstawowe funkcje blokujące

W jądrze Linuksa istnieje kilka podstawowych operacji blokujących, stosowanych w różnych sytuacjach zgodnie z ich właściwościami i ograniczeniami.

Semafory

Najprostszy jest tradycyjny semafor (ang. semaphore), który często wykorzystuje się jako wzajemnie wykluczającą blokadę pozwalającą różnym fragmentom kodu wykluczać się nawzajem. Oznacza to po prostu blokadę równoczesnego dostępu do struktur danych lub procedur.

Tradycyjnie semafor zawiera licznik (ang. counter), którego zawartość jest powiększana za pomocą operacji up i zmniejszana za pomocą operacji down. Zawartość licznika nigdy nie może stać się ujemna, więc gdy osiąga zero, to każda następna operacja down powoduje uśpienie wywołującego ją procesu aż do momentu, gdy jakiś inny proces wywoła operacje up. Jeżeli operacja up nie nastąpi, procesy próbujące wywołać operacje down będą usypiane na zawsze.

Implementacja semafora w Linuksie zazwyczaj polega na wprowadzeniu funkcji obsługujących operacje up i down — o takich właśnie nazwach. Są one zdefiniowane w pliku include/asm/semaphore.h, łącznie ze strukturą danych semaphore służącą do przechowywania licznika i innych wykorzystywanych wewnętrznie informacji o stanie:

void down(struct semaphore *sem);

void up(struct semaphore *sem);

Zanim struktura semaphore zostanie użyta, należy ją zainicjować. Zazwyczaj stosowane są do tego celu funkcje init_MUTEX lub init_MUTEX_LOCKED, które nadają wartości danym wewnętrznym i kasują zawartości liczników, nadając im odpowiednio wartości 0 i 1:

struct semaphore MySem, MySem2;

init_MUTEX(&MySem);

init_MUTEX_LOCKED(&MySem2);

Jeżeli semafor dysponuje statycznie przydzieloną pamięcią (czyli gdy struktura semaphore ma zasięg globalny, a nie lokalny wewnątrz funkcji), to jako alternatywnego sposobu inicjacji można użyć makropoleceń DECLARE_MUTEX i DECLARE_MUTEX_LOCKED zamiast deklarowania struktury sempahore. Powyższy przykład w takim wypadku ma następującą postać:

DECLARE_MUTEX (MySem);

DECLARE_MUTEX_LOCKED(MySem2);

Po poprawnym zainicjowaniu semafora do obsługi blokady można użyć funkcji up i down. Trzeba przy tym pamiętać, że operacja down w czasie oczekiwania na blokadę powoduje uśpienie procesu wywołującego oraz wywołuje funkcję jądra o nazwie schedule, która pozwala temu procesowi na korzystanie z CPU. Ponieważ nie jest dozwolone ustalanie kolejności czasowej operacji w programie obsługi przerwań, oznacza to, że taki program nie może posługiwać się funkcją down. Może natomiast korzystać z funkcji up bez ograniczeń, ponieważ nigdy nie powoduje ona uśpienia procesu, który ją wywołał.

Jest jeszcze inna funkcja działająca na semaforach, z której można skorzystać wówczas, gdy program wywołujący nie może być usypiany. Jest to funkcja down_trylock, która próbuje zmniejszyć wartość licznika w semaforze i zwraca kod błędu (wartość niezerową), jeśli nie uda się tego zrobić natychmiast (czyli gdy licznik ma już wartość równą zeru).

int down_trylock(struct semaphore *sem);

Blokady pętlowe

Do wzajemnego wykluczania procesów w jądrze używane są także blokady pętlowe (ang. spinlocks), ale różnią się one znacznie od semaforów. Podczas oczekiwania na uzyskanie blokady proces nie zaniecha korzystania z CPU, ale będzie (zgodnie z nazwą spinlock) w kółko nękał CPU, sprawdzając stan tej blokady aż do jej ewentualnego uzyskania. Oznacza to, że blokada pętlowa może być używana w programie obsługującym przerwania oraz że blokady powinny trwać bardzo krótko. Dodatkowo, proces podtrzymujący blokadę pętlową nigdy nie powinien zaprzestać korzystania z CPU, ponieważ gdyby inny fragment kodu próbował uzyskać blokadę, mogłoby to spowodować całkowity paraliż i zawieszenie systemu (nowa blokada nie mogłaby się pojawić i nie można byłoby przestać korzystać z CPU w celu zwolnienia pierwszej blokady).

Blokady pętlowe są deklarowane za pomocą typu spinlock_t i przed użyciem muszą być zainicjowane za pomocą funkcji spin_lock_init. Do uzyskiwania i zwalniania blokady pętlowej używane są odpowiednio funkcje spin_lock i spin_unlock. Ich definicje są umieszczone w pliku include/linux/spinlock.h, który dołącza także plik include/asm/spinlock.h.

void spin_lock(spinlock_t *lock);

void spin_unlock(spinlock_t *lock);

Ponieważ blokady pętlowe mogą być używane w programie obsługi przerwań, to powstają dalsze komplikacje. Mogłoby dojść do zawieszenia systemu w sytuacji, gdyby w czasie działania blokady pętlowej wystąpiło przerwanie, a program obsługi przerwania próbowałby uzyskać tę samą blokadę. Dlatego właśnie przy wywoływaniu blokady pętlowej, która może być także wywoływana z programu obsługi przerwań, należy wyłączać przerwania w lokalnym procesorze (czyli w procesorze, który pierwotnie wywołał funkcję spin_lock). Próby jednoczesnego uzyskania tej blokady pętlowej przez różne procesory nie powodują żadnych skutków ubocznych. Kolejne dwie funkcje zapewniają wymagany w takich przypadkach poziom zabezpieczeń; są to spin_lock_irq oraz spin_unlock_irq służące do blokowania i odblokowywania przerwań w lokalnym procesorze podczas uzyskiwania blokady pętlowej.

void spin_lock_irq(spinlock_t *lock);

void spin_unlock_irq(spinlock_t *lock);

Wielka blokada jądra

Gdy próbowano uruchamiać system Linux na maszynie wieloprocesorowej, korzystając z jądra z rozwojowej serii 1.3, używano bardzo prostej i zarazem bardzo nieefektywnej blokady. Była to pojedyncza blokada zwana ”wielką” (Big Kernel Lock, w skrócie BKL). Zabezpieczała ona system przed jednoczesną pracą dwóch procesorów w trybie chronionym. Proces działający na pierwszym procesorze i wywołujący funkcję systemową w czasie, gdy drugi procesor już korzystał z jądra, musiał czekać na zwolnienie dostępu do jądra. Taka blokada stosowana jest także i dzisiaj, ale obecnie większa część kodu przestała być chroniona za jej pomocą, co umożliwia jądru znacznie lepsze wykorzystanie architektury wieloprocesorowej. W kodzie inicjującym jądra z serii 2.4 oraz w kodzie obsługi systemów plików blokada BKL jest nadal utrzymywana przez większą część czasu. Można ją także uzyskać w wielu wywołaniach systemowych. Większość sterowników urządzeń nie korzysta jednak z BKL poza swoimi funkcjami inicjującymi. Takie rozwiązanie poprawia wprawdzie wydajność jąder z serii 2.4 na maszynach wieloprocesorowych, ale jednocześnie zmusza do przestrzegania zasad „wieloprocesorowości” w sterownikach urządzeń i nie dopuszczania do sytuacji „ścigania”.

Blokada jądra ma charakter szczególny, ponieważ jest ona automatycznie zwalniana, gdy proces zaniecha korzystania z CPU, i ponownie aktywowana, gdy proces wznawia działanie. Powszechnie popełnianym błędem jest w tym przypadku wywoływanie funkcji, która może być uśpiona, np. copy_from_user lub kmalloc, w czasie, gdy blokada BKL jest utrzymywana i zakładanie, że blokada nigdy nie będzie zwolniona. Jest to założenie fałszywe — jak wynika z powyższego opisu.

Więcej szczegółów na temat blokad dostępnych w jądrze Linuksa, oprócz książki Paula Russella Unreliable Guide To Locking, można znaleźć pod adresem: http://www.samba.org/netfilter/unreliable-guides/kernel-locking/lklockingguide.html.

Planowanie zadań i kolejki

Bywa tak, że sterownik musi czekać na coś, co ma nastąpić. W ogólnym przypadku użycie sygnałów busy lub cykli oczekiwania procesora nie jest dobrym rozwiązaniem, ponieważ procesor może ten czas poświecić na wykonanie innych operacji. Należy więc uśpić proces i spowodować, aby się obudził o odpowiedniej porze.

schedule()

Funkcja schedule, której prototyp znajduje się w pliku include/linux/sched.h, jest wywoływana wówczas, gdy proces chce przejąć CPU. Kod jądra Linuksa nie może być wywłaszczany, a więc jedyną metodą przeniesienia kodu poza procesor jest po prostu przeniesienie go poza listę zaplanowanych zadań oczekujących w kolejce, przy czym tego przeniesienia musi dokonać sam kod. Nie dotyczy to chwilowych przerwań, czyli np. przerwań generowanych przez sprzęt. Wywołana funkcja schedule zachowuje zawartość rejestrów CPU i wówczas może on bez przeszkód wykonywać kod innych procesów. Jeśli wywołujący proces „powraca” do CPU (zazwyczaj w wyniku „obudzenia” go przez funkcję, której wynik był oczekiwany), to wywołana funkcja schedule przywraca zachowaną zawartość rejestrów i przekazuje sterowanie do funkcji, z której została wywołana. Oprócz pewnego opóźnienia wszystko przebiega więc tak, jakby się nic nie stało.

void schedule(void);

set_current_state()

Po wywołaniu funkcji schedule kod może być ponownie wprowadzony na listę zadań po upływie krótkiego czasu, czyli po tym, jak inne procesy uzyskały dostęp do CPU na odpowiedni okres. Może okazać się przydatne wykonanie tej operacji samemu, np. w sytuacji, gdy procesor zajmuje się intensywnymi obliczeniami i nie chcemy, aby był nękany przez inne procesy.

Znacznie częściej mamy do czynienia z sytuacją, gdy kod oczekuje na zewnętrzne zdarzenie i nie powinien być ponownie wstawiany na listę zadań, jeśli to zdarzenie nie nastąpi. W takim przypadku można nadać kodowi status kodu unieruchomionego i to zabezpieczy go przed ponownym przekazaniem do procesora. Służy do tego makropolecenie set_current_state. Początkowo będą dla nas interesujące jedynie trzy stany. Pełna lista stanów znajduje się w pliku include/linux/sched.h:

TASK_RUNNING

Jest to normalny stan dla procesów, które mogą być uruchamiane.

TASK_UNINTERRUPTIBLE

Stan unieruchomienia. Proces musi być obudzony w specjalny sposób.

TASK_INTERRUPTIBLE

Stan unieruchomienia, lecz z możliwością automatycznego przejścia ponownie do stanu TASK_RUNNING, jeśli pojawi się sygnał.

schedule_timeout()

Zwykle wymaga się, aby sterownik czekał na jakieś zdarzenie, ale wstawia się tu ograniczenie czasu tego oczekiwania (tzw. timeout). Do ustawiania tego ograniczenia służy funkcja schedule_timeout, wymagająca podania jednego argumentu oznaczającego graniczny okres oczekiwania. Argument ten jest podawany w postaci liczby cykli zegarowych. Długość tego cyklu w jednostkach czasu zależy od systemu, ale istnieje także w pliku include/asm/param.h makrodefinicja HZ określająca liczbę cykli na sekundę. Dla 32-bitowych procesorów firmy Intel jest to zazwyczaj 100, co oznacza, że jeden cykl trwa 10 ms. W komputerach z procesorem Alpha wartość HZ wynosi 1024, co daje długość jednego cyklu równą w przybliżeniu 1 ms.

Funkcja schedule_timeout zwraca albo zero, jeżeli żądany czas upłynął, albo liczbę cykli pozostałą w momencie obudzenia procesu za pomocą innych metod (wyjaśnimy to za chwilę).

signed long schedule_timeout(signed long timeout);

wake_up()

Dowiedzieliśmy się już, w jaki sposób można uśpić proces, a więc nadeszła kolej na informację o budzeniu. W Linuksie służy do tego specjalna konstrukcja zwana kolejką (tzw. wait queue). Zanim śpiący proces zostanie oddany do dyspozycji CPU, sam musi wejść do kolejki procesów oczekujących na przebudzenie za pomocą określonego zdarzenia. Następnie, jeżeli to zdarzenie nastąpi, jakikolwiek kod odpowiedzialny za odbiór powiadomień o zdarzeniach (najczęściej jest to program obsługi przerwań) wywołuje funkcję wake_up, która przełącza stan wszystkich oczekujących procesów do wartości TASK_RUNNING i umieszcza je ponownie w terminarzu w kolejce procesów możliwych do uruchomienia.

„Czoło” kolejki jest deklarowane jako wielkość typu wait_queue_head_t i musi zostać zainicjowane przed użyciem za pomocą funkcji init_waitqueue_head. Podobnie jak przy semaforach i blokadach pętlowych, istnieje tu możliwość zastosowania deklaracji statycznej, jednocześnie deklarującej i inicjującej strukturę. Taka deklaracja ma następującą postać:

DECLARE_WAIT_QUEUE_HEAD(name);

Można ją zastosować zamiast:

wait_queue_head_t name;

init_waitqueue_head(&name);

Elementy są zdefiniowane w pliku include/linux/wait.h.

Sama funkcja wake_up jest w rzeczywistości makropoleceniem wywołującym funkcję __wake_up z dodatkowym argumentem. Odważni czytelnicy mogą się przyjrzeć jej definicji podanej w pliku include/linux/sched.h, ale w zasadzie wystarcza informacja, że jest ona definiowana jako:

void wake_up(wait_queue_head_t *q);

add_wait_queue()

Zanim proces będzie mógł zostać obudzony, należy umieścić go w kolejce oczekiwania. W tym celu trzeba zadeklarować strukturę typu wait_queue_t, następnie zainicjować ją, nadając jej wartość taką, jaką ma struktura zadania bieżącego procesu i dołączyć ją do kolejki. Odbywa się to w następujący sposób:

wait_queue_t wait;

init_waitqueue_entry(&wait, current);

add_wait_queue(&name, &wait0;

Tutaj także można użyć skróconej deklaracji i inicjacji wait_queue_t:

DECLARE_WAITQUEUE(wait, current);

Makrodefinicję current należy traktować jako zmienną globalną, która zawsze wskazuje na strukturę danych zadania wykonywanego przez bieżący proces. W tym momencie wystarczy tylko wiedzieć, że jest ona typu struct task_struct *. Szczęśliwie (lub nie) się składa, że pasuje do prototypu funkcji init_waitqueue_entry oraz jest zdefiniowana w pliku include/linux/sched.h.

void init_waitqueue_entry(wait_queue_t *q, struct task_struct *p);

void add_wait_queue(wait_queue_head_t *q, wait_queue_t * wait);

remove_wait_queue()

Po obudzeniu procesu należy usunąć z kolejki wpis, który go dotyczy. Ma to zapobiec sytuacjom, w których ten sam proces byłby ponownie budzony przez inne wystąpienie tego samego zdarzenia. Usunięcie procesu z kolejki odbywa się za pomocą funkcji remove_wait_queue, która wymaga podania dokładnie takich samych argumentów jak funkcja add_wait_queue:

void remove_wait_queue(wait_queue_head_t *q, wait_queue_t * wait);

sleep_on() i „wyścigi”

Istnieje prosta funkcja, która grupuje kilka wyżej opisanych funkcji. Dzięki temu można wstawić proces do kolejki i uśpić go, wywołując za pomocą tylko tego jednego wywołania. Funkcja ma nazwę sleep_on i wymaga podania jednego argumentu: adresu czoła kolejki. Istnieje również odmiana tej funkcji dla usypiania w stanie TASK_INTERRUPTIBLE, a także dla przypadku, gdy potrzebne jest zastosowanie schedule_timeout zamiast schedule.

Autor ma zamiar wyjaśnić sposób jej działania, ponieważ jest to dobry przykład tego, że nie należy używać kolejek, jeśli kod ma być bezpieczny. Jest to prosta odpowiedź na pytanie, czy należy stosować funkcję sleep_on. W rzeczywistości, Linus Thorvalds zgodził się na całkowite jej usunięcie z kodu rozwojowej serii jąder 2.5.

Oto kod funkcji sleep_on (można go znaleźć w pliku kernel/sched.c, tutaj jest podany z niewielkimi przestawieniami):

void sleep_on(wait_queue_head_t *q)

{

unsigned long flags;

wait_queue_t wait;

init_waitqueue_entry(&wait, current);

set_current_state(TASK_UNINTERRUPTIBLE);

add_wait_queue(q, &wait);

schedule();

remove_wait_queue(q, &wait);

}

Wszystko wygląda bardzo ładnie, ale często funkcja ta bywa wywoływana w następujący sposób:

while (!event_happened)

sleep_on(event_wait_queue);

Rozważmy teraz, co się stanie, gdy zdarzenie i wywołanie wake_up wystąpią na innym procesorze między sprawdzeniem i wywołaniem sleep_on. Powyższy prościutki kod zostanie po prostu uśpiony, nawet gdyby zdarzenie faktycznie już nastąpiło. Pozostanie on w stanie uśpienia, chyba że zdarzenie wystąpi powtórnie. W maszynie jednoprocesorowej nie ma to zbyt wielkiego znaczenia, ale jeśli funkcja wake_up jest wywoływana przez program obsługujący przerwania, to takie zjawisko może występować w podatnym na zaburzenia okresie między sprawdzeniem i wywołaniem sleep_on.

Należy więc spowodować, aby proces sam przenosił się do kolejki, potem sprawdzać status zdarzenia i w razie potrzeby wywoływać funkcję schedule. Następnie, jeżeli wystąpi wywołanie wake_up po sprawdzeniu tego stanu, będzie ono przenosić proces ponownie do stanu TASK_RUNNING, zaś wywołanie schedule będzie udostępniać go procesorowi jedynie w krótkim przedziale czasu.

Trzeba zwrócić uwagę na to, aby obowiązkowo ustawiać proces w stan TASK_INTERRUPTIBLE, zanim on sam przeniesie się do kolejki— w przeciwnym wypadku zdarzenie mogłoby wystąpić przed wejściem procesu do kolejki i uśpić go na zawsze.

Po tych ostrzeżeniach na temat funkcji sleep_on oraz pokrewnych interruptible_sleep_on, sleep_on_timeout i interruptible_sleep_on_timeout należałoby zapewne podać przykłady ich bezpiecznego użycia.

Przede wszystkim, jeżeli zarówno kod uśpiony, jak i kod obudzony są zabezpieczone za pomocą wielkiej blokady jądra (BKL), to można bezpiecznie korzystać ze wspomnianych funkcji. Blokada nie będzie w takim wypadku zwalniana aż do chwili, gdy funkcja sleep_on wywoła schedule, dzięki czemu wywołanie wake_up nie pojawi się w czasie usypiania procesu. Dotyczy to znacznej części kodu obsługi systemu plików w jądrach z serii 2.4, ale może ulec poważnej zmianie w rozwojowej serii 2.5. Nawet w tym wyjątku można znaleźć dodatkowe odstępstwo — pomiędzy sprawdzeniem stanu a wywołaniem funkcji sleep_on nie wolno robić niczego, co mogłoby uśpić proces i spowodować dzięki temu chwilowe zdjęcie blokady jądra. Do tych niedozwolonych operacji należy dostęp do obszaru pamięci użytkownika oraz niektóre wywołania kmalloc opisane wcześniej w podrozdziale poświęconym BKL.

Powrót do sterownika karty Applicom

Sterownika karty firmy Applicom nie można zaliczyć do tej grupy, dla której można bezpiecznie użyć funkcji sleep_on, a więc musimy w nim sami odpowiednio obsłużyć oczekiwanie w kolejkach. Powoduje to konieczność dopisania dodatkowego kodu do wcześniej pokazanego przykładu sterownika korzystającego ze struktur kiobuf.

Po zablokowaniu i odwzorowaniu bufora użytkownika nie wykonujemy dalszych operacji, jak to było robione we wcześniej podanym przykładzie, ale musimy zaczekać aż urządzenie będzie gotowe na przyjęcie pakietu danych. Jeżeli kopiowanie danych do bufora urządzenia odbyłoby się na ślepo w momencie, gdy urządzenie nie jest gotowe do odbioru, to stracilibyśmy zarówno dane, jak i czas procesora.

Po uzyskaniu blokady pętlowej i zablokowaniu przerwań przenosimy więc bieżące zadanie do kolejki, w której przebywają zadania chcące wpisać dane do urządzenia:

set_current_state(TASK_INTERRUPTIBLE);

add_wait_queue(&apbs[IndexCard].FlagSleepSend, &wait);

Użyjemy teraz pętli działającej do momentu, gdy urządzenie zasygnalizuje gotowość do przyjęcia danych, czyli po odczycie wartości zerowej z rejestru DATA_FROM_PC_READY. Musimy czekać dopóty, dopóki nie pojawi się tam wartość zerowa. Należy zwrócić uwagę, że w tym przypadku ważne jest użycie określonego rejestru w tym szczególnym urządzeniu. Większość kart peryferyjnych ma podobne rejestry służące do sprawdzania gotowości na przyjęcie lub pobranie danych:

while (readb(apbs[IndexCard].VirtIO + DATA_FROM_PC_READY) != 0) {

Jeżeli urządzenie zgłasza natychmiastową gotowość, to instrukcje wewnątrz pętli są pomijane i operacja jest kontynuowana. Jeśli jednak urządzenie nie będzie gotowe, to trzeba uśpić proces i czekać na generację przerwania. Zwalniamy wówczas blokadę pętlową, odblokowujemy przerwania i wywołujemy funkcję schedule, aby mieć zapas czasu na obudzenie:

spin_unlock_irq(&apbs[IndexCard].mutex;

schedule();

Od tego momentu proces jest uśpiony. Obudzenie nastąpi w chwili, gdy albo pojawi się przerwanie i program obsługi przerwań wywoła funkcję wake_up odwołującą się do kolejki FlagSleepSend, do której śpiący proces sam się przeniósł, albo w czasie oczekiwania nadejdzie sygnał (jeśli był ustawiony stan TASK_INTERRUPTIBLE). Sprawdzimy najpierw ten drugi warunek:

if (signal_pending(current)) {

Jeżeli ten warunek jest prawdziwy, proces zostanie obudzony przez sygnał, czyli musi sam usunąć się z kolejki, zwolnić wszystkie struktury kiobuf i wyjść z odpowiednim kodem błędu:

remove_wait_queue(&apbs[IndexCard].FlagSleepSend, &wait);

kunmap(iobuf->maplist[0]);

if (iobuf->nr_pages > 1)

kunmap(iobuf->maplist[1]);

unlock_kiovec(1, &iobuf);

unmap_kiobuf(iobuf);

free_kiovec(1, &iobuf);

return -EINTR;

}

Jeśli wyeliminujemy możliwość wystąpienia sygnału, to uzyskujemy pewność, że urządzenie może się komunikować ze światem zewnętrznym. Nie wolno jednak natychmiast przesłać do niego danych, ponieważ na taką okazję może czekać więcej procesów, które osiągnęły podobny stan. Ponownie włączamy więc blokadę pętlową i przechodzimy na początek pętli while. Jeżeli inny proces dotarł do tego punktu wcześniej, ponowne uzyskanie blokady pętlowej zajmie mu trochę czasu. Do chwili, gdy nie obsłużymy tego procesu, urządzenie będzie ponownie zajęte, a więc trzeba będzie zwolnić blokadę pętlową i ponownie czekać.

spin_lock_irq(&apbs[IndexCard].mutex);

Powyższe instrukcje kończą pętlę while. W momencie dojścia do tego miejsca w kodzie wiemy, że utrzymujemy blokadę pętlową zabezpieczającą urządzenie przed dostępem i że jest ono gotowe na przyjęcie naszego pakietu danych.

Proces sam usuwa się z kolejki i ustawia stan zadania na wartość TASK_RUNNING, jeżeli karta była gotowa w momencie pierwszego sprawdzenia. Nie musimy czekać na przerwanie, zatem możemy wykonać następujące zadania:

set_current_state(TASK_RUNNING);

remove_wait_queue(&apbs[IndexCard].FlagSleepSend, &wait);

Dopiero teraz można skopiować pakiet danych do urządzenia, jak w poprzednim przykładzie; po tej operacji po raz ostatni trzeba zwolnić blokadę pętlową.

Licznik wywołań w module

Kolejny błąd powszechnie popełniany przez osoby programujące jądro dotyczy obsługi licznika wywołań modułu. Zasada działania modułu z takim licznikiem jest bardzo prosta. Każdy moduł przechowuje liczbę odnoszących się do niego wywołań, a gdy osiągnie ona zero, moduł może być bezpiecznie usunięty. Do obsługi tych liczników są stosowane dwa makropolecenia o nazwach MOD_DEC_USE_COUNT i MOD_INC_USE_COUNT. Sterownik użyty jako moduł powinien dbać o to, aby zliczenie jego wywołań nie stało się równe zeru przez czas, gdy do jego kodu lub zawartych w nim struktur danych może się odwołać jądro.

W jądrach z serii 2.2 do powiększenia zawartości licznika odwołań stosowano powszechnie funkcję open, zaś do jej zmniejszania — funkcję release. Przed wywołaniem którejś z tych funkcji jądro powinno uzyskać blokadę BKL, która zabezpiecza moduł przed usunięciem podczas działania wywołanej funkcji (ponieważ funkcja powodująca usunięcie modułu także wymagała blokady BKL). Oczywiście, nie zapominajmy także, że dopóty moduł nie może być usunięty, dopóki wykonuje coś, co może pozostawać w stanie uśpienia.

Najczęściej stosowany był podany niżej kod:

int my_driver_open(struct inode *inode, struct file *filp)

{

struct my_driver_private priv;

priv =kmalloc(sizeof(*priv), GFP_KERNEL);

if (!priv)

return -EIOMEM;

filp->private_data = priv;

MOD_INC_USE_COUNT;

return 0;

}

Rozważmy teraz, co się stanie, jeśli funkcja kmalloc musi zostać uśpiona podczas żądania przydziału pamięci i jeśli podczas jej uśpienia inny proces próbuje usunąć moduł. Przy powrocie z wywołania funkcja kmalloc nie znajdzie już funkcji, która ją wywołała (bo ta została usunięta) i wówczas nastąpi... BUM!

Poprawnym rozwiązaniem tego problemu w jądrach z serii 2.2 jest celowe powiększenie wartości zliczeń, a następnie ich zmniejszenie, jeśli wydarzy się coś niewłaściwego. Oprócz tego trzeba pamiętać, że proces usuwania modułu przebiega dwuetapowo i jeżeli funkcja module_exit może być uśpiona, to moduł może zostać usunięty, jeśli był zaznaczony do usunięcia przed wywołaniem funkcji open. W jądrach z tej serii nie można temu zapobiec, co najwyżej można tylko powiedzieć: „Nie usypiaj procesów w funkcji oczyszczającej moduł”.

Aby zapobiec takim sytuacjom, w jądrach z serii 2.4 wprowadzono nową funkcję o nazwie try_inc_mod_count. Zwraca ona wartość 1 w wypadku udanego powiększenia zawartości licznika wywołań lub 0, jeśli moduł jest już zaznaczony do usunięcia. W odróżnieniu od makropolecenia MOD_INC_USE_COUNT funkcja try_inc_mod_count wymaga podania wskaźnika do struktury informacyjnej modułu, którego licznik ma być obsłużony. W kodzie modułu jest to zawsze zmienna o stałej wartości i nazwie __this_module.

Bezpieczna wersja wyżej podanego kodu ma więc następującą postać:

int my_driver_open(struct inode *inode, struct file *filp)

{

struct my_driver_private priv;

if (!try_inc_mod_count(&__this_module);

return - EAGAIN;

priv = kmalloc(sizeof(*priv), GFP_KERNEL);

if (!priv) {

MOD_DEC_USE_COUNT;

return -ENOMEM;

}

filp->private_data = priv;

return 0;

}

Obsługa licznika wywołań w jądrach z serii 2.2 dla karty firmy Applicom jest bardzo prosta, ponieważ funkcje open i release nie robią niczego więcej. Przy każdym otwarciu urządzenia zawartość licznika wywołań jest powiększana, natomiast przy każdym zamknięciu — licznik wywołań zmniejsza swoją zawartość:

static int ac_open(struct inode *inode, struct file *filp)

{

if (!try_inc_mod_count(&__this_module))

return -EAGAIN;

return 0;

}

static int ac_release(struct inode *inode, struct file *filp)

{

MOD_DEC_USE_COUNT;

return 0;

}

W jądrach z serii 2.4 staje się to jeszcze prostsze, ponieważ funkcje open i release nie są zupełnie potrzebne. Jądro Linuksa w wersji próbnej 2.4.0-test4 automatycznie powiększa zawartość licznika wywołań przed wywołaniem funkcji open i zmniejsza tę zawartość po wywołaniu funkcji release. Zostało to zmienione po to, aby umożliwić eliminację blokady BKL w celu podwyższenia wydajności Linuksa przy powiększaniu liczby procesorów.

Mówiąc jaśniej: w jądrach z serii 2.4 nie wymaga się, aby prosty sterownik manipulował swoim własnym licznikiem wywołań przy wywołaniach jego funkcji open i release, zaś w kodzie tych funkcji nie trzeba już dbać o utrzymanie blokady BKL w czasie ich działania.

Budowanie sterownika

Nadeszła chwila, gdy po napisaniu kodu trzeba włączyć sterownik do konfiguracji jądra i skompilować go. Polega to na dodaniu odpowiedniej opcji konfiguracyjnej do jądra i dopisaniu do plików makefile instrukcji, które będą odpowiedzialne za kompilację nowego sterownika po uaktywnieniu odpowiedniej opcji.

Opcje konfiguracyjne

Najpierw należy ustalić nazwę nowej opcji konfiguracyjnej. Istniejące nazwy można przejrzeć, uruchamiając polecenie make config w hierarchii plików źródłowych jądra. Obowiązkowo należy nadać nowej opcji nazwę, która wiąże się w jakiś sposób z nowym sterownikiem i nie koliduje z nazwą istniejącą. W przypadku kart firmy Applicom autorzy wybrali CONFIG_APPLICOM — nazwa ta spełnia wszystkie wymagania. Aby udostępnić tę opcję, należy dopisać ją do listy opcji zawartej w pliku Config.in umieszczonym w podkatalogu ze sterownikami. W naszym przypadku sterownik jest urządzeniem znakowym, a więc nazwę opcji należy dopisać do pliku drivers/char/Config.in. Format tego pliku jest zupełnie prosty i łatwo jest w nim uzależnić wybór nowej opcji od wyboru dokonanego poprzednio.

Najprostszym sposobem zadeklarowania nowej opcji konfiguracyjnej jest deklaracja bool, jak w podanym niżej przykładzie:

bool 'Direct Rendering Manager (XFree86 DRI support)' CONFIG_DRM

Dzięki takiej deklaracji użytkownik konfigurujący jądro może wybrać „Yes” lub „No”. Taki wpis jest stosowany dla kodu, który nie będzie działał jako moduł ładowany na życzenie (np. jak moduł oszczędzania energii) i w kilku innych sytuacjach. Przykładowo: opcja CONFIG_NET_ETHERNET nie dotyczy kodu bezpośrednio, lecz jeśli użytkownik odpowie „No”, to uzyska możliwość indywidualnego wyboru sterowników dla różnych typów kart sieciowych obsługiwanych przez system Linux.

Częściej stosuje się deklarację tristate, która umożliwia wybór jednej z trzech odpowiedzi powszechnie spotykanych przy konfiguracji jądra: „Yes”, No” i „Module”. Odpowiedź „Yes” oznacza statyczne wkompilowanie kodu sterownika do jądra, zaś „No” i „Module” mają oczywiste znaczenie.

Często wybór kolejnej opcji musi być uzależniony od opcji wybranych poprzednio. Przykładowo: nie można użyć sterownika karty Applicom, jeżeli nie została włączona obsługa magistrali PCI, a więc odpowiedź „Y” na pierwsze pytanie (przy wcześniejszym udzieleniu odpowiedzi „N” na pytanie drugie) jest całkowicie pozbawiona sensu. Jeżeli byłaby możliwa kompilacja sterowników obsługujących PCI jako modułów ładowanych na żądanie, to poprawnymi odpowiedziami dla CONFIG_APPLICOM byłyby „M” albo „N”, ale nigdy „Y” (kod sterownika karty Applicom zależy bowiem od kodu PCI i nie może działać samodzielnie).

Uwzględnianie takich zależności ułatwia deklaracja dep_tristate i ona właśnie nadaje się do naszego sterownika:

dep_tristate 'Applicom intelligent fieldbus card support' CONFIG_APPLICOM

$CONFIG_PCI

Powyższa dyrektywa powoduje, że użytkownik będzie mógł wybrać opcję CONFIG_APPLICOM zależnie od poprzednio ustawionej opcji CONFIG_PCI. Mogłoby to stanowić precyzyjne ograniczenie możliwości wkompilowania sterownika karty Applicom w jądro, gdyby nie była włączona obsługa PCI. Należy jednak założyć, że kod PCI jest włączany za pomocą deklaracji typu „Yes/No” (ponieważ nikt chyba nie podjąłby się przekształcić go na postać modułową) — powyższy przykład jest więc chyba zbyt wymyślny.

Pliki makefile

Po zadeklarowaniu nowej opcji konfiguracyjnej należy zmienić pliki makefile w taki sposób, aby kompilacja nowego sterownika przebiegała zgodnie z życzeniem użytkownika.

Pliki makefile dla jądra Linuksa są obecnie całkowicie zmieniane w celu pozbycia się z nich wywołań rekurencyjnych. Oznacza to, że wprowadza się pojedyncze drzewo zależności zamiast wielu oddzielnych drzew umieszczonych w różnych katalogach. Napisano nawet artykuł uzasadniający takie podejście, ale tutaj nie będziemy go cytować, odsyłając zainteresowanego Czytelnika pod adres http://www.tip.net.au/~millerp/rmch/recu-make-cons-harm.html.

Obecnie w jądrach z serii 2.4 nadal stosowane jest podejście rekurencyjne i w związku z tym należy zmodyfikować plik makefile w katalogu, w którym umieszczono kod źródłowy sterownika.

Ogólnie mówiąc, modyfikacja ta polega na dopisaniu nazwy pliku obiektowego (applicom.o) do odpowiedniej listy plików obiektowych tworzonych podczas kompilacji. Wszystko zaczyna się więc nieco komplikować.

Jeżeli nowy sterownik ma działać jako moduł, to jego nazwa powinna być dopisana do zmiennej M_OBJS. Jeśli sterownik ma być wkompilowany w jądro, należy sprawdzać, czy z katalogu tworzone jest archiwum lub pojedynczy plik obiektowy, w skład którego wchodzą wszystkie znajdujące się w nim pliki obiektowe. Jeżeli jest zdefiniowana zmienna L_TARGET, to będzie tworzone archiwum i wówczas należy dopisać nazwę sterownika do zmiennej L_OBJS. Z drugiej strony, jeżeli w pliku makefile zdefiniowano zmienną O_TARGET, powstanie pojedynczy plik obiektowy — wtedy nazwę sterownika należy dopisać do zmiennej O_OBJS.

Jeżeli sterownik eksportuje nazwy symboliczne używane przez inne moduły, czego tutaj raczej nie wymagamy, to zmienia się wszystko. Nazwę sterownika należy wówczas dodać odpowiednio do jednej ze zmiennych MX_OBJS, LX_OBJS albo OX_OBJS.

Wszystkie opcje konfiguracyjne są importowane jako zmienne do procesu make, więc modyfikacje w pliku makefile mogą wyglądać jak w poniższym przykładzie:

ifeq ($(CONFIG_APPLICOM),y)

L_OBJS += applicom.o

else

ifeq ($(CONFIG_APPLICOM),m)

M_OBJS += applicom.o

endif

endif

Podejmując próbę uproszczenia tej konfiguracji, zmieniliśmy niektóre pliki makefile (łącznie z plikami w katalogu drivers/char, w którym znajduje się nasz przykładowy sterownik). Zmodernizowane pliki makefile nadal tworzą takie same jak poprzednio pliki wynikowe, lecz cały proces tworzenia list plików obiektowych został ułatwiony. Jeżeli tworzony plik obiektowy musi eksportować symboliczne nazwy, to należy je dopisać do zmiennej export-list (niezależnie od tego, czy dotyczy to modułu, czy nie). Trzeba także dopisać je albo do zmiennej obj-m, albo do obj-y. Po tych modyfikacjach dołączenie sterownika karty Applicom daje się zapisać w jednym wierszu:

obj-$(CONFIG_APPLICOM) += applicom.o

Niektóre pliki makefile zostały już zmodernizowane. Należy oczekiwać, że w fazie tworzenia jądra z rozwojowej serii 2.5 wszystkie stare pliki zostaną zastąpione wersjami zmodernizowanymi. Obecnie trzeba trochę wiedzieć na temat plików makefile i podjąć decyzje na temat sposobu dodania własnego sterownika w odpowiednie miejsce. Nie należy się zbytnio obawiać zmian — jeżeli dokładnie przyjrzymy się tym plikom i regułom konfiguracji, łatwo da się zauważyć miejsce, w którym trzeba dopisać wiersze pasujące do danego sterownika. Jeśli zadaliśmy sobie tyle wysiłku, aby utworzyć sterownik, to członkowie listy dyskusyjnej zajmującej się jądrem Linuksa prawdopodobnie bardzo chętnie pomogą w jego odpowiedniej konfiguracji.

Co zrobić z nowym sterownikiem?

Po zakończeniu prac nad nowym sterownikiem nadejdzie prawdopodobnie chwila zastanowienia się, co dalej z nim można zrobić. Spotyka się dwa rozwiązania tego dylematu:

Wybór zależy od praw własności intelektualnej, którym podlega kod sterownika. Jeżeli informacje użyte do jego napisania wymagają przestrzegania wyłączności, to wybór jest ograniczony przez taką umowę.

Przed podjęciem decyzji należy zapoznać się z określeniami stosowanymi w Publicznej Licencji GNU (GPL), na podstawie której jest rozpowszechniane jądro systemu Linux. Gwarantuje ona użytkownikom prawo do otrzymania i modyfikacji kodu źródłowego dowolnego programu, który jest wkompilowany w jądro Linuksa. Jeżeli do sterownika nie dołącza się kodu źródłowego, można go rozpowszechniać tylko jako moduł ładowany na życzenie, czyli nie wolno wkompilować go w jądro. Jest prawdopodobne, że taki moduł może nie działać w systemie o takiej samej architekturze i z jądrem o takiej samej wersji, jakich użyto przy jego kompilacji. Wynika to zarówno z możliwości użycia różnych opcji konfiguracyjnych, jak i np. stosowania innego kompilatora. Nie decydując się na rozpowszechnianie kodu źródłowego, jesteśmy ograniczani koniecznością tworzenia różnych wersji sterownika spełniających wymagania różnych użytkowników.

Oprócz tego, użytkownicy takiego sterownika nie mogą spodziewać się pomocy ani od zespołu programistów tworzących jądro Linuksa, ani od wielu firm komercyjnych. Po otrzymaniu raportu o dostrzeżonych błędach, pochodzącego od użytkownika posługującego się sterownikiem rozpowszechnianym tylko w postaci binarnej, pierwszą odpowiedzią bywa prawie zawsze rada, aby użytkownik spróbował powtórnie uzyskać błędne działanie bez takiego sterownika. Jeżeli w takim przypadku problem nie wystąpi, to nikt więcej nie będzie się nim zajmował.

Sam Linus Thorvalds wyjaśnił bardzo dobitnie swoje poglądy na ten temat w lutym 1999 r. w wiadomości przesłanej do zespołu programistów zajmujących się jądrem:

Przede wszystkim chciałbym, aby osoby posługujące się modułami dostępnymi tylko w wersji binarnej zrozumiały, że jest to tylko ich WŁASNY problem. Chciałbym, aby zapamiętali to raz na zawsze i chciałbym wykrzyczeć to na cały świat. Chciałbym, aby budzili się zlani zimnym potem ci, którzy korzystają z binarnych modułów.

Dlatego właśnie usilnie zalecamy, aby każde nowe sterowniki korzystały z licencji GPL i były przesyłane do Linusa w celu dołączenia ich do oficjalnego wydania jądra. Wówczas użytkownicy sterownika nie tylko mogą oczekiwać na wsparcie dla swoich systemów, ale także na poprawę najdrobniejszych błędów w miarę rozwoju Linuksa i zmian dokonywanych w jego wewnętrznych interfejsach programowych.

Jak zgłosić nowy sterownik?

Aby zgłosić Linusowi nowy sterownik, należy najpierw upewnić się, że daje się on poprawnie kompilować i działa poprawnie z najnowszą rozwojową wersją jądra. Jeżeli można go wkompilowac w jądro lub użyć jako modułu, to obydwie konfiguracje także muszą zostać sprawdzone.

Programiści zajmujący się od niedawna Linuksem powszechnie popełniają błąd polegający na przekonaniu, że wszyscy żyją tylko w świecie komputerów typu PC. Linux działa na wielu platformach, łącznie z procesorami 64-bitowymi i posługującymi się odmiennym porządkiem bajtów, a więc zawsze istnieje możliwość, że ktoś zechce skorzystać ze sterownika właśnie na takiej maszynie. Jeżeli sterownik jest przeznaczony dla karty PCI, to może być używany na maszynach wieloprocesorowych, a więc należy go na takich maszynach przetestować — szczególnie dotyczy to zmiany porządku bajtów i rozmiaru słowa procesora. Do platform obsługujących karty PCI należą oprócz IA32 (x86) takie procesory, jak Alpha, PowerPC, IA64, SPARC, UltraSPARC i inne. Jeżeli programista nie ma dostępu do takich maszyn, powinien znaleźć osoby chętne do testowania na jakiejś liście dyskusyjnej. Chętni znajdują się zazwyczaj bardzo szybko, szczególnie gdy sterownik jest przeznaczony dla produktu komercyjnego i można dostarczyć do testowania całą kartę.

Po uzyskaniu pewności, że sterownik działa na wszystkich platformach i we wszystkich konfiguracjach, należy wprowadzić poprawkę (patch) dodającą go do nienaruszonej kopii najnowszej rozwojowej wersji jądra. Najprościej można zrobić taką łątkę, kopiując czyste jądro do jednego katalogu, a następnie do drugiego i do jednego z jąder dołączyć swój nowy sterownik. Potem wystarczy użyć polecenia diff:

diff -uNr linux-clean linux-patched

W ten właśnie sposób powstanie łatka. Najważniejszą częścią powyższego polecenia jest opcja -u (lub --unified), która służy do wyboru formatu zalecanego przez Linusa i innych twórców jądra. Dzięki tej opcji można łatwiej dostosować poprawkę do innych łatek i łatwiej zrozumieć, o co w niej chodzi.

Po utworzeniu łatki należy jej użyć na czystym jądrze, przebudować je i ponownie przetestować. Zdarza się, że w tym momencie programista stwierdza brak jakiegoś ważnego pliku, który był dołączany z roboczej hierarchii katalogów, albo że dołączono pliki całkowicie bezużyteczne. Cały proces tworzenia łatki trzeba wówczas powtórzyć.

Jeśli łatka działa poprawnie, powinni ją przetestować także inni użytkownicy. Jeśli nie ma ona wielkich rozmiarów, to można wysłać ją bezpośrednio na listę dyskusyjną dla programistów zajmujących się jądrem na adres linux-kernel@vger.rutgers.edu z prośbą o opinię. Jeśli rozmiary łatki są duże, należy ją udostępnić na serwerze FTP i wysłać na listę podobną informację, podając miejsce jej przechowywania. Aby korzystanie z listy było przyjemniejsze, należy przed wysłaniem informacji zapoznać się ze zbiorem najczęściej zadawanych pytań i uzyskanych odpowiedzi (FAQ) znajdującym się pod adresem http://www.tux/org/lkml/.

Dopiero po uzyskaniu pozytywnych opinii i zapoznaniu się z raportami o wykrytych błędach lub z uwagami krytycznymi nadesłanymi na listę dyskusyjną na temat proponowanego sterownika można pomyśleć o wysłaniu łatki do Linusa. Łatka ta powinna być umieszczona w treści listu, a nie dołączona w formacie MIME. W liście należy także podać krótki opis właściwości nowego sterownika. Jeżeli ktoś chce wysłać wiadomość w formacie HTML, powinien od razu o tym zapomnieć.

Od tego momentu trzeba cierpliwie czekać. Linus jest wyjątkowo zapracowanym człowiekiem i akceptowanie poprawek doprowadził do stanu doskonałości. Jeśli programista zgłaszający sterownik ma wyjątkowe szczęście, to jego łatka zostanie zaakceptowana lub odrzucona za pierwszym podejściem. Jeżeli nie, to trzeba być przygotowany na powtórki związane z poprawkami. Warto także przy każdej wysyłce łatki do Linusa skierować kopię wiadomości na listę dyskusyjną.

Podsumowanie

Omówiliśmy tu kilka zagadnień interesujących osoby zajmujące się programowaniem jądra, lecz nie wyczerpuje to całego tematu. W rzeczywistości do omówienia pozostało znacznie więcej, niż można zmieścić w tej książce. To, co pokazaliśmy, stanowi jednak niezbędne podstawy przy budowaniu własnego sterownika urządzenia PCI.

Do momentu wydania tej książki trzeba było wszystkiego szukać w dokumentacji (chociaż teraz także jest to konieczne, jak stwierdził jeden z autorów). Całkiem sporo materiału można znaleźć w podkatalogu Documentation umieszczonym w hierarchii plików źródłowych jądra, a także, co jest oczywiste, w książce Alessandro Rubiniego pt. Linux Device Drivers (ISBN 1-56592-292-1) — nie porusza ona jednak problemów występujacych w jądrach z serii 2.4.

25 Część I Podstawy obsługi systemu WhizBang (Nagłówek strony)

25 D:\1-dokumenty\Word\Zaawansowane programowanie w systemie Linux\R-26-05.doc



Wyszukiwarka

Podobne podstrony:
R-00-07(1), Informacje dot. kompa
R-25-07(1), Informacje dot. kompa
R04-05(2), Informacje dot. kompa
06(1), Informacje dot. kompa
r18-05(1), Informacje dot. kompa
r12-04(1), Informacje dot. kompa
r03-04(1), Informacje dot. kompa
r08-02(1), Informacje dot. kompa
01(1), Informacje dot. kompa
r04-03(1), Informacje dot. kompa

więcej podobnych podstron