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