Veamos como quedaría un sencillo driver para un dispositivo de bloques, por ejemplo un disco duro. En esta primera aproximación solo vamos a implementar las funciones init_module() y cleanup_module(), para probarlo crearemos un /dev dos dispositivos de bloque, /dev/mi_dsico0 y mi_disco1.
/* mpdisco.h */ /* el comportamiento por defecto es la asignación dinámica */ #define MPDISCO_MAYOR 0 /* mpdisco.c */ #include <linux/module.h> #include <linux/kernel.h> #include <linux/fs.h> #include "mpdisco.h" unsigned int mpdisco_mayor=MPDISCO_MAYOR; struct file_operations mpdisco_fops = { NULL, /* lseek */ block_read, /* read */ block_write, /* write */ NULL, /* readdir */ NULL, /* poll */ NULL, /* ioctl */ NULL, /* mmap */ NULL, /* open */ NULL, /* flush */ NULL, /* release */ NULL, /* fsync */ NULL, /* fasync */ NULL, /* check_media_change */ NULL, /* revalidate */ NULL, /* lock */ }; int init_module(void) { int result; EXPORT_NO_SYMBOLS; /* reserva dinamica del número mayor del módulo */ result=register_blkdev(mpdisco_mayor, "mpdisco", &mpdisco_fops); if (result < 0) { printk(KERN_WARNING "mpdisco> (init_module) no se pudo obtener el mayor %d\n",mpdisco_mayor); return result; } if (mpdisco_mayor == 0) mpdisco_mayor = result; /* mensaje y salimos */ printk( KERN_INFO "mpdisco> (init_module) cargado satisfactoriamente\n"); return 0; } void cleanup_module(void) { int result; result = unregister_blkdev(mpdisco_mayor, "mpdisco"); printk( KERN_INFO "mpdisco> (cleanup_module) descargado sin problemas\n"); }
Como vemos el código es prácticamente idéntico al del driver de caracteres, si bien cambiamos la función register_chrdev() por register_blkdev() y unregister_chrdev() por unregister_blkdev() . Los argumentos que reciben estas dos nuevas funciones son los mismos que para el caso de los drivers de caracter y su finalidad es la misma.
También es importante volver a hacer hincapié en el hecho de que nuestro driver no va a implementar ni read ni write, si no que usaremos las funciones predefinidas en el kernel para ello: block_read y block_write .
Es necesario informar al kernel de las características que va a tener nuestros dispositivos, como ya hemos mencionado antes, tenemos que rellenar unos cuantos arrays del kernel:
/* mpdisco.h */ /* el comportamiento por defecto es la asignación dinámica */ #define MPDISCO_MAYOR 0 /* numero máximo de dispositivos que vamos a tener */ #define MPDISCO_MAX_NUM_DEV 2 /* tamaño en kilobytes de los discos */ #define MPDISCO_SIZE 512 /* tamaño de los bloques en bytes */ #define MPDISCO_BLKSIZE 1024 /* Tamaño del sector de los discos en bytes*/ #define MPDISCO_HARDSECT_SIZE 512 /* ReadAHead que vamos a utilizar para nuestros discos */ #define MPDISCO_RAHEAD 3 /* mpdisco.c */ #include <linux/module.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/blkdev.h> #include "mpdisco.h" unsigned int mpdisco_mayor=MPDISCO_MAYOR; int mpdisco_blk_sizes[MPDISCO_MAX_NUM_DEV]; int mpdisco_blksize_size[MPDISCO_MAX_NUM_DEV]; int mpdisco_hardsect_size[MPDISCO_MAX_NUM_DEV]; struct file_operations mpdisco_fops = { NULL, /* lseek */ block_read, /* read */ block_write, /* write */ NULL, /* readdir */ NULL, /* poll */ NULL, /* ioctl */ NULL, /* mmap */ NULL, /* open */ NULL, /* flush */ NULL, /* release */ NULL, /* fsync */ NULL, /* fasync */ NULL, /* check_media_change */ NULL, /* revalidate */ NULL, /* lock */ }; int init_module(void) { int result,i; EXPORT_NO_SYMBOLS; /* reserva dinamica del número mayor del módulo */ result=register_blkdev(mpdisco_mayor, "mpdisco", &mpdisco_fops); if (result < 0) { printk(KERN_WARNING "mpdisco> (init_module) no se pudo obtener el mayor %d\n",mpdisco_mayor); return result; } if (mpdisco_mayor == 0) mpdisco_mayor = result; /* fijamos el tamaño de los discos */ for (i=0; i<MPDISCO_MAX_NUM_DEV; i++) { mpdisco_blk_sizes[i]=MPDISCO_SIZE; } blk_size[mpdisco_mayor]=mpdisco_blk_sizes; /* fijamos el tamaño de bloque de los discos */ for (i=0; i<MPDISCO_MAX_NUM_DEV; i++) { mpdisco_blksize_size[i]=MPDISCO_BLKSIZE; } blksize_size[mpdisco_mayor]=mpdisco_blksize_size; /* fijamos el tamaño de los sectores de disco */ for (i=0; i<MPDISCO_MAX_NUM_DEV; i++) { mpdisco_hardsect_size[i]=MPDISCO_HARDSECT_SIZE; } hardsect_size[mpdisco_mayor]=mpdisco_hardsect_size; /* fijamos el readahead */ read_ahead[mpdisco_mayor]=MPDISCO_RAHEAD; /* mensaje y salimos */ printk( KERN_INFO "mpdisco> (init_module) cargado satisfactoriamente\n"); return 0; } void cleanup_module(void) { int result; blk_size[mpdisco_mayor]=NULL; blksize_size[mpdisco_mayor]=NULL; hardsect_size[mpdisco_mayor]=NULL; read_ahead[mpdisco_mayor]=0; result = unregister_blkdev(mpdisco_mayor, "mpdisco"); printk( KERN_INFO "mpdisco> (cleanup_module) descargado sin problemas\n"); }
Si intentamos un dd if=un_fichero of=/dev/mi_disco0 comprobaremos como no se ha podido escribir en el dispositivo, esto es por que como es la primera vez que accedemos al dispositivo, no tenemos ninguno de sus bloque en la cache, por lo que la función block_write() mete en la cola de peticiones una petición de escritura, pero como no hemos implementado todavía ninguna función que procese dichas peticiones, esta se queda pendiente.
Vamos a implementar una función muy simple que, aunque procesará las peticiones no llegará a escribir ningún dato en ningún sitio, además vamos a publicarla utilizando la estructura blk_dev, como ya se ha explicado.
La estructura general de la función que atiende a las peticiones ha de ser la siguiente:
Finalmente, podemos implementar un sencillo ejemplo de nuestra función de atención a peticiones (mpdisco_request())de la siguiente manera:
/* mpdisco.c */ #include <linux/module.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/blkdev.h> #define MAJOR_NR mpdisco_mayor unsigned int mpdisco_mayor; #define DEVICE_OFF(dispositivo) /* nuestro dispositivo no apaga su motor, pues no tiene */ #define DEVICE_REQUEST mpdisco_request #include <linux/blk.h> #include "mpdisco.h" int mpdisco_blk_sizes[MPDISCO_MAX_NUM_DEV]; int mpdisco_blksize_size[MPDISCO_MAX_NUM_DEV]; int mpdisco_hardsect_size[MPDISCO_MAX_NUM_DEV]; struct file_operations mpdisco_fops = { NULL, /* lseek */ block_read, /* read */ block_write, /* write */ NULL, /* readdir */ NULL, /* poll */ NULL, /* ioctl */ NULL, /* mmap */ NULL, /* open */ NULL, /* flush */ NULL, /* release */ NULL, /* fsync */ NULL, /* fasync */ NULL, /* check_media_change */ NULL, /* revalidate */ NULL, /* lock */ }; int init_module(void) { int result,i; EXPORT_NO_SYMBOLS; mpdisco_mayor=MPDISCO_MAYOR; /* reserva dinamica del número mayor del módulo */ result=register_blkdev(mpdisco_mayor, "mpdisco", &mpdisco_fops); if (result < 0) { printk(KERN_WARNING "mpdisco> (init_module) no se pudo obtener el mayor %d\n",mpdisco_mayor); return result; } if (mpdisco_mayor == 0) mpdisco_mayor = result; /* fijamos el tamaño de los discos */ for (i=0; i<MPDISCO_MAX_NUM_DEV; i++) { mpdisco_blk_sizes[i]=MPDISCO_SIZE; } blk_size[mpdisco_mayor]=mpdisco_blk_sizes; /* fijamos el tamaño de bloque de los discos */ for (i=0; i<MPDISCO_MAX_NUM_DEV; i++) { mpdisco_blksize_size[i]=MPDISCO_BLKSIZE; } blksize_size[mpdisco_mayor]=mpdisco_blksize_size; /* fijamos el tamaño de los sectores de disco */ for (i=0; i<MPDISCO_MAX_NUM_DEV; i++) { mpdisco_hardsect_size[i]=MPDISCO_HARDSECT_SIZE; } hardsect_size[mpdisco_mayor]=mpdisco_hardsect_size; /* fijamos el readahead */ read_ahead[mpdisco_mayor]=MPDISCO_RAHEAD; /* avisamos al kernel de donde está nuestra función para atender peticiones */ blk_dev[mpdisco_mayor].request_fn = mpdisco_request; /* mensaje y salimos */ printk( KERN_INFO "mpdisco> (init_module) cargado satisfactoriamente\n"); return 0; } void cleanup_module(void) { int result; blk_size[mpdisco_mayor]=NULL; blksize_size[mpdisco_mayor]=NULL; hardsect_size[mpdisco_mayor]=NULL; read_ahead[mpdisco_mayor]=0; result = unregister_blkdev(mpdisco_mayor, "mpdisco"); printk( KERN_INFO "mpdisco> (cleanup_module) descargado sin problemas\n"); } void mpdisco_request(void) { for (;;) { INIT_REQUEST; printk(KERN_INFO "mpdisco> request cmd: %i, sec %li, num_sec: %li\n", CURRENT->cmd, CURRENT->sector, CURRENT->current_nr_sectors); end_request(1); } }
Para probar este ejemplo hemos de tener en cuenta que el kernel siempre intentará trabajar con los datos en cache. Por ello si ejecutamos un:
$ dd if=mpdisco.c of=/dev/mi_disco0
Veremos que los primeros mensajes que aparecen en /var/log/messages no son de escritura, si no de lectura, el kernel está leyendo los bloques que cree oportunos, los mete en la cache, y allí escribe los datos. Pasado un determinado tiempo, el kernel decide que ya no se va a trabajar más con esos bloques y le dice al dispositivo que los escriba, por lo que es entonces cuando vemos que aparecen los mensajes de escritura.
Si con los datos cargados en cache intentamos una nueva escritura, el driver tampoco se dará cuenta de ello, pues los datos están siendo modificados en la cache, evitando continuos accesos al dispositivo. Esto es lo que se llama trabajar con la cache ``caliente''. A la hora de hacer pruebas, tenemos que considerar el caso de la cache en frío y la cache en caliente .
Siempre podemos actualizar los datos del dispositivo con los de la cache, por medio del comando sync , veremos que ante la ejecución de ese comando se hacen efectivos en el dispositivo todas las escrituras que se hayan quedado en cache.
Una buena manera de limpiar la cache, para poder hacer pruebas en frío es cargar en ella un fichero de gran tamaño, eliminando así todos los posibles bloques de nuestro dispositivo que estuviesen almacenados en ella. Para ello podemos ejecutar el siguiente comando:
$ dd if=/dev/zero of=/tmp/fichero_con_ceros count=100000Que nos crea un fichero en /tmp relleno de ceros de unos 49M aproximadamente ( ). De esta manera, la cache a tenido que vaciarse para dar cabida a ese gran fichero.
Para asegurarnos de que hemos entendido perfectamente el funcionamiento de los drivers de bloques, y su relación con la cache, conviene practicar un poco con estos comandos, observando en cada momento las respuestas de nuestro driver ante tales estímulos. Por ejemplo, escribir datos al dispositivo /dev/mi_disco0 con la cache en frío y en caliente ¿qué diferencias se aprecian?, leer datos del dispositivo con la cache en frío y en caliente ¿qué diferencias se aprecian?, aumentar el readahead de nuestro módulo y repetir las mismas operaciones, ¿Se aprecia alguna diferencia?. ¿Qué diferencias hay entre hacer un sync y ``enfriar'' la cache?. ¿Qué pasaría si apagásemos el ordenador (cortando la alimentación) cuando parte de nuestras operaciones están aún en la cache?, ¿como puede ayudarnos sync en este caso?.