Autor: Marcin ZaborowskiUrządzenia blokowe i znakowe
W linuxie mamy do czynienia z dwoma rodzajami urządzeń wejścia/wyjścia. Mają one następujące charakterystyki:Przykładowe urządzenia blokowe to dyski twarde, cd-romy, dyskietki. Natomiast znakowe są drukarki, klawiatury, ...
- urządzenia blokowe:
- mogą przenosić w jednej operacji WE/WY bloki danych o stałej wielkości,
- bloki przechowywane na urządzeniu można swobodnie adresować: czas potrzebny do przeniesienia bloku danych można uznać za niezależny od adresu i aktualnego stanu urządzenia
- urządzenia znakowe:
- w jednej operacji WE/WY przenoszą dane o dowolnym rozmiarze,
- adresują dane sekwencyjnie
Spis treściPliki specjalne
Unixowe systemy operacyjne oparte są na pojęciu pliku. Dlatego urządzenia WE/WY są w linuxie traktowane tak jak pliki. Oznacza to tyle, że wywołanie systemowe funkcji write() może być używane i do drukarki, i do zwykłego pliku na dysku; drukarkę "widzimy" wówczas, np. jako /dev/lp0. Pliki reprezentujące urządzenia WE/WY noszą nazwę plików specjalnych (bądź też: plików urządzeń). Oprócz nazwy mają one trzy podstawowe atrybuty (są one częścia i-węzła tego pliku specjalnego):
Typ urządzenie blokowe lub znakowe Główny numer urządzenia (major) liczba z zakresu od 1 do 255, która określa rodzaj urządzenia; urządzenia o tym samym numerze głownym współdzielą operacje siebie dotyczące Podrzędny numer urządzenia (minor) liczba ta identyfikuje określone urządzenie w grupie urządzeń o tym samym numerze głównym Pliki urządzeń tworzy się funkcją mknod(), która wywołuje w jądrze funkcję sys_mknod(), a ta z kolei (jeżeli wszystko pójdzie dobrze) vfs_mknod(), która między innymi wywołuje funkcję init_special_inode() (linux/fs/devices.c), o której powiemy coś szerzej przy omawianiu tablic rozdzielczych . Mknod() pobiera jako argumenty nazwę pliku, jego typ oraz liczbę typu dev_t, będącą połączonym numerem głównym i podrzędnym (główny na starszych bitach). Przy obrabianiu ostatniego parametru z pomocą przychodzą makra: MAJOR, MINOR i MKDEV (zdefiniowane w include/linux/kdev_t.h). O znaczeniu tych numerów też przy tablicach rozdzielczych.
Większość (a zazwyczaj wszystkie) pliki specjalne znajdują się w katalogu /dev. Oto przykład (pokazujący, że ten sam numer główny mogą mieć urządzenia znakowe i blokowe).
[z@localhost linux]$ cat /proc/devices
Character devices:
1 mem
2 pty
3 ttyp
4 ttyS
5 cua
6 lp
7 vcs
10 misc
13 input
14 sound
21 sg
36 netlink
81 video_capture
108 ppp
128 ptm
136 pts
162 raw
180 usbBlock devices:
2 fd
3 ide0
11 sr
22 ide1Widzimy, że konsole (ttyp) mają ten sam numer główny co pierwszy dysk twardy (ide0).
Spis treściTablice rozdzielcze
Dla urządzeń znakowych (fs/devices.c):
struct device_struct {
const char * name;
struct file_operations * fops;
};static struct device_struct chrdevs[MAX_CHRDEV];
Dla blokowych (fs/block_dev.c):
static struct {
const char *name;
struct block_device_operations *bdops;
} blkdevs[MAX_BLKDEV];W strukturach tych przetrzymywane są adresy funkcji obsługujących żądania WE/WY dla odpowiednich urządzeń (indeks tablicy odpowiada numerowi głównemu urządzenia, np. informacje dotyczące pierwszego twardego dysku z przykładu są w blkdevs[3]). Do modyfikowania tablic rozdzielczych służą funkcje register_chrdev(), register_blkdev() oraz ich dualne odpowiedniki z przedrostkami un (definicje w linux/fs/devices.c i linux/fs/block_dev.c). Ich szerszy opis znaleźć można w dalszej części dokumentu. Wskaźnik do struktury file_operations poza tablicą rozdzielczą występuje jeszcze w i-węźle (jest częścią struktury inode). Inicjalizuje się go przy wywołaniu funkcji init_special_inode() (z mknod()). Funkcja ta wstawia adresy domyślnych podprogramów obsługi urządzeń: dla znakowych def_chr_fops, dla blokowych: def_blk_fops, zdefiniowane w linux/fs/devices.c i linux/fs/block_dev.c. W związku z tym operacje dotyczące urządzeń blokowych opisywane są przez obie struktury (file_operations oraz block_device_operations). Jądro nie dostarcza żadnej funkcji do zmiany wskaźnika z i-węzła, skąd konkluzja, że dla urządzeń blokowych domyślne funkcje w zupełności wystarczają. Przy poszukiwaniu właściwej funkcji najpierw zagląda się do tablicy rozdzielczej, co powoduje, że jeżeli przy wołaniu register_chrdev() podano podprogram obsługi urządzenia, to informacje z i-węzła są zakryte.
Spis treściPodprogramy obsługi urządzeń.
include/linux/fs.h:
struct block_device_operations {
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
int (*ioctl) (struct inode *, struct file *, unsigned, unsigned long);
int (*check_media_change) (kdev_t);
int (*revalidate) (kdev_t);
};struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, struct dentry *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, \
unsigned long, unsigned long);
};To co widać wyżej to zestawy funkcji, których używa system do komunikacji ze sprzętem. Omówimy teraz pokrótce ich cele.
Członkiem struktury file_operations jest jeszcze struct module *owner, który wskazuje na moduł implementujący metody stuktury file_operations (o ile taki w ogóle istnieje, może to być przecież bezpośrednio w jądrze). Przy wywołaniu makra fops_get (include/linux/fs.h) zwiększa się wtedy licznik odwołań do modułu, ażeby go za szybko nie usunąć. Dualnym makrem jest fops_put (znów fs.h).
- blokowe:
- int (*open) (struct inode *, struct file *)
Ma rozpocząć połączenie między procesem a urządzeniem, uprzednio sprawdziwszy poprawność działania drugiego i jego struktur danych (inicjalizacja, jeśli to konieczne). Wywołuje ją systemowa funkcja open() po zwiększeniu liczby odwołań do danego urządzenia w i-węźle odpowiadającego mu pliku specjalnego.- int (*release) (struct inode *, struct file *)
Koniec korzystania z urządzenia. Wywołuje ją systemowa funkcja close() wtedy, gdy zamykane jest ostatnie połączenie z urządzeniem (sprawdzany jest zatem licznik odwołań).- int (*ioctl) (struct inode *, struct file *, unsigned, unsigned long)
Zmiana parametrów pracy urządzenia takich jak: zmiana prędkości przesyłania danych, formatowanie ścieżki, itp.- int (*check_media_change) (kdev_t)
Sprawdza czy od ostatniego wywołania read/write zmienił się nośnik.- int (*revalidate) (kdev_t)
Jeżeli check_media_change zwraca wartość niezerową ta funkcja ma na celu zająć się nieakualnymi już buforami.- znakowe:
- loff_t (*llseek) (struct file *, loff_t, int)
Wołana jest gdy VFS chce zmienić pozycję w pliku. We wcześniejszych wersjach jądra nie było struktury block_device_operations, niezrozumiałe jest zatem położenie tej funkcji. Nie ma ona sensu dla urządzeń znakowych.- ssize_t (*read) (struct file *, char *, size_t, loff_t *)
Wywoływana przez systemowego read()'a. Czytanie z urządzenia. Jej implementacja różni się znacznie w zależności od typu urządzenia. Algorytm block_read omówiony zostanie później.- ssize_t (*write) (struct file *, const char *, size_t, loff_t *)
Analogicznie.- int (*readdir) (struct file *, void *, filldir_t)
Gdy VFS chce poznać zawartość katalogu woła tę funkcję.- unsigned int (*poll) (struct file *, struct poll_table_struct *)
Wołana gdy proces chce sprawdzić, czy dany plik jest w tej chwili wykorzystywany i (opcjonalnie) chce zasnąć do momentu jego zwolnienia. Wykonuje się przy systemowych funkcjach select() oraz poll().- int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long)
Tak jak w blokowych.- int (*mmap) (struct file *, struct vm_area_struct *)
Wołana z systemowego mmap(); służy do odwzorowywania pliku (tudzież jego części) do pamięci bieżącego procesu.- int (*open) (struct inode *, struct file *)
Tak jak w blokowych.- int (*flush) (struct file *)
Uruchamia się przy systemowym close() niezależnie od licznika odwołań. Wypróżnianie buforów związanych z urządzeniem.- int (*release) (struct inode *, struct file *)
Tak jak w blokowych.- int (*fsync) (struct file *, struct dentry *, int datasync)
Wywoływana przez systemowy fsync(). Kopiuje wewnątrzrdzeniowe części pliku na dysk (cokolwiek to znaczy).- int (*fasync) (int, struct file *, int)
W przypadku, gdy plik jest otwarty z asynchroniczną obsługą żądań WE/WY nie można stosować fsync(), stosuje się więc fasync(), którą woła systemowa funkcja fcntl(). Działanie analogiczne do fsync().- int (*lock) (struct file *, int, struct file_lock *)
Służy do zakłądania blokad wyłączności na plik. Wołana w systemowej funkcji fcntl().- ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *)
Czytanie danych z pliku i przekazanie treści w wektorze (nie jeden bufor, ale ich tablica). Korzysta z tej funkcji systemowy readv().- ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *)
Analogicznie.- ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int)
Wywołuje ją systemowa funkcja sendfile(). Służy do przesyłania strony w pamięci procesu.- unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long)
Zwraca zakres w przestrzeni adresowej, który jest aktualnie nieodwzorowany. Woła ją systemowa funkcja mmap().