Laborator 7 - Device drivere de tip bloc. Subsistemul de I/O

Obiectivele laboratorului

  • dobândirea de cunoștințe legate de funcționarea subsistemului de I/O pe Linux
  • acomodarea cu structurile și funcțiile de lucru cu dispozitive de tip bloc
  • obținerea unor deprinderi de bază de utilizare a API-ului pentru dispozitive de tip bloc prin rezolvarea exercițiilor

Cuvinte cheie

  • dispozitive de tip block
  • subsistemul de I/O
  • înregistrare/deînregistrare
  • struct gendisk
  • sector
  • struct block_device_operations
  • cereri – struct request
  • cozi de cereri – struct request_queue
  • prelucrarea unei cereri
  • struct bio, submit_bio

Materiale ajutătoare

Noțiuni generale

Dispozitivele de tip bloc se caracterizează prin accesul aleator la date organizate în blocuri de dimensiune fixă. Exemple de astfel de dispozitive sunt hard disk drive-urile, CD-ROM drive-urile, RAM disk-urile etc. Viteza dispozitivelor de tip bloc este, în general, mult mai ridicată decât a celor de tip caracter, iar performanța acestora este, de asemenea, importantă. Acesta este motivul pentru care nucleul Linux tratează diferit cele două tipuri de dispozitive (dispune de un API specializat).

Lucrul cu dispozitive de tip bloc este, astfel, mai complicat decât lucrul cu cele de tip caracter. Dispozitivele de tip caracter au o singură poziție curentă, în timp ce dispozitivele de tip bloc trebuie să se poată mișca la orice poziție din dispozitiv pentru a asigura accesul aleator la date. Pentru a simplifica lucrul cu dispozitivele de tip bloc, nucleul Linux pune la dispoziția programatorului un întreg subsistem denumit subsistemul block I/O (sau block layer).

Din perspectiva nucleului, cea mai mică unitate logică de adresare este blocul. Cu toate că dispozitivul fizic poate fi adresat la nivel de sector, nucleul efectuează toate operațiile cu discuri folosind blocuri. Întrucât cea mai mică unitate de adresare fizică este sectorul, dimensiunea blocului trebuie să fie un multiplu al dimensiunii sectorului. În plus, dimensiunea blocului trebuie să fie o putere a lui 2 și nu poate depăși dimensiunea unei pagini. Dimensiunea blocului poate varia în funcție de sistemul de fișiere folosit, cele mai frecvente valori fiind 512 bytes, 1 kilobyte și 4 kilobytes.

Device drivere de tip bloc în Linux

Înregistrarea unui dispozitiv de tip bloc

Pentru înregistrare se folosește funcția register_blkdev 1).

Pentru deînregistrarea unui dispozitiv de tip bloc se folosește funcția unregister_blkdev.

În versiunea 4.9 a kernel-ului Linux, apelul funcției register_blkdev este opțional. Singurele operații efectuate de această funcție sunt alocarea dinamică a unui major (dacă este apelată cu valoarea 0 pentru argumentul major) și crearea unei intrări în /proc/devices. În versiunile viitoare de kernel este posibil să fie eliminată; cu toate acestea majoritatea driverelor încă o apelează.

Ca de obicei, apelul funcției de înregistrare se realizează în funcția de inițializare a modulului, iar apelul funcției de deînregistrare în funcția de ieșire a modulului. Un scenariu obișnuit este prezentat în continuare:

#include <linux/fs.h>
 
#define MY_BLOCK_MAJOR           240
#define MY_BLKDEV_NAME          "mybdev"
 
static int my_block_init(void)
{
        int status;
 
        status = register_blkdev(MY_BLOCK_MAJOR, MY_BLKDEV_NAME);
        if (status < 0) {
                printk(KERN_ERR "unable to register mybdev block device\n");
                return -EBUSY;
        }
        //...
}
 
static void my_block_exit(void)
{
        //...
        unregister_blkdev(MY_BLOCK_MAJOR, MY_BLKDEV_NAME);
}

