i znakowych:stała wielkość (dla jednego urządzenia) przesyłanych danych (porcję danych o tej ustalonej wielkości nazywamy blokiem) - zwykle wynosi ona 512 lub 1024 bajty; swobodny dostępem do danych - urządzenie blokowe oferuje dostęp do dowolnego bloku danych, bez względu na jego adres na tym urządzeniu; buforowanie danych - przy przesyłaniu danych do/z urządzenia dane są buforowane, co przyspiesza ich wymianę pomiędzy pamięcią i urządzeniem a także zmniejsza liczbę rzeczywistych odwołań do urządzenia;
typedef unsigned short kdev_t;
Aby wydobyć numer podrzędny lub główny z kdev_t należy użyć makr MAJOR() i MINOR().
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);
};
Tablica rozdzielcza urządzeń znakowych jest zdefiniowana następująco:
struct device_struct {
const char * name;
struct file_operations
* fops;
} chrdevs[MAX_CHRDEV];
Po przygotowaniu funkcji dla urządzenia sterownik powinien zadeklarować strukturę file_operations zawierającą wskaźniki do tych funkcji. Tę strukturę można skojarzyć z urządzeniem za pomocą funkcji rejestrującej
int register_chrdev(unsigned int major, const char * name, struct file_operations *fops);
Funkcja register_chrdev() wpisuje do tablicy rozdzielczej chrdevs pod indeksem major wskaźnik fops i nazwę urządzenia name. Jeśli major jest równe 0, funkcja szuka wolnego miejsca w tablicy chrdevs. Urządzenie można wyrejestrować za pomocą funkcji
int unregister_chrdev(unsigned int major, const char * name)
Z każdym plikiem specjalnym odpowiadającym urządzeniu znakowemu skojarzona jest domyślna funkcja otwierania pliku:
int chrdev_open(struct inode * inode, struct file * file)
wywołanie której powoduje wpisanie do file->f_op wskaźnika
do struktury file_operations
znalezionego w tablicy rozdzielczej
chrdevs
pod
indeksem odpowiadającym numerowi głównemu urządzenia (numer skojarzonego
z plikiem urządzenia jest przechowywany w i-węźle inode - więcej
informacji o tym można znaleźć w opracowaniu dotyczącym obsługi plików
specjalnych). Następnie, jeśli w strukturze file_operations sterownik
dostarczył funkcję open, chrdev_open ją wywoła.
Po tej zmianie struktury file skojarzonej z tym plikiem, każde
wywołanie na nim funkcji bibliotecznej spowoduje wywołanie odowiedniej
funkcji sterownika.
Uwaga: sposobem na skojarzenie różnych funkcji z różnymi urządzeniami
o tym samym numerze głównym jest zmienianie struktur file->f_op
w zależności od numeru podrzędnego urządzenia - takiej zmiany dokonuje
się w dostarczanej przez sterownik funkcji open().
Oto schemat działania systemu przy wywołaniu open() na
pliku skojarzonym z urządzeniem znakowym:
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);
};
oraz w strukturze file_operations.
Jest to nowość - we wcześniejszych wersjach (jądro 2.2.*) funkcje dla
urządzeń blokowych były przechowywane tak jak w przypadku urządzeń znakowych
- tylko w strukturze file_operations.
Tablica rozdzielcza urządzeń znakowych jest zdefiniowana następująco:
struct block_device {
struct list_head
bd_hash;
atomic_t
bd_count;
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 */
};
static struct {
const char *name;
struct block_device_operations
*blkops;
} blkdevs[MAX_BLKDEV];
Po przygotowaniu funkcji dla urządzenia blokowego sterownik powinien zadeklarować strukturę block_device_operations zawierającą wskaźniki do tych funkcji. Strukturę tę można skojarzyć z urządzeniem za pomocą funkcji rejestrującej
int register_blkdev(unsigned int major, const char * name, struct block_device_operations *bdops);
Funkcja działa analogicznie do register_chrdev() - ustawia
odpowiednie wartości w tablicy blkdevs.
Urządzenie blokowe możemy wyrejestrować przy pomocy funkcji
int unregister_chrdev(unsigned int major, const char * name)
Z każdym plikiem specjalnym odpowiadającym urządzeniu blokowemu skojarzona jest domyślna struktura file_operations:
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,
};
blkdev_open identyfikuje urządzenie, z którym związany jest
otwierany plik specjalny, na podstawie pola i_bdev (wskaźnik do
strukturyw block_device) i-węzła skojarzonego z otwieranym plikiem.
Następnie, jeśli wskaźnik inode->i_bdev->bd_op do struktury block_device_operations
nie
jest zainicjalizowany, przypisuje mu wartość wskaźnika z miejsca tablicy
rozdzielczej odpowiadającego danemu urządzeniu. Następnie, jeśli inode->i_bdev->bd_op->open
jest określony, wywołuje tę funkcję.
Oto schemat działania systemu przy wywołaniu open() na
pliku skojarzonym z urządzeniem blokowym:
Funkcje block_read() i block_write() struktury def_blk_fops
są standardowymi funkcjami zapisu i odczytu dla urządzeń blokowych i zostały
opisane w odrębnym dokumencie.
Funkcja block_fsync() synchronizuje dane pomiędzy urządzeniem
a pamięcią.
Funkcja block_llseek() zadeklarowana jako
static loff_t block_llseek(struct file *file, loff_t offset, int
origin)
ustawia odpowiednio wskaźnik file->filepos.
Funkcja blkdev_close() wywołuje block_fsync() oraz
inode->i_bdev->bd_op->release().
Funkcja blkdev_ioctl() wywołuje inode->i_bdev->bd_op->ioctl().
Inne struktury danych, o których piszący sterownik powinien wiedzieć to:
struct blk_dev_struct {
request_queue_t
request_queue;
queue_proc *queue;
void *data;
};
Dla każdego urządzenie blokowego system przechowuje kolejkę kierowanych
do niego żądań. Struktury blk_dev_struct przechowują taką kolejkę,
a także wskaźnik do funkcji wybierającej odpowiednią kolejkę. Struktury
te są przechowywane w tablicy indeksowanej numerami głównymi:
struct blk_dev_struct blk_dev[MAX_BLKDEV];
Jeśli sterownik zamierza korzystać tylko z jednej kolejki żądań, powinien zainicjować pole request_queue. Jeśli tych kolejek będzie więcej, powinien ustawić pole queue, które ma wskazywać funkcję zwracającą wskaźnik do kolejki w zależności od numerów głównego i podrzędnego. Typ queue_proc zdefiniowany jest jako
typedef request_queue_t * (queue_proc) (kdev_t dev);
W request_queue_t znajdują się struktury danych służące do przechowywania żądań a także wskaźnik do funkcji odpowiedzialnej za niskopoziomowe procedury realizacji żądań (nazywaną procedurą strategii). Typ tej funkcji jest zdefiniowany jako
typedef void (request_fn_proc) (request_queue_t *q);
i sterownik powinien taką funkcję dostarczyć.
Zainicjować request_queue, przydzielając jej odpowiednią funkcję
request_fn_proc,
można przy pomocy funkcji blk_init_queue():
blk_init_queue(request_queue_t *queue, request_fn_proc *request);
Każdemu wywołaniu blk_init_queue() powinno odpowiadać wywołanie
void blk_cleanup_queue(request_queue_t *);
int * blk_size[MAX_BLKDEV];
Tablica tablic indeksowana MAJOR i MINOR - przechowywane
są w niej rozmiary urządzeń w kilobajtach.
int * blksize_size[MAX_BLKDEV];
Tablica tablic indeksowana MAJOR i MINOR - przechowywane
są w niej rozmiary bloków danych dla urządzeń.
int * hardsect_size[MAX_BLKDEV];
Tablica tablic indeksowana MAJOR i MINOR - przechowywane
są w niej rozmiary sektorów w urządzeniu.
int read_ahead[MAX_BLKDEV];
Tablica indeksowana MAJOR - liczba sektorów czytanych z wyprzedzeniem
dla każdego sterownika.
int * max_readahead[MAX_BLKDEV];
Tablica tablic indeksowana MAJOR i MINOR - liczba
sektorów czytanych z wyprzedzeniem dla każdego urządzenia.
int * max_sectors[MAX_BLKDEV];
Tablica tablic indeksowana MAJOR i MINOR - maksymalna
iczba sektorów w jednym żądaniu.
Uwaga: Sterownik powinien sam zaalokować pamięć dla tablic indeksowanych
MINOR.
static void do_foo_request(...) {
while(... /*kolejka żądań nie jest pusta*) {
INIT_REQUEST;
if (CURRENT->cmd ==
WRITE) { /*piszemy*/
if (foo_write(CURRENT->sector, CURRENT->buffer, CURRENT->nr_sectors <<
9)) {
/* pisanie się powiodło - foo_write musiało przesłać dane
* do urządzenia i odpytywać je, aż dostało odpowiedź
* o realizacji żądania */
end_request(1);
} else {
end_request(0);
}
}
if (CURRENT->cmd ==
READ) { /*czytamy*/
if (foo_read(CURRENT->sector, CURRENT->buffer, CURRENT->nr_sectors <<
9)) {
/* czytanie się powiodło - foo_write musiało przesłać dane
* do urządzenia i odpytywać je, aż dostało odpowiedź
* o realizacji żądania */
end_request(1);
} else {
end_request(0);
}
}
}
}
Natomiast w przypadku wykorzystania mechanizmu przerwań, kod do_request() mógłby wyglądać tak:
static int foo_busy; /* inicjowane na zero */
static void do_foo_request(...) {
if (foo_busy) /* inne żądanie jest właśnie przetwarzane
*/
return;
foo_busy = 1;
foo_initialize_io();
}
static void foo_initialize_io(void) {
if (CURRENT->cmd == READ) {
SET_INTR(foo_read_intr);
//ustawienie obsługi przerwania
} else {
SET_INTR(foo_write_intr);
//ustawienie obsługi przerwania
}
...
/* wysłanie polecenia do urządzenia
jeśli czytanie, to tylko
żądanie czytania,
jeśli pisanie trzeba
przygotować dane do
zapisania i też wysłać
polecenie */
}
static void foo_read_intr(void) {
int error=0;
...
/* przeczytanie z urządzenia (wiemy, że dane
są gotowe)
i umieszczenie w CURRENT->buffer;
ustawienie error=1 jeśli były jakieś błędy
*/
end_request(error?0:1);
if (!CURRENT) /* jeśli
nie ma nowych żądań */
foo_busy = 0;
INIT_REQUEST;
/* INIT_REQUEST spowoduje zakończenie foo_read_intr,
jeśli nie ma nowych żądań */
/* nowe żądanie */
foo_initialize_io();
}
static void foo_write_intr(void) {
int error=0;
...
/* dane zostały zapisane. error=1 jeśli nastąpił
błąd */
end_request(error?0:1);
if (!CURRENT) /* jeśli nie
ma nowych żądań */
foo_busy = 0;
/* INIT_REQUEST spowoduje zakończenie foo_write_intr,
jeśli nie ma nowych żądań */
INIT_REQUEST;
/* Przerobimy nowe żądanie */
foo_initialize_io();
}