Do spisu treści tematu 5

5.3 Urządzenia ogólnie. Urządzenia znakowe




Spis treści


WSTĘP

Opis obsługi urządzeń w systemie Linux zacznę od wytłumaczenia pojęcia podprogramu obsługi urządzenia. Jest to bowiem pojęcie bardziej fundamentalne niż pojęcie urządzenia - zdarzają się przypadki, gdy znaczenie terminu "urządzenie" jest wątpliwe. Definicja pojęcia podprogramu obsługi jest niestety tzw. masłem maślanym:
podprogram obsługi urządzenia jest to taki zbiór funkcji z jądra systemu, do którego dostęp otrzymuje się poprzez tablice rozdzielcze urządzeń.
Jak widać, definicja ta wiele nie wyjaśnia.

Podprogramy obsługi urządzeń mają jednak pewną cechę, która w dość naturalny sposób wyróżnia je spośród innych części jądra systemu. Cechą tą jest metoda dostępu do ich funkcji spoza jądra. Normalnie Linux udostępnia użytkownikom swoje usługi poprzez mechanizm funkcji systemowych. Tymczasem dostęp do podprogramów obsługi urządzeń otrzymuje się przez system plików.

W skrócie wygląda to tak: jeżeli program użytkowy wywoła taką funkcję systemu plików, jak np. open, to system plików wykonuje różne czynności zależnie od rodzaju otwieranego pliku:
1) jeżeli plik był plikiem zwykłym lub katalogiem, to wykonywane są zwykłe czynności - zależą one jedynie od rodzaju zamontowanego systemu plików,
2) jeżeli plik był plikiem specjalnym, to wywoływana jest funkcja open odpowiednia dla tego pliku, której kod znajduje się w odpowiednim podprogramie obsługi urządzenia.

Czym są owe pliki "specjalne"? Są to elementy systemu plików, w których zakodowana jest informacja o tym, który podprogram obsługi urządzenia im odpowiada. Podczas otwierania takiego pliku system próbuje "skontaktować" się z podprogramem obsługi, którego dane się w nim znajdują. Jeżeli podprogram obsługi nie zostanie odnaleziony, to następuje błąd. Jeżeli podprogram się odnajdzie, to zostanie wywołana funkcja open tego podprogramu, a zakodowana w pliku informacja zostanie przekazana dalej do tej funkcji.

Jeżeli funkcja open znajdująca się w podprogramie obsługi zakończy się sukcesem, to wszystkie dalsze operacje na pliku będą wykonywane przez ten podprogram, a nie (tak, jak jest zwykle) przez system plików. Tak więc jeśli ktoś otworzy plik specjalny, a następnie wykona na nim read lub write, to wywołane funkcje systemowe po sprawdzeniu uprawnień itp. przekażą sterowanie do funkcji podprogramu obsługi. Widzimy więc, że metodą na dostęp do funkcji zawartych w jądrze systemu może być napisanie podprogramu obsługi urządzenia - nie jest w tym celu konieczne tworzenie nowej funkcji systemowej.

Zwróćmy uwagę, że w dotychczasowych rozważaniach w ogóle nie wspomnieliśmy o samym urządzeniu, a jedynie o podprogramie jego obsługi. Termin "podprogram obsługi urządzenia" pochodzi oczywiście stąd, że często funkcje przez ten podprogram udostępniane są to funkcje pozwalające wykonywać operacje wejścia-wyjścia na fizycznych urządzeniach, takich jak dysk twardy lub drukarka. Jak jednak wynika z przedstawionego wyżej opisu, podprogram obsługi urządzenia nie musi sterować żadnym fizycznym urządzeniem. Urządzenie, którym steruje podprogram obsługi odpowiadający plikowi /dev/null, jest czystą abstrakcją - nie ma przecież potrzeby wyrzucania niepotrzebnych bajtów przez jakiś specjalny kabel przyłączony do komputera. W związku z tym proponuję następującą terminologię. Zamiast "podprogram obsługi urządzenia" będę pisał po prostu "podprogram obsługi". Jeżeli będę miał na myśli jakiś element systemu komputerowego, z którym procesor komunikuje się przez operacje wejścia-wyjścia, to będę pisał "urządzenie fizyczne" lub "urządzenie zewnętrzne". Pisząc "urządzenie" będę miał na myśli abstrakcyjny obiekt (istniejący lub nie), którym steruje podprogram obsługi.


