Podsystem Wejścia/Wyjścia w systemie Linux 2.4.7 | ||
Urządzenia znakowe i blokowe. Funkcja block_read(), block_write(). | ||
2. Odwołania do urządzenia przez pliki specjalne | ||
< Poprzednia strona | Spis treści | Następna strona > |
Aby urządzenie mogło być używane, najpierw musi zostać zarejestrowane w systemie. Rejestracja urządzenia oznacza związanie zestawu potrzebnych do jego obsługi funkcji z odpowiednim numerem pierwszorzędnym (i oczywiście typem urządzenia, mechanizm rejestracji został opisany w rozdziale 3). Funkcje te umożliwią jądru abstrakcję urządzenia jako pliku. Przyjrzyjmy się dokładniej działaniu tego mechanizmu.
Jak wiadomo, z każdym plikiem w wirtualnym systemie plików Linuxa związana jest struktura i-węzła.
W momencie, gdy i-węzeł wczytywany jest z dysku i okazuje się, że odpowiada on pewnemu urządzeniu
(lub kolejce FIFO itp.), informacja ta jest zawarta w polu i_mode struktury inode), wywoływana jest
funkcja init_special_inode(). Dobrze obrazuje to fragment pliku
fs/ext2/inode.c. Jest to fragment funkcji
ext2_read_inode() realizująca odczyt i-węzła w systemie plików ext2.
...
else if (S_ISREG(inode->i_mode)) {
...
} else if (S_ISDIR(inode->i_mode)) {
...
} else if (S_ISLNK(inode->i_mode)) {
...
} else
init_special_inode(inode, inode->i_mode,
le32_to_cpu(raw_inode->i_block[0]));
Przyjrzyjmy się teraz funkcji init_special_inode() (znajduje się ona w pliku fs/devices.c).
void init_special_inode(struct inode *inode, umode_t mode, int rdev)
{
inode->i_mode = mode;
if (S_ISCHR(mode)) {
inode->i_fop = &def_chr_fops;
inode->i_rdev = to_kdev_t(rdev);
inode->i_cdev = cdget(rdev);
} else if (S_ISBLK(mode)) {
inode->i_fop = &def_blk_fops;
inode->i_rdev = to_kdev_t(rdev);
inode->i_bdev = bdget(rdev);
} else if (S_ISFIFO(mode))
inode->i_fop = &def_fifo_fops;
else if (S_ISSOCK(mode))
inode->i_fop = &bad_sock_fops;
else
printk(KERN_DEBUG "init_special_inode: bogus imode (%o)\n", mode);
}
Jak widać funkcja ustawia pole i_rdev, na numer urządzenia (w liczbie zwracanej przez funkcję to_kdev_t
(zdefiniowaną w pliku include/linux/k_dev_t.h) zawarty jest zarazem
numer pierwszorzędny). Oprócz tego funkcja ustawia pola i_bdev lub i_cdev (o których później),
oraz ustawia pole i_fop. Przyjrzyjmy się dokładniej polu i_fop. To pole i-węzła zawiera zestaw funkcji
potrzebnych do operowania na pliku. Odpowiednie funkcje z tej struktury wywoływane są przez znane
wszystkim funkcje open, close, read, write itd. W kodzie funkcji widzimy, że w zależności od typu
urządzenia, pole i_fop ustawiane jest na &def_chr_fops, &def_blk_fops. Przyjrzyjmy się tym strukturom
z bliska.
Definicja struktury def_chr_fops znajduje się w pliku fs/devices.c. Wygląda ona
następująco:
static struct file_operations def_chr_fops = {
open: chrdev_open,
};
Struktura ta zawiera jedynie funkcje wywoływaną przy otwieraniu pliku wykorzystywaną np. przez sys_open().
Funkcja chrdev_open ma następujący nagłówek: int chrdev_open(struct inode * inode, struct file * filp)
i zajmuje się tym by strukturze file ustawić pole fops na wskaźnik do odpowiedniej
struktury dostarczonej jądru podczas rejestrowania urządzenia (rejestrację omówię później).
Następnie uruchamia funkcję open z dostarczonej struktury.
Struktura def_blk_fops (zdefiniowana w pliku fs/block_dev.c zawiera już dużo
więcej pól.
struct file_operations def_blk_fops = {
open: blkdev_open,
release: blkdev_close,
llseek: block_llseek,
read: block_read,
write: block_write,
fsync: block_fsync,
ioctl: blkdev_ioctl,
};
Wszystkie funkcje mają na celu umożliwienie traktowania urządzenia jako pliku specjalnego.
Skąd taka znacząca różnica pomiędzy tymi strukturami?
Wynika ona z innego sposobu komunikacji jądra z urządzeniami znakowymi niż blokowymi. Urządzenia znakowe
dostarczają podczas rejestracji wszystkie potrzebne funkcje w strukturze file operations.
Realizują one funkcjonalność potrzebną do implementowania odwołań do tych urządzeń przez pliki
specjalne. Sterowniki urządzeń blokowych zachowują się inaczej. Podczas
rejestracji urządzenia nie są dostarczane funkcje umożliwiające bezpośrednio wykonanie
operacji odczytu i zapisu do urządzenia blokowego. Jak więc jądro dokonuje operacji wejścia/wyjścia z
urządzeniem blokowym? Dla każdego urządzenia blokowego istnieje w jądrze kolejki
zleceń do tego urządzenia. W momencie inicjalizacji obsługi urządzenia blokowego sterownik oprócz
rejestracji tego urządzenia inicjalizuje kolejkę zleceń, oraz rejestruje funkcję uruchamianą przez jądro,
która powinna realizować zlecenia z tej kolejki. Co więcej w zleceniu
można poprosić o ilość danych będącą tylko wielokrotnością wielkości bloku. Przykład inicjalizacji
urządzenia blokowego opisany został w rodziale 4.
Przyczynami wprowadzenia takiego mechanizmu są znaczące opóźnienia w czasie realizacji zlecenia odczytu/zapisu dla urządzeń blokowych. Urządzenia blokowe to przeważnie bardzo wolne urządzenia: dyski twarde, dyskietki CDROMy i streamery itp., dla których pojawiła się potrzeba realizacji buforowania i zastosowania innych bardziej zaawansowanych technik w celu optymalizacji czasu potrzebnego jądru do realizowania operacji wejścia/wyjścia. Ponieważ podczas rejestracji urządzeń blokowych sterownik urządzenia nie dostarcza funkcji umożliwiających implementację dostępu do urządzenia przez plik specjalny, zaistniała potrzeba zdefiniowania ich w strukturze def_blk_fops (opis tych funkcji znajduje się w rozdziałach 5-8). Na obecnym poziomie abstrakcji wystarczy przyjąć, że funkcje te opakowują niewygodny mechanizm dostarczony przez sterownik urządzenia.