AOS - 33 - VIRTUAL FILE SYSTEM III


1 Lecture Info


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,

  1. 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
     } 
    
  2. 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.