I dischi SCSI

di Alessandro Rubini

Riprodotto con il permesso di Linux Magazine, Edizioni Master.

Una descrizione, con un esempio funzionante, di come si creano periferiche di memorizzazione usando il sottosistema SCSI del kernel.

Uno dei tipi principali di periferica è l'unità di memorizzazione, tipicamente rappresentata da un disco. Tali periferiche sono normalmente gestite dai cosiddetti device driver a blocchi, cui si accedede tramite un file speciale di tipo b nella directory /dev, come per esempio /dev/hda. La realizzazione di un driver a blocchi risulta però alquanto laboriosa, perché richiede la gestione di varie strutture dati associate al trasferimento dati come code di buffer e di richieste.

Un modo decisamente più semplice per pilotare una periferica di memorizzazione è appoggiarsi al sottosistema SCSI, scrivendo un driver che si presenti come un host SCSI che controlli un'unità disco.

È questo il meccanismo usato dal modulo usb-storage, che registra degli host SCSI per presentare le periferiche USB come dischi SCSI, provvedendo ad decodificare i comandi SCSI traducendoli in transazioni USB.

Questo articolo spiega come gestire una periferica di memorizzazione tramite il sottosistema SCSI riferendosi ad un pacchetto di esempio chiamato sdm (SCSI Disk in Memory) che implementa un ramdisk. Il codice completo è disponibile come http://www.linux.it/kerneldocs/scsi-storage/src.tar.gz ed è stato sviluppato sotto Linux-2.6.8.1. Per lo sviluppo mo sono basato pesantemente sui file contenuti in drivers/usb/storage, puntando a semplificare l'implementazione anche se a discapito della completezza e della stabilità.


Riquadro 1 - Host SCSI

Storicamente, i controllori SCSI si chiamano host, termine che viene più comunemente usato per indicare i grossi server (o i comuni PC di oggi, equivalenti ai grossi server di pochi anni fa).

In ambito SCSI, il termine è un'abbreviazione di host adapter, cioè l'interfaccia tra il cavo SCSI e la macchina ospite (host, appunto). Tale lessico è stato ripreso in ambito USB, dove le sigle UHCI, OHCI ed EHCI che si riferiscono ai controllori USB indicano proprio Host Controller Interface ed i loro driver risiedono in drivers/usb/host.


Il supporto SCSI

Il supporto per le periferiche SCSI si trova nella directory drivers/scsi, nella quale vengono definiti vari moduli: l'infrastruttura di base (scsi), il supporto per i dischi (sd) i CDROM (sr) e altri tipi di periferiche, i driver per i vari controllori hardware.

Per la gestione di periferiche di memorizzazione è necessario avere nel proprio kernel il supporto di base e quello per i dischi. Se si sceglie di caricare tale codice come moduli del kernel, i nomi dei moduli da caricare saranno scsi_mod e sd_mod.

Al caricamento di scsi_mod verranno generati i seguenti messaggi:

    <7>device class 'scsi_host': registering
    <7>bus type 'scsi' registered
    <7>device class 'scsi_device': registering
    <5>SCSI subsystem initialized

mentre il caricamento di sd_mod corrisponderà a questo messaggio:

    <7>bus scsi: add driver sd

Si noti come abbia scelto di presentare i messaggi nella loro forma grezza, completi di priorità, come appaiono in /proc/kmsg (e come spiegato nel numero 40 di Aprile 2004).

Ipotizzando che non ci siano controllori SCSI sulla macchina usata per le prove, il driver scsi non avrà alcun bus associato, come si può verificare in /proc/scsi/scsi . Anche il driver sd non starà gestendo alcun disco, ed non risulterà in uso, anche se /proc/devices mostrerà i major number associati ai dischi SCSI. A questo punto è ancora possibile rimuovere il driver sd e successivamente il modulo scsi.

Registrazione di un host

Perché il sistema possa gestire un disco, occorre prima di tutto registrare nel driver SCSI un controllore (host), sul cui bus si possa successivamente istanziare il disco.

Per attivare un controllore, occorre allocare una struttura dati di tipo struct Scsi_Host, darla in gestione al driver SCSI e richiedere la scansione del bus (virtuale, in questo caso).