DOSTĘP DO URZĄDZEŃ

Aby wywołać jakąś funkcję podprogramu obsługi urządzenia, musimy wykonać odpowiednią operację na pliku, który jest związany z tym podprogramem. W dużym uproszczeniu wygląda to tak, że pewne pliki oznaczone są jako specjalne. W takim pliku specjalnym zakodowana jest informacja o jego podprogramie obsługi. W momencie otwierania takiego pliku informacja ta jest odkodowywana i wywoływana jest funkcja otwarcia specyficzna dla danego podprogramu obsługi. O ile funkcja ta nie zwróciła błędu, to od tego momentu wszystkie operacje na tym pliku powodują wywołanie znajdujących się w podprogramie procedur obsługi.

Trzeba zwrócić uwagę, że operacje typu "utwórz plik", "usuń plik" nie są obsługiwane zgodnie z powyższym opisem. Są to operacje wykonywane wewnątrz systemu plików, skutkiem czego można bez kłopotów stworzyć plik specjalny odpowiadający nie istniejącemu podprogramowi obsługi (nie mówiąc już o rzeczywistych urządzeniach). Oznacza to, że nie mamy się co cieszyć z odkrycia pliku /dev/hdg - nie oznacza to wcale, że mamy 7 dysków, nie wynika stąd nawet, że podprogram obsługi siódmego twardego dysku w ogóle został zainstalowany (plikowi /dev/hdg odpowiada inny podprogram obsługi niż plikowi /dev/hda).

Aby stworzyć plik specjalny, trzeba skorzystać z funkcji lub polecenia mknod. Nie można użyć open z flagą O_CREAT. Wynika to właśnie stąd, że tworzenie plików specjalnych nie wymaga istnienia odpowiadających im podprogramów obsługi. Gdyby ktoś chciał stworzyć za pomocą open plik dla nie istniejącego podprogramu obsługi, to nie udałoby się wywołać funkcji open specyficznej dla tego podprogramu, a wszystkie późniejsze operacje nie miałyby sensu. Natomiast polecenie mknod tworzy tylko odpowiedni i-węzeł i dopisuje go do katalogu - informacje o odpowiadającym plikowi podprogramie obsługi również zostają zapisane. Te informacje są traktowane tak samo, jak np. informacje o prawach dostępu i można je odczytać za pomocą polecenia ls.


PRZYKŁAD, JAK TWORZYĆ I USUWAĆ URZĄDZENIA

Aby stworzyć plik specjalny korzystamy z mknod:

mknod dziwne_urządzenie c 100 101

Polecenie to tworzy plik specjalny o nazwie dziwne_urządzenie i zapisuje, że jego podprogram obsługi jest identyfikowany przez parę ('c', 100). Liczba 101 będzie przekazywana podprogramowi obsługi przy wszystkich operacjach na tym pliku. Aby utworzyć plik specjalny trzeba mieć uprawnienia nadzorcy, w przeciwnym wypadku kończy się ono błędem. Przyczyny tego są jasne: ponieważ Linux nie sprawdza niczego takiego, jak uprawnienia do tworzenia urządzenia ('c', 100) - nie sprawdza nawet, czy podprogram obsługi takiego urządzenia w ogóle jest wkompilowany w jądro - więc ktoś mógłby np. stworzyć plik specjalny odpowiadający twardemu dyskowi, a następnie odczytać z dysku dowolne informacje. Zwracam uwagę, że nie ma czegoś takiego, jak uprawnienia do korzystania z urządzenia. Kontrola uprawnień odbywa się wyłącznie na poziomie systemu plików, który sprawdza, czy mamy prawo wykonać taką lub inną operację na !pliku! specjalnym.

Wynika stąd m.in., że może istnieć urządzenie, do którego z jednego miejsca mamy dostęp, a z innego nie (tzn. dowiązane są do niego dwa różne pliki specjalne, których użytkowanie wymaga różnych uprawnień).

Skoro urządzenie już utworzyliśmy, spróbujmy coś z niego przeczytać:

cat dziwne_urządzenie

