58(1).DOC

(100 KB) Pobierz









Rozdział 58.
Tworzenie sterowników urządzeń









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ę.

Sterowniki urządzeń

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

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 poziomuwnego 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().

Struktura sterownika urządzenia dla systemu Linux

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++.

Pliki nagłówkowe

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.

Otwieranie urządzenia

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 */

Zamykanie urządzenia

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

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 */
}

Funkcje write()

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...

Zgłoś jeśli naruszono regulamin