Valid HTML 4.01!

Podsystem wejścia-wyjścia

Spis treści


Wstęp

W Linuksie urządzenia fizyczne są reprezentowane przez pliki - tzw. pliki specjalne. Do obsługi tych urządzeń służą specjalne podprogramy obsługi urządzeń (zwane też sterownikami urządzeń) - są one zbiorem funkcji z jądra systemu, do którego dostęp otrzymuje się poprzez tablice rozdzielcze urządzeń. Do podprogramów obsługi urządzeń sięga się poprzez interfejs systemu plików.

W skrócie wygląda to tak: jeżeli program użytkowy wywoła taką funkcję systemu plików jak np. open(), to system plików wykonuje różne czynności zależnie od rodzaju otwieranego pliku:

Podczas otwierania pliku specjalnego system próbuje odszukać odpowiadający mu podprogram obsługi. Jeżeli go nie ma, to sygnalizuje błąd, a jeżeli jest, to wywołuje funkcję open() tego podprogramu.

Jeżeli funkcja open() znajdująca się w podprogramie obsługi zakończy się sukcesem, to wszystkie dalsze operacje na pliku są wykonywane przez ten podprogram, a nie (tak jak jest zwykle) przez system plików. Tak więc jeśli ktoś otworzy plik specjalny, a następnie wykona na nim read() lub write(), to wywołane funkcje systemowe po sprawdzeniu uprawnień itp. przekażą sterowanie do funkcji podprogramu obsługi. Widzimy więc, że metodą na dostęp do funkcji zawartych w jądrze systemu może być napisanie podprogramu obsługi urządzenia - nie jest w tym celu konieczne tworzenie nowej funkcji systemowej.

W dotychczasowych rozważaniach w ogóle nie było mowy o samym urządzeniu, a jedynie o podprogramie jego obsługi. Termin "podprogram obsługi urządzenia" pochodzi oczywiście stąd, że często funkcje udostępniane przez ten podprogram to funkcje pozwalające wykonywać operacje wejścia-wyjścia na fizycznych urządzeniach, takich jak dysk twardy lub drukarka. W rzeczywistości jednak podprogram obsługi urządzenia nie musi sterować żadnym fizycznym urządzeniem. Urządzenie, którym steruje podprogram obsługi odpowiadający plikowi /dev/null jest czystą abstrakcją!

Aby stworzyć plik specjalny, trzeba skorzystać z funkcji lub polecenia mknod(). Tworzy ono odpowiedni i-węzeł i dopisuje go do katalogu - informacje o odpowiadającym plikowi podprogramie obsługi również zostają zapisane. Te informacje są traktowane tak samo jak np. informacje o prawach dostępu i można je odczytać za pomocą polecenia ls.

Polecenie:


   mknod dziwne_urządzenie c 100 101

tworzy plik specjalny o nazwie dziwne_urządzenie i zapisuje, że jego podprogram obsługi jest identyfikowany przez parę ('c', 100). Liczba 101 będzie przekazywana podprogramowi obsługi przy wszystkich operacjach na tym pliku. Aby utworzyć plik specjalny, trzeba mieć uprawnienia administratora, w przeciwnym razie kończy się ono błędem. Przyczyny tego są jasne: ponieważ Linux nie sprawdza niczego takiego jak uprawnienia do tworzenia urządzenia ('c', 100) - nie sprawdza nawet, czy podprogram obsługi takiego urządzenia w ogóle jest wkompilowany w jądro - więc ktoś mógłby np. stworzyć plik specjalny odpowiadający twardemu dyskowi, a następnie odczytać z dysku dowolne informacje. Nie ma czegoś takiego jak uprawnienia do korzystania z urządzenia. Kontrola uprawnień odbywa się wyłącznie na poziomie systemu plików, który sprawdza, czy mamy prawo wykonać taką lub inną operację na pliku specjalnym.

Wynika stąd m.in., że może istnieć urządzenie, do którego z jednego miejsca mamy dostęp, a z innego nie (tzn. dowiązane są do niego dwa różne pliki specjalne, których użytkowanie wymaga różnych uprawnień).

Skoro już utworzyliśmy urządzenie, to spróbujmy coś z niego przeczytać:


   cat dziwne_urządzenie