Ponieważ podprogram obsługi ('c', 100) nie istnieje, więc polecenie to kończy się błędem. Błąd powstaje w momencie wywołania funkcji systemowej open, gdyż próbuje ona wywołać specyficzna dla podprogramu ('c', 100) funkcję open, a urządzenia ('c', 100) nie ma (tzn. nie zostało ono zarejestrowane - o tym później). Jeżeli podprogramu obsługi w systemie nie ma, to funkcja open ponosi porażkę, a tym samym nie mamy sposobu wywołać read lub write na urządzeniu.

Jeżeli podprogram obsługi urządzenia istnieje, to on decyduje, czy pozwolić na otwarcie urządzenia (wszystko to jest już po sprawdzeniu uprawnień przez system plików). Przykładowo na moim komputerze funkcja open na /dev/hda15 (piętnasta partycja dysku numer 0) albo na /dev/hdb1 (pierwsza partycja dysku numer 1) nie zwraca błędu, choć oba urządzenia fizycznie nie istnieją (niestety...). Również funkcja read na tych urządzeniach działa bezbłędnie (ale potrafi przeczytać tylko 0 bajtów). Natomiast read na /dev/hdb (dysk numer 1) zwraca błąd (I/O error), mimo że open na nim działa dobrze, a read na /dev/hda działa poprawnie. Widać stąd, że gdy podprogram obsługi urządzenia istnieje, to sam decyduje, które operacje uznać za poprawne - czasami operacje na urządzeniach, które fizycznie nie są podłączone, wcale nie zwracają błędu.

Możemy teraz skasować poprzednio stworzone urządzenie:

rm dziwne_urządzenie

Oczywiście usuwając plik specjalny również nie wchodzimy w żadne interakcje ze związanym z nim podprogramem obsługi (w szczególności ten podprogram nie musi istnieć).


PODZIAŁ NA URZĄDZENIA ZNAKOWE I BLOKOWE

Jak już wspominałem, podprogram obsługi urządzenia jest identyfikowany parą (litera, liczba) - literą może być 'b' lub 'c', liczba zaś jest liczbą jednobajtową (przynajmniej w obecnej implementacji). Taka identyfikacja jest związana z podziałem na urządzenia znakowe (ang. character devices) i blokowe (ang. block devices). Pierwsze są oznaczane literą 'c', drugie literą 'b'.

Podział ten jest dość arbitralny. Powiemy, że urządzenie jest znakowe, jeżeli pozwala na dostęp do danych bajt po bajcie, jeżeli natomiast dane są obsługiwane w większych porcjach, to mówimy, że urządzenie jest blokowe (a te porcje nazywamy blokami). Zwróćmy uwagę, że może być jedno fizyczne urządzenie, dla którego mamy dwa podprogramy obsługi - np. znakowy i blokowy (po sektorze) dostęp do dysku. Podział ten łączy się też z kwestią buforowania danych - zwykle buforowane są urządzenia blokowe (choć można stworzyć urządzenie blokowe niebuforowane lub znakowe buforowane - to drugie wymagałoby jednak własnej implementacji buforów).

Podział ten związany jest bardzo silnie z pojęciem tablic rozdzielczych.


TABLICE ROZDZIELCZE

Istnieją dwie tablice urządzeń (właściwie: podprogramów obsługi urządzeń) w systemie: jedna dla urządzeń blokowych, druga dla znakowych. Każdemu podprogramowi obsługi odpowiada pozycja w tablicy (nie ma przeszkód technicznych, by ten sam podprogram był na paru pozycjach). Podprogram obsługi, jak już mówiłem, jest identyfikowany parą (litera, liczba). Litera określa w której z dwóch tablic, a liczba pod którym indeksem, znajduje się opisująca go struktura device_struct. W strukturze tej znajduje się wskaźnik do nazwy podprogramu obsługi oraz, co najważniejsze, wskaźnik do odpowiadającej mu struktury file_operations. Dostęp do podprogramu obsługi spoza jądra systemu przechodzi prawie zawsze przez funkcje, do których wskaźniki znajdują się w tej strukturze (ale są wyjątki, np. dla urządzenia "mem" ).

