View Single Post
Old 10-01-2005, 13:46   #13
ilsensine
Senior Member
 
L'Avatar di ilsensine
 
Iscritto dal: Apr 2000
Città: Roma
Messaggi: 15625
Introduzione ai block device

I char device offrono dei metodi di interfaccia (file_operations) molto completi, che coprono praticamente tutte le necessità. Perché allora sono stati introdotti i "block device"?
Immaginate il driver per una unità disco. Un simile driver deve soddisfare una serie di requisiti:
- supporto per il partizionamento
- supporto per il caching
- supporto per il read ahead
- supporto per una famiglia di ioctl specifiche per operazioni su disco
- semplicità di interfacciamento con altri layer del kernel (ad es. i file system)
- generalmente i dispositivi di storage permettono un accesso ai dati "per blocchi di byte"
Questi requisiti sono comuni con tutte le unità a disco, è superfluo che ogni driver ne duplichi le funzionalità. Il kernel di linux supporta automaticamente tutto questo; in particolare, supporta
- gestione avanzata della cache (buffer cache)
- ottimizzazione delle operazioni di i/o (i/o scheduler; al momento ne abbiamo quatto: anticipatory scheduler, cfq scheduler, deadline scheduler, null scheduler)
Il modello di servizi che il driver deve fornire è dunque diverso: anziché gestire direttamente le operazioni di accesso, il driver deve poter rispondere a delle operazioni che di tanto in tanto il kernel ci chiede di compiere (ad es. "leggi questa regione, scrivi quest'altra"...). Il resto è automaticamente svolto dal kernel.
Dicevo prima che l'implementazione di un block device è complicata: la complicazione sta appunto nel come il driver deve svolgere le richieste. L'esempio seguente è molto banale; implementeremo un driver che usa la memoria di sistema come storage per emulare un disco, e su di questo compie le operazioni di lettura/scrittura richieste dal kernel:
Codice:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/vmalloc.h>
#include <linux/genhd.h>
#include <linux/blkdev.h>
#include <linux/hdreg.h>

#define KERNEL_SECTOR_SIZE	512
#define DEVICE_SECTOR_SIZE	512
#define DEVICE_SECTORS		1024

static struct kblock_data {
	unsigned long ssize;
	unsigned long sectors;
	spinlock_t lock;
	unsigned char *map;
	struct gendisk *disk;
} kbdata;

static void kblock_do_request(struct kblock_data *kb, struct request *req)
{
	unsigned long offset = req->sector*KERNEL_SECTOR_SIZE;
	unsigned long nbytes = req->current_nr_sectors*KERNEL_SECTOR_SIZE;
	if ((req->sector+req->current_nr_sectors) >
			kb->sectors*(kb->ssize/KERNEL_SECTOR_SIZE)) {
		printk(KERN_ERR "kblock: Attempting to access beyond end of device\n");
		end_request(req, 0);
		return;
	}
	if (rq_data_dir(req)==WRITE)
		memcpy(kb->map+offset, req->buffer, nbytes);
	else
		memcpy(req->buffer, kb->map+offset, nbytes);
	end_request(req, 1);
}

static void kblock_request(request_queue_t *q)
{
	struct request *req;
	while ((req = elv_next_request(q)) != NULL) {
		if (!blk_fs_request(req)) {
			printk (KERN_INFO "Non-CMD request\n");
			end_request(req, 0);
			continue;
		}
		kblock_do_request(q->queuedata, req);
	}
}

static int kblock_open(struct inode *ino, struct file *filp)
{
	filp->private_data = &kbdata;
	return 0;
}

static int kblock_ioctl(struct inode *inode, struct file *filp,
		unsigned int cmd, unsigned long arg)
{
	struct hd_geometry geo;
	struct kblock_data *kb = filp->private_data;
	unsigned long  sz = kb->sectors*(kb->ssize/KERNEL_SECTOR_SIZE);
	int ret = 0;
	switch (cmd) {
	case HDIO_GETGEO:
		geo.cylinders = sz >> 6;
		geo.heads = 4;
		geo.sectors = 16;
		geo.start = 0;
		if(copy_to_user((void __user*) arg, &geo, sizeof(geo)))
			ret = -EFAULT;
		break;
	default:
		ret = -ENOTTY;
    }
    return ret;
}

