Systemy Operacyjne – semestr drugi Wyk ad pierwszy ł
Transkrypt
Systemy Operacyjne – semestr drugi Wyk ad pierwszy ł
Systemy Operacyjne – semestr drugi Wykład pierwszy „Rodowód” Linuksa Inspiracją dla autora systemu Linux był system Minix napisany przez Andrew Tanenbauma, który bazował z kolei na systemie Unix. Termin Unix w obecnych czasach oznacza całą rodzinę systemów operacyjnych opartych na wspólnej filozofii, której źródłem jest projekt systemu operacyjnego, jaki powstał w roku 1969 w Bell Labs, będących częścią amerykańskiego koncernu AT&T. System ten jest pochodną innego systemu operacyjnego Multics, nad którego powstaniem pracowano w trzech ośrodkach: w MIT, w AT&T i w General Electric. Firma AT&T po pewnym czasie wycofała się z prac, ale osoby zatrudnione przy tym projekcie pozostały jej pracownikami. Do ich grona należeli między innymi Ken Thompson, Dennis Ritchie i Douglas McIlroy. To ta trójka odegrała największą rolę w powstaniu pierwszej wersji systemu Unix. Pracę nad nią rozpoczęli Ken Thompson i Dennis Ritchie, pisząc cały kod w assemblerze komputera PDP-7, który poznali tworząc wcześniej grę „Space Travel” dla tej platformy sprzętowej. Znaczący wkład do projektu systemu wniósł Douglas McIlroy, opracowywując np.: łącza nienazwane, które są jednym z środków zapewniających komunikację między procesami, a także wyznaczając reguły tworzenia kodu, które dziś są częścią inżynierii oprogramowania. W roku 1973 kod źródłowy Uniksa został w większości przepisany w języku C, który opracował Dennis Ritchie1, dzięki czemu system ten stał się łatwy w przenoszeniu na inne platformy sprzętowe2. Firma AT&T podjęła decyzję o rozpoczęciu sprzedaży systemu Unix. Każda jego wersja była rozprowadzana razem z kodem źródłowym, a więc wiele firm i innych instytucji, które nabyło ten system, mogło go modyfikować wprowadzając nowe funkcje. Pod względem wprowadzonych do tego systemu innowacji wyróżniał się Uniwersytet Berkeley. Wszystkie wersje systemu Unix, które powstały w tej placówce były oznaczane nazwą BSD (Berkeley System Distribution). Największy wkład do prac nad Uniksem BSD wniósł Bill Joy. To w Berkeley powstał słynny edytor vi, powłoka csh, a także tu dodano do jądra podsystemy odpowiedzialne za obsługę protokołu TCP/IP i pamięć wirtualną. Z czasem te funkcje zostały włączone do dystrybucji rozpowszechnianej przez AT&T (wersja System III). W chwili obecnej rozwój gałęzi BSD jest kontynuowany przez takie projekty, jak FreeBSD, OpenBSD, NetBSD i DragonFly BSD. Aby uporządkować rozwój Uniksa opracowano kilka standardów, z których najważniejszymi są POSIX oraz SUS. Większość odmian Uniksa spełnia wymagania tych standardów. W chwili obecnej niemalże wszystkie odmiany Uniksa to systemy wielodostępne i wielozadaniowe, obsługujące wszystkie rozwiązania w nowoczesnych platformach sprzętowych. Jest jednakże kilka cech, które odróżniają go od innych systemów operacyjnych. Przede wszystkim jest on systemem o stosunkowo prostej budowie (jego twórcy stosowali zasadę Keep It Small and Simple – KISS3). Jądro Uniksa implementuje niewielką liczbę wywołań systemowych, podczas gdy w innych systemach ta liczba dochodzi do dziesiątek tysięcy. Wywołania systemowe w Uniksie zostały opracowane w myśl zasady sformułowanej przez Douglasa McIlroy'a: „Rób jedną rzecz, ale rób ją dobrze”, co oznacza, że wykonują jedną czynność, ale w sposób uniwersalny. Większość elementów w systemie Unix jest traktowana jako plik, co znacznie ułatwia zarządzanie urządzeniami i danymi. W końcu Unix ma mechanizmy, które pozwalają na tworzenie w krótkim czasie nowych procesów, oraz dostarcza środków prostej komunikacji między nimi4. Unix był także jednym z pierwszych systemów operacyjnych, które zostały napisane w języku wysokiego poziomu, co uczyniło go systemem przenośnym. Charakterystyka Linuksa Linux5 jest systemem uniksopodobnym (ang. Unix-like), ale nie jest Uniksem. Pracę nad jądrem tego systemu rozpoczął Linus Benedict Torvalds w roku 1991, wzorując się na wspomnianym systemie Minix. Nie oznacza to jednak, że kod przez niego napisany zawiera fragmenty kodu Miniksa lub oryginalnego Uniksa. Linux powstał „od zera”, ale jest zgodny ze standardami POSIX i SUS. Pierwsza wersja kodu źródłowego Linuksa miała ponad 10 tysięcy wierszy kodu. Bieżące wersje liczą około 14 milionów linii kodu i są tworzone przez obszerną grupę programistów, współpracujących ze sobą przez Internet. Prace tej grupy koordynuje oczywiście Linus Torvalds. Kod źródłowy Linuksa dostępny jest na zasadach licencji GNU GPL v2.0, która gwarantuje, że jest on wolnodostępny. Należy zauważyć, że słowo Linux ma dwa znaczenia. W powyższym tekście oznaczało ono po prostu jądro systemu operacyjnego. W drugim znaczeniu, oznacza podstawową wersję systemu operacyjnego, która oprócz jądra zawiera również zestaw narzędzi do kompilacji programów w językach C i assembler, bibliotekę języka C i powłokę systemową. Wszystkie wymienione tu elementy, oprócz jądra, zostały stworzone przez fundację GNU, założoną przez Richarda Stallmana. Istnieje wiele odmian systemu Linux, nazywanych dystrybucjami, które oprócz wymienionych tu podstawowych narzędzi zawierają również inne oprogramowanie np. system X Window, będący implementacją środowiska graficznego użytkownika, oraz 1 2 3 4 5 Pozostałą część pozostawiono w assemblerze. Dokładniej – łatwiej było go przenieść na inne platformy, niż system napisany w całości w assemblerze. Właściwie ten skrót rozwijano jako "Keep It Simple, Stupid!". Osobiście wolę wersję łagodniejszą :-) Ostatnio pojawiają się głosy, że te mechanizmy komunikacji stają się powoli przestarzałe. W kwestii językowej: piszemy Linux, ale odmieniamy: Linuksa, Linuksowi, itd. 1 Systemy Operacyjne – semestr drugi szereg innych aplikacji, niezbędnych użytkownikom. W dalszej części wykładu słowo „Linux” będzie oznaczało wyłącznie jądro systemu operacyjnego. Pierwotnie Linux przeznaczony był tylko na platformę sprzętową i386 (powstał na komputerze wyposażonym w procesor Intel 80386). Obecnie obsługuje obsługuje całą gamę mniej lub bardziej popularnych platform sprzętowych, takich jak : AMD64, PowerPC, Ultra Sparc, MIPS. Wspiera również architektury równoległe (SMP, NUMA). Zasada działania jądra Jądro (ang. kernel) systemu (nazywane czasami rdzeniem (ang. core)) jest częścią systemu operacyjnego, która stale rezyduje w pamięci komputera, nadzoruje pracę sprzętu i aplikacji użytkownika, oraz dostarcza tym ostatnim określonych usługi. Najczęściej jądro (monolityczne) zawiera takie elementy, jak: procedury obsługi przerwań, mechanizm szeregowania procesów, mechanizm zarządzania pamięcią i obsługi plików. Jądro pracuje w trybie uprzywilejowanym, co oznacza, że ma nieograniczony dostęp do wszystkich zasobów systemu komputerowego, w tym do pamięci operacyjnej. Obszar pamięci (w sensie zakresu adresów) zajmowany przez jądro nazywany jest przestrzenią adresową jądra. Dosyć często o czynnościach wykonywanych przez jądro mówimy, że zachodzą w przestrzeni jądra. Procesy użytkownika pracują w trybie procesora, w którym dostęp do zasobów systemu jest ograniczony. Obszary pamięci (również w sensie zakresu adresów) przyporządkowane aplikacjom użytkownika nazywamy przestrzenią adresową użytkownika. Analogicznie jak ma to miejsce w przypadku jądra, o czynnościach, które są wykonywane przez aplikacje mówimy, że są wykonywane w przestrzeni użytkownika. Do komunikacji między procesami użytkownika, a jądrem służą wywołania systemowe. Zazwyczaj aplikacje użytkownika nie wywołują ich bezpośrednio, lecz korzystają z funkcji dostępnych w standardowej bibliotece języka C (libc). Niektóre z tych funkcji zawierają sam kod wywołania funkcji systemowej, czyli są swego rodzaju „opakowaniami” na wywołania systemowe, inne funkcje wykonują przed zainicjalizowaniem wywołania systemowego dodatkowe czynności, jeszcze inne mogą korzystać z większej liczby wywołań systemowych. Są również funkcje, które nie korzystają w ogóle z wywołań systemowych. Jeśli jądro wykonuje za pośrednictwem wywołania systemowego jakąś usługę dla aplikacji użytkownika, to działa w kontekście procesu, co oznacza, że kod jądra realizujący to wywołanie ma dostęp do informacji o procesie, który korzysta z tego wywołania. W takiej sytuacji można również powiedzieć, że aplikacja wykonuje wywołanie systemowe w przestrzeni jądra, choć nie jest to określenie poprawne. Jądro systemu jest oprogramowaniem sterowanym zdarzeniami. Zdarzenia pochodzące od sprzętu są sygnalizowane za pomocą przerwań. Te przerwania pojawiają się zazwyczaj asynchronicznie6. Z każdym z nich jest związany pewien numer, na podstawie którego jądro odnajduje i wykonuje odpowiednią procedurę obsługi tego przerwania. Te procedury są wykonywane w kontekście przerwania, co oznacza, że nie są związane z żadnym konkretnym procesem. Pozwala to przyspieszyć ich działanie. Jądro ma również możliwość blokowania wszystkich przerwań lub tylko wybranych przerwań, na czas obsługi jednego z nich. Reasumując, procesor w systemie komputerowym, który kontroluje Linux, albo wykonuje kod aplikacji, albo kod jądra w kontekście procesu, albo kod jądra w kontekście przerwania. Porównanie z innymi systemami uniksowymi Jądro sytemu Linux jest podobne do rdzeni innych systemów wzorowanych na Uniksie, ale istnieje również kilka znaczących różnic. Podobnie jak w większości Uniksów, jądro Linuksa jest monolityczne, ale udostępnia możliwość korzystania z ładowanych dynamicznie modułów, które zazwyczaj są sterownikami urządzeń. Linux obsługuje również symetryczne przetwarzanie wieloprocesorowe (SMP) oraz architekturę NUMA, co nie jest cechą wszystkich Uniksów. Podobnie jak Solaris i IRIX, Linux może obsługiwać od wersji 2.6 wywłaszczanie zadań jądra. Jądro Linuksa nie rozróżnia wątków i procesów, traktuje je niemalże w ten sam sposób. Niektóre mechanizmy, które przez twórców Uniksa zostały uznane za mało wydajne lub wręcz za zbędne (jak np. obsługa strumieni) nie zostały w ogóle zaimplementowane w jądrze Linuksa. Ze względów wydajnościowych strony pamięci zajmowanej przez jądra Linuksa nie podlegają wymianie. Ta ostatnia cecha jest konsekwencją faktu, że Linux jest tworzony przez ogromną społeczność programistów – w wyniku dyskusji i rozważań członkowie tej społeczność doszli do wniosku, że taki mechanizm spowodowałby pogorszenie wydajności systemu. Kolejne wersje jądra są oznaczane symbolami składającymi się z trzech7 liczb rozdzielonych kropkami. Pierwsza liczba to główny numer wersji, druga to numer podwersji, trzecia oznacza kolejne wydanie, w którym dokonano znaczących zmian lub rozbudowano funkcjonalność jądra. Przed wydaniem jądra 2.6.0 nieparzysty numer podwersji oznaczał wersję rozwojową, która mogła być niestabilna, a numer parzysty podwersji, wersję stabilną. Od wersji 3.0 jądra numer podwersji oznacza kolejne wydanie, a trzecia liczba używana jest do oznaczania wersji, w których usunięto wykryte usterki. Wersje rozwojowe oznaczane są za pomocą liter rc, za którymi występuje numer wersji rozwojowej. Niektóre dystrybucje Linuksa mogą dodawać do tych oznaczeń dodatkowe liczby. 6 7 Istnieją również przerwania synchroniczne. Pojawiła się również czwarta liczba (jądro 2.6.8.1). Jej wprowadzenie związane było z naprawieniem poważnego błędu, który wykryto tuż po wypuszczeniu wersji 2.6.8. Tę numerację stosowano czasem w późniejszych wersjach. 2 Systemy Operacyjne – semestr drugi Krótka charakterystyka metodologii programowania jądra Ponieważ jądro systemu z definicji ma być optymalne pod względem wykorzystania pamięci i szybkości działania, to przeglądając kod źródłowy Linuksa możemy spotkać wiele fragmentów, które łamią niektóre przyjęte kanony dobrze napisanego oprogramowania (jak choćby nieużywanie instrukcji goto). Tak, jak w przypadku innych systemów uniksopodobnych, większość kodu Linuksa jest napisana w języku C, a tylko niewielka część, zależna od konkretnej architektury sprzętowej w assemblerze8. Pisząc własny fragment jądra, czy to w postaci modułu, czy ingerując w istniejący kod należy mieć świadomość pewnych ograniczeń. Przede wszystkim, pisząc kod jądra nie mamy dostępu do funkcji standardowej biblioteki C (libc, a konkretniej glibc). Jest to naturalną konsekwencją tego, że to jądro stanowi wsparcie dla tej biblioteki, a nie odwrotnie. Na szczęście zostały zaimplementowane w jądrze funkcje, będące substytutami tych z biblioteki glibc. Zamiast funkcji printf jest funkcja printk, zamiast funkcji do manipulowania łańcuchami znaków, które są dostępne po włączeniu pliku string.h są podobne funkcje dostępne po włączeniu pliku linux/string.h. Uogólniając – jądro może korzystać wyłącznie ze swoich plików nagłówkowych, niedozwolone jest korzystanie z zewnętrznych plików nagłówkowych. Kod jądra jest pisany z wykorzystaniem standardu ISO C99 języka C, oraz z wykorzystaniem rozszerzeń GNU C. Oznacza to, że może być kompilowany prawie wyłącznie przez kompilator C pochodzący z GCC (Gnu Compilers Collection). Do wspomnianych rozszerzeń należy wykorzystywanie funkcji rozwijanych w miejscu wywołania (ang. inline functions), obecnie wprowadzone już do standardu języka C. Takie funkcje są bardziej bezpieczne i eleganckie niż makra, a przy tym równie skuteczne. Funkcje rozwijane w miejscu wywołania są definiowane z użyciem słów kluczowych static i inline. Należy jednak pamiętać, aby nie nadużywać takich funkcji, bo prowadzi to do niepotrzebnego zwiększenia objętości kodu wynikowego. Innym z wykorzystywanych rozszerzeń jest możliwość wprowadzania specjalnych wstawek assemblerowych do kodu napisanego w języku C. Te wstawki są stosowane wszędzie tam, gdzie wymagana jest optymalizacja szybkości działania jądra. Kompilator gcc udostępnia dwie dyrektywy wspomagające optymalizację kodu. Są to likely() i unlikely(). Służą one do oznaczania prawdopodobieństwa wykonania kodu w instrukcjach warunkowych, np.: jako unlikely() możemy zaznaczyć warunki związane z obsługą większości wyjątków. Pisząc kod działający w przestrzeni jądra musimy pamiętać, że nie mamy „siatki zabezpieczającej” w postaci ochrony pamięci. Jeśli będziemy źle zarządzać pamięcią jądra, to możemy naruszyć stabilność systemu operacyjnego. Jeśli chcemy użyć liczb i arytmetyki zmiennoprzecinkowej, to musimy sami oprogramować FPU, jednakże nigdy dotąd potrzeba używania takiej arytmetyki w przestrzeni jądra nie zaistniała. Rozmiar stosu jądra9 jest ograniczony i wynosi w przypadku architektur 32 bitowych 8 KiB10, a w przypadku 64 bitowych procesorów Alpha 16 KiB. Procesy użytkownika dysponują stosem znajdującym się w przestrzeni użytkownika, który nie jest ograniczony11. Programując w jądrze musimy pamiętać o synchronizacji dostępu do zasobów współdzielonych. Mamy do dyspozycji np. takie środki synchronizacji jak semafory i rygle pętlowe (ang. spinlock), czyli semafory z aktywnym oczekiwaniem. Ostatnią ważną rzeczą, o której należy pamiętać pisząc lub zmieniając kod jądra, to przenośność. Nawet pisząc kod w języku C możemy doświadczyć różnych problemów, jak choćby porządek bitów i bajtów w słowie, czy rozmiary poszczególnych typów danych. Bardzo krótkie wprowadzenie do tworzenia modułów jądra 2.6 i nowszych 12 Moduły jądra są formą bibliotek współdzielonych, jednakże nie są one dynamicznie łączone z procesami użytkownika lecz z samym jądrem. Dzięki modułom możemy łatwo dodawać lub usuwać nowe funkcje do jądra, bez konieczności restartowania systemu. Pisząc własny moduł należy być świadomym tego, że błąd w module może spowodować poważne konsekwencje dla pracy systemu, a w niektórych przypadkach może również doprowadzić do uszkodzenia sprzętu. Sposób pisania modułów dla jądra Linuksa 2.6 i nowszych różni się od tego, który obowiązywał we wcześniejszych wersjach. Przede wszystkim, aby skompilować moduł należy mieć zainstalowany, w zależności od wersji jądra, cały jego kod źródłowy lub specjalną wersję do kompilacji modułów. Kompilację wykonujemy za pomocą programu narzędziowego „make”. W ramce poniżej przedstawiona została zawartość przykładowego pliku konfiguracyjnego (Makefile) dla tego programu. Treść tego pliku została zapożyczona z dokumentu pt. "The Linux Kernel Module Programming Guide", którego autorami są Peter Jay Salzman, Michael Burian i Ori Pomerantz, a który jest dostępny pod adresem http://www.tldp.org/LDP/lkmpg/2.6/lkmpg.pdf. Plik ten zawiera regułę „clean”, która usuwa pliki powstałe w wyniku 8 9 Dodajmy: w notacji AT&T. Określenia co najmniej mylące. Chodzi o stos jądra, który jest przydzielany każdemu procesowi użytkownika i używany jeśli ten proces zainicjalizuje wywołanie systemowe. 10 Istnieje możliwość zmniejszenia tego rozmiaru do 4 KiB. 11 Dokładniej: większość dystrybucji Linuksa ograniczają rozmiar stosu do 8MiB, ale użytkownik uprzywilejowany może znieść te ograniczenia. 12 Całość rozdziału (wraz z przykładami) bazuje na książce” Jonathan Corbet, Alessandro Rubini, Greg Kroah-Hartman, "Linux Device Drivers". 3 Systemy Operacyjne – semestr drugi kompilacji pliku z kodem źródłowym modułu. Aby program „make” wykonał regułę „clean” należy go wywołać następująco: „make clean”. Wywołania programu bez parametru spowoduje skompilowanie modułu o nazwie „first”. Który moduł ma zostać skompilowany określa wiersz „obj-m := first.o” pliku konfiguracyjnego. obj-m := first.o KERNELDIR = /lib/modules/$(shell uname -r)/build default: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules clean: $(MAKE) -C $(KERNELDIR) M=$(PWD) clean Moduł o nazwie „first” jest modułem typu „Hello World!”, który powinni stworzyć wszyscy początkujący programiści, chcący pisać własne moduły (kod należy umieścić w pliku o nazwie „first.c”): #include<linux/module.h> static int __init first_init(void) { printk(KERN_ALERT"Welcome\n"); return 0; } static void __exit first_exit(void) { printk(KERN_ALERT"Good bye\n"); } module_init(first_init); module_exit(first_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Arkadiusz Chrobot <[email protected]>"); MODULE_DESCRIPTION("Another \"Hello World!\" kernel module :-)"); MODULE_VERSION("1.0"); Plik nagłówkowy „linux/module.h” jest dołączny do kodu każdego modułu jądra. Zawiera on między innymi makra preprocesora, które zostaną opisane niżej. Funkcje o nazwach „first_init” i „first_exit” są odpowiedzialne odpowiednio za wykonanie inicjalizacji tuż po załadowaniu modułu i deinicjalizację tuż przed usunięciem modułu z pamięci operacyjnej. Obie funkcje zostały zdefiniowane z użyciem słowa kluczowego static, co oznacza, że są dostępne wewnątrz przestrzeni nazw modułu „first”, co zapobiega ewentualnemu konfliktowi nazw. Żadna z tych funkcji nie przyjmuje parametrów wywołania. Funkcja „first_exit” nic również nie zwraca, natomiast „first_init” zwraca wartość typu int. Jeśli użylibyśmy terminologii znanej z języków obiektowych, to o tych funkcjach możemy myśleć jako o konstruktorze i destruktorze. W nagłówkach takich funkcji mogą, ale nie muszą występować znaczniki __init i __exit. Pierwszy sygnalizuje, że funkcja jest używana wyłącznie podczas inicjalizacji modułu i po jej wykonaniu można zwolnić pamięć na nią przydzieloną. Drugi znacznik sygnalizuje, że funkcja jest używana wyłącznie podczas sprzątania. Ten znacznik ma znaczenie jeśli moduł jest włączany do kodu jądra na etapie kompilacji, lub gdy jądro jest tak skonfigurowane, że nie pozwala na usunięcie załadowanych modułów. Makra module_init oraz module_exit przyjmują jako parametry wywołania adresy funkcji odpowiedzialnych za inicjalizację i sprzątanie po module. Te makra pozwalają programiście powiadomić kompilator, które funkcje będą odpowiedzialne za inicjalizację i sprzątanie po module. Cztery pozostałe makra nie są obowiązkowe i pozwalają określić licencję na jakiej moduł jest rozpowszechniany, autora modułu (zalecane jest podanie jego adresu e-mail), opis modułu oraz jego wersję. Wszystkie te informacje są zapisane w postaci łańcuchów znaków, według konwencji obowiązującej w języku C. Mając plik wynikowy możemy je odczytać posługując się programem „modinfo” np.: „modinfo first.ko”. Nieumieszczenie w kodzie modułu makra MODULE_LICENSE lub podanie nazwy innej niż ta, która określa jedną z wolnych licencji spowoduje wystąpienie komunikatu ostrzegawczego, mówiącego o tym, że jądro zostało „skażone” modułem o nieznanej lub niedopuszczalnej licencji. Nazwy licencji, które nie generują takiego ostrzeżenia, to: „GPL”, „GPL v2”, „GPL additional rights”, „Dual BSD/GPL”, „Dual MPL/GPL”. Moduły na licencji niewolnej mogą jako parametr tego makra przekazać „Proprietary”. W kodzie modułu występują wywołania funkcji „printk”. Warto zwrócić uwagę na znacznik będący argumentem wywołania tej funkcji i poprzedzający właściwy łańcuch znaków. Określa on poziom ważności 4 Systemy Operacyjne – semestr drugi emitowanego komunikatu lub inaczej poziom logowania. Istnieje kilka takich znaczników: KERN_EMERG – sytuacja awaryjna, KERN_ALERT – problem wymagający natychmiastowej reakcji, KERN_CRIT – sytuacja krytyczna, KERN_ERR – błąd, KERN_WARNING – ostrzeżenie, KERN_NOTICE – sytuacja normalna, ale zaszło zdarzenie godne odnotowania, KERN_INFO – informacja, KERN_DEBUG – komunikat diagnostyczny. Komunikaty od modułów jądra zapisywane są w buforze komunikatów jądra, a stąd mogą być skopiowane do pliku dziennika systemowego (zazwyczaj /var/log/messages), wypisywane na konsolę i można je wyświetlić na ekran za pomocą polecenia „dmesg”. Moduły jądra możemy ładować lub usuwać będąc zalogowanymi jako użytkownik „root” lub jako użytkownik posiadający takie uprawnienia (patrz polecenia „sudo” i „su”). Korzystając z polecenia „insmod” możemy załadować moduł, a poleceniem „rmmod” usunąć, np.: „insmod first.ko”, „rmmod first”. Informacje o załadowanych modułach możemy uzyskać używając polecenia „lsmod”, nawet będąc zalogowanymi jako zwykły użytkownik. Istnieją również inne narzędzia do obsługi modułów, które nie będą tutaj opisywane (np. „modprobe”). Ze względu na dosyć częste zmiany w API jądra warto zapoznać się z działaniem makrodefinicji preprocesora LINUX_VERSION_CODE i KERNEL_VERSION. Pierwsza zwraca wersję źródeł jądra zainstalowanych w systemie w postaci wartości typu int, a druga zamienia numer wersji jądra przekazany jej przez argumenty w postaci trzech liczb (np. KERNEL_VERSION(2,6,19)) na postać numeryczną. Dzięki nim możemy napisać odpowiednie instrukcje warunkowe dla preprocesora, aby włączał fragmenty kodu przeznaczone dla danej wersji jądra podczas kompilacji. 5