Înregistrarea unui disc

Cu toate că funcția register_blkdev obține un major, nu pune la dispoziția sistemului un dispozitiv (disc). Pentru crearea și utilizarea de dispozitive de tip bloc (discuri), se folosește o interfață specializată definită în linux/genhd.h.

Funcțiile utile definite în linux/genhd.h sunt cele de înregistrare/alocare a unui disc, de adăugare a acestuia în sistem și de deînregistrare/dezalocare a discului.

Funcția alloc_disk este folosită pentru alocarea unui disc, iar funcția del_gendisk este utilizată pentru dezalocarea acestuia. Adăugarea discului în sistem se realizează cu ajutorul funcției add_disk.

Funcțiile alloc_disk și add_disk se folosesc, de obicei, în funcția de inițializare a modulului, iar funcția del_gendisk în funcția de ieșire a modulului.

#include <linux/fs.h>
#include <linux/genhd.h>
 
#define MY_BLOCK_MINORS	 1
 
static struct my_block_dev {
        struct gendisk *gd;
        //...
} dev;
 
static int create_block_device(struct my_block_dev *dev)
{
        dev->gd = alloc_disk(MY_BLOCK_MINORS);
        //...
        add_disk(dev->gd);
}
 
static int my_block_init(void)
{
        //...
        create_block_device(&dev);
}
 
static void delete_block_device(struct my_block_dev *dev)
{
        if (dev->gd)
                del_gendisk(dev->gd);
        //...
}
 
static void my_block_exit(void)
{
        delete_block_device(&dev);
        //...
}

Ca și la dispozitivele de tip caracter, se recomandă folosirea unei structuri de tipul my_block_dev în care să se regăsească elemente importante ce descriu dispozitivul de tip bloc.

Trebuie reținut faptul că imediat după apelul funcției add_disk (de fapt chiar încă din timpul apelului) discul este activ și metodele sale pot fi apelate la orice moment de timp. Ca urmare, această funcție nu trebuie apelată înainte ca driverul să fie complet inițializat și gata să răspundă cererilor adresate discului înregistrat.

Se observă că structura de bază în lucrul cu dispozitive de tip bloc (discuri) este structura struct gendisk.

După un apel del_gendisk este posibil ca structura struct gendisk să continue să existe (și operațiile asupra dispozitivului să fie apelate în continuare), în cazul în care există utilizatori ai acesteia (s-a apelat o operație open asupra dispozitivului, dar încă nu a fost apelată operația release asociată). O soluție este păstrarea numărului de utilizatori ai dispozitivului și apelarea funcției del_gendisk numai atunci când nu există utilizatori ai acestuia.

Structura gendisk

Structura struct gendisk reține informațiile referitoare la un disc. După cum s-a afirmat și mai sus, o astfel de structură se obține în urma apelului alloc_disk și trebuie completată înainte de a fi transmisă funcției add_disk.

Structura struct gendisk are următoarele câmpuri importante:

  • major, first_minor, minors, care descriu identificatorii folosiți de disc; un disc trebuie să aibă cel puțin un minor; dacă discul permite operația de partiționare, trebuie alocat un minor pentru fiecare partiție posibilă
  • disk_name, care reprezintă numele discului, așa cum apare în /proc/partitions și în sysfs (/sys/block)
  • fops, care reprezintă operațiile asociate discului
  • queue, care reprezintă coada de cereri 2)
  • capacity, care reprezintă capacitatea discului în sectoare de 512 octeți; se inițializează folosind funcția set_capacity
  • private_data, care reprezintă un pointer către datele private

Un exemplu de completare a unei structuri struct gendisk este prezentat în continuare:

#include <linux/genhd.h>
#include <linux/fs.h>
#include <linux/blkdev.h>
 
#define NR_SECTORS			1024
 
#define KERNEL_SECTOR_SIZE		512
 
static struct my_block_dev {
       //...
       spinlock_t lock;                /* For mutual exclusion */
       struct request_queue *queue;    /* The device request queue */
       struct gendisk *gd;             /* The gendisk structure */
       //...
} dev;
 