Le tre funzioni che occorre chiamare per fare questa operazione sono le seguenti, dichiarate in <scsi/scsi_host.h>:

    struct Scsi_Host \*scsi_host_alloc(struct scsi_host_template \*, int);
    int scsi_add_host(struct Scsi_Host \*, struct device \*);
    void scsi_scan_host(struct Scsi_Host \*);

qui, la struttura scsi_host_template serve a definire le caratteristiche del nostro host e viene usata come riferimento per la creazione di una nuova struttura Scsi_Host. Con lo stesso schema si possono più di un host, se necessario.

L'argomento di tipo struct device * viene usato per inserire questo host nella struttura complessiva di sistema relativa ai driver. Nel programma di esempio questa funzionalità, come altre, non viene utilizzata per non appesantire il codice e concentrarsi solo sul nocciolo del problema.

Il momento in cui chiamare queste funzioni dipende dalle caratteristiche del driver. Nel caso di usb-storage esse vengono chiamate nella fase di probe del bus USB (che avviene all'inserimento del cavo USB, o subito se la periferica è già collegata): ogni periferica di tipo usb storage viene istanziata come un nuovo controllore SCSI mentre l'inizializzazione del modulo non fa altro che registrare la funzione di probe nel sottosistema USB. Nel caso del nostro programma di esempio, invece, il nuovo controllore SCSI viene creato e scandito durante l'inizializzazione del modulo, in quanto la nostra periferica, la RAM, è già disponibile nel sistema.

Il modulo sdm-data

Il programma di esempio si chiama sdm, cioè "SCSI Disk in Memory", anche se il file dei sorgenti si chiama src.tar.gz come per tutti gli altri articoli di questa serie.

Una caratteristica fondamentale delle periferiche di memorizzazione è la loro persistenza nel tempo, cosa che non si applica invece alla RAM. Per simulare la rimozione e il successivo ricollegamento di un disco, l'esempio sdm è stato diviso in due moduli del kernel: sdm-data per gestire l'area dati e sdm-code per implementare il controllore SCSI. E` cosi` possibile rimuovere il modulo sdm-code e reinserirlo successivamente senza perdita di dati nel ramdisk.

Il codice di sdm-data è estremamente semplice: si tratta di un modulo configurabile tramite due parametri interi (sdm_size e sdm_verbose) che esporta alcuni simboli in modo che sdm-code possa usufruirne. Il codice completo di sdm-data è rappresentato nel riquadro 2.

sdm-data alloca un vettore di puntatori, in cui verranno successivamente memorizzate le pagine di memoria associate al ramdisk. Inizialmente vengono allocati solo i puntatori ed e` l'uso del ramdisk che porterà ad allocare lo spazio dati necessario. Si noti che non è possibile, a questo livello, liberare la RAM quando l'occupazione del ramdisk diminuisce.

Nel momento in cui sdm-data sia eliminato dal sistema, tutto il ramdisk viene liberato e la memoria torna disponibile.

I due parametri del modulo sono sdm_size, che specifica la dimensione in megabyte del ramdisk (valore predefinito: 128), e sdm_verbose che controlla il livello di prolissità del driver: 0 per nessun messaggio, 1 (default) per stampare i nomi dei comandi SCSI, 2 per stampare ulteriori dettagli delle operazioni di lettura e scrittura sul disco.

Riquadro 2 - sdm-data.c
/* Copyright (c) 2004 Alessandro Rubini. GNU GPL 2 or later */
#include <linux/config.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/mm.h>
#include "sdm.h"

MODULE_LICENSE("GPL");

int sdm_size = 128; /* megs */
int sdm_npages;
int sdm_verbose = 1;
unsigned long *sdm_pages;

module_param(sdm_size, int, 0);
module_param(sdm_verbose, int, 0);

EXPORT_SYMBOL(sdm_size);
EXPORT_SYMBOL(sdm_npages);
EXPORT_SYMBOL(sdm_verbose);
EXPORT_SYMBOL(sdm_pages);

static int sdm_d_init(void)
{
	sdm_npages = sdm_size << (20 - PAGE_SHIFT);
	sdm_pages = kmalloc(sdm_npages * sizeof(unsigned long *), GFP_KERNEL);
	if (!sdm_pages) return -ENOMEM;
	memset(sdm_pages, 0, sdm_npages * sizeof(unsigned long *));
	return 0;
}

static void sdm_d_exit(void)
{
	int i;
	for (i = 0; i < sdm_npages; i++) {
		if (sdm_pages[i])
			free_page(sdm_pages[i]);
	}
	kfree(sdm_pages);
}
module_init(sdm_d_init);
module_exit(sdm_d_exit);


Il modulo sdm-code

La gestione vera e propria del controllore SCSI viene fatta dal modulo sdm-code, che consta di circa 400 righe di codice, piu` una tabella di stringhe per i messaggi di diagnostica. La procedura di inizializzazione (sdm_init) registra il controllore SCSI e chiede la scansione del bus, che porterà all'identificazione di un disco SCSI della dimensione specificata in sdm-data.

Se il parametro sdm_verbose è posto a zero, i messaggi stampati al caricamento del modulo saranno quelli riportati nel riquadro 3. Si noti come dopo aver rimosso e ricaricato il modulo il nome scsi0 viene sostituito da scsi1 (e così via in seguito), ma questa gestione degli identificativi non e` dovuta ad un problema nell'allocazione e nel rilascio delle risorse.

Le prime 7 righe riportate nel riquadro dei messaggi indicano una delle leggerezze del codice di esempio: non è stata implementata nessuna gestione dell'errore. Tale mancanza è considerato così grave da causare una chiamata alla funzione dump_stack, che decodifica lo stack del processo corrente per aiutare ad identificare il problema; tale decodifica occupa le quattro righe dopo le tre contrassegnate da "ERROR". Nonostante questo straziante grido di dolore, il codice del driver SCSI si comporta correttamente anche in mancanza di una gestione degli errori, che risulta in effetti superflua per un host che non genera errori né timeout.

Per semplificare al massimo il codice di sdm_data, sia la dimensione del blocco hardware sia la dimensione massima di un trasferimento dati sono definiti pari a PAGE_SIZE; in questo modo ogni operazione di lettura o scrittura deve solo trasferire il contenuto di una pagina, evitando cicli iterativi e calcoli di offset. La dimensione massima di un trasferimento è una caratteristica del bus, definita in sdm_host_template (lo schema usato per la creazione dell'host), mentre la dimensione del blocco hardware è una caratteristica del disco e viene riportata insieme alla dimensione totale in risposta al comando SCSI READ_CAPACITY.


Riquadro 3 - I messaggi di sdm-code
<3>ERROR: SCSI host `sdm' has no error handling
<4>ERROR: This is not a safe way to run your SCSI host
<4>ERROR: The error handling must be added to this driver
<4> [<e009ffe9>] scsi_host_alloc+0x89/0x2d0 [scsi_mod]
<4> [<e00883ce>] sdm_init+0x5e/0xb0 [sdm_code]
<4> [<c0121921>] sys_init_module+0xe1/0x1c0
<4> [<c0103a03>] syscall_call+0x7/0xb
<6>scsi0 : SCSI emulation for RAM disk
<7>DEV: registering device: ID = 'host0'
<7>CLASS: registering class device: ID = 'host0'
<5>  Vendor: scsi-mem  Model: LM-DEMO (rubini)  Rev: 1.0 
<5>  Type:   Direct-Access                      ANSI SCSI revision: 02
<7>DEV: registering device: ID = '0:0:0:0'
<7>bus scsi: add device 0:0:0:0
<5>SCSI device sda: 32769 4096-byte hdwr sectors (134 MB)
<5>sda: Write Protect is off
<7>sda: Mode Sense: 00 00 00 00
<3>sda: asking for cache data failed
<3>sda: assuming drive cache: write through
<6> sda: unknown partition table
<5>Attached scsi removable disk sda at scsi0, channel 0, id 0, lun 0
<7>bound device '0:0:0:0' to driver 'sd'
<7>CLASS: registering class device: ID = '0:0:0:0'


I comandi SCSI

Tutte le operazioni sul controllore, a partire dalla scansione delle periferiche, vengono effettuate tramite l'invio di comandi, il cui formato è standardizzato. Normalmente, tali comandi vengono inviati direttamente alle periferiche SCSI e possono essere elaborati anche in maniera asincrona. Nel nostro caso semplificato i comandi vengono elaborati solo sequenzialmente in base a quanto specificato nella struttura host_template.

Il riquadro 4 mostra i comandi SCSI effettuati durante la scansione del bus; si noti come INQUIRY viene effettuato per 8 periferiche SCSI, ma sdm-code risponde solo per l'identificativo 0, al quale vongono diretti ulteriori comandi. La stampa dei nomi dei comandi SCSI, che avviene solo se sdm_verbose è maggiore di zero, usa una tabella di stringhe derivata da <include/scsi/scsi.h>.


Riquadro 4 - I comandi di scansione del bus
<4>sdm: "INQUIRY" len 6 dir 2 (id 0, lun 0)
<4>sdm: "TEST_UNIT_READY" len 6 dir 3 (id 0, lun 0)
<4>sdm: "READ_CAPACITY" len 10 dir 2 (id 0, lun 0)
<4>sdm: "MODE_SENSE" len 6 dir 2 (id 0, lun 0)
<4>sdm: "MODE_SENSE" len 6 dir 2 (id 0, lun 0)
<4>sdm: "TEST_UNIT_READY" len 6 dir 3 (id 0, lun 0)
<4>sdm: "ALLOW_MEDIUM_REMOVAL" len 6 dir 3 (id 0, lun 0)
<6>sdm: "READ_10" len 10 dir 2 (id 0, lun 0)
<4>sdm: "ALLOW_MEDIUM_REMOVAL" len 6 dir 3 (id 0, lun 0)
<4>sdm: "INQUIRY" len 6 dir 2 (id 1, lun 0)
<4>sdm: "INQUIRY" len 6 dir 2 (id 2, lun 0)
<4>sdm: "INQUIRY" len 6 dir 2 (id 3, lun 0)
<4>sdm: "INQUIRY" len 6 dir 2 (id 4, lun 0)
<4>sdm: "INQUIRY" len 6 dir 2 (id 5, lun 0)
<4>sdm: "INQUIRY" len 6 dir 2 (id 6, lun 0)
<4>sdm: "INQUIRY" len 6 dir 2 (id 7, lun 0)


I comandi SCSI sono descritti da una struttura Scsi_Cmnd, che purtroppo viene definita nel file drivers/scsi/scsi.h, al di fuori dell'albero include del kernel. Il metodo canonico, usato per esempio in drivers/usb/storage, per includere questo file consiste nello specificare

	EXTRA_CFLAGS := -Idrivers/scsi

nel Makefile e includere "scsi.h" nel sorgente C. Un via alternativa sarebbe includere <../drivers/scsi/scsi.h> senza modificare il Makefile. In sdm-code mi sono attenuto alla forma suggerita dagli autori del kernel.

Il puntatore alla struttura Scsi_Cmnd viene chiamato in modi diversi nei vari file sorgente del kernel; sdm-code usa lo stesso nome scelto da usb-storage, su cui si basa: srb (SCSI Request Block), che richiama il nome urb (USB Request Block).

Il sottosistema SCSI chiede l'esecuzione di comandi (o "richieste") chiamando una funzione dell'host interessato, alla quale viene passata la richiesta stessa e una funzione da invocare a completamento della richiesta; nel nostro caso le richieste vengono ricevute da sdm_queuecommand, che consta di poche righe:

    static int sdm_queuecommand(Scsi_Cmnd *srb,
	    void (*done)(Scsi_Cmnd *))
    {
	    srb->scsi_done = done;
	    sdm_dev.srb = srb;
	    up(&sdm_dev.sema);
	    return 0;
    }

Il ruolo di sdm_queuecommand, quindi, è quello di incapsulare le informazioni nella struttura sdm_dev e poi delegare ad altri l'effettiva esecuzione del comando e l'invocazione della funzione done al completamento dello stesso.

L'uso della delega è estremamente importante perché la funzione queuecommand può essere invocata in qualsiasi circostanza, anche in un contesto di interruzione. In tale contesto il codice del kernel non può eseguire alcuna operazione "bloccante", cioe` le operazioniin cui occorre attendere un evento; sappiamo bene, pero`, che una richiesta di trasferimento dati deve normalmente attendere che il disco effettui il trasferimento e comunichi al driver di aver completato l'operazione. Tale comunicazione, che spesso avviene tramite un'interruzione, è spesso proprio ciò che causa l'inoltro della richiesta successiva, che viene quindi accodata mentre ci si trova in un contesto di interruzione.

Nel caso di sdm-code, che ricalca ancora una volta usb-storage, queuecommand delega l'esecuzione dei comandi ad un processo che, come tutti i processi, può attendere che si verifichino eventi esterni senza causare danni al sistema e senza perdere il proprio contesto di esecuzione. Tale processo è un kernel_thread che viene risvegliato dal semaforo sdm_dev.sema nell'ultima istruzione di sdm_queuecommand

Uso di kernel-thread

Il thread relativo al dispositivo sdm viene creato tramite la chiamata a kernel_thread. La funzione che implementa il thread è mostrata nel riquadro 5.

La chiamata a daemonize (definita in kernel/exit.c) ha l'effetto di staccare completamente il processo corrente dallo spazio utente, permettendogli di lavorare come kernel thread senza occupare risorse inutili. L'argomento passato è il nome da dare al processo, visibile tramite il comando ps. Si tratta della codifica in ambito kernel della funzione descritta da Stevens (e altri) in contesto di rete per creare processi "demoni".

La funzione comunica di essere pronta invocando complete per poi entrare in un ciclo infinito, dal quale uscire solo quando un campo nella struttura sdm_dev indica che è stata richiesta la terminazione del thread. In tale ciclo infinito, la funzione attende (tramite down_interruptible) il rilascio del semaforo, che avverra` in smd_queuecommand quando arriva una nuova richiesta SCSI.

In caso di uscita dal ciclo di elaborazione, la funzione complete_and_exit comunica la distruzione del processo ed esce (si veda il riquadro 6).

Nonostante la parte piu` impegnativa del thread sia stata nascosta in sdm_scsi_command, si tratta di una funzione estremamente semplice che mostra fedelmente come deve comportarsi un processo associato ad un trasferimento dati su bus SCSI; l'unica differenza nel caso di periferiche reali e` nella presenza di attese, timeout, interruzioni ed errori, che in questo caso non si verificano.

Riquadro 5 - sdm_thread
static int sdm_thread(void *arg)
{
	struct sdm_dev *dev = (struct sdm_dev *)arg;

	daemonize("sdm-thread");
	current->flags |= PF_NOFREEZE;

	complete(&dev->notify); /* we are running */
	
	while (1) {
		/* sleep */
                if(down_interruptible(&dev->sema))
                        break;

		/* check if we should exit */
		if (dev->doexit) break;

		/* run command (possibly printk it too) */
		sdm_scsi_command(dev, dev->srb);

		/* tell we are done */
		dev->srb->scsi_done(dev->srb);
	}
	complete_and_exit(&dev->notify, 0);
}


Riquadro 6 - Completion
struct completion e le funzioni associate sono dichiarate in <linux/completion.h> e definite in kernel/sched.c (tranne completeandexit, che si trova altrove).

Si tratta di strumenti per notificare ad uno o più processi il realizzarsi di un evento. La struttura dati contiene una variabile intera e una coda di attesa: il processo che chiama wait_for_completion dovrà aspettare finchè una chiamata a complete non abbia incrementato la variabile, che sarà subito decrementata nuovamente dal processo appena svegliato. Questo meccanismo permette, tramite una sola riga di codice per processo ed una struttura dati condivisa, di realizzare in modo generalizzato punti di sincronizzazione tra due o più processi, senza corse critiche.

Nel nostro caso la stessa struttura dati viene usata due volte: per notificare l'avvenuto avvio e l'avvenuta uscita del thread. Poiché i due eventi vengono attesi e notificati nello stesso ordine, l'uso della stessa struttura dati non crea problemi.


Implementazione dei comandi SCSI

Un comando (o richiesta) SCSI viene eseguito decodificando il buffer binario ricevuto dall'host: il primo byte indica il tipo di comando e i byte successivi ne specificano i parametri. Ad esecuzione avvenuta, il campo srb->result dovra` contenere il risultato dell'operazione: in caso di successo si tratta del valore SAM_STAT_GOOD (zero), dove il prefisso SAM indica "SCSI Architecture Model". Una volta assegnato il campo result, viene invocata la funzione done che era stata passata come argomento a queuecommand.

L'implementazione dei comandi in sdm-code risulta abbastanza semplice, anche perché il numero di comandi diversi e` molto limitato.

Il comando START_STOP serve per accendere o spegnere la periferica e in questo caso viene ignorato. Il comando INQUIRY è gestito da sdm_fill_inquiry; la risposta presenta la periferica come un disco e restituisce le stringhe identificative che vediamo riportate nel riquadro 3.

La lettura e la scrittura vengono effettuate allocando e copiando pagine dal vettore sdm_pages, esportato da sdm-data. L'unica peculiarità di queste funzioni sta nella necessità di accedere ad un buffer in cui i numeri sono memorizzati in formato "big endian" e i dati non sono allineati a dovere. Il codice contiene quindi espressioni come la seguente:

    be16_to_cpu(get_unaligned((u16*)(srb->cmnd+7)))

La funzione be16_to_cpu converte interi a 16 bit da big-endian al formato nativo del processore (little-endian nel caso del PC), mentre get_unaligned è descritta nel riquadro 7.

Il comando TEST_UNIT_READY ritorna sempre successo, mentre MODE_SENSE permette di specificare se il disco è scrivibile o in sola lettura; il codice relativo è presente ma disabilitato in sdm (che si presenta sempre come disco scrivibile).

L'ultimo comando gestito da sdm è ALLOW_MEDIUM_REMOVAL. Si tratta di un meccanismo tramite il quale il driver SCSI permette o inibisce la rimozione della periferica: ogni volta che il disco viene usato (tramite open(2) o mount(2)) se ne impedisce la rimozione; quando l'uso della periferica ha termine viene autorizzata la rimozione. sdm implementa questo comando tramite le funzioni try_module_get e module_put, per incrementare o decrementare il numero di riferimenti al modulo corrente. Tali funzioni rimpiazzano le precedenti, ora deprecate, MOD_INC_USE_COUNT e MOD_DEC_USE_COUNT:

	if (srb->cmnd[4] & 1) /* removal is denied */
		try_module_get(THIS_MODULE); 
	else  /* removal is allowed */;
		module_put(THIS_MODULE);
	srb->result = SAM_STAT_GOOD;


Riquadro 7 - Unaligned
Normalmente, una variabile a 16 bit dovrebbe essere memorizzato a partire da un indirizzo pari mentre una a 32 bit dovrebbe stare ad un indirizzo multiplo di 4, e cosi` via. Molte famiglie di processori generano una interruzione quando la richiesta di accesso in memoria non e` allineata; in certi casi il sistema operativo simula l'esecuzione dell'istruzione sbagliata, in certi altri viene semplicemente segnalato un "Bus Error" al processo, giusta punizione per programmatori male educati.

I progettisti hardware preferiscono fare bene i processori (meno transistor, più velocità, meno consumo, meno riscaldamento) anche se questo richiede un po' di disciplina da parte dei programmatori (o piu` che altro da parte dei compilatori).

Per questo motivo, ogni accesso a valori di piu` byte non allineati in memoria dovrebbe essere effettuato tramite istruzioni speciali, che leggano o scrivano un byte alla volta: meglio essere 4 volte più lenti trasferendo 4 byte uno alla volta piuttosto che 4000 volte più lenti generando un'interruzione e un'emulazione software dell'istruzione.

Le funzioni getunaligned e putunaligned sono definite in <asm/unaligned.h> e servono per leggere e scrivere valori non allineati in memoria. Tale situazione succede spesso quando bisogna comunicare con periferiche hardware tramite strutture dati definite in modo non naturale per il processore ospite. I comandi SCSI di lettura e scrittura sono esempi in tal senso.

Naturalmente non bisogna stupirsi se le funzioni di accesso non allineato su piattaforma PC sono macro che fanno un accesso normale (e non allineato): l'x86 è un pessimo esempio di processore, ma per fortuna non esiste solo il PC.


Approfondimenti

Una volta caricati i moduli sdm-data ed sdm-code è possibile usare il proprio ramdisk sda come se fosse un disco reale, partizionandolo con fdisk, creando filesystem e montandoli. È anche possibile studiare il codice, breve abbastanza da essere compreso velocemente.

Purtroppo, però, sdm non è un driver di qualità e risulta instabile in ambienti multi-processore o dove sia stata attivata la preemption del kernel, in quanto per semplificare e rendere leggibile il codice ho eliminato tutti gli accorgimenti per evitare corse critiche e altre situazioni improbabili ma spiacevoli.

Per chi volesse approfondire consiglio la lettura di drivers/usb/storage, in particolare di usb.c e scsiglue.c in tale directory. Nonostante per una comprensione accurata del codice occorra conoscere il sottosistema USB di Linux, si tratta di codice scritto molto bene e ricco di commenti utili. Con l'aiuto dei testi in Documentation si tratta sicuramente di una lettura istruttiva per chi vuole andare più a fondo nella programmazione del kernel.