Podprogram obsługi urządzenia jest uważany za zainstalowany w systemie tylko wtedy, gdy zajmuje jakąś pozycję w tablicy rozdzielczej. Zauważmy, że ze względu na to, że dostęp do urządzeń następuje przez pliki, każde urządzenie powinno mieć własny, ogólnie znany, numer. Gdyby tak nie było, to, aby skorzystać z urządzenia, trzeba by najpierw znaleźć na podstawie nazwy jego numer, następnie stworzyć odpowiadający mu plik, a dopiero potem z niego korzystać. Dlatego większość urządzeń ma standardowe numery, można je znaleźć w pliku linux/Documentation/devices.txt.


NUMER GŁÓWNY I NUMER DRUGORZĘDNY URZĄDZENIA

Wspominałem już, że i-węzeł opisujący plik specjalny zawiera trzy ważne wartości:

  1. flagę (litery 'b' lub 'c'), która określa typ urządzenia,
  2. liczbę, która jest indeksem w tablicy rozdzielczej, pod którym znajduje się pozycja odpowiadająca temu urządzeniu,
  3. liczbę, którą przekazuje się podprogramowi obsługi urządzenia podczas wszelkich operacji na urządzeniu.

Pierwszą z dwóch liczb nazywa się numerem głównym urządzenia (ang. major device number), drugą drugorzędnym (ang. minor device number). Pierwsza z nich decyduje, który podprogram obsługi będzie używany, natomiast interpretacja drugiej zależy od samego podprogramu. Numer drugorzędny, podobnie jak główny, jest liczbą ośmiobitową. Oba te numery łącznie tworzą numer urządzenia (tzn. numer_urządzenia=(numer_główny<<8)|numer_drugorzędny).

Numer drugorzędny może decydować o tym, o które z wielu takich samych urządzeń nam chodzi, np. dla urządzenia blokowego o numerze głównym 3 numer drugorzędny oznacza:

0       dysk 0
1       partycja 1 dysku 0
...
63      partycja 63 dysku 0
64      dysk 1
65      partycja 1 dysku 1
...
127     partycja 63 dysku 1

Może też być tak, że różne urządzenia współdzielą ten sam numer główny, np. dla urządzenia znakowego o numerze głównym 1 numer drugorzędny oznacza m.in.:

1       pamięć fizyczna
2       pamięć wirtualna jądra
3       urządzenie /dev/null
4       dostęp do portów wejścia/wyjścia
5       generator zer ( /dev/zero )
8       generator liczb losowych

Powyższe urządzenia spełniają dość różne funkcje, jednak wszystkie są obsługiwane przez ten sam podprogram obsługi (jak się okaże przy omawianiu funkcji open dla urządzeń, współdzielenie jednego podprogramu obsługi, czyli współdzielenie jednego numeru głównego, jest równoważne temu, że jest się obsługiwanym przez tę samą funkcję open specyficzną dla podprogramu obsługi).


OBSŁUGA TABLIC ROZDZIELCZYCH

Wszystko, co dotyczy tablic rozdzielczych, jest zgrupowane w pliku /fs/devices.c, reszta jądra nie ma do nich bezpośredniego dostępu. Tablice rozdzielcze są to tablice statyczne, nazywają sie chrdevs i blkdevs. Wszystko w tym pliku, co dotyczy urządzeń znakowych, działa dokładnie tak samo dla blokowych, dlatego opiszę tylko te pierwsze.

UWAGA: jeżeli filp jest typu file *, to filp->f_op zawiera adresy funkcji, które są wywoływane w momencie wykonywania operacji na pliku opisanym przez filp. Dla zwykłych plików funkcje te są elementem systemu plików. Jeżeli jednak filp opisuje plik specjalny, to funkcje te są wybierane przez podprogram obsługi i często są częścią kodu tego podprogramu. Od momentu, gdy coś zostanie zapisane do pola filp->f_op, wszystkie operacje na pliku filp są obsługiwane przez funkcje, których adresy znajdują się w tym polu.

Pozycja o numerze major w tablicy jest uważana za pustą, jeśli chrdevs[major].fops==NULL, w przeciwnym wypadku jest zajęta. Pozycja chrdevs[0] nie jest używana - nie da się pod nią niczego wpisać.

WAŻNE: jeżeli inode jest i-węzłem odpowiadającym plikowi specjalnemu, to pole inode.i_rdev zawiera numer urządzenia, któremu ten plik odpowiada.

MAJOR(dev) - numer główny urządzenia o numerze dev

MINOR(dev) - numer drugorzędny urządzenia o numerze dev

Zdefiniowano m.in. następujące funkcje (w komentarzu mniej więcej opisuję jak działają):


int register_chrdev(unsigned int major, char *name, struct file_operations *fops)
/* 
jeśli major>0, 
        jeśli chrdevs[major] zajęta, zwróć błąd;
        chrdevs[major].name=name;
        chrdevs[major].fops=fops;
        zwróć 0;
jeśli major==0
        znajdź wolną pozycję, wykonaj to, co powyżej, dla tej znalezionej
        pozycji, zwróć zamiast zera tę pozycję
*/

int unregister_chrdev(unsigned int major, char *name);
/*
jeśli chrdevs[major].name jest różne od name albo pozycja major jest
        pusta, to błąd;
zaznacz pozycję major jako pustą;
*/


int chrdev_open(struct inode *inode, struct file *filp);
/*
(inode i filp opisują otwierany plik specjalny)
jeśli pozycja MAJOR(inode->i_rdev) jest pusta, to błąd;
wpp. zapisz fops spod tej pozycji w polu filp->f_op;
jeśli zdefiniowana jest specyficzna funkcja open dla urządzenia, to ją
        wywołaj i zwróć to samo, co ona;
wpp. zwróć 0;
*/

Budowa powyższej funkcji pozwala na to, by wiele urządzeń współdzieliło jeden numer główny. Otóż do funkcji open specyficznej dla urządzenia przekazuje się inode i filp. Funkcja ta może, zależnie od wartości MINOR(inode->i_rdev), przypisać różne wartości na filp->f_op (tak dzieje się np. dla urządzenia "mem" ). Może też niczego na to pole nie przypisywać (tak się dzieje np. dla dysków), wtedy filp->f_op jest takie samo niezależnie od numeru drugorzędnego. Właśnie ta cecha funkcji open powoduje, że wcześniej napisane jest "prawie zawsze", a nie "zawsze".

WAŻNE: Od tej chwili !wszystkie! operacje na danym pliku specjalnym będą powodowały wywołanie jednej z funkcji, do których wskaźniki znajdują się w filp->f_op.

Funkcja chrdev_open jest wołana wtedy, gdy wołane jest sys_open dla znakowego pliku specjalnego. Natomiast blkdev_open może być wołane w dwóch przypadkach: otwierania blokowego pliku specjalnego lub montowania systemu plików na urządzeniu (patrz fs/super.c:880).

W pliku devices.c zdefiniowano też dwie struktury danych:

struct file_operations def_chr_fops

oraz

struct inode_operations chrdev_inode_operations

(oraz ich analogi dla urządzeń blokowych).

Jedyne niezerowe pole def_chr_fops to właśnie pole dla open, gdzie znajdziemy wartość &chrdev_open. Natomiast jedyne niezerowe pole w chrdev_inode_operations to pole dla default_file_ops, gdzie znajdziemy wartość &def_chr_fops. W momencie, gdy system będzie tworzył i-węzeł inode odpowiadający urządzeniu znakowemu, to na inode.i_op przypisze wartość &chrdev_inode_operations (patrz np. fs/ext2/inode.c:565, funkcja ext2_read_inode lub fs/ext2/namei.c:421, funkcja ext2_mknod). Z kolei z pola inode.i_op->default_file_ops system pobierze adres funkcji chrdev_open i ją wywoła.

W devices.c znajdziemy znajdziemy też dwie funkcje dotyczące wyłącznie urządzeń blokowych:

void blkdev_release(struct inode *inode) /* wywołuje specyficzną dla urządzenia inode->i_rdev funkcję release */ int check_disk_change(kdev_t dev) /* jeżeli funkcja check_media_change odpowiadająca urządzeniu dev zwraca FALSE, to nic nie robi; wpp. unieważnia wszystkie struktury danych systemu plików związane z urządzeniem oraz wywołuje odpowiadająca urządzeniu dev funkcję revalidate */


INICJALIZACJA PODPROGRAMÓW OBSŁUGI URZĄDZEŃ