static int create_block_device(struct my_block_dev *dev)
{
        ...
        /* Initialize the gendisk structure */
        dev->gd = alloc_disk(MY_BLOCK_MINORS);
        if (!dev->gd) {
                printk (KERN_NOTICE "alloc_disk failure\n");
                return -ENOMEM;
        }
 
        dev->gd->major = MY_BLOCK_MAJOR;
        dev->gd->first_minor = 0;
        dev->gd->fops = &my_block_ops;
        dev->gd->queue = dev->queue;
        dev->gd->private_data = dev;
        snprintf (dev->gd->disk_name, 32, "myblock");
        set_capacity(dev->gd, NR_SECTORS);
 
        add_disk(dev->gd);
 
        return 0;
}
 
static int my_block_init(void)
{
        int status;
        //...
        status = create_block_device(&dev);
        if (status < 0)
                return status;
        //...
}
 
static void delete_block_device(struct my_block_dev *dev)
{
        if (dev->gd) {
                del_gendisk(dev->gd);
        }
        //...
}
 
static void my_block_exit(void)
{
        delete_block_device(&dev);
        //...
}

După cum s-a precizat, kernel-ul consideră un disc ca fiind un vector de sectoare cu dimensiunea 512 octeți. În realitate, dispozitivele pot avea altă dimensiune a sectorului. Pentru a lucra cu aceste dispozitive, trebuie informat kernel-ul asupra dimensiunii reale a unui sector, și pentru toate operațiile trebuie făcute conversiile necesare.

Pentru a informa kernel-ul asupra dimensiunii sectorului dispozitivului, trebuie setat un parametru al cozii de cereri, imediat după ce coada este alocată folosind funcția blk_queue_logical_block_size. Toate cererile generate de kernel vor fi multiplu de această dimensiune a sectorului și vor fi aliniate corespunzător. Totuși, comunicația între dispozitiv și driver se va realiza în continuare în sectoare de 512 octeți, astfel încât trebuie făcută conversia de fiecare dată (un exemplu de astfel de conversie este la apelul funcției set_capacity în codul de mai sus).

Structura block_device_operations

La fel cum pentru un dispozitiv de tip caracter trebuiau completate operațiile din file_operations, și pentru un dispozitiv de tip bloc trebuie completate operațiile din block_device_operations. Asocierea operațiilor se realizează prin intermediul câmpului fops din structura struct gendisk.

Câteva din câmpurile structurii block_device_operations sunt prezentate în continuare:

struct block_device_operations {
        int (*open) (struct block_device *, fmode_t);
        int (*release) (struct gendisk *, fmode_t);
        int (*locked_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
        int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
        int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
        int (*direct_access) (struct block_device *, sector_t,
                                                void **, unsigned long *);
        int (*media_changed) (struct gendisk *);
        int (*revalidate_disk) (struct gendisk *);
        int (*getgeo)(struct block_device *, struct hd_geometry *);
        struct module *owner;
}

Operațiile open și release sunt apelate direct din user space de către utilitare de partiționare, creare de sisteme de fișiere sau verificare de sisteme de fișiere. La o operație mount, se apelează open direct din kernel space, identificatorul de fișier fiind reținut de către kernel. Un driver de tip bloc nu poate face diferența între apelurile open din user space și cele din kernel space.

Un exemplu de utilizare a celor două funcții este prezentat în continuare:

#include <linux/fs.h>
#include <linux/genhd.h>
 
static struct my_block_dev {
        //...
        struct gendisk * gd;
        //...
} dev;
 
static int my_block_open(struct block_device *bdev, fmode_t mode)
{
        //...
 
        return 0;
}
 
static void my_block_release(struct gendisk *gd, fmode_t mode)
{
        //...
}
 
struct block_device_operations my_block_ops = {
        .owner = THIS_MODULE,
        .open = my_block_open,
        .release = my_block_release
};
 
static int create_block_device(struct my_block_dev *dev)
{
        //....
        dev->gd->fops = &my_block_ops;
        dev->gd->private_data = dev;
        //...
}

Este de remarcat că nu există operațiile read și write. Aceste operații sunt efectuate de funcția request asociată cu coada de cereri a discului.

Cozi de cereri

Driverele de tip bloc folosesc cozi de cereri pentru a păstra cererile block I/O care urmează să fie procesate. O coadă de cereri este reprezentată de structura struct request_queue. Coada de cereri este formată dintr-o listă dublu înlănțuită de cereri și informația de control asociată. Cererile sunt adăugate la coadă de cod kernel de nivel mai înalt (de exemplu, sistemele de fișiere). Cât timp coada de cereri nu este vidă, driver-ul asociat cozii va trebui să extragă prima cerere din coadă și să o transmită dispozitivului de tip bloc asociat. Fiecare element din lista de cereri este o cerere reprezentată de tipul struct request.

Cozile de cereri implementează o interfață care permite utilizarea mai multor planificatoare I/O (I/O schedulers). Un planificator trebuie să sorteze cererile și să le prezinte driver-ului într-o ordine care să maximizeze performanța. De asemenea, planificatorul se ocupă de combinarea cererilor adiacente (care se referă la sectoare adiacente de pe disc).

Crearea și ștergerea cozii de cereri

O coadă de cereri este creată cu ajutorul funcției blk_init_queue și este ștearsă cu ajutorul funcției blk_cleanup_queue.

Un exemplu de folosire a acestor funcții este următorul:

#include <linux/fs.h>
#include <linux/genhd.h>
#include <linux/blkdev.h>
 
static struct my_block_dev {
        //...
        struct request_queue *queue;
        //...
} dev;
 
static void my_block_request(struct request_queue *q);
//...
 
static int create_block_device(struct my_block_dev *dev)
{
	/* Initialize the I/O queue */
	spin_lock_init(&dev->lock); 
	dev->queue = blk_init_queue(my_block_request, &dev->lock);
	if (dev->queue == NULL)
		goto out_err;
	blk_queue_logical_block_size(dev->queue, KERNEL_SECTOR_SIZE);
	dev->queue->queuedata = dev;
        //...
 
out_err:
        return -ENOMEM;
}
 
static int my_block_init(void)
{
        int status;
        //...
        status = create_block_device(&dev);
        if (status < 0)
                return status;
        //...
}
 
static void delete_block_device(struct block_dev *dev)
{
        //...
        if (dev->queue)
                blk_cleanup_queue(dev->queue);
}
 
static void my_block_exit(void)
{
        delete_block_device(&dev);
        //...
}

Funcția blk_init_queue primește ca prim argument un pointer la funcția de prelucrare a cererilor pentru dispozitiv (de tipul request_fn_proc). În exemplul de mai sus, funcția este my_block_request. Parametrul lock este un spinlock (inițializat de către driver) pe care kernel-ul îl deține în timpul apelului funcției request pentru a asigura accesul exclusiv la coada de cereri. Acest spinlock se poate folosi și în alte funcții ale driver-ului, pentru a proteja accesul la date partajate cu funcția request.

Ca parte a inițializării cozii de cereri se poate configura câmpul queuedata, care este echivalent cu câmpul private_data din alte structuri.

Funcții utile pentru prelucrarea cozilor de cereri

Funcția de tipul request_fn_proc este utilizată pentru tratarea cererilor de lucru cu dispozitivul de tip bloc. Această funcție este echivalentul funcțiilor de citire și scriere întâlnite la dispozitivele de tip caracter. Funcția primește ca argument coada de cereri asociată dispozitivului și poate folosi diverse funcții pentru prelucrarea cererilor din coadă.

Funcțiile utilizate pentru prelucrarea cererilor din coadă, descrise mai jos, sunt:

Înainte de a apela aceste funcții, trebuie obținut spinlock-ul asociat cozii. Dacă aceste funcții sunt apelate în funcția de tip request_fn_proc, spinlock-ul este deja deținut.

Cereri pentru dispozitive de tip bloc

O cerere pentru un dispozitiv de tip bloc este descrisă de structura struct request.

Câmpurile structurii struct request includ:

  • cmd_flags, o serie de flag-uri printre care și direcția (citire sau scriere); pentru a afla direcția se folosește macrodefiniția rq_data_dir, care returnează 0 pentru o cerere de citire și 1 pentru o cerere de scriere pe dispozitiv;
  • __sector, primul sector al cererii de transfer; dacă sectorul dispozitivului are altă dimensiune, trebuie facută conversia corespunzătoare; pentru accesarea acestui câmp se folosește macro-ul blk_rq_pos;
  • __data_len, numărul total de octeți de transferat; pentru accesarea acestui câmp se folosește macro-ul blk_rq_bytes;
  • în general, se vor transfera date din bio-ul curent; dimensiunea datelor se obține cu ajutorul macro-ului blk_rq_cur_bytes ;
  • bio, o listă dinamică de structuri bio care reprezintă un set de bufere asociate cu cererea; acest câmp se accesează cu ajutorul macrodefiniției rq_for_each_segment pentru cazul în care există mai multe buffere sau cu macrodefiniția bio_data pentru cazul în care există un singur bufer asociat; bio_data intoarce adresa buferului asociat cererii
  • despre structura bio și operațiile asociate se va discuta în secțiunea Structura bio

Crearea unei cereri

Cererile de citire/scriere sunt create de nivelurile de cod superioare subsistemului de I/O din nucleu. De obicei, subsistemul care creează cereri pentru dispozitive de tip bloc este subsistemul de gestiune a fișierelor. Subsistemul de I/O acționează ca intermediar între subsistemul de gestiune a fișierelor și driverul de dispozitiv de tip bloc. Principalele operații care intră în responsabilitatea subsistemului de I/O sunt adăugarea cererilor în coada de cereri a dispozitivului de tip bloc specific și sortarea și comasarea cererilor (sorting and merging) din considerente de performanță.

Terminarea unei cereri

Când driverul a terminat de transferat toate sectoarele dintr-o cerere către/dinspre dispozitiv, trebuie să informeze subsistemul de I/O prin apelarea funcției blk_end_request. Dacă lock-ul aferent cozii de cereri este deja obținut, se poate folosi funcția __blk_end_request.

În situația în care driverul dorește să încheie cererea chiar dacă nu a transferat toate sectoarele aferente acesteia, poate apela respectiv funcțiile blk_end_request_all sau __blk_end_request_all. Funcția __blk_end_request_all se apelează dacă lock-ul aferent cozii de cereri este deja obținut.

Prelucrarea cererilor

Partea centrală a unui driver de tip bloc este metoda de tip request_fn_proc. În exemplele anterioare, funcția care satisfăcea acest rol era my_block_request. După cum s-a precizat și în secțiunea Crearea și ștergerea cozii de cereri, această funcție este atașată driverului prin apelarea blk_init_queue.

Această metodă este apelată atunci când kernel-ul consideră că driverul trebuie să proceseze cereri de I/O. Metoda trebuie să pornească procesarea cererilor din coadă, dar nu este obligată să le și termine, cererile putând fi terminate din alte părți ale driverului.

Parametrul lock, transmis la crearea unei cozi de cereri, reprezintă un spinlock pe care kernel-ul îl deține atunci când execută metoda request. Din acest motiv, metoda request rulează în context atomic și trebuie să respecte regulile pentru cod atomic (nu trebuie să apeleze funcții care pot duce la sleep etc.). Acest lock asigură și faptul că nu vor fi adăugate în coada alte cereri pentru device în timp ce se execută metoda request.

Apelarea funcției de prelucrare a cozii de cereri este asincronă relativ la acțiunile oricărui proces din userspace și nu trebuie făcute presupuneri privind procesul în contextul căruia rulează. De asemenea nu trebuie presupus că buffer-ul oferit de o cerere este din kernelspace sau userspace, orice operație care accesează userspace-ul fiind eronata.

În continuare este prezentată una dintre cele mai simple metode de tip request_fn_proc:

static void my_block_request(struct request_queue *q)
{
        struct request *rq;
        struct my_block_dev *dev = q->queuedata;
 
	while (1) {
		rq = blk_fetch_request(q);
		if (rq == NULL)
			break;
 
		if (blk_rq_is_passthrough(rq)) {
			printk (KERN_NOTICE "Skip non-fs request\n");
			__blk_end_request_all(rq, -EIO);
			continue;
		}
 
                /* do work */
                ...
 
		__blk_end_request_all(rq, 0);
	}
}

Funcția my_block_request conține un ciclu while de parcurgere a cererilor din coada de cereri transmisă ca argument. Operațiile realizate în cadrul acestui ciclu sunt:

  • Se citește prima cerere din coadă folosind blk_fetch_request. Așa cum a fost descrisă aici, funcția blk_fetch_request obține primul element din coada de cereri și pornește cererea.
    • Dacă funcția întoarce NULL, s-a ajuns la sfârșitul cozii de cereri (nu mai este nici o cerere de prelucrat) și se iese din funcție.
  • Un dispozitiv de tip bloc poate primi cereri care nu transferă blocuri de date (operații low-level asupra discului, instrucțiuni referitoare la moduri speciale de accesare a dispozitivului). Majoritatea driverelor nu știu cum să trateze aceste cereri și întorc eroare.
  • Se prelucrează cererea conform nevoilor dispozitivului aferent.
  • Se încheie cererea. În cazul de față, se apelează funcția __blk_end_request_all pentru a încheiea complet cererea. Dacă toate sectoarele cererii au fost prelucrate, se folosește funcția __blk_end_request.

Structura bio

Fiecare structură struct request reprezintă o cerere block I/O, dar poate proveni din combinarea mai multor cereri independente de la un nivel mai înalt. Sectoarele ce trebuie transferate pentru o cerere pot fi dispersate în memoria principală, dar întotdeauna corespund unui set de sectoare consecutive de pe dispozitiv. Cererea este reprezentată ca o mulțime de segmente, fiecare corespunzând unui buffer din memorie. Kernel-ul poate combina cereri care se referă la sectoare adiacente, dar nu va combina cereri de scriere cu cereri de citire într-o singură structură struct request.

O structură struct request este implementată ca o listă înlănțuită de structuri bio împreună cu informații care permit driver-ului să-și rețină poziția curentă în timp ce procesează cererea.

Structura bio este o descriere low-level a unei porțiuni dintr-o cerere block I/O.

struct bio {
        //...
        struct gendisk          *bi_disk;
        unsigned int            bi_opf;          /* bottom bits req flags, top bits REQ_OP. Use accessors. */
	//... 
        struct bio_vec          *bi_io_vec;     /* the actual vec list */
        //...
        struct bvec_iter        bi_iter;
        /...
        void                    *bi_private;
	//...
};

La rândul ei, structura bio conține un vector bi_io_vec de tipul struct bio_vec. Acesta este format din paginile individuale din memoria fizică ce trebuie transferate, offsetul în cadrul paginii și dimensiunea buferului. Pentru a parcurge o structura bio, trebuie parcurs acest vector de structuri struct bio_vec și transferate datele din fiecare pagină fizică. Pentru a simplifica parcurgerea vectorului se folosește struct bvec_iter. Această structură menține informații despre câte bufere și sectoare au fost consumate în timpul parcurgerii. Tipul cererii este encodat în câmpul bi_opf, pentru determinarea acestuia folosiți funcția bio_data_dir.

Crearea unei structuri bio

Pentru crearea unei structuri bio se pot folosi două funcții:

