Systemy Operacyjne – semestr drugi
Wyk a
ł d dwunasty
Urządzenia znakowe i blokowe w Linuksie
Jednym z zastosowań wirtualnego systemu plików opisanego na poprzednim wykładzie jest obsługa urz d ą zeń wej c
ś ia – wyj c
ś ia. Pojęcie „urządzenie” niekoniecznie
musi oznaczać fizyczny układ, mo e
ż to również być urządzenie wirtualne. W systemach operacyjnych kompatybilnych z Uniksem wyró n ż ia się trzy rodzaje urządzeń –
blokowe, znakowe i sieciowe. Zanim przejdziemy do opisu zagadnień związanych c ś i l
ś e z jądrem Linuksa, przedstawmy typową strukturę sprz t
ę owego urządzenia
wejścia – wyj c
ś ia biorąc za przykład architekturę i3861. Każde urządzenie, które współpracuje z procesorem jest z nim połączone przy pomocy magistrali I/O (ang. Input
– Output). Ta magistrala jest podzielona na trzy składowe: magistralę danych, adresową i sterowania. Procesory serii Pentium u y ż wają 16 z 32 linii do adresowania
urządzeń i 8, 16 lub 32 z 64 linii do przesyłania danych. Szyna wej c ś ia – wyj c
ś ia nie jest bezpośrednio połączona z urządzeniem lecz za pośrednictwem struktury sprz t
ę owej, która sk a
ł da się maksymalnie z trzech komponentów: portów I/O, interfejsu i/lub kontrolera. Porty są specjalnym zestawem adresów, które są przypisane danemu urz d
ą zeniu. W komputerach kompatybilnych z IBM PC mo n
ż a wykorzystać do 65536 portów 8 – bitowych, które mo n
ż a łączyć razem w wi k
ę sze jednostki.
Procesory Intela i kompatybilne z nimi obsługują porty za pomocą odrębnych rozkazów maszynowych, ale można również odwzorować je w przestrzeni adresowej pamięci operacyjnej2. Ten drugi sposób jest ch t
ę niej wykorzystywany, ponieważ jest szybszy i umożliwia wspó p ł racę z DMA. Porty wej c
ś ia – wyj c
ś ia są uło on
ż
e
w zestawy rejestrów umo l
ż iwiających komunikację z urządzeniem. Do typowych rejestrów należą: rejestr statusu, sterowania, wej c ś ia i wyj c
ś ia. Dosy
ć cz s
ę to zdarza si ,
ę
e
ż ten sam rejestr pe n
ł i dwie funkcje, np.: jednocze n
ś ie jest rejestrem wej c
ś iowym i wyj c
ś iowym lub rejestrem sterowania i stanu. Interfejsy I/O są układami elektronicznymi, które t u
ł maczą warto c
ś i w portach na polecenia dla urządzenia oraz wykrywają zmiany w stanie urządzenia i uaktualniają odpowiednio rejestr statusu. Dodatkowo są one połączone z kontrolerem przerwań i to one odpowiadają za zgłaszanie przerwania na rzecz urz d ą zenia. Istnieją dwa rodzaje interfejsów:
wyspecjalizowane, przeznaczone dla pewnego szczególnego urządzenia, jak np.: klawiatura, karta graficzna, dysk, mysz, karta sieciowa i interfejsy ogólnego przeznaczenia, które mogą obs u
ł giwać kilka różnych urządze ,
ń np.: port równoległy, szeregowy, magistrala USB, interfejsy PCMCIA i SCSI. W przypadku obsługi bardziej skomplikowanych urządzeń potrzebny jest kontroler, który interpretuje wysokopoziomowe polecenia otrzymywane z interfejsu I/O i przekształca je na szereg impulsów elektrycznych zrozumia y
ł ch dla urz d
ą zenia lub na podstawie sygnałów otrzymanych z urządzenia I/O modyfikuje zawartość rejestrów z nim związanych.
W systemach kompatybilnych z Uniksem urządzenia są traktowane jak pliki, tzn. są reprezentowane w systemie plików3 i są obsługiwane przez te same wywo a ł nia
systemowe co pliki. Pliki reprezentujące urz d
ą zenia są nazywane plikami specjalnymi lub po prostu plikami urządze .
ń Posiadają one, oprócz nazwy trzy atrybuty: typ
– okre l
ś ający, czy dane urządzenie jest blokowe, czy znakowe, główny numer urz d ą zenia (ang. major device number) oraz poboczny numer urządzenia (ang. minor device number). W jądrach Linuksa serii 2.6 te dwie ostatnie wartości są zapisywane w jednym 32 – bitowym słowie pamięci, przy czym 12 – bitów przeznaczonych jest na numer g ów
ł
ny, a kolejne 20 na numer poboczny. Pisz c
ą swój w a
ł sny sterownik urz d
ą zenia nie nale y
ż polegać na tym podziale, gdyż we wcześniejszych wersjach
Linuksa wielkość tego słowa była 16 – bitowa, a nie jest wykluczone, e
ż w przyszłych wersjach nie ulegnie ona zmianie, dlatego nale y
ż się zawsze pos u
ł giwać typem
dev_t i makrodefinicjami MAJOR, MINOR i MKDEV, które odpowiednio ustalają na podstawie zmiennej typu dev_t, wartość numeru głównego, wartość numeru pobocznego oraz łączą te numery w jedną wartość typu dev_t. Numer główny identyfikuje sterownik, który obsługuje dane urządzenie lub grupę urządze , ń natomiast
numer poboczny słu y
ż sterownikowi do ustalenia, które urz d
ą zenie z tej grupy jest w danej chwili obs u
ł giwane.
Urządzenia znakowe adresują dane sekwencyjnie i mogą je przesyłać wzgl d
ę nie ma y
ł mi porcjami o różnej wielko c
ś i. Są prostsze w obs u
ł dze, wi c
ę zostaną opisane jako
pierwsze, przed urządzeniami blokowymi.
Ka d
ż e urządzenie znakowe, które jest obecne w systemie, musi posiadać swój sterownik będący cz c
ęś ią jądra systemu. Mo e
ż on występować w dwóch postaciach: albo
mo e
ż być wkompilowany na stałe w obraz j d
ą ra lub być dołączany w postaci modu u
ł . Pierwszą czynno c
ś ią jaką musi taki sterownik wykonać jest uzyskanie jednego lub
większej liczby numerów urz d
ą zeń. Wykonuje to przy pomocy funkcji:
int register_chrdev_region(dev_t first, unsigned int count, char *name);
Parametr first oznacza wartość pierwszego numeru z puli jaka ma zostać przydzielona (jakie numery są już zajęte mo n ż a sprawdzić w dostarczanej z jądrem
dokumentacji oraz w pliku /proc/devices lub w katalogu /sys). Argument count okre l ś a liczb
ę numerów, a name nazw
ę urządzenia, które zostanie stowarzyszone z tymi
numerami. Je l
ś i operacja przydziału się powiedzie funkcja zwraca wartość „0”. Bardziej u y ż teczną i elastyczną jest inna funkcja pozwalająca na dynamiczne rezerwowanie numerów urządzeń:
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name); Parametr dev jest parametrem wyj c
ś iowym zawierającym (po wywołaniu zakończonym sukcesem) pierwszy numer z puli numerów przydzielonych urządzeniu, drugi parametr okre l
ś a wartość pobocznego numeru i zazwyczaj jest równy zero, pozostałe parametry mają takie samo znaczenie, jak w poprzedniej funkcji. Je l ś i numery
urządzeń nie b d
ę ą d u
ł
ej
ż potrzebne, nale y
ż je zwolnić przy pomocy funkcji:
void unregister_chrdev_region(dev_t first, unsigned int count);
Sterowniki urządzeń znakowych korzystają z trzech struktur związanych z VFS: obiektu pliku, struktury operacji pliku i obiektu i- węzła. Struktury te zostały opisane na poprzednim wyk a
ł dzie, teraz wyjaśnimy tylko sposób korzystania z nich, je l
ś i są wykorzystywane do operacji na urządzeniach a nie na zwykłych plikach. Struktura operacji na pliku powinna oczywi c
ś ie zawierać wskaźniki do metod służących do obsługi urządzenia. Jej polu owner powinna być przypisana wartość makrodefinicji THIS_MODULE, je l
ś i sterownik jest ładowany jako moduł. Zapobiega to usunięciu modu u
ł , w momencie gdy wykonywana jest jedna z metod. Najcz
c
ęś iej autorzy
sterowników urz d
ą zeń oprogramowywują cztery metody: open(), release(), read() i write(), choć nie jest to działanie obowiązkowe. Jeśli zachodzi potrzeba obsługi specyficznych dla danego urządzenia funkcji, które nie mogą być obsłużone przez wymienione wcze n ś iej metody, to implementowana jest metoda ioctl(). Część metod
mo e
ż pozosta
ć niezaimplementowana, wówczas ich wska n
ź ikom przypisujemy wartoś
ć NULL4, ale nale y
ż sprawdzi
ć w jaki sposób jądro obs u
ł guje takie przypadki, gdyż
dla ka d
ż ej metody ta obsługa mo e
ż być inna. W obiekcie pliku (struct file) b d
ę ą nas interesowa y
ł : pole mode, które zawiera prawa dost p
ę u do urządzenia, pole f_pos
zawierające wskaźnik bieżącej pozycji pliku, pole f_flags, zawierające flagi, pole f_ops, będące wska n ź ikiem do struktury metod, pole private_data i pole f_dentry b d
ę
c
ą e
wska n
ź ikiem na obiekt wpisu do katalogu. Pole mode mo e
ż być badane przez metodę open, ale nie jest to wymogiem – jądro samo sprawdza prawa dostępu do urządzenia. Podobnie rzadko korzysta się z pola flag, które określają czy operacje dotyczące urządzenia mają by ć blokujące, czy nieblokujące. Zawartoś
ć pola f_pos (64 –
bity) mo e
ż być zmieniana bezpo r
ś ednio tylko przez wywołanie llseek(), inne metody, takie jak read() i write() powinny obs u ł giwać go pośrednio, przez wska n
ź ik, który
jest im przekazywany jako ostatni argument. Pole private_data jest wska n
ź ikiem bez okre l
ś onego typu. Mo n
ż a je wykorzysta
ć do przechowywania adresu dynamicznie
przydzielonego obszaru pami c
ę i, w którym mo n
ż a przechowywać dane, które powinny odznaczać się trwało c
ś i ,
ą tzn. nie powinny być niszczone mi d
ę zy kolejnymi
wywo a
ł niami systemowymi. Przydzielenie pami c
ę i na te dane powinno być przeprowadzane w metodzie open() przy jej pierwszym wywołaniu, a zwolnienie w metodzie release() po ostatnim wywołaniu close(). Programi c
ś i piszący sterowniki nie muszą się martwić o inicjalizację pola f_dentry. W obiekcie i-w z ę ła (struct i-node) mo e
ż my
użyć pola i_rdev zawierającego numer urządzenia. Typ tego pola zmieniał się kilkukrotnie podczas rozwoju, więc obecnie, aby odczytać z obiektu i-węz a ł główny
i poboczny numer urządzenia nale y
ż u y
ż
ć następujących makr:
1
Nie jest to przykład idealny, ale najbardziej popularny.
2
Inne procesory, jak np.: procesory Motoroli obs u
ł gują urządzenia wyłącznie odwzorowując ich porty w pamięci operacyjnej. To pozwala na ujednolicenie obsługi urządzeń peryferyjnych i pamięci.
3
Za wyjątkiem interfejsów sieciowych.
4
Jedyną metodą, która zawsze musi pozosta
ć w sterowniku urządzenia nieoprogramowana jest metoda readdir().
1
Systemy Operacyjne – semestr drugi
unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);
Innym polem, które nale y
ż zainicjalizować w tym obiekcie jest wska n
ź ik i_cdev, wskazujący na strukturę jądra, która reprezentuje urz d
ą zenie znakowe. Taką strukturę
mo n
ż a stworzy
ć dynamicznie, za pomocą funkcji cdev_alloc(), lub statycznie za pomoc :
ą
void cdev_init(struct cdev *cdev, struct file_operations *fops);
W obu przypadkach trzeba zainicjalizować pole owner takiej struktury, które powinno mieć wartość makra THIS_MODULE. Inicjalizacja za pomocą cdev_alloc() wymaga również bezpośredniej inicjalizacji pola ops struktury cdev, które powinno wskazywać na strukturę metod obiektu pliku. Po stworzeniu cdev nale y ż dodać ją do
innych tego typu struktur przechowywanych przez jądro za pomocą funkcji:
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);
Komplementarną do niej jest funkcja void cdev_del(struct cdev *dev). Ka d ż e z urządzeń obsługiwanych przez sterownik musi być opisywane wewn t
ę rznie przez taką
strukturę. W starszych wersjach jądra rejestrowanie urządzenia nie wymagało tworzenia struktury cdev i odbywało się poprzez funkcję register_chrdev(). Usuni c ę ie
urządzenia odbywa o
ł się z kolei za po r
ś ednictwem unregister_chrdev(). Ten sposób nie będzie tu szerzej omawiany. Metody obs u ł gujące urządzenia powinny dzia a
ł ć
według określonego protokołu. Metoda open() powinna wykonywać następujące czynno c ś i:
Zidentyfikować urządzenie, które jest obsługiwane, czyli określi
ć jego numer poboczny.
Sprawdzi ,
ć czy nie wystąpiły specyficzne dla urz d
ą zenia bł d
ę y.
Zainicjalizować urządzenie, je l
ś i jest to pierwsze jego otwarcie.
Zaktualizować wska n
ź ik pozycji pliku, je l
ś i zachodzi taka koniecznoś .
ć
Zaalokować i wypełnić pamięć na dane prywatne, je l
ś i jest taka potrzeba.
Metoda release() powinna działać według nast p
ę ującego scenariusza:
Zwolnić pami
,
ęć która była przydzielana w metodzie open().
Wyłączy
ć (ang. shut down) urządzenie przy ostatnim wywołaniu close().
Również metody read() i write() muszą działać według pewnego „standardu”. Metoda read() powinna zwraca ć iloś
ć faktycznie przeczytanych informacji z urządzenia lub
bł d
ę y -EINTR (otrzymano sygnał) lub -EFAULT (bł d
ę ny adres). Podobnie powinna zachowywać się metoda write().
Z podobnych struktur i operacji korzystają sterowniki urządzeń blokowych, jednak ich obsługa jest bardziej skomplikowana, wi c ę część szczegółów zostanie
przedstawiona dopiero na następnym wykładzie. Urządzenia blokowe przesy a ł ją dane porcjami nazywanymi blokami (stąd nazwa urządze )
ń , których wielkość jest
parzystą wielokrotnością rozmiaru sektora5.
Pierwszą czynnością wykonywaną przez sterownik urządzenia blokowego jest pozyskanie numeru głównego, za pomocą wykonania funkcji register_blkdev() zadeklarowanej w pliku nagłówkowym <linux/fs.h>:
int register_blkdev(unsigned int major, const char *name);
Jeśli w wywołaniu wartość parametru major będzie równa zero, to jądro automatycznie przydzieli pierwszy wolny numer główny urządzeniu obsługiwanemu przez sterownik. Numer g ów
ł
ny urządzenia mo n
ż a zwolnić wywołując funkcję unregister_blkdev(), o prototypie:
int unregister_blkdev(unsigned int major, const char *name);
Urządzenia blokowej nie korzystają ze struktury operacji na pliku lecz posiadają własną strukturę, która jest jej odpowiednikiem. Ta struktura jest zdefiniowana w tym samym pliku nagłówkowym co funkcja register_blkdev() i nazywa się struct block_device_operations. Zawiera ona pole owner oraz wska n ź iki na funkcje open(), release(),
ioctl(), media_change() i revalidate_disk(). Metoda media_change() jest wywo y ł wana wówczas je l
ś i zmienił się no n
ś ik w urządzeniu, czyli działa tylko dla urządzeń
wymiennych, revalidate_disk() w odpowiedzi na wywo a
ł nie tej wcze n
ś iejszej.
Rolę struktury cdev dla urządzeń blokowych pe n
ł i struktura struct gendisk zdefiniowana w pliku nagłówkowym <linux/genhd.h>. Zawiera ona pola major (numer g ów
ł
ny urządzenia), first_minor (pierwszy numer poboczny), minors (liczba numerów pobocznych), disk_name (nazwa dysku – maksymalnie 32 znaki), fops (wska n ź ik
na strukturę struct block_device_operations), queue (wska n
ź ik na kolejkę żądań), flags (flagi – rzadko u y
ż wana), capacity (pojemność w sektorach), oraz private_data
(dane prywatne sterownika). Pole capacity nie powinno być modyfikowane bezpo r ś ednio, tylko za po r
ś ednictwem funkcji set_capacity(). Pamięć na tę strukturę jest
przydzielana za pomocą funkcji alloc_disk(), a zwalniana za pomocą del_gendisk(): struct gendisk *alloc_disk(int minors);
void del_gendisk(struct gendisk *gd);
Ka d
ż a taka struktura jest zwi z
ą ana z pojedynczym urz d
ą zeniem obs u
ł giwanym przez sterownik. Najcz
c
ęś iej jest to partycja dysku twardego. Aby takie urządzenie stało
się dostępne dla systemu nale y
ż przekazać tę strukturę do wywołania funkcji add_disk():
void add_disk(struct gendisk *gd);
5
Sektor ma najcz
c
ęś iej wielkość 512 bajtów.
2
Systemy Operacyjne – semestr drugi
Najwa n
ż iejszym polem tej struktury jest pole queue b d
ę ące wska n
ź ikiem na kolejkę żądań. Pami
ęć na tę kolejkę jest przydzielana za pomocą funkcji blk_init_queue():
request_queue_t *blk_init_queue(request_fn_proc *request, spinlock_t *lock); Pierwszym argumentem wywołania tej funkcji jest wska n
ź ik na funkcję request(), która odpowiedzialna jest za realizację pojedynczego żądania. Je l ś i sterownik
obsługuje urządzenia o rzeczywistym dost p
ę ie swobodnym, takie jak np. pamięć flash, to kolejka żądań jest zbędna. W takim przypadku pole queue struktury struct gendisk jest inicjalizowane za pomocą wywo a
ł nia funkcji blk_alloc_queue():
request_queue_t *blk_alloc_queue(int flags);
Sterownik powinien dostarczyć funkcji make_request(), która jest odpowiednikiem request(). Ta funkcja jest rejestrowana przez sterownik za pomocą wywo a ł nia funkcji
blk_queue_make_request():
void blk_queue_make_request(request_queue_t *queue, make_request_fn *func); Szczegóły budowy struktury opisującej pojedyncze żądanie oraz inne zagadnienia związane z obs u ł gą urządzeń blokowych zostaną opisane w nast p
ę nym wyk a
ł dzie.
3