Autor: Dominik Kamiński

Obsługa urządzenia blokowego

Wstęp

Urządzenia blokowe to np. dyski twarde, dyski CD-ROM, dyskietki. Skąd to odróżnienie od innych urządzeń. Otóż dwie zasadnicze cechy decydują o ich odmienności:
  • - Po pierwsze dane przechowywane są na urządzeniu w postaci bloków, to znaczy części o z góry znanym rozmiarze. Oznacza to, że komunikacja pomiędzy urządzeniem a pamięcią RAM jest także realizowana w sposób blokowy. W Linuksie na rozmiar bloku nalożone sa następujące wymagania:
    • nie może być większy niż blok stronicowy
    • musi być potęgą liczby dwa
    • musi być wielokrotnością rozmiaru sektora, czyli podstawowej jednostki transferu danych (jego wielkość dla każdego urządzenia jest przechowywana w tablicy extern int *hardsect_size).
    Jądro przechowuje rozmiary bloków urządzeń w tablicy extern int *blksize_size, która jest indeksowana numerem głównym i podrzędnym urządzenia. Jeśli blksize_size[M] == NULL, to wszystkie urządzenia o numerze głównym M mają rozmiar bloku równy domyślnemu BLOCK_SIZE (jest to 1024 bajty).
  • - Drugą wyróżniającą cechą jest swobodne adresowanie danych na urządzeniu, co oznacza dostęp w zamortyzowanym czasie stałym do dowolnego bloku danych. Możliwie krótki czas dostępu zapewniają odpowiednie procedury szeregowania zleceń i czytanie z wyprzedzeniem.

Spis treści

Struktury

Podstawową tablicą do spamiętywania danych o urządzeniach blokowych jest
static struct {
        const char *name;                       /*nazwa urządzenia*/
        struct block_device_operations *bdops;  /*wskaźnik do funkcji obsługi*/
} blkdevs[MAX_BLKDEV]
gdzie: MAX_BLKDEV=255
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);
};
Jądro używa jeszcze jednej struktury dla urządzen blokowych:
struct block_device {
        struct list_head        bd_hash;
        atomic_t                bd_count;
/*      struct address_space    bd_data; */
        dev_t                   bd_dev;  /* not a kdev_t - it's a search key */
        atomic_t                bd_openers;
        const struct block_device_operations *bd_op;
        struct semaphore        bd_sem; /* open/close mutex */
};
Służy ona do pamiętania charakterystycznych danych o urządzeniu w i-weźle pliku specjalnego.
Spis treści

Rejestracja

Po stworzeniu pliku specjalnego dla urządzenia (czynności te omówione są w rozdziale dotyczącym plików specjalnych) musimy wskazać systemowi program komunikacji z tym urządzeniem.
Do rejestrowania klasy urządzeń blokowych o danym numerze głównym, czyli wypełniania tablicy blkdevs, służy funkcja

int register_blkdev(unsigned int major, const char * name, struct block_device_operations *bdops) {...}

Jej wywołanie powoduje wstawienie nazwy (name) urządzenia i wskaźnika do struktury block_device_operation na miejsce o numerze major (numer główny rejestrowanej klasy urządzeń) w tablicy block_devs . Jeśli pierwszy argument jest zerem, funkcja próbuje przydzielić największy wolny numer mniejszy od MAX_BLKDEV =255, zostawiając miejsce o numerze MAX_BLKDEV wolne. Odtąd jądro może korzystać z charakterystycznych funkcji urządzenia wskazanych przez bdops.
Do odrejestrowywania służy funkcja:

int unregister_chrdev(unsigned int major, const char * name) {...}


Spis treści

Funkcje open() i close()

Jeśli funkcja open() pojawi się w aplikacji, to powoduje wywołanie funkcji

asmlinkage long sys_open(const char * filename, int flags, int mode)

z jądra, która przez wywołanie funkcji

struct file *filp_open(const char * filename, int flags, int mode)

tworzy nową strukturę zawierającą informacje o otwartym pliku, kopiuje odpowiedni i_node do pamięci, przepisuje z niego domyślne funkcje obsługi

f->f_op = fops_get(inode->i_fop);
po czym wywołuje funkcję blkdev_open() , która znajduje się w domyślnych funkcjach obsługi dla pliku specjalnego urządzenia blokowego:
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,
\};
Funkcja blkdev_open() instaluje odpowiednie dla pliku operacje z tablicy blkdevs w jednym z pól inode'a ( i_bdev->bd_op ) i wywołuje open() charakterystyczne dla urządzenia.
Każdemu udanemu wywołaniu funkcji open() powinno towarzyszyć wywołanie funkcji close() , której odpowiednik w jądrze, czyli sys_close() , zwalnia deskryptor pliku, zmniejsza liczbę odwołan do pliku, jeśli liczba odwołań do pliku jest równa zero woła funkcję release() charakterystyczną dla każdego urządzenia.
Spis treści