  • bio_alloc - alocă spațiu pentru o nouă structură; structura trebuie inițializată;
  • bio_clone - realizează o copie a unei structuri bio existente; structura nou obținută este inițializată cu valorile câmpurilor structurii clonate; buferele sunt partajate cu structura bio care a fost clonată astfel încât accesul la bufere trebuie făcut cu atenție pentru a evita accesul la aceași zonă de memorie din cele două clone

Ambele funcții întorc o nouă structură bio.

Transmiterea unei structuri bio

De obicei o structură bio este creată de nivelurile superioare ale kernel-ului (de obicei sistemul de fișiere). O structură astfel creată este apoi transmisă subsistemului de I/O care adună mai multe structuri bio într-o cerere.

Pentru transmiterea unei structuri bio către driverul dispozitivului I/O asociat se folosește funcția submit_bio. Funcția primește ca argument o structură bio inițializată care va fi adăugată unei cereri din coada de cereri a unui dispozitiv I/O. Din acea coadă de cereri va putea fi prelucrată de driverul dispozitivului I/O cu o funcție specializată.

Așteptarea încheierii unei structuri bio

Transmiterea unei structuri bio unui driver are ca efect adăugarea acesteia într-o cerere din coada de cereri de unde va fi ulterior prelucrată. Astfel, în momentul în care funcția submit_bio se întoarce, nu se garantează încheierea prelucrării structurii. Dacă se dorește așteptarea prelucrării cererii se va folosi funcția submit_bio_wait.

Pentru a fi notificați atunci când se încheie prelucrarea unei structuri bio (atunci când nu folosim submit_bio_wait), va trebui folosit câmpul bi_end_io al structurii. În acest câmp se precizează funcția care va fi apelată la încheierea prelucrării structurii bio. Pentru a pasa informații către funcție se poate folosi câmpul bi_private al structurii.

Inițializarea unei structuri bio

După ce o structură bio a fost alocată și înainte de a fi transmisă, trebuie inițializată.

Inițializarea structurii presupune completarea câmpurilor importante. După cum s-a precizat anterior, câmpul bi_end_io este folosit pentru a preciza funcția apelată la încheierea prelucrării structurii. Câmpul bi_private este folosit pentru a stoca date utile ce pot fi accesate în funcția bi_end_io.

Câmpul bi_opf specifică tipul operației. Folosiți bio_set_op_attrs pentru a inițializa tipul operației.

       struct bio *bio = bio_alloc(GFP_NOIO, 1);  
       //...
       bio->bi_disk = bdev->bd_disk;
       bio->bi_iter.bi_sector = sector;
       bio_set_op_attrs(bio, REQ_OP_READ, 0);
       bio_add_page(bio, page, size, offset);
       //...

În extrasul de mai sus se specifică dispozitivul de tip bloc către care va fi trimis bio-ul, sectorul de început, operația (REQ_OP_READ or REQ_OP_WRITE) și conținutul. Conținutul unui bio este un bufer descris prin: o pagină fizică, offset-ul în pagină și dimensiunea buferului. O pagină poate fi alocată folosind apelul alloc_page.

Câmpul size al apelului bio_add_page trebuie să fie multiplu de dimensiunea sectorului dispozitivului.

Folosirea conținutului unei structuri bio

Pentru folosirea conținutului unei structuri bio, paginile de suport ale structurii trebuie mapate în spațiul de adresă nucleu de unde vor putea fi accesate. Pentru mapare/demapare se folosesc macrourile kmap_atomic și kunmap_atomic 3).

Un exemplu tipic de folosire este:

static void my_block_transfer(struct my_block_dev *dev, size_t start, size_t len, char *buffer, int dir);
 
 
static int my_xfer_bio(struct my_block_dev *dev, struct bio *bio)
{
    struct bio_vec bvec;
    struct bvec_iter i;
    int dir = bio_data_dir(bio);
 
    /* Do each segment independently. */
    bio_for_each_segment(bvec, bio, i) {
        sector_t sector = i.bi_sector;
        char *buffer = kmap_atomic(bvec.bv_page);
        unsigned long offset = bvec.bv_offset;
        size_t len = bvec.bv_len;
 
        /* process mapped buffer */
        my_block_transfer(dev, sector, len, buffer + offset, dir);
 
        kunmap_atomic(buffer);
    }
 
    return 0;
}