Każdy podprogram obsługi urządzenia, aby mógł być wykorzystywany, musi się zarejestrować odpowiednią funkcją register_chrdev/blkdev, podając jej jako parametr numer główny, pod którym chce być dostępny. Poza tym często podprogram obsługi musi zaincjalizować swoje struktury danych, znaleźć linie IRQ na których "siedzą" związane z nim urządzenia itp. Wszystkie te czynności są wykonywane w funkcjach xxx_init() (poza jednym wyjątkiem - urządzeniem "mem" ), gdzie xxx jest "nazwą" urządzenia. Z kolei funkcje xxx_init są wywoływane przez chr_dev_init oraz blk_dev_init. Opiszę pokrótce ich działanie:

int chr_dev_init(); 
/* zarejestruj urządzenie "mem" (wyjątek!)
rand_initialize();
tty_init();
lp_init(); (...)
soundcard_init(); (...)
return 0; */

Jak widać, ta funkcja sama inicjalizuje "mem", resztę powierza funkcjom xxx_init().

int blk_dev_init();
/*
wyzeruj kolejki żądań dostępu dla wszystkich urządzeń blokowych
hd_init() (...)
floppy_init() (...)
return 0; */


DZIAŁANIE FUNKCJI SYSTEMOWYCH DOTYCZĄCYCH PLIKÓW W PRZYPADKU URZĄDZEŃ (tzn. jeśli plik jest plikiem specjalnym)

Poniżej opisuję, co się dzieje, jeżeli zwykłą funkcję systemową związaną z plikami wywołamy dla pliku, który odpowiada jakiemuś urządzeniu. Nie opisuję tu wszystkich funkcji, a jedynie te, które mają istotne znaczenie dla podprogramów obsługi urządzeń.

W nawiasach kwadratowych umieszczam nazwę funkcji, która w danym momencie się wykonuje.

FUNKCJA SYS_OPEN:

[ sys_open i do_open ]

Otwieramy plik struct inode *inode (specjalny):

najpierw postępuje tak samo, jak dla zwykłych plików.
f->f_op=inode->i_op->default_file_ops; (czyli def_chr/blk_fops);
wywołuje f->f_op->open(inode, f); (czyli chr/blkdev_open);
        [chr/blkdev_open]
        f->f_op=chr/blkdev[MAJOR(inode->i_rdev)].fops; /* czyli file
		operations standardowe dla tego podprogramu obsługi */
        wywołuje f->f_op->open(inode, f), o ile istnieje; /* czyli funkcję
                open specyficzną dla urządzenia */
                [open specyficzna]
                różnie:
                np. dla "mem" robi:
                f->f_op=&file_operations_zależne_od_MINOR(inode->i_rdev);
                np. dla "hd" robi:
                nr_urządzenia= (2 najstarsze bity MINOR(inode->i_rdev));
                czeka, aż dysk nr_urządzenia nie używany.
                zwiększa licznik otwarć dysku nr_urządzenia.
                        (tak naprawdę starszy z dwóch bitów jest
                        nieużywany, patrz podprogram obsługi hd)
        zwraca to, co zwróciła open specyficzna dla urządzenia lub
                błąd, jeżeli nie ma urządzenia o tym numerze
dalej postępuje tak samo, jak dla zwykłych plików.

Funkcje open specyficzne dla urządzenia mogą zajmować się m.in. następującymi rzeczami:

1. Zależnie od MINOR(inode->i_rdev) przypisywać różne wartości na f->f_op (jak w "mem" ).
2. Inicjalizować struktury danych związanych z urządzeniem (np. "lp" ).
3. Sprawdzać, czy urządzenie istnieje i zdobywać o nim informacje (np. "lp" ).
4. Przechowywać informacje o tym, ile razy urządzenie było otwierane (np. "hd" ). Jest to często konieczne z następującego powodu. Do jednego urządzenia może się odwoływać w systemie plików wiele różnych plików specjalnych. W związku z tym system plików nie jest w stanie określić, czy w danym momencie ktoś używa pewnego urządzenia, czy też nie (tzn. teoretycznie byłoby to możliwe, ale bardzo czasochłonne). Tymczasem, o ile fizyczne zamknięcie urządzenia jest operacją konieczną (tak, jak konieczne jest synchroniczne zapisanie wszystkich bloków itp. w urządzeniu blokowym w momencie zamykania), to podprogram obsługi musi wiedzieć, kto zamyka urządzenie jako ostatni. Jest to rozwiązywane w ten sposób, że taki podprogram sam utrzymuje licznik, który jest zwiększany przy open i zmniejszany przy release.

