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 >

2. Odwołania do urządzenia przez pliki specjalne


2.1 Funkcje umożliwiające dostęp do pliku specjalnego

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.



2.2 Struktura def_chr_fops

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.



2.3 Struktura def_blk_fops

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.


Autor: Filip Łukasik