static struct block_device_operations kblock_ops = {
	.owner	= THIS_MODULE,
	.open	= kblock_open,
	.ioctl	= kblock_ioctl
};

static int __init kblock_init(void)
{
	int ret = -ENOMEM;

	kbdata.ssize = DEVICE_SECTOR_SIZE;
	kbdata.sectors = DEVICE_SECTORS;
	spin_lock_init(&kbdata.lock);

	kbdata.map = vmalloc(kbdata.ssize*kbdata.sectors);
	if (!kbdata.map)
		return -ENOMEM;
	memset(kbdata.map, 0, kbdata.ssize*kbdata.sectors);

	kbdata.disk = alloc_disk(16);
	if (!kbdata.disk)
		goto out;
	kbdata.disk->first_minor = 0;
	kbdata.disk->fops = &kblock_ops;
	kbdata.disk->private_data = &kbdata;
	sprintf(kbdata.disk->disk_name, "kblock");
	sprintf(kbdata.disk->devfs_name, "kblock");
	set_capacity(kbdata.disk, kbdata.sectors*(kbdata.ssize/KERNEL_SECTOR_SIZE));

	kbdata.disk->major = register_blkdev(0, "kblock");
	if (kbdata.disk->major<0) {
		ret = kbdata.disk->major;
		goto out_disk;
	}

	kbdata.disk->queue = blk_init_queue(kblock_request, &kbdata.lock);
	if (!kbdata.disk->queue)
		goto out_unregister;
	blk_queue_hardsect_size(kbdata.disk->queue, kbdata.ssize);
	kbdata.disk->queue->queuedata = &kbdata;

	add_disk(kbdata.disk);
	printk(KERN_INFO "Device kblock registered on major %d\n", kbdata.disk->major);
	return 0;
out_unregister:
	unregister_blkdev(kbdata.disk->major, "kblock");
out_disk:
	put_disk(kbdata.disk);
out:
	vfree(kbdata.map);
	return ret;
}

static void __exit kblock_exit(void)
{
	request_queue_t *rq = kbdata.disk->queue;
	int major = kbdata.disk->major;
	del_gendisk(kbdata.disk);
	put_disk(kbdata.disk);
	unregister_blkdev(major, "kblock");
	blk_cleanup_queue(rq);
	vfree(kbdata.map);
}

module_init(kblock_init);
module_exit(kblock_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ilsensine");
MODULE_DESCRIPTION("Sample block device driver");
In questo driver sono presenti parecchie cose nuove, che esamineremo in dettaglio. Innanzitutto notate le file_operations per i block device (struct block_device_operations, definita in linux/fs.h): questa struttura è molto piu semplice delle fops relative a un char device. Nel nostro esempio i metodi open e ioctl non fanno praticamente nulla (l'ioctl emula semplicemente una rappresentazione chs del disco, non effettuabile in maniera generale dal kernel). Concentriamoci quindi sul resto.

Nella funzione di inizializzazione, viene dapprima allocata la memoria che conterrà il nostro disco:
Codice:
	kbdata.ssize = DEVICE_SECTOR_SIZE;
	kbdata.sectors = DEVICE_SECTORS;
	spin_lock_init(&kbdata.lock);
	kbdata.map = vmalloc(kbdata.ssize*kbdata.sectors);
	if (!kbdata.map)
		return -ENOMEM;
	memset(kbdata.map, 0, kbdata.ssize*kbdata.sectors);
Ogni block device è identificato da un certo numero di settori, e dalla dimensione del settore. Il kernel internamente accede a un block device in settori multipli di 512 byte; questo valore è indipendente dalla dimensione fisica del settore del nostro disco (nell'esempio sono stati impostati uguali). L'unico vincolo, è che la dimensione "hadrware" del settore deve essere un multimplo di 512.

Proseguiamo oltre. Tutta la gestione della "interfaccia come disco" del block device, compreso il supporto per il partizionamento, è gestito dall'interfaccia "gendisk" del kernel. Possiamo allocare un gendisk tramite la funzione alloc_disk:
Codice:
	kbdata.disk = alloc_disk(16);
che accetta come parametro il massimo numero di minor destinati al disco (pari al numero massimo delle partizioni più uno). Se utilizziamo "1", rinunciamo al supporto automatico per il partizionamento. Più avanti impostiamo la dimensione del disco:
Codice:
	set_capacity(kbdata.disk, kbdata.sectors*(kbdata.ssize/KERNEL_SECTOR_SIZE));
specificata in unità da 512 byte (KERNEL_SECTOR_SIZE).
Quindi dobbiamo "registrare" il nostro block device, riservando un major number:
Codice:
	kbdata.disk->major = register_blkdev(0, "kblock");
Avendo indicato "0", il major number verrà assegnato dinamicamente.
La funzione successiva è molto importante, è la vera differenza con i char device ed entra nel cuore del driver:
Codice:
	kbdata.disk->queue = blk_init_queue(kblock_request, &kbdata.lock);
Quello che il nostro driver dovrà fare non è gestire direttamente read/write/ecc, ma delle richieste di i/o provenienti dal kernel. Le richieste sono rappresentate da una "request queue" (v. linux/blkdev.h). Per allocare una request queue, dobbiamo indicare la funzione che svolgerà le richieste, e uno spinlock. Lo spinlock sarà automaticamente bloccato dal kernel prima di ogni chiamata alla funzione kblock_request.

Soffermiamoci quindi su questa funzione, che è sorprendentemente (ingannevolmente) semplice:
Codice:
static void kblock_request(request_queue_t *q)
{
	struct request *req;
	while ((req = elv_next_request(q)) != NULL) {
		if (!blk_fs_request(req)) {
			printk (KERN_INFO "Non-CMD request\n");
			end_request(req, 0);
			continue;
		}
		kblock_do_request(q->queuedata, req);
	}
}
Eseguiamo il ciclo while finché ci sono richieste preparate per noi dall'elevator (altro nome dell'i/o scheduler). Gestire bene una richiesta è complesso: ognuna è composta da una serie di richieste elementari (BIO) che possiamo demandare al dispositivo. Non seguiremo questa strada ma ci limiteremo a gestire la richiesta come un tutt'uno, così come ci è stata gentilmente accorpata dall'elevator. Di questo se ne occupa la funzione kblock_do_request, che è autoesplicativa vista la sua semplicità. Un driver hw ben scritto probabilmente vorrà sottomettere all'hw direttamente le singole richieste BIO, per sfruttare eventuali parallelismi, operazioni dma in scatter-gather o altro. Non entriamo in ulteriori dettagli.
Note:
- Dove scrivono le funzioni memcpy in kblock_do_request? Nella buffer cache del kernel.
- Occhio che kblock_request (quindi kblock_do_request) è eseguita sotto spinlock (v. drivers/block/ll_rw_blk.c). Quindi deve essere veloce, e in particolare non deve per nessun motivo schedulare. Sarebbe una buona idea demandare a "qualcun altro" lo svolgimento fisico delle operazioni, per motivi di latenza. Farò un esempio più avanti, tempo permettendo.
- blk_init_queue non è l'unico modo di gestire una request queue, ma è il più semplice. Esistono infatti diversi modi di lavorare con le richieste, dal più semplice (il nostro) al più dettagliato, a basso livello. Ad es. il driver loop.c utilizza un altro approccio.
__________________
0: or %edi, %ecx; adc %eax, (%edx); popf; je 0b-22; pop %ebx; fadds 0x56(%ecx); lds 0x56(%ebx), %esp; mov %al, %al
andeqs pc, r1, #147456; blpl 0xff8dd280; ldrgtb r4, [r6, #-472]; addgt r5, r8, r3, ror #12

Ultima modifica di ilsensine : 23-02-2006 alle 15:58.
ilsensine è offline