FUNKCJA SYS_CLOSE

[ zamykamy deskryptor fd pliku specjalnego ]

najpierw postępuje jak dla zwykłych plików
(struct file *filp - struktura związana z fd)
        [ close_fp(filp) ]
        postępuje jak dla zwykłych plików
        (struct inode *inode - i-węzeł związany z filp)
                [ fput(filp, inode) ]
                filp->f_count--;
                jeśli filp->f_count==0, to wywołuje __fput(filp, inode)
                        [ __fput(filp, inode) ]
                        jeśli istnieje filp->f_op->release, to ją wywołuje
                                [ release(inode, filp) specyficzne ]
                                np. hd_release zapisuje wszystko
                                synchronicznie i zmniejsza licznik odwołań 
				do dysku.
                        dalej postępuje jak dla zwykłych plików.
        dalej postępuje jak dla zwykłych plików.
dalej postępuje jak dla zwykłych plików.

Z budowy tej funkcji wynika ten morał, że wywołanie funkcji release (specyficznej dla urządzenia) o niczym jeszcze nie świadczy - może istnieć wiele innych odwołań do tego samego urządzenia. Jedyne, co wtedy wiemy, to fakt, że ktoś odłącza się od urządzenia. Jeżeli więc informacja, czy do urządzenia ktoś jeszcze jest przyłączony, jest istotna, to twórca podprogramu obsługi musi sam utworzyć w podprogramie odpowiedni licznik odwołań.

FUNKCJA SYS_LSEEK

[ sys_lseek ]


najpierw dokonuje sprawdzeń takich samych jak zawsze.
niech struct file *file będzie związana z obsługiwanym plikiem.
jeśli file->f_op->lseek jest ==NULL to postępuje jak zwykle.
wpp. wywołuje file->f_op->lseek(file->inode, file, ...);
        [ lseek specyficzne ]
        np. lseek dla większości urządzeń z "mem" działa normalnie, ale
        nie pozwala przewinąć pliku do końca (bo to nie ma sensu - nie
        wiadomo, co to jest koniec pamięci wirtualnej).


W większości wypadków funkcja lseek albo nie jest implementowana (używa się standardowej, która tylko zmienia file->f_pos ), albo zawsze zwraca błąd (przewijanie drukarki jest bez sensu).

FUNKCJA SYS_IOCTL

[ sys_ioctl ]


dokonuje zwykłych sprawdzeń, niech struct file *file będzie związane z
obsługiwanym plikiem.
jeśli jest to komenda standardowa, to ją obsługuje normalnie.
wpp. jeśli jest to zwykły plik, to obsługuje go normalnie.
wpp. jeśli file->f_op->ioctl != NULL, to ją wywołuje.
        [ ioctl specyficzne ]
        Funkcje ioctl wykonują najróżniejsze czynności i ich działanie
        jest zupełnie różne dla różnych urządzeń.

FUNKCJE SYS_READ I SYS_WRITE

Jeżeli plik jest urządzeniem, to wykonywane są te same czynności co w przypadku zwykłych plików, a następnie jest wywoływana funkcja file->f_op->read/write (przy zwykłych plikach również, ale wówczas pod tym adresem znajdują się standardowe funkcje systemu plików). Dla plików specjalnych ta funkcja jest to specyficzna dla urządzenia funkcja read/write. Oczywiście zwykle większość kodu podprogramu obsługi zajmuje się obsługą tych funkcji - przy czym dla urządzeń blokowych zwykle używa się tych samych funkcji block_read/write, a różnice są na innym poziomie szczegółowości- patrz Urządzenia Blokowe.

INNE FUNKCJE

Istnieje jeszcze parę funkcji, do których wskaźniki znajdziemy w strukturze file_operations, ale występują one rzadko, a większość używana jest głównie dla urządzeń blokowych i tam jest odpowiednie miejsce na ich opis (np. fsync, check_media_change, revalidate).


DWA RODZAJE PODPROGRAMÓW OBSŁUGI URZĄDZEŃ

