Do spisu tresci tematu 7

7.1.6 Algorytmy open, close, read, write, ioctl, lseek, select




Spis tresci


Wprowadzenie

W Linuxie, tak jak w innych Unixach, dostep do urzadzen zewnetrznych odbywa sie poprzez system plikow. Wobec tego operacje wykonywane na urzadzeniach zewnetrznych sa takie same, jak na plikach - sa to: open, close, read, write, lseek lecz takze ioctl, select, check_media_change i revalidate.


Fakty ogolne

Jesli plik, na ktorym ktoras z tych operacji jest wykonywana jest plikiem specjalnym (tzn. plikiem urzadzenia), to wowczas przypisana mu w jego i-wezle tablica rozdzielcza file_operations wskazuje na tablice danego urzadzenia i jest wykonywana funkcja specyficzna dla tego urzadzenia.

Tablica rozdzielcza zdefiniowana jest za pomoca nastepujacej struktury (zawartej w pliku include/linux/fs.h):

struct file_operations {
        int (*lseek) (struct inode *, struct file *, off_t, int);
        int (*read) (struct inode *, struct file *, char *, int);
        int (*write) (struct inode *, struct file *, const char *, int);
        int (*readdir) (struct inode *, struct file *, void *, filldir_t);
        int (*select) (struct inode *, struct file *, int, select_table *);
        int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
        int (*mmap) (struct inode *, struct file *, struct vm_area_struct *);
        int (*open) (struct inode *, struct file *);
        void (*release) (struct inode *, struct file *);
        int (*fsync) (struct inode *, struct file *);
        int (*fasync) (struct inode *, struct file *, int);
        int (*check_media_change) (kdev_t dev);
        int (*revalidate) (kdev_t dev);
};

Jak widac, wszystkie funkcje dostaja wskaznik na i-wezel oraz na plik, lub zakodowany numer glowny i drugorzedny urzadzenia. W pierwszym przypadku poznaja drugorzedny numer urzadzenia (potrzebny do jego zidentyfikowania) ze struktury reprezentujacej i-wezel (pole rdev).

Nalezy jednak zaznaczyc, ze znaczna wiekszosc rzeczy, ktore system musi zrobic, wykonuje sama funkcja systemowa, wobec czego charakterystyczna dla urzadzenia funkcja moze byc bardzo krotka, lub wrecz nie robic nic (tak jest czesto np. w przypadku lseek).

Niektore pola tablicy rozdzielczej nie maja nic wspolnego z urzadzeniami ( readdir). fsync sluzy do synchronizacji (zapis buforow na dysk) i jest opisany w Temacie 6.

Wobec tego, proces wykonywania funkcji systemowych dla plikow specjalnych mozna podzielic na dwie czesci:

  1. Wykonanie funkcji systemowej, praktycznie identyczne jak dla pliku zwyklego;
  2. Wykonanie funkcji specyficznej dla urzadzenia, wskazywanej przez odpowiedni element tablicy rozdzielczej.

Funkcje specyficzne dla urzadzen sa zdefiniowane w programach obslugi urzadzen znajdujacych sie w katalogu drivers/char lub drivers/block (dla urzadzen znakowych i blokowych, odpowiednio). Sa one inne dla kazdego rodzaju urzadzenia (tzn. dla kazdego urzadzenia o innym numerze glownym).

Dlatego tez duzo ciekawszymi funkcjami sa specyficzne dla plikow specjalnych funkcje select, ioctl, check_media_change i revalidate.


lseek

- zmiana offsetu (standardowy algorytm - patrz Temat 6)

DEFINICJA: asmlinkage long sys_lseek(unsigned int fd, off_t offset, unsigned int origin)
    WYNIK: nowa pozycja lub
           EBADF (fd jest blednym deskryptorem),
           EINVAL (origin jest bledna wartoscia).

Funkcja lseek sluzy do zmiany miejsca, w ktorym zostanie wykonana nastepna operacja odczytu lub zapisu. Realizuje ja funkcja systemowa sys_lseek. offset oznacza miejsce (prawdopodobnie liczbe bajtow od poczatku), od ktorego ma sie rozpoczac najblizsza operacja odczytu/zapisu, a origin ma nastepujace znaczenie:

0 = liczymy offset od samego poczatku;

1 = liczymy offset od aktualnej pozycji;

2 = liczymy offset od konca.

Jesli w tablicy rozdzielczej urzadzenia wskaznik lseek jest ustawiony na NULL (tak jest w przypadku wiekszosci urzadzen), wykonywana jest standardowa operacja, polegajaca na odpowiednim zmodyfikowaniu wskaznika file->f_pos.

Funkcja lseek zwraca pozycje w pliku.

Jesli zdefiniowana jest lseek w programie obslugi urzadzenia, jest ona wykonywana (tasma np. moze zostac przewinieta w odpowiednie miejsce).


open/close (release)

