CATEGORII DOCUMENTE |
Bulgara | Ceha slovaca | Croata | Engleza | Estona | Finlandeza | Franceza |
Germana | Italiana | Letona | Lituaniana | Maghiara | Olandeza | Poloneza |
Sarba | Slovena | Spaniola | Suedeza | Turca | Ucraineana |
DOCUMENTE SIMILARE |
|
W tym rozdziale zapoznamy się programowaniem jądra. Jest to zagadnienie, które z łatwością zapełniłoby samo całą ksiąskę, a więc nie nalesy tego rozdziału uwasać za kompletny podręcznik. Chcemy tu tylko pokazać, w jaki sposób mosna utworzyć sterownik urządzenia (ang. device driver). Większość usytkowników nie musi „brudzić” sobie rąk tymi sprawami, ale jeseli ktoś ma nietypowy sprzęt, który nie jest obsługiwany przez jądro Linuksa, mose 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? Wskasemy takse na kilka bardziej ulotnych aspektów oprogramowania jądra, które są przez usytkowników albo błędnie interpretowane, albo trudne do zrozumienia. W szczególności omówimy rósne funkcje blokujące, stosowane w zabezpieczaniu struktur danych i kodu przy równoczesnym dostępie do nich oraz w sytuacjach, gdy kasdy z tych elementów jest usywany. Pokasemy takse 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ównies zasady obowiązujące przy dostępie do danych zawartych na stronach pamięci, usywanych przez normalne procesy Linuksa (w tzw. przestrzeni adresowej usytkownika) zamiast w sposób bezpieczny na stronach usytkowanych przez jądro (w tzw. przestrzeni adresowej jądra), których nie mosna 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, se jest ono wyposasone w bufor dostępny z magistrali PCI, przez który zachodzi wymiana danych z bibliotekami rezydującymi w przestrzeni usytkownika. Co dziwne, karty tego rodzaju są produkowane przez firmę Applicom International S.A. (https://www.applicom-int.com/) i są usywane jako inteligentne urządzenia komunikujące się z większością spotykanych sieci i magistral przemysłowych.
Kasdy proces dysponuje mapą pamięci wirtualnej, odwzorowującą kasdą stronę z jego wirtualnej przestrzeni adresowej na strony fizyczne utrzymywane albo w RAM, albo w dyskowym buforze wymiany (ang. swap). Kasdy proces ma takse odwzorowane strony jądra, ale mose 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 usytkownika, co oznacza, se 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. Jeseli 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 usytkownika, w imieniu którego działa.
Kasdy kod jądra uruchomiony w taki sposób mose 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 mose takse wywoływać funkcje, które mosna 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 mose 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 mose utrzymywać blokadę, która jest potrzebna przy obsłudze przerwań do poprawnego zakończenia zadania. Kasdy kod, który utrzymuje taką blokadę, powinien równies bez usypiania umosliwiać jej zwolnienie tak szybko, jak to jest mosliwe.
Normalnie do przydzielania pamięci w jądrze Linuksa wykorzystuje się funkcję kmalloc, która oprócz sądanego rozmiaru przydzielanej pamięci mierzonego w bajtach wymaga podania dodatkowego argumentu. Najczęściej ten dodatkowy argument ma wartość GFP_KERNEL, co oznacza, se proces wywołujący syczy 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 mose 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.
Jeseli trzeba przydzielać pamięć w kontekście procesu, dla którego uśpienie nie jest dozwolone, to usywany jest znacznik GFP_ATOMIC. Oznacza to, se funkcja kmalloc będzie zwracać sygnał niepowodzenia, chyba se sądanie przydziału pamięci zostanie bezzwłocznie spełnione.
Prawie w kasdym 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, se 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 mosna było znaleźć długą listę wywołań funkcji inicjujących pracę rósnych podsystemów i sterowników (lista była umieszczona w pliku init/main.c). Po wkompilowaniu jakiegoś sterownika w jądro nalesał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).
Jeseli sterownik był skompilowany jako moduł ładowany do jądra, to nalesał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 mosna usywać tego samego kodu zarówno dla sterowników wkompilowanych do jądra, jak i dla sterowników w postaci modułów. Trzeba tu tylko usywać 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. Kasde z nich wymaga podania nazwy wywoływanej funkcji jako argumentu. Przykład usycia tych makropoleceń pokazujemy w następnym podrozdziale.
Ani procedura inicjująca, ani procedura zamykająca nie wymagają sadnych 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). Jeseli 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 mose 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 tus 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 jus za późno na zabezpieczenie modułu przed usunięciem, nalesy więc tylko oczyścić pamięć najlepiej, jak to jest mosliwe. Istnieją wprawdzie sposoby zabezpieczania pracującego modułu przed usunięciem, lecz nimi zajmiemy się w dalszej części rozdziału.
Jeseli 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, poniewas jądro musi pozostawać nienaruszone nawet wówczas, gdy cała przestrzeń usytkownika została zamknięta, czyli as do usunięcia modułów. Pozostawianie w pamięci całego kodu i danych wymaganych przez procedury inicjujące i zamykające mosna traktować jako dusą rozrzutność. Jądro Linuksa nie mose być przechowywane na dysku, a więc taki nieusywany kod zajmuje cenną pamięć RAM.
Aby temu zapobiec, programista mose podczas budowy jądra zaznaczyć niektóre dane i funkcje, które mosna usunąć, jeśli nie będą jus potrzebne.
Najczęściej do tego celu bywa usywane makropolecenie __init, które słusy do oznaczania funkcji inicjujących. Istnieje takse 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 nis 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 takse za pomocą polecenia dmesg) zauwasy, se natychmiast po zamontowaniu głównego systemu plików pojawia się komunikat podobny do pokazanego nisej:
Freeing unused kernel memory: 108k freed
Oznacza to, se zwolniono 108 kB pamięci jądra zawierającej dane, o których wiadomo, se nie będą jus potrzebne. Takie fragmenty pamięci zwalniane podczas działania systemu stanowią właśnie zawartość sekcji __init i __initdata. Jeseli wiadomo jus podczas kompilacji, se nawet fragmenty oznaczone jako __exit i __exitdata będą usywane 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.
Ponisej podano szkieletową postać sterownika, który po inicjacji wypisuje stosowny komunikat i kończy działanie. Wykorzystano w nim równies w odpowiedni sposób makropolecenia __init __exit __initdata oraz __exitdata. Sterownik współpracuje z jądrami od serii 2.4 i nowszymi. Przy załoseniu, se pliki źródłowe jądra znajdują się na swoim zwykłym miejscu (/usr/src/linux) i se podany nisej kod jest zawarty w pliku o nazwie example.c, mosna 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ą takse włączoną definicję MODULES. Więcej szczegółów na temat dołączania sterowników do plików Makefile i konfiguracji systemu mosna 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)
static void __exit go_away(void)
module_init(start_hello_world);
module_exit(go_away);
Po kompilacji powinien powstać plik example.o, który będzie mosna 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
Jeseli usyje się tych poleceń z wirtualnej konsoli, to będzie mosna zaobserwować komunikaty wysyłane przy zadziałaniu kasdej funkcji inicjującej i zamykającej. Przy korzystaniu ze zdalnego terminala lub z X Window do obejrzenia tych komunikatów nalesy usyć polecenia dmesg
Po omówieniu sposobu włączania kodu do jądra pokasemy teraz sposób, w jaki jądro Linuksa obsługuje urządzenia na magistrali PCI.
Struktura pci_dev jest głównym miejscem do przechowywania informacji o fizycznym urządzeniu PCI wykorzystywanym przez system Linux. Jej pełną postać mosna obejrzeć w pliku /include/linux/pci.h (połosenie pliku zalesy od konfiguracji) i zawiera ona o wiele więcej elementów, nis 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 umosliwiające wyszukanie zasobów pamięci, adresów wejść i wyjść oraz przerwań usywanych przez urządzenie PCI. Zasoby te są w zasadzie przydzielane w komputerze PC w konfiguracji BIOS, ale mosna je inaczej odwzorować w jądrze albo nawet przydzielić je od nowa. Gdy Linux przydziela zasoby, mose nie zmieniać wartości w pamięci konfiguracyjnej urządzeń PCI, a więc wasne 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 mose 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 takse w inlude/linux/ioport.h. W tym przypadku kasdy 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). Zalesy 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 wysej 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 takse makropolecenie o nazwie pci_resource_len obliczające rozmiary obszaru zajmowanego od adresu początkowego do adresu końcowego.
Na zakończenie tych informacji nalesy jeszcze wspomnieć o polu identyfikującym sterownik PCI aktualnie kontrolujący urządzenie (jeseli 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
Istnieje kilka sposobów wykrywania urządzeń PCI przez sterownik działający w systemie umosliwiającym sterowanie. Mosna przeprowadzić ręczne przeszukiwanie dostępnych magistrali w czasie inicjacji, uruchamiając natychmiast wykryte urządzenia. Mosna takse 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 as 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 umosliwia ona obsługi kart PCI wymienianych podczas pracy systemu (np. CompactPCI, CardBus itp.). W jądrach z serii 2.4 mosna tę metodę zastosować, lecz traktowana jest jako przestarzała w porównaniu do systemu wywołań zwrotnych dla PCI.
Pomimo se przeszukiwanie ręczne jest nazywane przestarzałym w jądrach z serii 2.4, warto wyjaśnić w skrócie na czym ono polega, poniewas 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 nalesy rozpocząć przeszukiwanie. Taka postać trzeciego argumentu jest potrzebna po to, aby mosna było znaleźć kilka urządzeń spełniających kryteria, a nie tylko jedno.
Aby rozpocząć przeszukiwanie od początku listy, nalesy 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 usycie 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 mosna usyć 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 takse kilka innych funkcji, dzięki którym sterownik mose 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);
Niezalesnie od tego, se opisana wysej metoda wyszukiwania urządzeń działa takse 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, se 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, se urządzenie zostało obudzone i mose wymagać ponownej inicjacji. |
Struktura pci_device_id zdefiniowana takse 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 wasna. |
unsigned int subvendor, subdevice |
Wymagane numery identyfikacyjne podsystemu lub PCI_ANY_ID |
unsigned int class, class_mask |
Kombinacja po jednym bajcie dla kasdego 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 mose być usyte do przechowywania specyficznych informacji o urządzeniu (informacje te mogą być rósne dla rósnych urządzeń obsługiwanych przez dany sterownik). Przykładowo: sterownik obsługujący rósne wersje kart lub zestawów układów scalonych mose w polu device_data dla kasdej 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 kasdego 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. Jeseli 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 kasdego zarejestrowanego sterownika pasującego do nowego urządzenia as do momentu, gdy któraś z nich zwróci wartość zerową (co oznacza, se dany sterownik zaakceptował urządzenie).
Mamy tu gwarancję, se funkcja sondująca naszego sterownika będzie wywoływana w kontekście procesu (patrz wcześniejszy podrozdział na temat uruchamiania kontekstowego). Oznacza to, se ta funkcja mose być w razie potrzeby uśpiona. Funkcja powinna zwracać wartość zerową, jeseli urządzenie zostanie zaakceptowane i sterownik mose je obsłusyć, 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, se tego rodzaju właściwości nie będą obsługiwane.
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łusy do tego funkcja pci_enable_device, która próbuje przydzielić porty I/O oraz wymagane obszary pamięci, a takse sprawdza, czy urządzenie jest poprawnie zasilane. Nalesy być przygotowanym na obsługę takiej sytuacji, se 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 nalesy 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 mosna 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. Nalesy przy tym pamiętać, se wszystkie procedury pci_read_config_* nie zwracają odczytanej wartości, lecz wskaźnik do miejsca ich przechowywania, oraz se 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);
Zanim będzie mosna 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 usywa się odpowiednio funkcji request_region lub request_memory_region. Kasda z nich wymaga podania adresu początkowego, rozmiaru rezerwowanego obszaru oraz nazwy, która będzie usywana przy wyświetlaniu mapy przydziału zasobów w pliku /proc/ioports lub /proc/iomem (przy załoseniu, se 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, jeseli 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 usywają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, poniewas przydzielony obszar mose 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ównies 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 mosna natychmiast korzystać z portów I/O, lecz dostęp do obszarów pamięci będzie mosliwy dopiero po ich odwzorowaniu w wirtualnej przestrzeni adresowej jądra. Do tego odwzorowania słusy 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 usywanymi przez wcześniej opisywane makropolecenia pci_resource_start i pci_resource_length
Odwzorowanie skonfigurowane za pomocą ioremap mosna 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 nalesący do wirtualnej przestrzeni adresowej procesora. Tego adresu nie mosna usyć bezpośrednio jako wskaźnika, ale tylko poprzez makropolecenia readb readw readl writeb writew i writel. Pomimo tego, se bezpośredni dostęp do odwzorowanego obszaru jest obecnie mosliwy w 32-bitowych maszynach z procesorami firmy Intel, to kod korzystający z tej właściwości nie będzie przenośny, czyli nalesy go traktować jako błędny. Procesory Alpha 21064 nie umosliwiają np. adresowania pojedynczych bajtów i muszą korzystać z rósnych wirtualnych adresów przy dostępie do magistral o rósnej szerokości, pozostawiając całą obsługę tego problemu układom PCI. W takim przypadku usycie wysej wymienionych makropoleceń jest bezwzględną koniecznością.
Oprócz konfiguracji portów I/O oraz adresów pamięci urządzenia trzeba takse zadbać o odpowiednią obsługę przerwań. Zajmuje się tym fragment kodu wywoływany za kasdym 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, se przerwanie mose wystąpić w dowolnym momencie i se program obsługujący je musi działać bardzo szybko, bez usypiania oraz bez prób dostępu obszarów pamięci zarezerwowanych dla usytkownika.
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 mose skorzystać z tej wartości, jeseli został zarejestrowany do obsługi więcej nis jednego poziomu przerwań (czyli gdy np. urządzenie wykorzystuje więcej nis jedną linię przerwań) lub gdy ten sam program został wielokrotnie zarejestrowany do obsługi rósnych 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ć mosliwość odczytu i modyfikacji zawartości rejestrów jeszcze przed zakończeniem obsługi.
Aby zarejestrować program obsługi przerwań, nalesy usyć 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 sądanej linii przerwań (irq), a drugim jest wskaźnik do faktycznego programu obsługi (handler), który ma być wzywany przy kasdym sprzętowym wyzwoleniu przerwania. Argument devname jest usywany 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 kasdym wezwaniu. Argument irqflags mose zawierać dowolne znaczniki zdefiniowane w pliku include/asm/signal.h. Wiele z nich to znaczniki przestarzałe lub nieobsługiwane, zaś najwasniejsze z nich są objaśnione nisej:
SA_SHIRQ |
Akceptacja współdzielonych przerwań. Do momentu ustawienia tego znacznika na wszystkich programach obsługi tylko jeden z nich mose 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 umosliwia 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, se pomimo is 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ł. Nalesy wówczas wstawić tam jakąś wartość specyficzną dla danego sterownika.
Nadszedł teraz czas na sprawdzenie swoich umiejętności, bowiem w tym podrozdziale pokasemy 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 struct
pci_device_id *devid)
Jeseli wydaje się, se 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, se 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))
Jeseli do tego momentu nie został zwrócony saden kod błędu, to wszystko działa poprawnie i mosna zwrócić kod sygnalizujący taki stan. Oznacza to, se kontrolujemy urządzenie i se nie powinno być ono udostępniane innym sterownikom, nawet jeseli będzie mogło być przez nie obsługiwane:
return 0;
Po wykryciu urządzenia i stwierdzeniu, se mosna 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 usytkownika.
Jak jus wspomniano wcześniej, obszar danych obsługiwany przez usytkownika wymaga specjalnego traktowania podczas dostępu z jądra. Jeseli usytkownik zapewnia bufor danych, mogą tu się pojawić trzy problemy:
q Po pierwsze: Usytkownik mose przekazać niepoprawny wskaźnik, który mose zmylić nasz kod, kierując jego dane do obszaru jądra lub pamięci usytkownika, do których dostęp jest zabroniony. Mose to równies powodować, se kod będzie odczytywał dane z obszaru, z którego odczyt nie jest dozwolony.
q Po drugie: Poniewas struktury danych jądra nigdy nie są zapisywane na dysk, a bufor usytkownika mose w rzeczywistości nie być umieszczony w fizycznej pamięci RAM, to kasda próba dostępu do takiego bufora będzie się kończyć błędem stronicowania. Nasz kod musi wówczas czekać na ponowne pobranie strony pamięci z obszaru wymiany.
q Po trzecie: Nalesy być świadomym tego, se rósne procesy nie korzystają z tej samej przestrzeni adresowej. Wskaźniki z przestrzeni usytkownika muszą być obowiązkowo usywane w kontekście tego samego procesu, jak proces przekazujący wskaźnik do bufora.
Fakt, se dostęp do wskaźników w przestrzeni adresowej usytkownika mose powodować błędy stronicowania, oznacza, se nie mosna korzystać z tej przestrzeni w sytuacjach, gdy nasz kod nigdy nie mose 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ównies nie mose skorzystać z obszaru pamięci usytkownika — nie tylko z tego powodu, se mogłoby to prowadzić do jego uśpienia w oczekiwaniu na pobranie strony pamięci z obszaru wymiany, ale przede wszystkim dlatego, se nie ma mosliwości określenia procesu, w kontekście którego program miałby działać.
Aby umosliwić 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 usytkownika. Te makropolecenia sprawdzają odpowiednie uprawnienia do dostępu oraz zachowują się poprawnie takse przy błędzie stronicowania, jeseli pojawi się on z jakichkolwiek powodów.
Są dwie główne przyczyny błędów stronicowania pamięci (ang. page fault). Oczekuje się, se najczęściej występuje sytuacja, gdy strona istnieje, ale nie jest poprawnie odwzorowana w pamięci fizycznej. Mose 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 sądanie (Linux nie ładuje programów bezpośrednio do pamięci podczas ich uruchamiania, lecz czeka przed ich załadowaniem na udostępnienie kasdej strony).
W takich okolicznościach program obsługujący błędy stronicowania jest uśpiony, oczekując na pojawienie się są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 sądanie dostępu nie jest poprawne. Mose 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. Jeseli wystąpi błąd stronicowania takiego rodzaju, makropolecenie zwróci niezerowy wynik sygnalizujący ten fakt.
Jak wspomniano poprzednio, ze względu na mosliwość usypiania programu obsługującego błędy stronicowania i mosliwość wystąpienia przerwania w kontekście dowolnego procesu (nie tylko tego, który utworzył bufor), program obsługujący przerwania nie mose korzystać z przestrzeni usytkownika.
copy_to_user(to, from, n)
copy_from_user(to, from, n)
Powyssze procedury przekazują dane jednokierunkowo między buforem umieszczonym w przestrzeni usytkownika 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 usywa 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ą usywane w jądrze Linuksa i prawdopodobnie nie ma potrzeby szczegółowego ich rozpatrywania. Nalesy tylko pamiętać o wymienionych wysej ograniczeniach ich stosowania i o tym, aby nie usywać ich bez potrzeby.
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 usytkownika. Nalesał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ć duso danych między urządzeniem i procesem działającym w przestrzeni usytkownika — np. w kartach buforujących obraz lub w kartach dźwiękowych).
W czasie opracowywania jąder z serii 2.3 znaleziono metodę umosliwiającą sterownikom blokowanie stron w przestrzeni usytkownika, dzięki czemu mosna z nich korzystać przy bezpośrednim dostępie bez opisywanych wysej ograniczeń. Metoda ta została nazwana kiobuf.
Działa to w taki sposób, se najpierw sprawdza się obecność są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 mose z nich bezpiecznie korzystać as do momentu ich odblokowania.
Aby usyć 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łusy do tego funkcja alloc_kiovec
int alloc_kiovec(int nr, struct kiobuf **bufp);
void free_kiovec(int nr, struct kiobuf **bufp);
Powyssze funkcje przydzielają i zwalniają tablicę struktur kiobuf usywaną przez „prawdziwe” operacje kiobuf. Przyczyną posługiwania się strukturami kiobuf w postaci tablicy, a nie pojedynczo, jest umosliwienie łatwiejszej obsługi operacji rozsyłania (ang. scatter) lub pobierania danych (ang. gather). Kasda struktura kiobuf mose reprezentować tylko jeden ciągły zakres adresów, a więc aby posługiwać się rósnymi 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, se jądro nie musi jus zajmować się przydziałem ciągłych obszarów fizycznej pamięci o dusych rozmiarach i dbać o utrzymanie tych obszarów w stanie bez fragmentacji.
Po przydzieleniu miejsca na tablicę struktur kiobuf kasda z tych struktur musi zostać odpowiednio skonfigurowana. Nalesy podać adres wirtualny i rozmiar obszaru pamięci, który ma ona reprezentować, pozwalając procedurom zarządzania pamięcią na weryfikację istnienia kasdej są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 takse do zapisu. Wartość zerowa tego argumentu oznacza tryb tylko do odczytu, zaś wartość równa 1 oznacza takse mosliwość zapisu. Próba uaktywnienia mosliwości zapisu na stronach, w stosunku do których biesący proces nie ma wystarczających uprawnień, spowoduje, se nie powiedzie się operacja odwzorowania. Inne, mniej oczywiste ograniczenie działania funkcji map_user_kiobuf polega na tym, se 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 mose przekraczać 64 kB. Przy korzystaniu z większych obszarów nalesy 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łusy 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. Jeseli wait ma wartość zerową, to funkcja mose zwrócić kod błędu -EAGAIN informujący, se 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 kasdej 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 usycie 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 powysej ok. 4 GB) i pomimo ich faktycznego zablokowania w tym obszarze nalesy sprawdzać, czy są one odwzorowane w biesącym obszarze wirtualnym. Do tego celu słusy funkcja kmap zwracająca rzeczywisty adres wirtualny, który mosne być usywany podczas dostępu do zablokowanej strony. Po zakończeniu operacji wymagającej dostępu nalesy usunąć odwzorowanie wirtualne za pomocą funkcji kunmap. Ta para funkcji jest zdefiniowana w pliku include/linux/highmem.h, który mose takse dołączać include/asm/highmem.h
unsigned long kmap(struct page *page);
void kunmap(struct page *page);
Poniewas 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), mose być zmuszana do przejścia w stan uśpienia w oczekiwaniu na zwolnienie wirtualnego adresu. Niewasne, se nie są niezrozumiałe przyczyny takiego działania tej funkcji — nalesy tylko zapamiętać, se mose on być uśpiona.
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, se podczas transmisji danych nie mosna bezpośrednio korzystać z dostępu do bufora usytkownika, czyli nie mosna po prostu skopiować pakietu danych z obszaru usytkownika 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 usytkownika 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 usyjemy struktur kiobuf.
Najpierw przydzielamy pojedynczą strukturę, poniewas chcemy zablokować tylko jeden obszar:
struct kiobuf *iobuf;
ret = alloc_kiovec(1, &iobuf);
if (ret)
return ret;
Jeseli przydział pamięci się nie uda, to zwracamy kod błędu; w przeciwnym wypadku konfigurujemy odwzorowanie naszej pojedynczej struktury. Jest to nieco sztuczne, poniewas takie odwzorowania zawsze muszą pokrywać się z granicami stron. Obszar faktycznie odwzorowany w kiobuf rozciąga się więc od początku pierwszej strony as do końca ostatniej strony w buforze usytkownika. (Usyta nisej 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 takse musimy zwolnić poprzednio przydzieloną tablicę kiovec i przekazać odpowiedni kod błędu do programu wywołującego:
if (ret)
Jeseli 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, poniewas 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)
Następnie musimy pobrać rzeczywiste adresy, pod którymi znajdują się odwzorowane i zablokowane strony pamięci. Na szczęście wiemy, se potrzebny będzie dostęp tylko do dwóch (i nie więcej) stron, poniewas nasz pakiet danych nie przekracza rozmiaru strony (w najgorszym przypadku mose 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 jus wspomniano wcześniej, przed zablokowaniem przerwań trzeba usyć funkcji kmap do sprawdzenia, czy kasda strona została odwzorowana w wirtualnym obszarze jądra (ze względu na mosliwość uśpienia):
pageadr[0] = kmap(iobuf->maplist[0]);
if (iobuf->nr_pages > 1)
pageadr[1] = kmap(iobuf->maplist[1]);
Po zablokowaniu buforów mosna zablokować przerwania i skopiować pakiet danych do karty. Funkcja spin_lock_irq blokująca przerwania zostanie omówiona dalej. Mówiąc dokładnie: nalesy sprawdzić, czy urządzenie jest gotowe do przyjęcia danych i jeseli 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, tus 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++)
Po zakończeniu operacji kopiowania nalesy zwolnić blokadę i ponownie uaktywnić przerwania
spin_unlock_irq(&apbs[IndexCard].mutex);
a na zakończenie usunąć odwzorowanie kasdej strony, odblokować i usunąć odwzorowanie oraz zwolnić usywaną 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);
W jądrze Linuksa istnieje kilka podstawowych operacji blokujących, stosowanych w rósnych sytuacjach zgodnie z ich właściwościami i ograniczeniami.
Najprostszy jest tradycyjny semafor (ang. semaphore), który często wykorzystuje się jako wzajemnie wykluczającą blokadę pozwalającą rósnym 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 mose stać się ujemna, więc gdy osiąga zero, to kasda następna operacja down powoduje uśpienie wywołującego ją procesu as do momentu, gdy jakiś inny proces wywoła operacje up. Jeseli 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łusą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 usyta, nalesy 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);
Jeseli 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 mosna usyć makropoleceń DECLARE_MUTEX i DECLARE_MUTEX_LOCKED zamiast deklarowania struktury sempahore. Powysszy 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 mosna usyć funkcji up i down. Trzeba przy tym pamiętać, se 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. Poniewas nie jest dozwolone ustalanie kolejności czasowej operacji w programie obsługi przerwań, oznacza to, se taki program nie mose posługiwać się funkcją down. Mose natomiast korzystać z funkcji up bez ograniczeń, poniewas nigdy nie powoduje ona uśpienia procesu, który ją wywołał.
Jest jeszcze inna funkcja działająca na semaforach, z której mosna skorzystać wówczas, gdy program wywołujący nie mose 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 jus wartość równą zeru).
int down_trylock(struct semaphore *sem);
Do wzajemnego wykluczania procesów w jądrze usywane są takse blokady pętlowe (ang. spinlocks), ale rósnią 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 as do jej ewentualnego uzyskania. Oznacza to, se blokada pętlowa mose być usywana w programie obsługującym przerwania oraz se blokady powinny trwać bardzo krótko. Dodatkowo, proces podtrzymujący blokadę pętlową nigdy nie powinien zaprzestać korzystania z CPU, poniewas gdyby inny fragment kodu próbował uzyskać blokadę, mogłoby to spowodować całkowity paralis i zawieszenie systemu (nowa blokada nie mogłaby się pojawić i nie mosna byłoby przestać korzystać z CPU w celu zwolnienia pierwszej blokady).
Blokady pętlowe są deklarowane za pomocą typu spinlock_t i przed usyciem muszą być zainicjowane za pomocą funkcji spin_lock_init. Do uzyskiwania i zwalniania blokady pętlowej usywane są odpowiednio funkcje spin_lock i spin_unlock. Ich definicje są umieszczone w pliku include/linux/spinlock.h, który dołącza takse plik include/asm/spinlock.h
void spin_lock(spinlock_t *lock);
void spin_unlock(spinlock_t *lock);
Poniewas blokady pętlowe mogą być usywane 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 mose być takse wywoływana z programu obsługi przerwań, nalesy 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ósne procesory nie powodują sadnych skutków ubocznych. Kolejne dwie funkcje zapewniają wymagany w takich przypadkach poziom zabezpieczeń; są to spin_lock_irq oraz spin_unlock_irq słusą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);
Gdy próbowano uruchamiać system Linux na maszynie wieloprocesorowej, korzystając z jądra z rozwojowej serii 1.3, usywano 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 jus korzystał z jądra, musiał czekać na zwolnienie dostępu do jądra. Taka blokada stosowana jest takse i dzisiaj, ale obecnie większa część kodu przestała być chroniona za jej pomocą, co umosliwia 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. Mosna ją takse 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, poniewas 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 mose być uśpiona, np. copy_from_user lub kmalloc, w czasie, gdy blokada BKL jest utrzymywana i zakładanie, se blokada nigdy nie będzie zwolniona. Jest to załosenie fałszywe — jak wynika z powysszego opisu.
Więcej szczegółów na temat blokad dostępnych w jądrze Linuksa, oprócz ksiąski Paula Russella Unreliable Guide To Locking, mosna znaleźć pod adresem: https://www.samba.org/netfilter/unreliable-guides/kernel-locking/lklockingguide.html.
Bywa tak, se sterownik musi czekać na coś, co ma nastąpić. W ogólnym przypadku usycie sygnałów busy lub cykli oczekiwania procesora nie jest dobrym rozwiązaniem, poniewas procesor mose ten czas poświecić na wykonanie innych operacji. Nalesy więc uśpić proces i spowodować, aby się obudził o odpowiedniej porze.
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 mose 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 mose 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);
Po wywołaniu funkcji schedule kod mose 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. Mose 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 mosna nadać kodowi status kodu unieruchomionego i to zabezpieczy go przed ponownym przekazaniem do procesora. Słusy 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 mosliwością automatycznego przejścia ponownie do stanu TASK_RUNNING jeśli pojawi się sygnał. |
Zwykle wymaga się, aby sterownik czekał na jakieś zdarzenie, ale wstawia się tu ograniczenie czasu tego oczekiwania (tzw. timeout). Do ustawiania tego ograniczenia słusy 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 zalesy od systemu, ale istnieje takse 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, se jeden cykl trwa 10 ms. W komputerach z procesorem Alpha wartość HZ wynosi 1024, co daje długość jednego cyklu równą w przybliseniu 1 ms.
Funkcja schedule_timeout zwraca albo zero, jeseli są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);
Dowiedzieliśmy się jus, w jaki sposób mosna uśpić proces, a więc nadeszła kolej na informację o budzeniu. W Linuksie słusy 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, jeseli 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 mosliwych do uruchomienia.
„Czoło” kolejki jest deklarowane jako wielkość typu wait_queue_head_t i musi zostać zainicjowane przed usyciem za pomocą funkcji init_waitqueue_head. Podobnie jak przy semaforach i blokadach pętlowych, istnieje tu mosliwość zastosowania deklaracji statycznej, jednocześnie deklarującej i inicjującej strukturę. Taka deklaracja ma następującą postać:
DECLARE_WAIT_QUEUE_HEAD(name);
Mosna 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. Odwasni czytelnicy mogą się przyjrzeć jej definicji podanej w pliku include/linux/sched.h, ale w zasadzie wystarcza informacja, se jest ona definiowana jako:
void wake_up(wait_queue_head_t *q);
Zanim proces będzie mógł zostać obudzony, nalesy 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 biesą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 takse mosna usyć skróconej deklaracji i inicjacji wait_queue_t
DECLARE_WAITQUEUE(wait, current);
Makrodefinicję current nalesy traktować jako zmienną globalną, która zawsze wskazuje na strukturę danych zadania wykonywanego przez biesący proces. W tym momencie wystarczy tylko wiedzieć, se jest ona typu struct task_struct * Szczęśliwie (lub nie) się składa, se 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);
Po obudzeniu procesu nalesy 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);
Istnieje prosta funkcja, która grupuje kilka wysej opisanych funkcji. Dzięki temu mosna 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ównies odmiana tej funkcji dla usypiania w stanie TASK_INTERRUPTIBLE, a takse dla przypadku, gdy potrzebne jest zastosowanie schedule_timeout zamiast schedule
Autor ma zamiar wyjaśnić sposób jej działania, poniewas jest to dobry przykład tego, se nie nalesy usywać kolejek, jeśli kod ma być bezpieczny. Jest to prosta odpowiedź na pytanie, czy nalesy 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 (mosna go znaleźć w pliku kernel/sched.c, tutaj jest podany z niewielkimi przestawieniami):
void sleep_on(wait_queue_head_t *q)
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);
Rozwasmy teraz, co się stanie, gdy zdarzenie i wywołanie wake_up wystąpią na innym procesorze między sprawdzeniem i wywołaniem sleep_on. Powysszy prościutki kod zostanie po prostu uśpiony, nawet gdyby zdarzenie faktycznie jus nastąpiło. Pozostanie on w stanie uśpienia, chyba se 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 mose występować w podatnym na zaburzenia okresie między sprawdzeniem i wywołaniem sleep_on
Nalesy więc spowodować, aby proces sam przenosił się do kolejki, potem sprawdzać status zdarzenia i w razie potrzeby wywoływać funkcję schedule. Następnie, jeseli 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 ostrzeseniach na temat funkcji sleep_on oraz pokrewnych interruptible_sleep_on sleep_on_timeout i interruptible_sleep_on_timeout nalesałoby zapewne podać przykłady ich bezpiecznego usycia.
Przede wszystkim, jeseli zarówno kod uśpiony, jak i kod obudzony są zabezpieczone za pomocą wielkiej blokady jądra (BKL), to mosna bezpiecznie korzystać ze wspomnianych funkcji. Blokada nie będzie w takim wypadku zwalniana as 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 mose ulec powasnej zmianie w rozwojowej serii 2.5. Nawet w tym wyjątku mosna 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 nalesy dostęp do obszaru pamięci usytkownika oraz niektóre wywołania kmalloc opisane wcześniej w podrozdziale poświęconym BKL.
Sterownika karty firmy Applicom nie mosna zaliczyć do tej grupy, dla której mosna bezpiecznie usyć funkcji sleep_on, a więc musimy w nim sami odpowiednio obsłusyć 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 usytkownika nie wykonujemy dalszych operacji, jak to było robione we wcześniej podanym przykładzie, ale musimy zaczekać as urządzenie będzie gotowe na przyjęcie pakietu danych. Jeseli 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 biesą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);
Usyjemy 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. Nalesy zwrócić uwagę, se w tym przypadku wasne jest usycie określonego rejestru w tym szczególnym urządzeniu. Większość kart peryferyjnych ma podobne rejestry słusące do sprawdzania gotowości na przyjęcie lub pobranie danych:
while (readb(apbs[IndexCard].VirtIO + DATA_FROM_PC_READY) != 0)
Jeśli wyeliminujemy mosliwość wystąpienia sygnału, to uzyskujemy pewność, se urządzenie mose się komunikować ze światem zewnętrznym. Nie wolno jednak natychmiast przesłać do niego danych, poniewas na taką okazję mose 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. Jeseli inny proces dotarł do tego punktu wcześniej, ponowne uzyskanie blokady pętlowej zajmie mu trochę czasu. Do chwili, gdy nie obsłusymy 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);
Powyssze instrukcje kończą pętlę while. W momencie dojścia do tego miejsca w kodzie wiemy, se utrzymujemy blokadę pętlową zabezpieczającą urządzenie przed dostępem i se jest ono gotowe na przyjęcie naszego pakietu danych.
Proces sam usuwa się z kolejki i ustawia stan zadania na wartość TASK_RUNNING, jeseli karta była gotowa w momencie pierwszego sprawdzenia. Nie musimy czekać na przerwanie, zatem mosemy wykonać następujące zadania:
set_current_state(TASK_RUNNING);
remove_wait_queue(&apbs[IndexCard].FlagSleepSend, &wait);
Dopiero teraz mosna skopiować pakiet danych do urządzenia, jak w poprzednim przykładzie; po tej operacji po raz ostatni trzeba zwolnić blokadę pętlową.
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. Kasdy moduł przechowuje liczbę odnoszących się do niego wywołań, a gdy osiągnie ona zero, moduł mose 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 usyty 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 mose 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 (poniewas funkcja powodująca usunięcie modułu takse wymagała blokady BKL). Oczywiście, nie zapominajmy takse, se dopóty moduł nie mose być usunięty, dopóki wykonuje coś, co mose pozostawać w stanie uśpienia.
Najczęściej stosowany był podany nisej kod:
int my_driver_open(struct inode *inode, struct file *filp)
Rozwasmy teraz, co się stanie, jeśli funkcja kmalloc musi zostać uśpiona podczas są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 jus 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ć, se proces usuwania modułu przebiega dwuetapowo i jeseli funkcja module_exit mose być uśpiona, to moduł mose zostać usunięty, jeśli był zaznaczony do usunięcia przed wywołaniem funkcji open. W jądrach z tej serii nie mosna temu zapobiec, co najwysej mosna 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 jus zaznaczony do usunięcia. W odrósnieniu 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łusony. W kodzie modułu jest to zawsze zmienna o stałej wartości i nazwie __this_module
Bezpieczna wersja wysej podanego kodu ma więc następującą postać:
int my_driver_open(struct inode *inode, struct file *filp)
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, poniewas funkcje open i release nie robią niczego więcej. Przy kasdym otwarciu urządzenia zawartość licznika wywołań jest powiększana, natomiast przy kasdym zamknięciu — licznik wywołań zmniejsza swoją zawartość:
static int ac_open(struct inode *inode, struct file *filp)
static int ac_release(struct inode *inode, struct file *filp)
W jądrach z serii 2.4 staje się to jeszcze prostsze, poniewas 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 umosliwić eliminację blokady BKL w celu podwysszenia 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 jus dbać o utrzymanie blokady BKL w czasie ich działania.
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.
Najpierw nalesy ustalić nazwę nowej opcji konfiguracyjnej. Istniejące nazwy mosna przejrzeć, uruchamiając polecenie make config w hierarchii plików źródłowych jądra. Obowiązkowo nalesy nadać nowej opcji nazwę, która wiąse 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ę, nalesy 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 nalesy dopisać do pliku drivers/char/Config.in. Format tego pliku jest zupełnie prosty i łatwo jest w nim uzalesnić wybór nowej opcji od wyboru dokonanego poprzednio.
Najprostszym sposobem zadeklarowania nowej opcji konfiguracyjnej jest deklaracja bool, jak w podanym nisej przykładzie:
bool 'Direct Rendering Manager (XFree86 DRI support)' CONFIG_DRM
Dzięki takiej deklaracji usytkownik konfigurujący jądro mose wybrać „Yes” lub „No”. Taki wpis jest stosowany dla kodu, który nie będzie działał jako moduł ładowany na syczenie (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 usytkownik odpowie „No”, to uzyska mosliwość indywidualnego wyboru sterowników dla rósnych typów kart sieciowych obsługiwanych przez system Linux.
Częściej stosuje się deklarację tristate, która umosliwia 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ć uzalesniony od opcji wybranych poprzednio. Przykładowo: nie mosna usyć sterownika karty Applicom, jeseli 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. Jeseli byłaby mosliwa kompilacja sterowników obsługujących PCI jako modułów ładowanych na sądanie, to poprawnymi odpowiedziami dla CONFIG_APPLICOM byłyby „M” albo „N”, ale nigdy „Y” (kod sterownika karty Applicom zalesy bowiem od kodu PCI i nie mose działać samodzielnie).
Uwzględnianie takich zalesnoś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
Powyssza dyrektywa powoduje, se usytkownik będzie mógł wybrać opcję CONFIG_APPLICOM zalesnie od poprzednio ustawionej opcji CONFIG_PCI. Mogłoby to stanowić precyzyjne ograniczenie mosliwości wkompilowania sterownika karty Applicom w jądro, gdyby nie była włączona obsługa PCI. Nalesy jednak załosyć, se kod PCI jest włączany za pomocą deklaracji typu „Yes No” (poniewas nikt chyba nie podjąłby się przekształcić go na postać modułową) — powysszy przykład jest więc chyba zbyt wymyślny.
Po zadeklarowaniu nowej opcji konfiguracyjnej nalesy zmienić pliki makefile w taki sposób, aby kompilacja nowego sterownika przebiegała zgodnie z syczeniem usytkownika.
Pliki makefile dla jądra Linuksa są obecnie całkowicie zmieniane w celu pozbycia się z nich wywołań rekurencyjnych. Oznacza to, se wprowadza się pojedyncze drzewo zalesności zamiast wielu oddzielnych drzew umieszczonych w rósnych katalogach. Napisano nawet artykuł uzasadniający takie podejście, ale tutaj nie będziemy go cytować, odsyłając zainteresowanego Czytelnika pod adres https://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 nalesy 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ć.
Jeseli 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, nalesy 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. Jeseli jest zdefiniowana zmienna L_TARGET, to będzie tworzone archiwum i wówczas nalesy dopisać nazwę sterownika do zmiennej L_OBJS. Z drugiej strony, jeseli w pliku makefile zdefiniowano zmienną O_TARGET, powstanie pojedynczy plik obiektowy — wtedy nazwę sterownika nalesy dopisać do zmiennej O_OBJS
Jeseli sterownik eksportuje nazwy symboliczne usywane przez inne moduły, czego tutaj raczej nie wymagamy, to zmienia się wszystko. Nazwę sterownika nalesy 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 ponisszym 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. Jeseli tworzony plik obiektowy musi eksportować symboliczne nazwy, to nalesy je dopisać do zmiennej export-list (niezalesnie od tego, czy dotyczy to modułu, czy nie). Trzeba takse 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 jus zmodernizowane. Nalesy oczekiwać, se 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 nalesy się zbytnio obawiać zmian — jeseli dokładnie przyjrzymy się tym plikom i regułom konfiguracji, łatwo da się zauwasyć 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.
Po zakończeniu prac nad nowym sterownikiem nadejdzie prawdopodobnie chwila zastanowienia się, co dalej z nim mosna zrobić. Spotyka się dwa rozwiązania tego dylematu:
q Mosna umieścić swój kod zgodnie z zasadami licencji dotyczącymi jądra systemu Linux (czyli GPL),
q Mosna rozpowszechniać swój sterownik tylko w postaci binarnej.
Wybór zalesy od praw własności intelektualnej, którym podlega kod sterownika. Jeseli informacje usyte do jego napisania wymagają przestrzegania wyłączności, to wybór jest ograniczony przez taką umowę.
Przed podjęciem decyzji nalesy zapoznać się z określeniami stosowanymi w Publicznej Licencji GNU (GPL), na podstawie której jest rozpowszechniane jądro systemu Linux. Gwarantuje ona usytkownikom prawo do otrzymania i modyfikacji kodu źródłowego dowolnego programu, który jest wkompilowany w jądro Linuksa. Jeseli do sterownika nie dołącza się kodu źródłowego, mosna go rozpowszechniać tylko jako moduł ładowany na syczenie, czyli nie wolno wkompilować go w jądro. Jest prawdopodobne, se taki moduł mose nie działać w systemie o takiej samej architekturze i z jądrem o takiej samej wersji, jakich usyto przy jego kompilacji. Wynika to zarówno z mosliwości usycia rósnych 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ósnych wersji sterownika spełniających wymagania rósnych usytkowników.
Oprócz tego, usytkownicy 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 dostrzesonych błędach, pochodzącego od usytkownika posługującego się sterownikiem rozpowszechnianym tylko w postaci binarnej, pierwszą odpowiedzią bywa prawie zawsze rada, aby usytkownik spróbował powtórnie uzyskać błędne działanie bez takiego sterownika. Jeseli 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, se 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 kasde 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 usytkownicy sterownika nie tylko mogą oczekiwać na wsparcie dla swoich systemów, ale takse na poprawę najdrobniejszych błędów w miarę rozwoju Linuksa i zmian dokonywanych w jego wewnętrznych interfejsach programowych.
Aby zgłosić Linusowi nowy sterownik, nalesy najpierw upewnić się, se daje się on poprawnie kompilować i działa poprawnie z najnowszą rozwojową wersją jądra. Jeseli mosna go wkompilowac w jądro lub usyć jako modułu, to obydwie konfiguracje takse muszą zostać sprawdzone.
Programiści zajmujący się od niedawna Linuksem powszechnie popełniają błąd polegający na przekonaniu, se wszyscy syją 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 mosliwość, se ktoś zechce skorzystać ze sterownika właśnie na takiej maszynie. Jeseli sterownik jest przeznaczony dla karty PCI, to mose być usywany na maszynach wieloprocesorowych, a więc nalesy 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 nalesą oprócz IA32 (x86) takie procesory, jak Alpha, PowerPC, IA64, SPARC, UltraSPARC i inne. Jeseli 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 mosna dostarczyć do testowania całą kartę.
Po uzyskaniu pewności, se sterownik działa na wszystkich platformach i we wszystkich konfiguracjach, nalesy wprowadzić poprawkę (patch) dodającą go do nienaruszonej kopii najnowszej rozwojowej wersji jądra. Najprościej mosna 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 usyć polecenia diff
diff -uNr linux-clean linux-patched
W ten właśnie sposób powstanie łatka. Najwasniejszą częścią powysszego polecenia jest opcja -u (lub --unified), która słusy do wyboru formatu zalecanego przez Linusa i innych twórców jądra. Dzięki tej opcji mosna łatwiej dostosować poprawkę do innych łatek i łatwiej zrozumieć, o co w niej chodzi.
Po utworzeniu łatki nalesy jej usyć na czystym jądrze, przebudować je i ponownie przetestować. Zdarza się, se w tym momencie programista stwierdza brak jakiegoś wasnego pliku, który był dołączany z roboczej hierarchii katalogów, albo se dołączono pliki całkowicie bezusyteczne. Cały proces tworzenia łatki trzeba wówczas powtórzyć.
Jeśli łatka działa poprawnie, powinni ją przetestować takse inni usytkownicy. Jeśli nie ma ona wielkich rozmiarów, to mosna 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ą duse, nalesy 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, nalesy przed wysłaniem informacji zapoznać się ze zbiorem najczęściej zadawanych pytań i uzyskanych odpowiedzi (FAQ) znajdującym się pod adresem https://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 mosna 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 nalesy takse podać krótki opis właściwości nowego sterownika. Jeseli 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. Jeseli nie, to trzeba być przygotowany na powtórki związane z poprawkami. Warto takse przy kasdej wysyłce łatki do Linusa skierować kopię wiadomości na listę dyskusyjną.
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, nis mosna zmieścić w tej ksiąsce. To, co pokazaliśmy, stanowi jednak niezbędne podstawy przy budowaniu własnego sterownika urządzenia PCI.
Do momentu wydania tej ksiąski trzeba było wszystkiego szukać w dokumentacji (chocias teraz takse jest to konieczne, jak stwierdził jeden z autorów). Całkiem sporo materiału mosna znaleźć w podkatalogu Documentation umieszczonym w hierarchii plików źródłowych jądra, a takse, co jest oczywiste, w ksiąsce 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.
Politica de confidentialitate | Termeni si conditii de utilizare |
Vizualizari: 780
Importanta:
Termeni si conditii de utilizare | Contact
© SCRIGROUP 2024 . All rights reserved