După cum se observă din exemplul de mai sus, parcurgerea unui bio presupune iterarea prin toate segmentele acestuia. Un segment (bio_vec) este definit de pagina adresei fizice, offset-ul în pagină și dimensiunea acestuia.

Pentru a simplifica procesarea unui bio se folosește macrodefiniția bio_for_each_segment. Aceasta va itera prin toate segmentele și de asemenea va actualiza informații globale, stocate într-un iterator (bvec_iter), cum ar fi sectorul curent precum și alte informații interne (indexul în vectorul de segmente, numărul de octeți rămași de procesat, etc.).

Se pot stoca informații în buffer-ul mapat sau se pot extrage informații.

În cazul în care se folosesc cozile de cereri și se dorește prelucrarea cererilor la nivel de structură bio, se va folosi macrodefiniția rq_for_each_segment în locul bio_for_each_segment. Această macrodefiniție parcurge fiecare segment din fiecare structură bio a unei cereri struct request și actualizează o structură struct req_iterator. Structura struct req_iterator conține structura curentă bio și iteratorul ce parcurge segmentele acestuia. Un exemplu tipic de folosire este:

    struct bio_vec bvec;
    struct req_iterator iter;
 
    rq_for_each_segment(bvec, req, iter) {
        sector_t sector = iter.iter.bi_sector;
        char *buffer = kmap_atomic(bvec.bv_page);
        unsigned long offset = bvec.bv_offset;
        size_t len = bvec.bv_len;
        int dir = bio_data_dir(iter.bio);
 
        my_block_transfer(dev, sector, len, buffer + offset, dir);
 
        kunmap_atomic(buffer);
   }

Eliberarea unei structuri bio

După ce un subsistem al nucleului folosește o structură bio va trebui să elibereze referința către aceasta. Acest lucru se realizează cu ajutorul funcției bio_put.

Configurarea unei cozi de cerere la nivel de bio

Cu ajutorul funcției blk_init_queue se putea specifica o funcție care să fie folosită pentru prelucrarea cererilor transmise driverului. Funcția primea ca argument coada de cereri și realiza prelucrări la nivel de structuri request.

Dacă, din motive de flexibilitate, se dorește specificarea unei funcții care să realizeze prelucrări la nivel de bio, trebuie folosită funcția blk_queue_make_request în conjuncție cu funcția blk_alloc_queue. Mai jos este prezentat un exemplu tipic de inițializare a unei funcții de prelucrare la nivel de bio:

// semnatura functiei de tratare
static void my_make_request(struct request_queue *q, struct bio *bio);
 
 
// ...
// crearea cozii
dev->queue = blk_alloc_queue (GFP_KERNEL);
if (dev->queue == NULL) {
        printk (KERN_ERR "cannot allocate block device queue\n");
        return -ENOMEM;
}
// inregistrarea functiei de tratare
blk_queue_make_request (dev->queue, my_make_request);
dev->queue->queuedata = dev;

Funcția my_make_request este de tipul make_request_fn. Un exemplu de folosire a unei astfel de funcții se găsește în drivers/md/md.c.

În cazul în care se folosește această metodă, nu se mai folosesc cozile de cereri, fiecare cerere fiind reprezentă de o structură bio. Astfel, transferul de date se reduce la parcurgerea fiecărei structuri bio după cum a fost prezentat mai sus și semnalarea terminării prelucrării acesteia cu ajutorul funcției bio_endio.

Resurse utile

Linux

1) Pentru mai multe detalii despre alegerea identificatorilor major și minor, consultați laboratorul 4
2) Despre structura struct request_queue și operațiile pentru prelucrarea cozilor de cereri se va discuta în secțiunea Cozi de cereri
3) Mai multe informații despre maparea atomică a memoriei găsiți în Linux Device Drivers 3rd Edition, Chapter 15. Memory Mapping and DMA - The Memory Map and Struct Page
so2/laboratoare/lab07.txt · Last modified: 2018/05/02 12:31 by theodor.stoican
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0