Ponieważ podprogram obsługi ('c', 100) nie istnieje, więc polecenie to kończy się błędem. Błąd powstaje w momencie wywołania funkcji systemowej open(), gdyż próbuje ona wywołać specyficzną dla podprogramu ('c', 100) funkcję open(), a urządzenia ('c', 100) nie ma (tzn. nie zostało ono zarejestrowane - o tym później). Jeżeli podprogramu obsługi w systemie nie ma, to funkcja open() kończy się błędem, a tym samym nie mamy jak wywołać read() lub write() na urządzeniu.

Można teraz skasować poprzednio utworzone urządzenie:


   rm dziwne_urządzenie

Oczywiście usuwając plik specjalny również nie wchodzimy w żadne interakcje ze związanym z nim podprogramem obsługi (w szczególności ten podprogram nie musi istnieć).


Urządzenia znakowe i blokowe

Podprogram obsługi urządzenia jest identyfikowany parą (litera, liczba) - literą może być 'b' lub 'c', liczba zaś jest liczbą jednobajtową. Taka identyfikacja jest związana z podziałem na urządzenia znakowe (ang. character devices) i blokowe (ang. block devices). Pierwsze są oznaczane literą 'c', drugie literą 'b'.

Podział ten jest dość arbitralny. Powiemy, że urządzenie jest znakowe, jeżeli pozwala na dostęp do danych bajt po bajcie, jeżeli natomiast dane są obsługiwane w większych porcjach, to mówimy, że urządzenie jest blokowe (a te porcje nazywamy blokami). Można losowo sięgać do bloków danych i różnica w czasie dostepu będzie niezauważalna dla człowieka. Typowe urządzenia blokowe to twardy dysk, floppy dysk, CD-ROM, DVD. Do danych na urządzeniach znakowych nie można sięgać losowo, a jeśli można to czas dostępu zależy w dużym stopniu od położenia danych na urządzeniu (np. taśma magnetyczna).

Dla jednego fizycznego urządzenia można mieć dwa podprogramy obsługi - np. znakowy i blokowy (po sektorze). Podział ten łączy się też z kwestią buforowania danych - zwykle buforowane są urządzenia blokowe (choć można stworzyć urządzenie blokowe niebuforowane lub znakowe buforowane - to drugie wymagałoby jednak własnej implementacji buforów).

Karty sieciowe są dość nietypowymi urządzeniami fizycznymi, gdyż nie są związane bezpośrednio z plikami urządzeń.

Podział na urządzenia znakowe i blokowe jest związany z pojęciem tablic rozdzielczych.


Tablice rozdzielcze

W systemie istnieją dwie tablice urządzeń (właściwie: podprogramów obsługi urządzeń): jedna dla urządzeń blokowych, druga dla znakowych. Każdemu podprogramowi obsługi odpowiada pozycja w tablicy (ten sam podprogram może wystąpić na paru pozycjach). Podprogram obsługi jest identyfikowany parą (litera, liczba). Litera określa, w której z dwóch tablic, a liczba pod którym indeksem, znajduje się opisująca go struktura. W strukturze tej znajduje się wskaźnik do nazwy podprogramu obsługi oraz, co najważniejsze, wskaźnik do odpowiadającej mu struktury file_operations lub block_device_operations. Dostęp do podprogramu obsługi spoza jądra systemu przechodzi prawie zawsze przez funkcje, do których wskaźniki znajdują się w tej strukturze (ale są wyjątki, np. dla urządzenia mem).

Podprogram obsługi urządzenia jest zainstalowany w systemie tylko wtedy, gdy zajmuje jakąś pozycję w tablicy rozdzielczej. Ponieważ dostęp do urządzeń następuje przez pliki (pliki urządzeń są tradycyjnie umieszczane w katalogu /dev), więc każde urządzenie powinno mieć przypisany plik urządzenia z dobrze zdefiniowanym numerem. Gdyby tak nie było, to aby skorzystać z urządzenia, trzeba by najpierw znaleźć na podstawie nazwy jego numer, następnie stworzyć odpowiadający mu plik, a dopiero potem z niego korzystać. Dlatego większość urządzeń ma standardowe numery.

Oficjalnym rejestrem przydzielonych numerów urządzeń i węzłów w katalogu /dev jest plik Documentation/devices.txt.