- otwarcie/zamkniecie pliku specjalnego (urzadzenia) - standardowy algorytm - patrz Temat 6.

DEFINICJA: asmlinkage int sys_open(const char * filename,int flags,int mode)
    WYNIK: nowy deskryptor lub
           ENFILE (przekroczona max liczba otwartych deskryptorow procesu),
           EMFILE (przekroczona max liczba otwartych deskryptorow pliku),
           ENOTDIR, ENOENT, ENAMETOOLONG, EFAULT, EACCES, ETXTBSY, EISDIR.

DEFINICJA: asmlinkage int sys_close(unsigned int fd)
    WYNIK: 0 (sukces) lub
           EBADF (fd nie jest poprawnym, otwartym deskryptorem).

Znaczenie flags i mode opisane jest dobrze w manie.

Funkcja open sluzy do otwierania pliku specjalnego (rozpoczecie pracy z urzadzeniem), a funkcja close - do zamkniecia pliku specjalnego. Funkcje systemowe sys_open i sys_close dzialaja identycznie jak w przypadku plikow zwyklych; ich algorytmy podane sa w Temacie 6.

Podczas otwierania pliku system przydziela miejsce w pamieci na i-wezel, zwieksza jego licznik odwolan, przydziela pozycje w tablicy plikow i deskryptor. Warto pamietac, ze w i-wezle pliku specjalnego, w polu rdev zakodowany jest numer glowny i drugorzedny urzadzenia, ktorego tenze plik dotyczy.

Funkcja sys_open wywoluje funkcje *_open specyficzna dla urzadzenia (jesli jest ona zdefiniowana w tablicy rozdzielczej). Funkcja otwarcia urzadzenia musi utworzyc wszystkie potrzebne sobie struktury danych, ew. uruchomic urzadzenie.

Funkcja sys_close wywoluje funkcje *_release tylko wtedy, gdy juz zaden proces nie uzywa i-wezla.

Funkcje sys_open i sys_close sa zdefiniowane w pliku fs/open.c

Funkcja *_open zdefiniowana w programie obslugi bardzo czesto liczy liczbe odwolan do urzadzenia (nie mozna do tego celu wykorzystac liczby odwolan do i-wezla, poniewaz jedno urzadzenie moze miec kilka i-wezlow; mozna utworzyc kilka plikow specjalnych reprezentujacych ten sam numer glowny i drugorzedny). Ten licznik jest wykorzystywany pozniej, przy *_release. Jesli zaden proces nie uzywa urzadzenia, mozna je (w razie potrzeby) np. wylaczyc. W przypadku urzadzen blokowych, funkcja *_release musi jeszcze pamietac o ew. zapisaniu wszystkich buforow znajdujacych sie wciaz w pamieci (robi to np. wykonujac block_fsync).


read/write

- czytanie/zapis z/na urzadzenie - standardowy algorytm - patrz Temat 6

DEFINICJA: asmlinkage int sys_read(unsigned int fd,char * buf,int count)
    WYNIK: liczba przeczytanych bajtow lub
           EFAULT (buf poza przestrzenia adresowa procesu)
           EINVAL (nie mozna czytac z urzadzenia)
           EBADF (bledny deskryptor), EISDIR (katalog)
           EAGAIN (operacja nieblokujaca nie dala rezultatow)
           EINTR (przerwanie przez sygnal)    

DEFINICJA: asmlinkage int sys_write(unsigned int fd,char * buf,unsigned int count)
    WYNIK: liczba zapisanych bajtow lub
           EFAULT, EINVAL, EBADF, EAGAIN, EINT (jak wyzej), EPIPE.

Argumenty:

buf - wskaznik na poczatek bufora;

count - liczba bajtow do odczytania/zapisania;

Wynik: ilosc odczytanych/zapisanych bajtow.

Te funkcje sluza do czytania i pisania z/na urzadzenie. Sa one obslugiwane przez funkcje systemowe sys_read i sys_write zdefiniowane w pliku fs/read_write.c.

Funkcje systemowe uruchamiaja odpowiednie funkcje zdefiniowane w programie obslugi urzadzenia. W przypadku urzadzen znakowych sa to po prostu funkcje *_read i *_write, ktore odpowiednio czytaja/zapisuja na urzadzenie. W przypadku urzadzen blokowych sprawa jest bardziej skomplikowana, poniewaz odczyt i zapis jest buforowany i odbywa sie poprzez tzw. procedury strategii. Najczesciej wowczas wskazniki w tablicy rozdzielczej wskazuja na funkcje (odpowiednio) block_read i block_write zdefiniowane w pliku fs/block_dev.c, ktore wywoluja nastepnie procedury strategii (patrz Operacje dyskowe i Procedury strategii). (Dokladnie tak jest w przypadku dysku twardego; w przypadku stacji dyskow elastycznych istnieja funkcje floppy_read i floppy_write, ktorych jedyna funkcja oprocz uruchamiania block_read/block_write jest jedynie sprawdzanie, czy nie zostala zmieniona dyskietka).


