SO2_wyklad_12.pdf

(53 KB) Pobierz
Systemy Operacyjne – semestr drugi
Wykład dwunasty
Urządzenia znakowe i blokowe w Linuksie
Jednym z zastosowań wirtualnego systemu plików opisanego na poprzednim wykładzie jest obsługa urządzeń wejścia – wyjścia. 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óżnia się trzy rodzaje urządzeń –
blokowe, znakowe i sieciowe. Zanim przejdziemy do opisu zagadnień związanych ściśle z jądrem Linuksa, przedstawmy typową strukturę sprzętowego urządzenia
wejścia – wyjścia biorąc za przykład architekturę i386
1
. 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żywają 16 z 32 linii do adresowania
urządzeń i 8, 16 lub 32 z 64 linii do przesyłania danych. Szyna wejścia – wyjścia nie jest bezpośrednio połączona z urządzeniem lecz za pośrednictwem struktury
sprzętowej, która składa 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ądzeniu. W komputerach kompatybilnych z IBM PC można wykorzystać do 65536 portów 8 – bitowych, które można łączyć razem w większe 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 operacyjnej
2
. Ten drugi sposób jest chętniej wykorzystywany, ponieważ jest szybszy i umożliwia współpracę z DMA. Porty wejścia – wyjścia są ułożone
w zestawy rejestrów umożliwiających komunikację z urządzeniem. Do typowych rejestrów należą: rejestr statusu, sterowania, wejścia i wyjścia. Dosyć często zdarza się,
że ten sam rejestr pełni dwie funkcje, np.: jednocześnie jest rejestrem wejściowym i wyjściowym lub rejestrem sterowania i stanu. Interfejsy I/O są układami
elektronicznymi, które tłumaczą wartości 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ądzenia. 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ługiwać 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łych dla urządzenia 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ów
3
i są obsługiwane przez te same wywołania
systemowe co pliki. Pliki reprezentujące urządzenia są nazywane plikami specjalnymi lub po prostu plikami urządzeń. Posiadają one, oprócz nazwy trzy atrybuty: typ
– określający, czy dane urządzenie jest blokowe, czy znakowe, główny numer urządzenia (
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łówny, a kolejne 20 na numer poboczny. Pisząc swój własny sterownik urządzenia 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ługiwać 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ądzenia lub grupę urządzeń, natomiast
numer poboczny służy sterownikowi do ustalenia, które urządzenie z tej grupy jest w danej chwili obsługiwane.
Urządzenia znakowe adresują dane sekwencyjnie i mogą je przesyłać względnie małymi porcjami o różnej wielkości. Są prostsze w obsłudze, więc zostaną opisane jako
pierwsze, przed urządzeniami blokowymi. Urządzeniami sieciowymi nie będziemy się zajmować.
Każde urządzenie znakowe, które jest obecne w systemie, musi posiadać swój sterownik będący częścią jądra systemu. Może on występować w dwóch postaciach: albo
może być wkompilowany na stałe w obraz jądra lub być dołączany w postaci modułu. Pierwszą czynnością jaką musi taki sterownik wykonać jest uzyskanie jednego lub
większej liczby numerów urządzeń. 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żna sprawdzić w dostarczanej z jądrem
dokumentacji oraz w pliku
/proc/devices
lub w katalogu /sys). Argument
count
określa liczbę numerów, a
name
nazwę urządzenia, które zostanie stowarzyszone z tymi
numerami. Jeśli operacja przydziału się powiedzie funkcja zwraca wartość „0”. Bardziej użyteczną 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ściowym zawierającym (po wywołaniu zakończonym sukcesem) pierwszy numer z puli numerów przydzielonych urządzeniu, drugi
parametr określa wartość pobocznego numeru i zazwyczaj jest równy zero, pozostałe parametry mają takie samo znaczenie, jak w poprzedniej funkcji. Jeśli 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ładzie, teraz wyjaśnimy tylko sposób korzystania z nich, jeśli są wykorzystywane do operacji na urządzeniach a nie na zwykłych plikach. Struktura
operacji na pliku powinna oczywiście zawierać wskaźniki do metod służących do obsługi urządzenia. Jej polu
owner
powinna być przypisana wartość makrodefinicji
THIS_MODULE, jeśli sterownik jest ładowany jako moduł. Zapobiega to usunięciu modułu, w momencie gdy wykonywana jest jedna z metod. Najczęściej autorzy
sterowników urządzeń 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śniej metody, to implementowana jest metoda
ioctl().
Część metod
może pozostać niezaimplementowana, wówczas ich wskaźnikom przypisujemy wartość NULL
4
, ale należy sprawdzić w jaki sposób jądro obsługuje takie przypadki, gdyż
dla każdej 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ępu 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źnikiem do struktury metod, pole
private_data
i pole
f_dentry
będące
wskaźnikiem 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średnio tylko przez wywołanie
llseek(),
inne metody, takie jak
read()
i
write()
powinny obsługiwać go pośrednio, przez wskaźnik, który
jest im przekazywany jako ostatni argument. Pole
private_data
jest wskaźnikiem bez określonego typu. Można je wykorzystać do przechowywania adresu dynamicznie
przydzielonego obszaru pamięci, w którym można przechowywać dane, które powinny odznaczać się trwałością, tzn. nie powinny być niszczone między kolejnymi
wywołaniami systemowymi. Przydzielenie pamięci 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ści piszący sterowniki nie muszą się martwić o inicjalizację pola
f_dentry.
W obiekcie i-węzła (
struct i-node)
możemy
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
2
3
4
Nie jest to przykład idealny, ale najbardziej popularny.
Inne procesory, jak np.: procesory Motoroli obsługują urządzenia wyłącznie odwzorowując ich porty w pamięci operacyjnej. To pozwala na ujednolicenie obsługi
urządzeń peryferyjnych i pamięci.
Za wyjątkiem interfejsów sieciowych.
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źnik
i_cdev,
wskazujący na strukturę jądra, która reprezentuje urządzenie znakowe. Taką strukturę
można 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żde z urządzeń obsługiwanych przez sterownik musi być opisywane wewnętrznie 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ęcie
urządzenia odbywało się z kolei za pośrednictwem
unregister_chrdev().
Ten sposób nie będzie tu szerzej omawiany. Metody obsługujące urządzenia powinny działać
według określonego protokołu. Metoda
open()
powinna wykonywać następujące czynności:
Zidentyfikować urządzenie, które jest obsługiwane, czyli określić jego numer poboczny.
Sprawdzić, czy nie wystąpiły specyficzne dla urządzenia błędy.
Zainicjalizować urządzenie, jeśli jest to pierwsze jego otwarcie.
Zaktualizować wskaźnik pozycji pliku, jeśli zachodzi taka konieczność.
Zaalokować i wypełnić pamięć na dane prywatne, jeśli jest taka potrzeba.
Metoda
release()
powinna działać według następują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łędy -EINTR (otrzymano sygnał) lub -EFAULT (błędny 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łają dane porcjami nazywanymi blokami (stąd nazwa urządzeń), których wielkość jest
5
parzystą wielokrotnością rozmiaru sektora .
Pierwszą czynnością wykonywaną przez sterownik urządzenia blokowego jest pozyskanie numeru głównego, za pomocą wykonania funkcji r
egister_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łówny urządzenia można 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źniki na funkcje
open(), release(),
ioctl(), media_change()
i
revalidate_disk().
Metoda
media_change()
jest wywoływana wówczas jeśli zmienił się nośnik w urządzeniu, czyli działa tylko dla urządzeń
wymiennych,
revalidate_disk()
w odpowiedzi na wywołanie tej wcześniejszej.
Rolę struktury
cdev
dla urządzeń blokowych pełni struktura
struct gendisk
zdefiniowana w pliku nagłówkowym <linux/genhd.h>. Zawiera ona pola
major
(numer
główny urządzenia),
first_minor
(pierwszy numer poboczny),
minors
(liczba numerów pobocznych),
disk_name
(nazwa dysku – maksymalnie 32 znaki),
fops
(wskaźnik
na strukturę
struct block_device_operations), queue
(wskaźnik na kolejkę żądań),
flags
(flagi – rzadko używana),
capacity
(pojemność w sektorach), oraz
private_data
(dane prywatne sterownika). Pole
capacity
nie powinno być modyfikowane bezpośrednio, tylko za pośrednictwem 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żda taka struktura jest związana nie z pojedynczym urządzeniem obsługiwanym przez sterownik. Najczęściej 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ęściej wielkość 512 bajtów.
2
Systemy Operacyjne – semestr drugi
Najważniejszym polem tej struktury jest pole
queue
będące wskaźnikiem 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źnik na funkcję
request(),
która odpowiedzialna jest za realizację pojedynczego żądania. Jeśli sterownik
obsługuje urządzenia o rzeczywistym dostępie 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łania 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łania 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ługą urządzeń blokowych zostaną opisane w następnym wykładzie.
3
Zgłoś jeśli naruszono regulamin