Numer główny i numer podrzędny urządzenia

I-węzeł opisujący plik specjalny zawiera trzy ważne wartości:

  1. flagę (litery 'b' lub 'c'), która określa typ urządzenia,
  2. liczbę, która jest indeksem w tablicy rozdzielczej, pod którym znajduje się pozycja odpowiadająca temu urządzeniu,
  3. liczbę, którą przekazuje się podprogramowi obsługi urządzenia podczas wszelkich operacji na urządzeniu.

Pierwszą z dwóch liczb nazywa się numerem głównym urządzenia (ang. major device number), drugą podrzędnym (ang. minor device number). Pierwsza z nich decyduje, który podprogram obsługi będzie używany, natomiast interpretacja drugiej zależy od samego podprogramu. Numer główny zajmuje 12 bitów, a podrzędny 20 bitów (po 8 w starszych wersjach Linuksa). Oba te numery łącznie tworzą numer urządzenia (tzn. numer_urządzenia = (numer_główny << 20) | numer_podrzędny).

Numer podrzędny może decydować o tym, o które z wielu takich samych urządzeń nam chodzi, np. dla urządzenia blokowego o numerze głównym 3 numer podrzędny oznacza:


0     dysk 0 
1     partycja 1 dysku 0 
...  
63    partycja 63 dysku 0 
64    dysk 1 
65    partycja 1 dysku 1
...  
127   partycja 63 dysku 1 

Może też być tak, że różne urządzenia współdzielą ten sam numer główny, np. dla urządzenia znakowego o numerze głównym 1, numer podrzędny oznacza m.in.:


1     pamięć fizyczna 
2     pamięć wirtualna jądra 
3     urządzenie /dev/null> 
4     dostęp do portów wejścia/wyjścia 
5     generator zer (/dev/zero) 
8     generator liczb losowych 

Powyższe urządzenia spełniają dość różne funkcje, jednak wszystkie są obsługiwane przez ten sam podprogram obsługi (jak się okaże przy omawianiu funkcji open() dla urządzeń, współdzielenie jednego podprogramu obsługi, czyli współdzielenie jednego numeru głównego, jest równoważne temu, że jest się obsługiwanym przez tę samą funkcję open() specyficzną dla podprogramu obsługi).


A teraz przejdziemy do szczegółów ...

Tworzenie plików specjalnych

Pliki specjalne, tak jak wszystkie inne pliki, są reprezentowane przez i-węzły:

iwezel

Na rysunku przedstawiono i-węzeł z wyszczególnionymi polami, które są wykorzystywane wtedy, gdy i-węzeł odnosi się do pliku specjalnego. Są to:


struct inode {
    ....
    dev_t                   i_rdev;

    const struct file_operations  *i_fop;

    struct block_device     *i_bdev;
    struct cdev             *i_cdev;
    int                      i_cindex;
    ....
};

Pole i_rdev przechowuje numer urządzenia. Jest to pole typu dev_t co oznacza, że przechowuje równocześnie informacje o numerze głównym oraz numerze podrzędnym. Zatem pole to jednoznacznie identyfikuje urządzenie. Za pomocą makr MAJOR i MINOR można się dostawać do poszczególnych numerów:

MAJOR(dev) - numer główny urządzenia o numerze dev
MINOR(dev) - numer podrzędny urządzenia o numerze dev


#define MINORBITS       20
#define MINORMASK       ((1U << MINORBITS) - 1)
 
#define MAJOR(dev)      ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev)      ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi))

Tylko jedno z pól i_cdev i i_bdev może być wykorzystywane w danym i-węźle. Jeśli struktura i-węzła odnosi się do urządzenia znakowego, to używane będzie pole i_cdev i będzie ono wskazywać na strukturę opisującą urządzenie (typu struct cdev). Analogicznie dla urządzeń blokowych (w tym przypadku pole i_bdev wskazuje na strukturę typu block_device).


struct cdev {
    struct kobject kobj;
    struct module *owner;
    const struct file_operations *ops;
    struct list_head list;
    dev_t dev;
    unsigned int count;
};
 