Jeżeli podprogram obsługi urządzenia otrzymuje żądanie wykonania jakiejś operacji na tym urządzeniu i przekaże to żądanie do urządzenia, to musi w jakiś sposób zorientować się, że zostało ono wykonane (albo że wystąpił błąd). Zwróćmy uwagę, że dotyczy to zarówno niemal wszystkich podprogramów obsługi urządzeń zewnętrznych, jak również niektórych urządzeń wirtualnych, które wykonują np. read w sposób blokujący (jak /dev/random). Urządzenia wirtualne mogą wieszać się po prostu na jakiejś kolejce (jak w random_read), czekając na zajście pewnego wydarzenia w systemie. Inaczej jest z obsługą urządzeń zewnętrznych.

Podprogram obsługi takiego urządzenia może:

W systemie Linux większość podprogramów obsługi działa przerwaniowo, ale niektóre, np. drukarka, mogą działać w trybie odpytywania. Jeżeli podprogram obsługi ma działać przerwaniowo, to musi umieć zdobyć sobie linię IRQ i zainstalować procedurę obsługi przerwania. Opisuję to na stronie Przerwania


URZĄDZENIA ZNAKOWE

Jak programem "Skamandra" była bezprogramowość, tak specyfiką urządzeń znakowych jest brak wszelkiej specyfiki. Oznacza to, że w zasadzie jest to możliwie najbardziej ogólny rodzaj urządzeń - nie mają one, tak jak urządzenia blokowe, wspólnych funkcji do obsługi pewnego rodzaju żądań. Są one wyróżnione chyba dlatego, że nie korzystają z żadnych wspólnych procedur ani struktur danych - po prostu wszystkie urządzenia nieblokowe trzeba jakoś nazwać. Wśród urządzeń znakowych najważniejszą grupę tworzą terminale. Poza tym znakowe są też modemy, myszy itp.

Jako przykład podprogramu obsługi urządzenia znakowego wykorzystam urządzenie, w którego obsłudze nie spotykamy się z masą szczegółów niskopoziomowych i które skonfigurowane jest w każdym jądrze, a mianowicie urządzenie "mem". Jego kod znajdziemy w pliku drivers/char/mem.c. Nie będę opisywał wszystkich podurządzeń, które można tam znaleźć, a jedynie niektóre (przez "podurządzenie" rozumiem urządzenie, które ma ten sam numer główny, co inne urządzenia, ale różni się numerem drugorzędnym). Będę też opisywał raczej co dana funkcja robi, a nie jak to robi, gdyż to drugie jest albo proste, albo należy raczej do takich tematów jak pamięć itp.


URZĄDZENIE "MEM"

Urządzenie "mem" obsługuje następujące podurządzenia:

numer drugorzędnystandardowa nazwadziałanie
1 /dev/mem dostęp do pamięci
2 /dev/kmem dostęp do pamięci wirtualnej jądra
3 /dev/null "pożera" wszystko
4 /dev/port dostęp do portów I/O
5 /dev/zero zwraca same zera
7 /dev/full zwraca -ENOSPC przy pisaniu
8 /dev/random liczby losowe - w razie czego generator czeka, aż zwiększy się poziom "entropii" w systemie
9 /dev/urandom liczby losowe - przy "braku entropii" staje się generatorem liczb pseudolosowych

Kod wszystkich tych podurządzeń poza random i urandom znajduje się w pliku drivers/char/mem.c, kod zaś pozostałych dwóch w pliku drivers/char/random.c. Sprawa generowania liczb losowych jest bardzo skomplikowana i nie będę jej tutaj omawiał. Generator liczb losowych jest opisany w tym pliku.

Jedyną ciekawą rzeczą w podprogramie obsługi urządzenia "mem" jest opisana już wyżej funkcja memory_open, która pokazuje jak przy pomocy jednego numeru głównego obsłużyć wiele różnych podurządzeń. Działanie pozostałych funkcji jest w miarę oczywiste - ich nazwy są chyba wystarczająym objaśnieniem. Szczególnie kod urządzeń /dev/null, /dev/zero, /dev/full i /dev/port jest prosty i zrozumiały sam przez się. Do zrozumienia kodu /dev/mem i /dev/kmem potrzebna jest znajomość zagadnień związanych z pamięcią w systemie Linux. Osoby, które zrządzenie losu zmusza do napisania jakiegoś podprogramu obsługi urządzenia dla Linuxa namawiam do lektury kodu urządzenia "mem". Jest on naprawdę prosty i może wyjaśnić wiele spraw.


Bibliografia


Autor: Piotr Hoffman