Obsługa żądania

Gdy aplikacja wywoła funkcję read() lub write() natychmiast jest wywołana odpowiednia funkcja jądra systemu: sys_read() lub sys_write() . Funkcje te po sprawdzeniu poprawności deskryptora i uprawnień procesu przez znajomość numeru głównego i podrzędnego urządzenia wywołają odpowiednią funkcję odczytu lub zapisu.
W przypadku urządzenia blokowego są to funkcje block_read() i block_write() , które dokładnie omówione będą w rozdziale im poświęconym. Ostatnia para funkcji działa zgrubsza tak, że najpierw odczytuje dane charakterystyczne dla urządzenia takie jak: numer główny i podrzędny, wielkość bloku itp, następnie funkcją getblk() sprawdzają, czy danego bloku nie ma już w pamięci podrecznej (pobierając adres bloku w pamięci), jeśli go nie ma, to przez wywołanie funkcji ll_rw_block() tworzy żadanie i wstawia je do kolejki żądan. Dalej proces musi czekać, aż jego żądanie zostanie obsłużone. Procedura strategii obsługi żądań charakterystyczna dla każdego urządzenia jest ukryta w strukturze:
struct blk_dev_struct {
        /*
         * queue_proc has to be atomic
         */
        request_queue_t request_queue;  /*procedura strategii*/
        queue_proc              *queue; /*kolejka żądań*/
        void                    *data;
\};
Szeregowanie żądań i procedura strategii będą dokladniej omówione w punkcie im poświęconym.
Spis treści

Obsługa urządzenia znakowego

Wstęp

Urządzenia znakowe to urządzenia o dostępie sekwencyjnym, jednak nie mają konkretniejszej definicji, jak urządzenia blokowe, dlatego często mówi się, ze urządzenie znakowe to takie, które nie jest blokowe. Każde z nich w sposób istotny różni się od pozostalych. Dlatego po krótkim ogólnym omówieniu zajmę się urządzeniami terminalowymi.
Spis treści

Ogólne omówienie

Funkcje rejestracji:
int register_chrdev(unsigned int major, const char * name,
                    struct file_operations *fops)

int unregister_chrdev(unsigned int major, const char * name)
dzialają analogicznie do odpowiednich funkcji urządzenia blokowego. Podobny jest także ogólny szkic działania funkcji open(), read(), write(), close(). Działanie open różni się tym, że jedyna domyślna funkcja obsługi urządzeń znakowych
static struct file_operations def_chr_fops = {
        open:           chrdev_open,
};
podstawia charakterystyczne operacje obsługi do struktury file , a nie do i-wezła. Drugą różnicą jest to, iż nie istnieją dla urządzeń znakowych standardowe funkcje takie jak block_read() i block_write() , dlatego sys_read() i sys_write() wołają bezpośrednio funkcje obsługi charakterystyczne dla urządzenia, które funkcja chrdev_open() umieściła pod wskaźnikiem do tablicy operacji odpowiedniej struktury file otwartego deskryptora pliku specjalnego.
Spis treści

Urządzenia terminalowe

Terminale to jedne z najważniejszych urządzeń znakowych. Choć pojęcie terminala jest dosyć abstrakcyjne, można powiedzieć, że jest to urządzenie odpowiadające za komunikację między systemem a użytkownikiem lub między systemem a odległą maszyną za pomocą sieci.
Ze względu na to, że terminal zwykle pośredniczy między systemem i człowiekiem wchodzące i wychodzące dane muszą być przez niego traktowane w sposób specjalny. Jest to spowodowane faktem, iż człowiek często popełnia różne pomyłki. Stąd po pierwsze wymaga się istnienia klawiszy ścierających (del i backspace), po drugie klawisza akceptacji (enter). Dlatego właśnie terminal nie wysyła danych w ścisle ustalonych porcjach, ale po linii.
Ponieważ w linii komend terminala możemy uruchamiać różne procesy, mają sens pojęcia: terminal sterujący procesu, grupa terminalowa procesów i lider grupy. Terminal musi więc umieć wysylać sygnaly.
Dla wygody użytkownika kolejną funkcjonalnoscią terminala powinno być udostępnianie wszelkich skrótów. Niektóre skróty odpowiadają za wysyłanie sygnałów inne za przełączanie konsoli. Użytkownik używający klawiszy skrótów nie chciałby, aby na ekranie pojawiały się różne dziwne znaki, stąd sekwencje skrótowe muszą być przetwarzane w sposób specjalny.
Spis treści