struct block_device {
        dev_t                   bd_dev; 
        struct inode *          bd_inode;  
        int                     bd_openers;
        struct mutex            bd_mutex;       /* open/close mutex */
        struct mutex            bd_mount_mutex; /* mount mutex */
        struct list_head        bd_inodes;
        void *                  bd_holder;
        int                     bd_holders;
        struct block_device *   bd_contains;
        unsigned                bd_block_size;
        struct hd_struct *      bd_part;
        unsigned                bd_part_count;
        int                     bd_invalidated;
        struct gendisk *        bd_disk;
        struct list_head        bd_list;
        struct backing_dev_info *bd_inode_backing_dev_info;
        unsigned long           bd_private;
};

W polu i_fop jest przechowywany wskaźnik do struktury ze standardowymi funkcjami obsługi. Wskaźnik ten może wskazywać na jeden z dwóch standardowych zestawów operacji:


const struct file_operations def_chr_fops = {
     .open = chrdev_open,
};

const struct file_operations def_blk_fops = {
     .open           = blkdev_open,
     .release        = blkdev_close,
     .llseek         = block_llseek,
     .read           = generic_file_read,
     .write          = blkdev_file_write,
     .aio_read       = generic_file_aio_read,
     .aio_write      = blkdev_file_aio_write, 
     .mmap           = generic_file_mmap,
     .fsync          = block_fsync,
     .......
};

Jedyne niezerowe pole def_chr_fops, to pole dla funkcji open(), gdzie znajdziemy wartość &chrdev_open.

W chwili, gdy system będzie tworzył i-węzeł odpowiadający urządzeniu znakowemu (w funkcji sys_mknod()), na inode.i_fop przypisze wartość &def_chr_fops. Natomiast w chwili tworzenia i-węzła odpowiadającego urządzeniu blokowemu wstawi w to pole wartość &def_blk_fops.

Oto rysunek przedstawiający i-węzeł dla urządzenia blokowego. W tym przypadku pole i_fop wskazuje na zestaw standardowych operacji dla urządzeń blokowych. Na rysunku można też zobaczyć nazwy rzeczywiście zaimplementowanych funkcji dla urządzeń blokowych.

inode

Do tworzenia plików specjalnych służy funkcja systemowa mknod(), której realizacją w jądrze jest funkcja sys_mknod() z pliku fs/namei.


asmlinkage long sys_mknod(const char * filename, int mode, unsigned dev)

Parametry funkcji:

Aby utworzyć plik specjalny urządzenia znakowego parametr mode powinien mieć ustawiony bit S_IFCHR, podobnie przy tworzeniu pliku blokowego trzeba ustawić bit S_IFBLK. Przy tworzeniu plików specjalnych funkcja ta wywołuje funkcję vfs_mknod(), a ta z kolei funkcję, której adres uzyskuje z pole i_op->mknod obiektu i-węzła związanego z katalogiem, w którym chcemy utworzyć dany plik. Widać więc, że to w jaki sposób plik zostanie utworzony, zależy od zbioru operacji związanych z obiektem i-węzła katalogu, a co za tym idzie od systemu plików, w którym znajduje się ten katalog.


Tablica rozdzielcza urządzeń znakowych i rejestrowanie urządzeń

Tablica rozdzielcza urządzeń znakowych są to tablica statyczna o nazwie chrdevs.


#define CHRDEV_MAJOR_HASH_SIZE  255

static struct char_device_struct {
        struct char_device_struct *next;
        unsigned int major;
        unsigned int baseminor;
        int minorct;
        char name[64];
        struct file_operations *fops;
        struct cdev *cdev;              /* will die */
} *chrdevs [CHRDEV_MAJOR_HASH_SIZE];

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);
    .....
};

Do rejestrowania podprogramów obsługi urządzeń, służą funkcje, odpowiednio, register_chrdev() i register_blkdev(). Operację wyrejestrowania podrpogramu obsługi urządzenia wykonuje funkcja, odpowiednio, unregister_chrdev() i unregister_blkdev.

Pozycja o numerze major w tablicy jest uważana za pustą, jeśli chrdevs[major].fops==NULL, w przeciwnym wypadku jest zajęta. Pozycja chrdevs[0] nie jest używana - nie da się pod nią niczego wpisać.


Inicjowanie podprogramów obsługi urządzeń