Procedury strategii

Procedury strategii sluza do czytania/zapisu z urzadzen blokowych za pomoca buforow.

Jesli program zada odczytania/zapisania danych, system okresla blok, w ktorym te dane sie znajduja. Jesli ten blok jest w buforze (w pamieci), pobiera go stamtad. Jesli nie, bufor tworzy zadanie (request), ktore ma wykonac operacje dyskowa.

Wobec tego, programy obslugi urzadzen blokowych nie posiadaja funkcji *_read i *_write, tylko procedury obslugi zadan do_*_request.

Dziala to nastepujaco (omawiamy czytanie; zapis jest analogiczny):

Dokladniejszy opis operacji dyskowych znajduje sie w Operacjach dyskowych.


ioctl

DEFINICJA: asmlinkage int sys_ioctl(unsigned int fd, unsigned int cmd, unsigned long arg)
    WYNIK: 0 (sukces) lub
           EBADF (bledny deskryptor),
           ENOTTY/EINVAL (bledny kod zadania dla danego urzadzenia).

Argumenty:

cmd - kod polecenia

arg - argument (zazwyczaj wskaznik do struktury)

ioctl jest realizowana przez funkcje systemowa sys_ioctl, zawarta w pliku fs/ioctl.c. Jest to ogolny interface dla wszystkich polecen specyficznych dla urzadzen, ktorych nie da sie zrealizowac za pomoca pozostalych funkcji. Sa to przewaznie funkcje sterownicze, definiujace parametry pracy danego urzadzenia.

Dla kazdego urzadzenia komendy ioctl sa inne. Sama funkcja sys_ioctl obsluguje ich tylko kilka (np. FIOCLEX - ustawia flage close_on_exit itd.).

W przypadku stacji dyskow elastycznych np. ioctle sluza do: wyjmowania nosnika, formatowania dyskow, pobierania parametrow nosnikow i samego napedu itd. W przypadku dyskow twardych mozna za ich pomoca pobrac geometrie dysku, odczytac tablice partycji itd. Duze znaczenie ma ioctl przy ustawianiu parametrow terminali (patrz Terminale).


check_media_change/revalidate

Te funkcje sa zdefiniowane tylko dla urzadzen z wymienialnym nosnikiem (np. stacja dyskow elastycznych). check_media_change sprawdza, czy nosnik zostal zmieniony i, ewentualnie, przez revalidate uaktualnia wszystkie informacje, jakie system posiada o urzadzeniu (np. tablice i-wezlow). W szczegolnosci, w pliku devices.c jest zdefiniowana funkcja

int check_disk_change (kdev_t dev)

Jest ona wywolywana podczas mount i open, a jej dzialanie bardzo dobrze pokazuje korzystanie z dwoch opisywanych funkcji:


select

DEFINICJA: asmlinkage int sys_select(int n, fd_set *inp, fd_set *outp, fd_set *exp, struct timeval *tvp)
    WYNIK: liczba deskryptorow, ktorych stan sie zmienil lub
           EBADF (w jednym ze zbiorow znalazl sie bledny deskryptor),
           EINTR, EINVAL (n<0), ENOMEM (brak pamieci).

Argumenty:

n - najwiekszy numer deskryptora uzytego w zbiorach inp, outp i exp plus 1.

inp - zbior deskryptorow urzadzen, na ktorych maja pojawic sie dane;

outp - zbior deskryptorow urzadzen, na ktorych ma sie pojawic mozliwosc zapisu;

exp - zbior deskryptorow urzadzen, na ktorych maja sie pojawic wyjatki;

tvp - maksymalny czas, ktory mamy czekac (gdy = 0, czekamy do skutku).

select sluzy do odpytywania urzadzen. Stosuje sie je w sytuacjach, kiedy nie chcemy tracic czasu procesora na aktywne sprawdzanie, czy na ktoryms z urzadzen mozna juz cos zapisac lub z niego odczytac. Funkcja sluzy do ominiecia aktywnego czekania w petlach z operacja read/write z flaga O_NDELAY.

Select konczy sie, gdy:

Po zakonczeniu dzialania funkcji zbiory deskryptorow sa modyfikowane, aby zaznaczyc, ktore z nich zmienily stan (tzn. na ktorych odpowiednie operacje moga sie powiesc).

Zbiory deskryptorow (struktura fd_set) obsluguje sie za pomoca makr zdefiniowanych w include/asm-i386/posix_types.h:

FD_CLR(int fd, fd_set *set) - usuwa fd ze zbioru set;

FD_ISSET(int fd, fd_set *set) - zwraca 1 jesli fd nalezy do set;

FD_SET(int fd, fd_set *set) - dodaje fd do zbioru set.

FD_ZERO(fd_set *set) - czysci zbior set.

Funkcja select dziala nastepujaco: