AOS - 33 - VIRTUAL FILE SYSTEM III
1 Lecture Info
Data:
Sito corso: link
Slides: [AOS - 8 VIRTUAL FILE SYSTEM
Progresso unità: 3/4
Questa lezione è stata esclusivamente dedicata al discutere codice di running examples.
2 DRIVER-CONCURRENCY/driver-concurrency.c
In questo RE implementiamo un driver che può esporre tre diversi livelli di concorrenza, che sono:
SINGLE_ISTANCE: ovvero che permette una sola sessione rispetto a tutti i nodi del VFS. In questo modo si deve solo gestire la concorrenza rispetto al singolo nodo.
SINGLE_SESSION_OBJECT: ovvero che permette una sessione per ogni nodo del VFS avente un minor number diverso. In questo modo si deve gestire sia la concorrenza rispetto al singolo nodo e sia la concorrenza rispetto a nodi diversi con diversi minor numbers.
FULL_CONCURRENCY: In questa modalità si deve gestire la concorrenza sia rispetto al singolo nodo, sia rispetto a nodi diversi con diversi minor numbers, e sia rispetto a nodi diversi con lo stesso minor numbers. Rappresenta quindi il massimo livello di concorrenza per un driver.
Al fine di semplificare l'estrazione dei major e minor numbers vengono definite le seguenti macro
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 0, 0) #define get_major(session) MAJOR(session->f_inode->i_rdev) #define get_minor(session) MINOR(session->f_inode->i_rdev) #else #define get_major(session) MAJOR(session->f_dentry->d_inode->i_rdev) #define get_minor(session) MINOR(session->f_dentry->d_inode->i_rdev) #endif
Per gestire invece i meta-dati e i dati associati a nodi del VFS con diversi minor numbers viene utilizzata la seguente struttura dati
typedef struct _object_state{ #ifdef SINGLE_SESSION_OBJECT struct mutex object_busy; #endif struct mutex operation_synchronizer; int valid_bytes; char * stream_content;//the I/O node is a buffer in memory } object_state;
in particolare si definisce un fissato numero di MINORS supportati,
e si definisce un array di object_state
, ciascuno dei quali può
puntare al massimo ad un buffer di 4096 bytes, che è il size di una
pagina.
#define MINORS 8 object_state objects[MINORS]; #define OBJECT_MAX_SIZE (4096) //just one page
Segue quindi la discussione del driver vero e proprio, che implementa le seguenti funzioni
static int dev_open(struct inode *, struct file *); static int dev_release(struct inode *, struct file *); static ssize_t dev_write(struct file *, const char *, size_t, loff_t *); static ssize_t dev_read(struct file *filp, char *buff, size_t len, loff_t *off)
2.1 dev_open()
Prima di aprire la sessione di I/O il driver deve effettuare vari controlli. In particolare controlla se il minor numbers è un minor numbers valido
static int dev_open(struct inode *inode, struct file *file) { int minor; minor = get_minor(file); if(minor >= MINORS){ return -ENODEV; } // ...
successivamente controlla, in caso si sta lavorando in modalità SINGLE_INSTANCE, se il lock del mutex che controlla la sincronizzazione è già stato preso
#ifdef SINGLE_INSTANCE // this device file is single instance if (!mutex_trylock(&device_state)) { return -EBUSY; } #endif
altrimenti, se si lavora in SINGLE_SESSION_OBJECT, si controlla se il minor dell'oggetto di I/O che si vuole manipolare è già occupato da un altro nodo del VFS
#ifdef SINGLE_SESSION_OBJECT if (!mutex_trylock(&(objects[minor].object_busy))) { goto open_failure; } #endif
se questi controlli vanno bene si ritorna 0
per indicare successo
printk("%s: device file successfully opened for object with minor %d\n", MODNAME, minor); //device opened by a default nop return 0;
altrimenti si gestiscono i vari possibili stati di errore
#ifdef SINGLE_SESSION_OBJECT open_failure: #ifdef SINGE_INSTANCE mutex_unlock(&device_state); #endif return -EBUSY; #endif }
2.2 dev_release()
La funzione di release è molto semplice, e non fa altro che rilasciare le risorse prese, a seconda se si sta lavorando in SINGLE_INSTANCE, e quindi si libera il mutex globale, oppure in SINGLE_SESSION_OBJECT, e quindis si libera il mutex preso per il particolare minor number.
static int dev_release(struct inode *inode, struct file *file) { int minor; minor = get_minor(file); #ifdef SINGLE_SESSION_OBJECT mutex_unlock(&(objects[minor].object_busy)); #endif #ifdef SINGLE_INSTANCE mutex_unlock(&device_state); #endif printk("%s: device file closed\n",MODNAME); //device closed by default nop return 0; }
2.3 dev_write()
La funzione write inizia prendendo il minor tramite la macro
get_minor(filp)
e utilizza il minor ottenuto per prendere la
specifica struct object_state
che rappresenta i meta-dati associati
a quel particolare minor number.
static ssize_t dev_write(struct file *filp, const char *buff, size_t len, loff_t *off) { int minor = get_minor(filp); int ret; object_state *the_object; the_object = objects + minor; printk("%s: somebody called a write on dev with [major,minor] number [%d,%d]\n", MODNAME, get_major(filp), get_minor(filp));
A questo punto il driver utilizza un semaforo per prendere il lock
sul campo operation_synchronizer
della struttura
object_state
. Questo lock è necessario in quanto potenzialmente è
possibile avere più nodi del VFS che hanno lo stesso minor number,
e dunque dobbiamo gestire eventuali situazioni di
sincronizzazione. Questa volta il lock è preso in modo sincrono,
ovvero si aspetta fino a quando è libero.
// need to lock in any case mutex_lock(&(the_object->operation_synchronizer));
Dopo aver preso il lock effettua dei controlli per sapere se è possibile scrivere. In particolare controlla che,
L'offset non sia troppo grande rispetto alla massima quantità di dati associata ad ogni minor number, che avevamo detto essere 4KB.
if(*off >= OBJECT_MAX_SIZE) {//offset too large mutex_unlock(&(the_object->operation_synchronizer)); return -ENOSPC;//no space left on device }
L'offset punti a dei dati effettivamente validi, ovvero allocati, tra quelli disponibili.
if(*off > the_object->valid_bytes) {//offset bwyond the current stream size mutex_unlock(&(the_object->operation_synchronizer)); return -ENOSR;//out of stream resources }
Successivamente sistema la lunghezza dei dati da scrivere, e
utilizza una copy_from_user()
per scriverli nella memoria del
kernel.
if((OBJECT_MAX_SIZE - *off) < len) len = OBJECT_MAX_SIZE - *off;
ret = copy_from_user(&(the_object->stream_content[*off]),buff,len);
Notiamo come la copy_from_user()
potrebbe scrivere una quantità più
piccola di len
di bytes. Dobbiamo quindi aggiornare l'offset in modo
da rispettare la quantità di dati che sono stati effettivamente
scritti, per poi togliere il lock e ritornare il numero di bytes
scritti alla funzione superiore che ci ha chiamato.
*off += (len - ret);
the_object->valid_bytes = *off;
mutex_unlock(&(the_object->operation_synchronizer));
return len - ret;
}
2.4 dev_read()
La funzione read() è essenzialmente analoga alla funzione write(), e la riportiamo solo per completezza.
static ssize_t dev_read(struct file *filp, char *buff, size_t len, loff_t *off) { int minor = get_minor(filp); int ret; object_state *the_object; the_object = objects + minor; printk("%s: somebody called a read on dev with [major,minor] number [%d,%d]\n", MODNAME, get_major(filp), get_minor(filp)); // need to lock in any case mutex_lock(&(the_object->operation_synchronizer)); if(*off > the_object->valid_bytes) { mutex_unlock(&(the_object->operation_synchronizer)); return 0; } if((the_object->valid_bytes - *off) < len) len = the_object->valid_bytes - *off; ret = copy_to_user(buff,&(the_object->stream_content[*off]),len); *off += (len - ret); mutex_unlock(&(the_object->operation_synchronizer)); return len - ret; }
Alla fine le funzioni del driver vengono incapsulate nella
struttura dati file_operation
static struct file_operations fops = { .owner = THIS_MODULE,//do not forget this .write = dev_write, .read = dev_read, .open = dev_open, .release = dev_release, .unlocked_ioctl = dev_ioctl };
2.5 init_module()
Il driver è poi registrato nella funzione init_module()
, che viene
chiamata non appena montiamo il modulo nel kernel. La funzione non
fa altro che inizializzare lo stato interno del driver
int init_module(void) { int i; //initialize the drive internal state for(i = 0; i < MINORS; i++){ #ifdef SINGLE_SESSION_OBJECT mutex_init(&(objects[i].object_busy)); #endif mutex_init(&(objects[i].operation_synchronizer)); objects[i].valid_bytes = 0; objects[i].stream_content = NULL; objects[i].stream_content = (char*)__get_free_page(GFP_KERNEL); if(objects[i].stream_content == NULL) goto revert_allocation; }
per poi registrare il driver nel kernel utilizzando la funzione
__register_chrdev
.
Major = __register_chrdev(0, 0, 256, DEVICE_NAME, &fops); //actually allowed minors are directly controlled within this driver if (Major < 0) { printk("%s: registering device failed\n", MODNAME); return Major; } printk(KERN_INFO "%s: new device registered, it is assigned major number %d\n", MODNAME, Major); return 0;
infine abbiamo una possibile gestione di errori di allocazione
revert_allocation: for(;i>=0;i--){ free_page((unsigned long)objects[i].stream_content); } return -ENOMEM; }
2.6 cleanup_module()
La funzione di cleanup è molto semplice e non fa altro che de-allocare la memoria allocata per la gestione dei dati per ciascun minor number e de-registrare il driver.
void cleanup_module(void) { int i; for(i = 0; i < MINORS; i++){ free_page((unsigned long)objects[i].stream_content); } unregister_chrdev(Major, DEVICE_NAME); printk(KERN_INFO "%s: new device unregistered, it was assigned major number %d\n", MODNAME, Major); return; }
2.7 Esecuzione e Testing
Per testare questo running example possiamo compilarlo e montarlo nel kernel con
make sudo insmod driver-concurrency.ko
in questo modo dovremmo vedere, utilizzando dmesg
, una cosa del
genere
[33544.927907] CHAR DEV: new device registered, it is assigned major number 235
e poi utilizzare il codice user-space che si trova in
./user/user.c
. Tale codice non fa altro che spawnare un certo
numero di nodi nel VFS, ciascun con un diverso minor number, e poi
creare un thread per ogni nodo creato.
int main(int argc, char** argv){ int ret, major, minors; char *path; pthread_t tid; if(argc < 4){ printf("useg: prog pathname major minors"); return -1; } path = argv[1]; major = strtol(argv[2], NULL, 10); minors = strtol(argv[3], NULL, 10); printf("creating %d minors for device %s with major %d\n", minors, path, major); for(i = 0; i < minors; i++){ sprintf(buff, "mknod %s%d c %d %i\n", path, i, major, i); system(buff); sprintf(buff, "%s%d", path, i); pthread_create(&tid, NULL, the_thread, strdup(buff)); } pause(); return 0; }
La funzione eseguita da ogni thread non fa altro che aprire il nodo con quel particolare minor number e scrivere, per un certo numero di volte, la stringa "ciao a tutti\n" nel nodo.
int i; char buff[4096]; #define DATA "ciao a tutti\n" #define SIZE strlen(DATA) void * the_thread(void* path){ char* device; int fd; device = (char*)path; sleep(1); printf("opening device %s\n",device); fd = open(device,O_RDWR); if(fd == -1) { printf("open error on device %s\n",device); return NULL; } printf("device %s successfully opened\n",device); ioctl(fd,1); for(i = 0; i < 5; i++) write(fd,DATA,SIZE); return NULL; }
Per poter utilizzare correttamente questo codice dobbiamo quindi
ricordarmi il major number assegnato al driver che montiamo nel
kernel, e che possiamo ottenere utilizzando la shell dmesg
. Nel
nostro caso il major number era 235, e quindi possiamo procedere
come segue
cd ./user
make
./user /dev/my-device 235 5
e dovremmo vedere il seguente output
[34187.209913] CHAR DEV: device file successfully opened for object with minor 0 [34187.209926] CHAR DEV: somebody called an ioctl on dev with [major,minor] number [235,0] and command 1 ..... [34187.215483] CHAR DEV: device file successfully opened for object with minor 1 [34187.215490] CHAR DEV: somebody called an ioctl on dev with [major,minor] number [235,1] and command 1 [34187.215493] CHAR DEV: somebody called a write on dev with [major,minor] number [235,1] .... [34187.220394] CHAR DEV: device file successfully opened for object with minor 2 [34187.220399] CHAR DEV: somebody called an ioctl on dev with [major,minor] number [235,2] and command 1 [34187.220400] CHAR DEV: somebody called a write on dev with [major,minor] number [235,2] ..... [34187.224783] CHAR DEV: device file successfully opened for object with minor 3 [34187.224788] CHAR DEV: somebody called an ioctl on dev with [major,minor] number [235,3] and command 1 [34187.224789] CHAR DEV: somebody called a write on dev with [major,minor] number [235,3] .....
3 BROADCAST-DEV/broadcast.c
In questo RE invece facciamo vedere come sia possibile far interagire un driver con un'altro.
Il problema che vogliamo affrontare è il seguente: vogliamo avere un driver che durante la scrittura manda in broadcast il contenuto della scrittura ad un determinato numero di terminali presenti nel sistema. Per fare questo quindi il driver che vogliamo implementare dovrà interagire con il driver dei terminali. A tale fine utilizzeremo le API offerte dal kernel.
Da un punto di vista implementativo possiamo iniziare identificando i terminali tramite la loro path in /dev e mettendoli in un array
//registered ttys char * targets[] = { "/dev/pts/1", "/dev/pts/2", "/dev/pts/3", "/dev/pts/4", "/dev/pts/5", "/dev/pts/6", "/dev/pts/7", "/dev/pts/8", NULL };
successivamente l'operazione di scrittura del driver utilizzerà la
filp_open()
per ottenere l'indirizzo del driver del terminale, e
una volta ottenuto tale indirizzo possiamo o chiamare direttamente
la funzione che ci interessa, oppure, in modo ancora più
trasparente, chiamare la funzione vfs_write()
per effettuare
l'operazione che desideriamo.
static ssize_t print_stream_everywhere(const char *stream, size_t size) { int i; struct file * f = NULL; printk("%s: print stream function of broadcast dev called - tageting all registered ttys\n", MODNAME); for (i = 0; targets[i] != NULL; i++){ printk("%s: tageting %s\n", MODNAME, targets[i]); f = filp_open(targets[i], O_WRONLY, 0666); if(IS_ERR(f)) continue; vfs_write(f, stream, size, &f->f_pos); filp_close(f, NULL); } return size; }
L'operazione di scrittura vera e propria esposta dal driver sarà
quindi un wrapper alla funzione print_stream_everywhere
e sarà
definita come segue
static ssize_t broadcast_write(struct file *filp, const char *buff, size_t len, loff_t *off) { #if LINUX_VERSION_CODE >= KERNEL_VERSION(4,0,0) printk("%s: somebody called a write on broadcast dev with [major,minor] number [%d,%d]\n", MODNAME, get_major(filp), get_minor(filp)); #else printk("%s: somebody called a write on broadcast dev with [major,minor] number [%d,%d]\n", MODNAME, get_major(filp), get_minor(filp)); #endif return print_stream_everywhere(buff, len); }
Per utilizzare il RE dobbiamo montare il modulo come al solito, ottenere il major number, creare un nodo I/O con quel particolare major number, e scriverci qualche cosa. Una volta che scriviamo dobbiamo vedere ciò che abbiamo scritto apparire in tutti i dispositvi del sistema che abbiamo inserito nel nostro array.
4 Homework: PC
5 Note generali
5.1 __register_chrdev()
Nelle release più recenti del kernel linux è possibile controllare
in modo automatico se il minor number
dell'oggetto di I/O che sta
utilizzando il driver è supportato dal driver stesso. Per fare
questo dobbiamo utilizzare la funzione __register_chrdev()
, che
permette, come terzo parametro, di passare l'intervallo di valori
associati ai minor numbers che vogliamo pilotare con il driver che
stiamo registrando.
Questo cambiamento deriva dal fatto che nelle versioni moderne del kernel i drivers non vengono più rappresentati da una semplice tabella di puntatori a funzioni, ma da delle hash tables che contengono varie informazioni, tra cui un puntatore alla tabella che punta alle funzioni del driver.