Każdy podprogram obsługi urządzenia, aby mógł być wykorzystywany, musi zostać zarejestrowany. Poza tym często podprogram obsługi musi zaincjować swoje struktury danych, znaleźć linie IRQ, z których korzystają związane z nim urządzenia itp., przydzielić ramki na bufory do transmisji DMA. Wszystkie te czynności są wykonywane w funkcjach xxx_init() (poza jednym wyjątkiem - urządzeniem mem), gdzie xxx jest "nazwą" urządzenia. Z kolei funkcje xxx_init są wywoływane przez chr_dev_init oraz blk_dev_init.

Podprogramy obsługi rejestruje się jak najszybciej, by były dostępne dla programów użytkowników. Natomiast inicjuje się je jak najpóźniej, by niepotrzebnie nie alokować zasobów. System utrzymuje licznik procesów mających dostęp do pliku urządzenia. Licznik jest zwiększany o 1 w każdej operacji open() i zmniejszany o 1 w każdej operacji release(). Metoda open() sprawdza wartość licznika przed zwiększeniem jego wartości. Jeśli licznik ma wartość zero, to podprogram obsługi urządzenia przydziela zasoby i włącza przerwania oraz DMA na urządzeniach fizycznych. Przy release() wykonuje czynność odwrotną.


Działanie funkcji systemowych dla plików specjalnych

Obiekt typu file odpowiada instacji otwartego pliku. Jeżeli filp jest typu file*, to filp->f_op zawiera adresy funkcji, które są wywoływane w momencie wykonywania operacji na pliku opisanym przez filp. Dla zwykłych plików funkcje te są elementem systemu plików. Jeżeli jednak filp opisuje plik specjalny, to funkcje te pochodzą z podprogramu obsługi urządzenia. Od momentu, gdy coś zostanie zapisane do pola filp->f_op, wszystkie operacje na pliku filp są obsługiwane przez funkcje, których adresy znajdują się w tym polu.

Wypełnienie pola filp->f_op to zadanie funkcji open().


Funkcja sys_open()

Prześledźmy działanie funkcji systemowej open(), które w kodzie jądra odpowiada funkcja sys_open().


[sys_open(), filp_open(), dentry_open()]
   najpierw postępuje się tak samo, jak dla zwykłych plików; 
   następnie podstawia się f->f_op = inode->i_fop  
       (czyli def_chr_fops lub def_blk_fops);
   po czym wywołuje  się f->f_op->open(inode, f) 
       (czyli chrdev_open lub blkdev_open());

Funkcja chrdev_open() jest wołana podczas otwierania znakowego pliku specjalnego. Natomiast blkdev_open() może być wołane w dwóch przypadkach: otwierania blokowego pliku specjalnego lub montowania systemu plików na urządzeniu.


int chrdev_open(struct inode *inode, struct file *filp);
{ 
    /* inode i filp opisują otwierany plik specjalny */
    int ret = -ENODEV;
 
    filp->f_op = get_chrfops(MAJOR(inode->i_rdev), MINOR(inode->i_rdev));
    if (filp->f_op) {
        ret = 0;
        if (filp->f_op->open != NULL) {
            lock_kernel();
            ret = filp->f_op->open(inode,filp);
            unlock_kernel();
        }
     }
     return ret;
}

Funkcja pobiera fops z podprogramu obsługi urządzenia i wstawia do obiektu otwartego pliku. Następnie wywołuje funkcję open() specyficzną dla danego urządzenia.

open()

Funkcje open() specyficzne dla urządzenia mogą zajmować się m.in. następującymi sprawami:

  1. Zależnie od MINOR(inode->i_rdev) przypisywać różne wartości na f->f_op (jak w mem).
  2. Inicjować struktury danych związanych z urządzeniem (np. lp).
  3. Sprawdzać, czy urządzenie istnieje i zdobywać o nim informacje (np. lp).
  4. Przechowywać informacje o tym, ile razy urządzenie było otwierane (np. hd). Do jednego urządzenia może się odwoływać w systemie plików wiele różnych plików specjalnych. W związku z tym system plików nie jest w stanie określić, czy w danym momencie ktoś używa pewnego urządzenia, czy też nie. Tymczasem jeśli trzeba fizycznie zamknąć urządzenie (tak jak konieczne jest synchroniczne zapisanie wszystkich bloków itp. w urządzeniu blokowym w momencie zamykania), to podprogram obsługi musi wiedzieć, kto zamyka urządzenie jako ostatni. Taki podprogram sam utrzymuje licznik, który jest zwiększany przy open() i zmniejszany przy release().

W przypadku urządzeń blokowych sys_open() wywołuje blkdev_open(), a ta z kolei funkcję do_open() - wspólną dla różnych urządzeń blokowych. Dopiero ona wywołuje funkcję open() specyficzną dla urządzenia.

Funkcja open() pozwala na to, by wiele urządzeń współdzieliło jeden numer główny. Otóż do funkcji open() specyficznej dla urządzenia przekazuje się inode i filp. Funkcja ta może, zależnie od wartości MINOR(inode->i_rdev), przypisać różne wartości na filp->f_op (tak dzieje się np. dla urządzenia mem). Może też niczego na to pole nie przypisywać (tak się dzieje np. dla dysków), wtedy filp->f_op jest takie samo niezależnie od numeru podrzędnego.

Po otwarciu pliku specjalnego wszystkie operacje na danym pliku będą powodowały wywołanie jednej z funkcji, do których wskaźniki znajdują się w filp->f_op.


Urządzenia znakowe

Specyfiką urządzeń znakowych jest brak wszelkiej specyfiki. Nie mają one, tak jak urządzenia blokowe, wspólnych funkcji do obsługi pewnego rodzaju żądań. Nie korzystają z żadnych wspólnych procedur ani struktur danych. Wśród urządzeń znakowych najważniejszą grupę tworzą terminale. Poza tym znakowe są też modemy, myszki itp.

Ciekawym przykładem urządzenia znakowego jest mem, które standardowo jest dostępne w jądrze.


Urządzenie mem

Urządzenie mem obsługuje następujące podurządzenia:

numer podrzędny standardowa nazwa działanie
1 /dev/mem dostęp do pamięci
2 /dev/kmem dostęp do pamięci wirtualnej jądra
3 /dev/null "pożera" wszystko
4 /dev/port dostęp do portów we-wy
5 /dev/zero przekazuje same zera
7 /dev/full przekazuje -ENOSPC przy pisaniu
8 /dev/random liczby losowe - w razie czego generator czeka, aż zwiększy się poziom "entropii" w systemie
9 /dev/urandom liczby losowe - przy "braku entropii" staje się generatorem liczb pseudolosowych

Ciekawą rzeczą w podprogramie obsługi urządzenia mem jest funkcja memory_open(), która pokazuje jak przy pomocy jednego numeru głównego obsłużyć wiele różnych podurządzeń.


Tryby pracy urządzeń

Jeżeli podprogram obsługi urządzenia otrzymuje żądanie wykonania jakiejś operacji na tym urządzeniu i przekaże to żądanie do urządzenia, to musi w jakiś sposób zorientować się, że zostało ono wykonane (albo że wystąpił błąd). Dotyczy to niemal wszystkich podprogramów obsługi urządzeń zewnętrznych, jak również niektórych urządzeń wirtualnych, które wykonują np. read() w sposób blokujący (jak /dev/random). Podprogramy dla urządzeń wirtualnych mogą zatrzymywać się po prostu w jakiejś kolejce (jak w random_read()), czekając na zajście pewnego zdarzenia w systemie. Inaczej jest z obsługą urządzeń zewnętrznych.

Podprogram obsługi takiego urządzenia może:

W systemie Linux większość podprogramów obsługi działa przerwaniowo, ale niektóre, np. drukarka, mogą działać w trybie odpytywania. Jeżeli podprogram obsługi ma działać przerwaniowo, to musi umieć zdobyć sobie linię IRQ i zainstalować procedurę obsługi przerwania.


DMA (Direct Memory Access)

DMA to mechanizm sprzętowy, który umożliwia urządzeniu na bezpośrednie przesyłanie danych z/do pamięci operacyjnej bez angażowania procesora. Bardzo istotnie pozwala to zwiększyć efektywność transmisji, bo eliminuje się źródła możliwego narzutu.

Impulsem do transmisji danych mogą być dwa zdarzenia:

  1. program prosi o dane (np. wykonując read()),

    1. Kiedy proces woła read() metoda podprogramu obsługi urządzenia alokuje bufor DMA, przekazuje do sprzętu polecenie transmisji danych, proces zostaje uśpiony.
    2. Sprzęt pisze dane do bufora, po zakończeniu zgłasza przerwanie.
    3. Podprogram obsługi przerwania dostaje dane wejściowe, potwierdza przerwanie, budzi proces, który może teraz czytać dane.
  2. sprzęt asynchronicznie przesyła dane do systemu.

    1. Sprzęt zgłasza przerwanie informując o nadejściu nowych danych.
    2. Podprogram obsługi przerwania alokuje bufor i przekazuje jego adres do sprzętu.
    3. Urządzenie czyta dane do bufora i po zakończeniu zgłasza kolejne przerwanie.
    4. Podprogram obsługi przekazuje dalej nowe dane, budzi odpowiedni proces, robi porządki.

W trybie asynchronicznym zwykle działają karty sieciowe (zakładają, że mają dostęp do cyklicznego bufora).

Jeśli bufor DMA zajmuje więcej niż jedną stronę, to musi być zaalokowany w spójnym obszarze pamięci fizycznej, gdyż transmisje danych po szynie systemowej ISA lub PCI są opisywane adresami fizycznymi. Trzeba też pamiętać o rezerwacji pamięci z właściwej strefy (strefa DMA). Większość urządzeń na współczesnych szynach potrafi obsługiwać adresy 32-bitowe. Jednak niektóre urządzanie PCI mogą operować tylko adresami 24-bitowymi (także urządzenia ISA mają takie ograniczenie).

DMA musi uwzględnić problem spójności pamięci podręcznej. Współczesne procesory trzymają kopie ostatnio używanych danych w sprzętowej pamięci podręcznej. Jeśli urządzenie zmienia obszar pamięci, to powoduje, że dane w pamięci podręcznej tracą ważność. Podobnie gdy urządzenie czyta coś z pamięci najpierw powinno się uaktualnić ten obszar danym z pamięci podręcznej. Może to być powodem trudnych do zidentyfikowania błędów. Niektóre architektury zapewniają spójność pamięci podręcznej sprzętowo, inne wymagają wsparcia oprogramowania. Zapewnienie poprawnego działania na wszystkich architekturach wymaga przestrzegania pewnych reguł.

W transmisji DMA uczestniczy: sterownik DMA, urządzenie, podprogram obsługi urządzenia. Kontroler przechowuje informacje o przebiegu transmisji: kierunek, adres w pamięci, rozmiar. Zawiera też licznik, służacy do zliczania przesłanych bajtów. Kiedy sterownik dostaje sygnał z żądaniem DMA, przejmuje kontrolę nad szyną i inicjuje operację odczytu/zapisu z/do urządzenia.

Konflikty między procesorem a sterownikiem DMA w dostępie do komórki pamięci rozstrzyga arbiter pamięci.

dma

Direct Memory Access (DMA) (Źródło: Maarten van Steen)



Wejście-wyjście odwzorowane do pamięci (Memory Mapped Input/Output)

Urządzenie zewnętrzne 'odwzorowane do pamięci' jest podłączone do szyny adresowej i szyny danych dokładnie tak samo jak pamięć, więc gdy CPU czyta lub zapisuje dane pod adres związany z urządzeniem, następuje przesłanie danych. Mechanizm ten ma wiele zalet i tylko kilka wad.

Główną zaletą jest to, że CPU może wykonywać operacje na porcie wejścia-wyjścia dokładnie tak samo jak na pamięci. Jest to dużo wygodniejsze niż przesłanie danych z urządzenia do CPU, wykonanie operacji na danych i przesłanie danych spowrotem do portu wejścia-wyjścia.

Wadą jest to, że takie odwzorowanie odbywa się kosztem adresów. Zwykle najmniejsza ilość danych jakie można przydzielić urządzeniu to strona 4 KB. Jeśli mamy kilka urządzeń, to w sumie może to spowodować zużycie zauważalnej ilości pamięci. Nie jest to problemem dla zwykłego PC, może zacząć być problemem, gdy podłącza się urządzenia takie jak karty video, które mogą mieć 32 MB pamięci na płycie głównej.


Janina Mincer-Daszkiewicz