Autor: Marcin Zaborowski

Urządzenia blokowe i znakowe

W linuxie mamy do czynienia z dwoma rodzajami urządzeń wejścia/wyjścia. Mają one następujące charakterystyki:
  1. 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
  2. urządzenia znakowe:
    • w jednej operacji WE/WY przenoszą dane o dowolnym rozmiarze,
    • adresują dane sekwencyjnie
Przykładowe urządzenia blokowe to dyski twarde, cd-romy, dyskietki. Natomiast znakowe są drukarki, klawiatury, ...
 
Spis treści

Pliki 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 usb

Block devices:
  2 fd
  3 ide0
 11 sr
 22 ide1

Widzimy, że konsole (ttyp) mają ten sam numer główny co pierwszy dysk twardy (ide0).
 


Spis treści

Tablice 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ści

Podprogramy 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.

  • blokowe:
    1. int (*open) (struct inode *, struct file *)

    2. 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.
    3. int (*release) (struct inode *, struct file *)

    4. 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ń).
    5. int (*ioctl) (struct inode *, struct file *, unsigned, unsigned long)

    6. Zmiana parametrów pracy urządzenia takich jak: zmiana prędkości przesyłania danych, formatowanie ścieżki, itp.
    7. int (*check_media_change) (kdev_t)

    8. Sprawdza czy od ostatniego wywołania read/write zmienił się nośnik.
    9. int (*revalidate) (kdev_t)

    10. Jeżeli check_media_change zwraca wartość niezerową ta funkcja ma na celu zająć się nieakualnymi już buforami.
  • znakowe:
    1. loff_t (*llseek) (struct file *, loff_t, int)

    2. 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.
    3. ssize_t (*read) (struct file *, char *, size_t, loff_t *)

    4. 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.
    5. ssize_t (*write) (struct file *, const char *, size_t, loff_t *)

    6. Analogicznie.
    7. int (*readdir) (struct file *, void *, filldir_t)

    8. Gdy VFS chce poznać zawartość katalogu woła tę funkcję.
    9. unsigned int (*poll) (struct file *, struct poll_table_struct *)

    10. 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().
    11. int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long)

    12. Tak jak w blokowych.
    13. int (*mmap) (struct file *, struct vm_area_struct *)

    14. Wołana z systemowego mmap(); służy do odwzorowywania pliku (tudzież jego części) do pamięci bieżącego procesu.
    15. int (*open) (struct inode *, struct file *)

    16. Tak jak w blokowych.
    17. int (*flush) (struct file *)

    18. Uruchamia się przy systemowym close() niezależnie od licznika odwołań. Wypróżnianie buforów związanych z urządzeniem.
    19. int (*release) (struct inode *, struct file *)

    20. Tak jak w blokowych.
    21. int (*fsync) (struct file *, struct dentry *, int datasync)

    22. Wywoływana przez systemowy fsync(). Kopiuje wewnątrzrdzeniowe części pliku na dysk (cokolwiek to znaczy).
    23. int (*fasync) (int, struct file *, int)

    24. 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().
    25. int (*lock) (struct file *, int, struct file_lock *)

    26. Służy do zakłądania blokad wyłączności na plik. Wołana w systemowej funkcji fcntl().
    27. ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *)

    28. Czytanie danych z pliku i przekazanie treści w wektorze (nie jeden bufor, ale ich tablica). Korzysta z tej funkcji systemowy readv().
    29. ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *)

    30. Analogicznie.
    31. ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int)

    32. Wywołuje ją systemowa funkcja sendfile(). Służy do przesyłania strony w pamięci procesu.
    33. unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long)

    34. Zwraca zakres w przestrzeni adresowej, który jest aktualnie nieodwzorowany.  Woła ją systemowa funkcja mmap().
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).