E:\Moje dokumenty\HELION\Linux Unleashed\Indeks\58.DOC 855
Rozdzia³ 58. ¨ Tworzenie sterowników urządzeń 855
Tim Parker
W tym rozdziale:
u Sterowniki urządzeń
u Przerwania
u Struktura sterownika urządzenia systemu Linux
u Używanie nowego sterownika
Sterownik urządzenia stanowi interfejs pomiędzy systemem operacyjnym a urządzeniem dołączonym do systemu. Typowy sterownik składa się z pewnej liczby funkcji potrafiących obsługiwać operacje wejścia / wyjścia zlecane przez system. Takie rozwiązanie pozwala na ujednolicenie interfejsu pomiędzy jądrem systemu i urządzeniami.
Nie możemy niestety omówić wszystkich aspektów tworzenia sterowników urządzeń w jednym rozdziale. Na ten temat napisano kilka całkiem pokaźnych książek. Ponieważ sterowniki urządzeń nie są raczej tworzone przez zwykłych użytkowników, a przez utalentowanych programistów, informacje przedstawione poniżej mają jedynie charakter przedstawienia zagadnienia.
Fragmenty kodu prezentowane w tym rozdziale pochodzą z zestawu przykładowych sterowników napisanych w języku C. Sterowniki te są przenośne i choć zostały zaprojektowane dla systemu UNIX, będą działać poprawnie również w Linuxie. Jeśli zdecydujesz się na tworzenie własnych sterowników, podane tu przykłady powinieneś traktować tylko jako wskazówki. Jeżeli zamierzasz zająć się tym zagadnieniem na poważnie, powinieneś zaopatrzyć się w jedną ze specjalistycznych książek poświęconych mu w całości.
Tworzenie sterowników urządzeń jest teoretycznie całkiem proste. Mimo tego przy próbie napisana pierwszego sterownika będziesz zaskoczony liczbą problemów, z którymi się zetkniesz. Wiele osób uważa tworzenie sterowników za swego rodzaju sztukę, ale tak naprawdę wymaga to jedynie odrobiny praktyki i doświadczenia. Napisanie dobrego sterownika daje ogromną satysfakcję.
System Linux używa sterownika dla każdego urządzenia podłączonego do systemu. Najbardziej podstawowe sterowniki są częścią jądra systemu i są ładowane podczas jego uruchamiania. Dzięki sterownikom urządzenia mogą być traktowane przez system tak samo jak pliki – można je adresować, przekierowywać do nich dane czy używać podczas wykorzystywania mechanizmu potoków, zupełnie tak, jak w przypadku zwykłych plików.
Każde urządzenie podłączone do systemu linuxowego jest opisane w pliku programu sterownika, a niektóre parametry są również zapisane w pliku urządzenia, przechowywanym zwykle w katalogu /dev. Jeśli do systemu zostanie dodane nowe urządzenie, należy zainstalować lub też napisać własnoręcznie odpowiedni sterownik. Każdemu urządzeniu musi również odpowiadać plik w katalogu /dev. Jeśli warunki te nie zostaną spełnione, urządzenie nie będzie mogło zostać wykorzystane w systemie linuxowym.
Do każdego pliku urządzenia przypisany jest numer urządzenia, pozwalający systemowi operacyjnemu jednoznacznie je zaadresować. W systemie Linux numer urządzenia składa się z dwóch części: numeru głównego, określającego ogólny typ urządzenia, na podstawie którego wybierany jest odpowiedni sterownik, oraz numeru pobocznego pozwalającego wyszczególnić każde z urządzeń obsługiwanych przez jeden sterownik. Dla przykładu, kilka dysków twardych w systemie może posiadać taki sam numer główny urządzenia (ponieważ są obsługiwane przez ten sam sterownik), ale każdy z nich musi posiadać inny numer poboczny, jednoznacznie go identyfikujący.
Istnieją dwa podstawowe typy sterowników urządzeń: blokowe i znakowe. Każde urządzenie w systemie UNIX-owym używa jednego lub obu tych typów sterowników. Najczęściej spotykane są sterowniki blokowe. Pozwalają one na przesyłanie do i z urządzenia danych buforowanych przez jądro systemu (kopiowanych w odpowiednie miejsce w pamięci operacyjnej). Pierwotnie sterowniki takie były wykorzystywane tylko dla dysków twardych, ale szybko znalazły zastosowanie również do obsługi innych urządzeń pamięci masowej, takich jak napędy taśmowe, magnetooptyczne, a nawet modemy synchroniczne i niektóre szybkie drukarki.
Urządzenia pracujące w trybie znakowym różnią się od blokowych pod dwoma zasadniczymi względami. Po pierwsze, wyniki operacji wejścia / wyjścia mogą być przekazywane bezpośrednio do przestrzeni adresowej procesu, z pominięciem buforowania przez jądro systemu. Po drugie, żądania operacji wejścia / wyjścia zwykle przekazywane są bezpośrednio do urządzenia. Przykładami urządzeń znakowych są terminale i drukarki, podobnie jak modemy asynchroniczne i niektóre modele napędów taśmowych.
Urządzenia blokowe opierają swe działanie na funkcji „strategii”, pozwalającej na odczytanie i zapisanie do urządzenia bloku danych. Dla urządzeń znakowych dostępny jest szereg specjalnych funkcji pozwalających na bezpośrednią komunikację, nazywanych ioctl(). Urządzenia blokowe czasem również działają jako znakowe, właśnie po to, by możliwe było użycie funkcji ioctl(). Dobrym przykładem jest napęd taśmowy, który może współpracować zarówno ze sterownikiem znakowym, jak i blokowym, zależnie od typu zapisywanych danych.
Niezależnie od typu, sterownik urządzenia podejmuje szereg podstawowych kroków za każdym razem, gdy ma nawiązać komunikację z urządzeniem. Najpierw sprawdza on, czy urządzenie jest gotowe i dostępne. Jeśli tak, jest ono „otwierane”, co umożliwia procesowi dostęp do niego. Następnie zwykle wywoływane są funkcje read i write, służące odpowiednio do odczytu i zapisywania danych do urządzenia, po czym urządzenie jest „zamykane”, dzięki czemu inne procesy mogą mieć do niego dostęp.
Przerwania to sygnały przesyłane przez urządzenia do systemu operacyjnego, powiadamiające go o konieczności obsługi danego urządzenia. Przerwania są generowane przy wszystkich operacjach wejścia / wyjścia. System przerwań wykorzystywany w Linuxie jest podobny do DOS-owego, więc jeśli jesteś obeznany z przerwaniami DOS-owymi, znasz już większą część teorii.
Po odebraniu sygnału przerwania system operacyjny wstrzymuje wszystkie wykonywane procesy i obsługuje przerwanie. W większości przypadków przerwania są obsługiwane przez sterownik urządzenia. Przerwania nie powinny w żaden sposób zaburzać działania innych procesów, za wyjątkiem co najwyżej chwilowego ich wstrzymania.
Pewien problem stwarza fakt, że wywołanie przerwania nie powinno prowadzić do zawieszenia działania jądra systemu ani procesu obsługi urządzenia, za wyjątkiem ściśle określonych sytuacji. Przerwania, które nie są prawidłowo obsługiwane lub sprawdzane, mogą prowadzić do zawieszenia sterownika urządzenia, który obsługiwał komunikację z nim.
Obsługa przerwań jest zwykle wstrzymywana tylko na czas wykonywania operacji o istotnym znaczeniu. Obszary kodu sterownika, które nie powinny być przerywane, nazywane są sekcjami krytycznymi lub niewstrzymywalnymi. Zwykle zawieszenie obsługi przerwań na czas realizacji sekcji krytycznych realizowane jest przez podniesienie priorytetu procesora do poziomu równego lub wyższego od priorytetu przerwań. Po zakończeniu wykonywania takiej sekcji priorytet procesora jest obniżany do poprzedniej wartości.
Priorytet przerwań można modyfikować za pomocą funkcji spl5(), spl6(), spl7() i splx(). Wywołanie jednej z trzech pierwszych funkcji powoduje zablokowanie obsługi przerwań. Funkcja spl5() wyłącza obsługę przerwań generowanych przez dyski, drukarki i klawiaturę, funkcja spl6() wyłącza zegar systemowy, natomiast spl7() wyłącza obsługę wszystkich przerwań, w tym pochodzących od urządzeń szeregowych. Funkcje te zwracają kod oznaczający wartość poziomu przerwań, do którego można wrócić za pomocą funkcji splx().
Przed wejściem do sekcji krytycznej kodu należy więc wykonać polecenie
pierw_poz = spl5();
które spowoduje zawieszenie obsługi przerwań aż do momentu wywołania funkcji
splx(): splx(pierw_poz);
Poniższy przykład przedstawia wielokrotne zmiany poziomu obsługiwanych przerwań w sterowniku urządzenia.
int poz_a, poz_b;poz_a = spl5();/* kod, ktory nie powinien byc *//* przerywany przez dyski */poz_b = spl7();/* instrukcje, ktore nie mogą *//* byc w ogole przerywane */splx(poz_b);/* kod koncowy, ktory nie moze *//* byc przerwany przez dyski */splx(poz_a);
Ta dość dziwaczna na pierwszy rzut oka żonglerka poziomami przerwań jest niezbędna, aby uniknąć wstrzymania obsługi sterownika urządzenia i jądra systemu, co może prowadzić do nieprawidłowej pracy systemu. Mechanizmy zabezpieczające powinny być uruchamiane na tak krótko, jak tylko jest to możliwe.
Zwykle nie należy używać funkcji spl6() i spl7(). W niektórych przypadkach wywołanie funkcji spl6() może spowodować opóźnienie zegara systemowego, natomiast wywołanie funkcji spl7() może spowodować utratę danych przesyłanych przez urządzenia szeregowe, chyba że blokada jest aktywna przez bardzo krótki okres czasu. Mimo wszystko jednak w większości przypadków przed wejściem do sekcji krytycznej wystarcza wywołanie funkcji spl5().
Kod źródłowy sterownika urządzenia pod względem struktury nie odbiega od kodu zwykłych programów. W systemie Linux sterowniki w zasadzie tworzone są w języku C, choć czasem korzysta się również z asemblera i języka C++.
Typowy sterownik urządzenia zawiera plik nagłówkowy, w którym znajdują się dyrektywy włączające do kodu deklaracje funkcji systemowych, adresów rejestrów urządzeń, definicje zawartości oraz definicje zmiennych globalnych używanych przez sterownik. W większości sterowników wykorzystuje się następujący zestaw plików nagłówkowych:
param.h parametry jądra systemu;
dir.h parametry katalogów;
user.h definicje użytkownika
tty.h definicje terminali i bufora clist
buf.h informacje o buforowaniu
Plik tty.h jest wykorzystywany w sterownikach pracujących w trybie znakowym, natomiast plik buf.h – w trybie blokowym.
Adresy rejestrów urządzenia, definiowane w jednym z plików nagłówkowych, są zależne od samego urządzenia. Dla urządzeń znakowych odpowiadają one zwykle adresom portów, takich jak port wejścia / wyjścia czy zawierający bity stanu u kontrolne. Polecenia zmieniające stan urządzenia są zdefiniowane jako kody urządzenia.
Oto przykład inicjalizacji rejestrów urządzenia dla sterownika terminalu standardowego (UART):
/* definicja rejestrow */#define RRDATA 0x01 /* odbior */#define RTDATA 0x02 /* nadawanie */#define RSTATUS 0x03 /* stan */#define RCONTRL 0x04 /* bity kontrolne */...itd./* definicja rejestrow stanu */#define SRRDY 0x01 /* dane odebrane */#define STRDY 0x02 /* nadajnik gotowy */#define SPERR 0x03 /* blad parzystosci */#define SCTS 0x04 /* gotowy do wyslania inf. o stanie */...itd.
Funkcje, które musi udostępniać sterownik, zależą od natury samego urządzenia. Wszystkie sterowniki muszą posiadać funkcje open() i close(), pozwalające na realizację operacji wejścia / wyjścia.
Podprogram open() musi sprawdzać, czy wybrane urządzenie jest prawidłowe, czy proces wywołujący może uzyskać do niego dostęp (czy ma prawo dostępu i czy urządzenie jest gotowe) oraz inicjalizować urządzenie. Podprogram open() jest wywoływany za każdym razem, gdy jakiś proces korzysta z urządzenia.
Poniżej prezentujemy procedurę open() obsługującą ogólne urządzenie terminalu o nazwie td.
tdopen (device, flag)int device, flag;{ /* definicje zmiennych lokalnych oraz szczegoly */ /* zostaly pominiete */ /* sprawdz numer urzadzenia / if (UNMODEM (device) >= NTDEVS) { seterror(ENXIO); return; } /* sprawdz, czy urzadzenie jest uzywane */ /* jeśli tak, to wymus zwolnienie jesli */ /* uzytkownikiem jest root (suser) */ tp = &td_tty[UNMODEM(device)]; adres = td_address[UNMODEM(device)]; if ( ( tp->t_lflag & XCLUDE ) && !suser() ) { seterror(EBBUSY); return; { /* Jesli urzadzenie nie jest otwarte, zainicjalizuj je */ /* wywolujac funkcje ttinit() */ if ( (tp->t_state & (ISOPEN|WOPEN)) == 0 ) { ttinit(tp); /* inicjalizacja znacznikow i wywolanie tdparam() */ /* aby ustalic parametry linii */ tdparam (device); } /* Jesli uzywany jest modem, sprawdz stan nosnej */ /* Jesli polaczenie bezposrednie, ustal */ /* wartosc znacznika wykrywania nosnej */ /* ustal priorytet przerwan aby zapobiec nadpisania */ /* czekaj na sygnal wykrycia nosnej */ /* na potrzeby tego przykladu */ /* implementacja zostala pominieta */
Podprogram close() używany jest tylko po zakończeniu komunikacji z urządzeniem. Powoduje on wyłączenie obsługi przerwań pochodzących od urządzenia oraz przesłanie odpowiednich informacji kończących jego pracę. Wszystkie wewnętrzne odniesienia do urządzenia są usuwane. Podprogram close() w większości przypadków nie jest wymagany, ponieważ urządzenia są traktowane jako dostępne przez cały czas. Wyjątek stanowią dyski wymienne i urządzenia wymagające wyłączności na używanie. Również niektóre modemy wymagają, aby w procedurze close() umieścić kod pozwalający na zwolnienie linii telefonicznej.
Również tu posłużymy się przykładem pochodzącym ze sterownika obsługującego terminal.
tdclose (device){ register struct tty *tp; tp = &td_tty[UNMODEM(device)]; (*linesw[tp->t_line].l_close) (tp); if (tp->t_cflag & HUPCL) tdmodem(device, TURNOFF); /* usun znacznik wylacznosci */ ip->t_lflag &= ~XCLUDE;}
Funkcje strategii (używane tylko przez sterowniki urządzeń blokowych) są wywoływane z parametrem prowadzącym do nagłówka bufora, do którego jądro systemu składa dane. Nagłówek bufora zawiera informacje dotyczące odczytu czy zapisu danych, wraz z adresem miejsca pamięci, w którym znajdują się dane do wysłania lub mają znaleźć się odbierane dane. Rozmiar bufora jest zwykle ustalany podczas instalacji i waha się pomiędzy 512 i 1024 bajtami. Rozmiar ten definiowany jest w pliku param.h jako wartość zmiennej BSIZE. Rozmiar bloku danych używanego przez urządzenie może być mniejszy od rozmiaru bufora – w takim przypadku sterownik wykona kilka operacji odczytu czy zapisu pod rząd.
Funkcja strategii zostanie przedstawiona na przykładzie prostego sterownika dysku twardego. Nie zamieszczamy samego kodu źródłowego, a jedynie szkieletowe informacje o działaniu funkcji.
int hdstrategy (bp)register struct buf *bp;{ /* inicjalizuj dysk i numery partycji */ /* ustal wartosci zmiennych lokalnych */ /* sprawdz, czy dysk i partycja sa prawidlowe */ /* ustal numer cylindra docelowego */ /* wylacz przerwania */ /* wstaw zadanie do kolejki */ /* sprawdz kontroler, jesli nie jest aktywny, uruchom go */ /* przywroc poprzedni poziom przerwan */}
Urządzenia pracujące w trybie znakowym używają funkcji write(), która sprawdza, czy jej argumenty są poprawne, a następnie kopiuje dane z obszaru pamięci procesu do bufora sterownika urządzenia. Po skopiowaniu wszystkich danych lub zapełnieniu bufora uruchamiane są procedury wejścia / wyjścia aż do opróżnienia bufora, po czym pr...
wrim