Numeracja

Urządzenia terminalowe w systemie Linux mają numer główny 4. Numery podrzędne opisuje tabelka:
0 aktywna konsola
1-63 konsole
64-127 terminale szeregowe (serial)
128-191 pseudoterminale nadrzedne
192-255 pseudoterminale podrzedne

Spis treści

Dyscyplina linii

Dyscyplina linii jest mechanizmem pomocniczym urządzenia terminalowego. Jej zadaniem jest tłumaczenie znaków w komunikacji między urządzeniem i procesem (określa też sposób realizacji niektórych kombinacji klawiszy).
W Linuksie istnieje kilka rodzajów dyscyplin, najczęściej spotykana jest N_TTY, jest ona bowiem używawna domyślnie. Interfejs jest opisany w następnym punkcie.
Spis treści

Struktury

Jedna z głównych struktur dla urządzeń terminalowych jest
struct termios {
        tcflag_t c_iflag;       /* input mode flags */
        tcflag_t c_oflag;       /* output mode flags */
        tcflag_t c_cflag;       /* control mode flags */
        tcflag_t c_lflag;       /* local mode flags */
        cc_t c_line;            /* line discipline */
        cc_t c_cc[NCCS];        /* control characters */
};
Oto, co oznaczają jej pola:
  • c_iflag - sposób przetwarzania znaków wejściowych (otrzymanych od urządzenia)
  • c_oflag - sposób przetwarzania znaków wyjściowych (wysyłanych do urządzenia)
  • c_cflag - parametry techniczne (np. szybkość transmisji)
  • c_lflag - sposób działania dyscypliny linii (wygląd echa znaków na ekran)
  • c_line - sposób związania dyscypliny linii z urządzeniem terminalowym
  • c_cc[NCCS]- tablica znaków specjalnych (np. koniec linii).
struct tty_driver opisuje sterownik urządzenia terminalowego. Nie będę jej cytował, gdyż jest dość rozbudowana. Opiszę tylko kilka pól, np.:
  • name - nazwa urządzenia
  • major - numer główny urządzenia
  • minor_start - najmniejszy numer podrzędny urządzenia
  • num - ilość urządzeń
  • type - typ sterownika (konsola, terminal szeregowy, pseudoterminal)
  • subtype - podtyp strownika
  • init_termios - początkowe ustawienia
  • flags - flagi
poza tym w powyższej struturze umieszczone są funkcje takie jak otwieranie, zamykanie, czytanie i pisanie do urządzenia.
Kolejna struktura to tty_ldisc , która zawiera interfejs dyscypliny linii:
  • open - przypisanie dyscypliny do terminala
  • close - odłączenie dyscypliny od terminala
  • read - odczyt z terminala
  • write - zapis na terminal
  • flush_buffer - czyszczenie bufora znaków przaznaczonych do wysłania do procesu
  • chars_in_buffer - ilość znaków w buforze dyscypliny
  • set_termios - zmiana trybu pracy zdefiniowanego w struct termios.
Najważniejsza struktura opisująca urządzenie terminalowe to
struct tty_struct {
        int     magic;
        struct tty_driver driver;       /*strownik urządzenia*/
        struct tty_ldisc ldisc;         /*przypisana dyscyplina linii
                                         standardowo - n_tty*/
        struct termios *termios, *termios_locked; /*parametry*/
        int pgrp;                       
        int session;                    /*numer sesji*/
        kdev_t  device;                 /*numer urządzenia*/
        unsigned long flags;
        int count;
        struct winsize winsize;
        ...
        /*bufor sterownika, kolejki procesów czekających na operacje
        I/O na terminalu i kilka pól, które twórcy Linuxa opisują tak:*/
        /*
         * The following is data for the N_TTY line discipline.  For
         * historical reasons, this is included in the tty structure.
         */
        ...
};

Spis treści

Interfejs

Oto funkcje obsługi terminala:
  • tty_open - inicjuje potrzebne struktury
  • tty_release - wywołuje kończącą działanie urządzenia funkcję release_dev
  • tty_read - jeśli nie zaszedł błąd wywołuje funkcję read dyscypliny linii
  • tty_write - jeśli nie zaszedł błąd wywołuje funkcję write dyscypliny linii
  • do_tty_hangup - powiadamia procesy terminala o zerwaniu komunikacji