Así que ahora somos unos valientes programadores del núcleo y sabemos escribir módulos que no hacen nada. Estamos orgullosos de nosotros mismos y llevamos la cabeza bien alta. Pero de algún modo sentimos que falta algo. Los módulos catatónicos no son muy divertidos.
Hay dos formas principales de que un módulo del núcleo se comunique con los procesos. Una es a través de los ficheros de dispositivos (como los que están en el directorio /dev) y la otra es usar el sistema de ficheros proc. Ya que uno de los principales motivos para escribir algo en el núcleo es soportar algún tipo de dispositivo de hardware, empezaremos con los ficheros de dispositivos.
El propósito original de los ficheros de dispositivo es permitir a los procesos comunicarse con los controladores de dispositivos en el núcleo, y a través de ellos con los dispositivos físicos (módems, terminales, etc.). La forma en la que esto se implementa es la siguiente.
A cada controlador de dispositivo, que es responsable de algún tipo de hardware, se le asigna su propio número mayor. La lista de los controladores y de sus números mayores está disponible en /proc/devices. A cada dispositivo físico administrado por un controlador de dispositivo se le asigna un número menor. El directorio /dev se supone que incluye un fichero especial, llamado fichero de dispositivo, para cada uno de estos dispositivos, tanto si está realmente instalado en el sistema como si no.
Por ejemplo, si haces ls -l /dev/hd[ab]*, verás todas las particiones de discos duros IDE que posiblemente estén conectadas a una máquina. Date cuenta de que todos ellos usan el mismo número mayor, 3, pero el número menor cambia de uno a otro Nota: Esto es así suponiendo que estás usando una arquitectura PC. No sé nada sobre dispositivos en Linux ejecutándose en otras arquitecturas.
Cuando el sistema se instaló, todos esos ficheros de dispositivos se crearon mediante la orden mknod. No existe un motivo técnico por el que tienen que estar en el directorio /dev, es sólo una convención útil. Cuando creamos un fichero de dispositivo con el propósito de prueba, como aquí para un ejercicio, probablemente tenga más sentido colocarlo en el directorio en donde compilas el módulo del núcleo.
Los dispositivos están divididos en dos tipos: los dispositivos de carácter y los dispositivos de bloque. La diferencia es que los dispositivos de bloque tienen un búfer para las peticiones, por lo tanto pueden escoger en qué orden las van a responder. Esto es importante en el caso de los dispositivos de almacenamiento, donde es más rápido leer o escribir sectores que están cerca entre sí, que aquellos que están más desperdigados. Otra diferencia es que los dispositivos de bloque sólo pueden aceptar bloques de entrada y de salida (cuyo tamaño puede variar según el dispositivo), en cambio los dispositivos de carácter pueden usar muchos o unos pocos bytes como ellos quieran. La mayoría de los dispositivos del mundo son de carácter, porque no necesitan este tipo de buffering, y no operan con un tamaño de bloque fijo. Se puede saber cuándo un fichero de dispositivo es para un dispositivo de carácter o de bloque mirando el primer carácter de la salida de ls -l. Si es `b' entonces es un dispositivo de bloque, y si es `c' es un dispositivo de carácter.
Este módulo está dividido en dos partes separadas: la parte del módulo que registra el dispositivo y la parte del controlador del dispositivo. La función init_module llama a module_register_chrdev para añadir el controlador de dispositivo a la tabla de controladores de dispositivos de carácter del núcleo. También devuelve el número mayor que usará el controlador. La función cleanup_module libera el dispositivo.
Esto (registrar y liberar algo) es la funcionalidad general de estas dos funciones. Las cosas en el núcleo no funcionan por su propia iniciativa, como los procesos, sino que son llamados por procesos a través de las llamadas al sistema, o por los dispositivos hardware a través de las interrupciones, o por otras partes del núcleo (simplemente llamando a funciones específicas). Como resultado, cuando añades código al núcleo, se supone que es para registrarlo como parte de un manejador o para un cierto tipo de evento y cuando lo quitas, se supone que lo liberas..
El controlador del dispositivo se compone de cuatro funciones device_acción, que se llaman cuando alguien intenta hacer algo con un fichero de dispositivo con nuestro número mayor. La forma en que el núcleo sabe cómo llamarlas es a través de la estructura file_operations, Fops, que se dio cuando el dispositivo fue registrado, e incluye punteros a esas cuatro funciones.
Otro punto que hemos de recordar aquí es que podemos permitir que el módulo del núcleo sea borrado cuando root quiera. El motivo es que si el fichero del dispositivo es abierto por un proceso y entonces quitamos el módulo del núcleo, el uso del fichero causaría una llamada a la posición de memoria donde la función apropiada (read/write) usada debería estar. Si tenemos suerte, ningún otro código fue cargado allí, y obtendremos un feo mensaje. Si no tenemos suerte, otro módulo del núcleo fue cargado en la misma posición, lo que significará un salto en mitad de otra función del núcleo. El resultado sería imposible de predecir, pero no sería positivo.
Normalmente, cuando no quieres permitir algo, devuelves un código de error (un número negativo) desde la función que se supone que lo tendría que hacer. Con cleanup_module esto es imposible porque es una función void. Una vez que se llama a cleanup_module, el módulo está muerto. En todo caso, hay un contador que cuenta cuántos otros módulos del núcleo están usando el módulo, llamado contador de referencia (que es el último número de la línea en /proc/modules). Si este número es distinto de cero, rmmod fallará. La cuenta de referencia del módulo está disponible en la variable mod_use_count_. Como hay macros definidas para manejar esta variable (MOD_INC_USE_COUNT y MOD_DEC_USE_COUNT), preferimos usarlas, mejor que utilizar mod_use_count_ directamente, por lo tanto será más seguro si la implementación cambia en el futuro.
/* chardev.c * Copyright (C) 1998-1999 by Ori Pomerantz * * Crea un dispositivo de carácter (sólo lectura) */ /* Los ficheros de cabeceras necesarios */ /* Estándar en los módulos del núcleo */ #include <linux/kernel.h> /* Estamos haciendo trabajo del núcleo */ #include <linux/module.h> /* Específicamente, un módulo */ /* Distribuido con CONFIG_MODVERSIONS */ #if CONFIG_MODVERSIONS==1 #define MODVERSIONS #include <linux/modversions.h> #endif /* Para dispositivos de carácter */ #include <linux/fs.h> /* Las definiciones de dispositivos * de carácter están aquí */ #include <linux/wrapper.h> /* Un envoltorio que * no hace nada actualmente, * pero que quizás ayude para * compatibilizar con futuras * versiones de Linux */ /* En 2.2.3 /usr/include/linux/version.h incluye * una macro para esto, pero 2.0.35 no lo hace - por lo * tanto lo añado aquí si es necesario */ #ifndef KERNEL_VERSION #define KERNEL_VERSION(a,b,c) ((a)*65536+(b)*256+(c)) #endif /* Compilación condicional. LINUX_VERSION_CODE es * el código (como KERNEL_VERSION) de esta versión */ #if LINUX_VERSION_CODE > KERNEL_VERSION(2,2,0) #include <asm/uaccess.h> /* for put_user */ #endif #define SUCCESS 0 /* Declaraciones de Dispositivo **************************** */ /* El nombre de nuestro dispositivo, tal como aparecerá * en /proc/devices */ #define DEVICE_NAME "char_dev" /* La máxima longitud del mensaje desde el dispositivo */ #define BUF_LEN 80 /* ¿Está el dispositivo abierto correctamente ahora? Usado para * prevenir el acceso concurrente en el mismo dispositivo */ static int Device_Open = 0; /* El mensaje que el dispositivo dará cuando preguntemos */ static char Message[BUF_LEN]; /* ¿Cuánto más tiene que coger el proceso durante la lectura? * Útil si el mensaje es más grande que el tamaño * del buffer que cogemos para rellenar en device_read. */ static char *Message_Ptr; /* Esta función es llamada cuando un proceso * intenta abrir el fichero del dispositivo */ static int device_open(struct inode *inode, struct file *file) { static int counter = 0; #ifdef DEBUG printk ("Dispositivo abierto(%p,%p)\n", inode, file); #endif /* Esto es como coger el número menor del dispositivo * en el caso de que tengas más de un dispositivo físico * usando el controlador */ printk("Dispositivo: %d.%d\n", inode->i_rdev >> 8, inode->i_rdev & 0xFF); /* No queremos que dos procesos hablen al mismo tiempo */ if (Device_Open) return -EBUSY; /* Si había un proceso, tendremos que tener más * cuidado aquí. * * En el caso de procesos, el peligro es que un * proceso quizás esté chequeando Device_Open y * entonces sea reemplazado por el planificador por otro * proceso que ejecuta esta función. Cuando * el primer proceso regrese a la CPU, asumirá que el * dispositivo no está abierto todavía. * * De todas formas, Linux garantiza que un proceso no * será reemplazado mientras se está ejecutando en el * contexto del núcleo. * * En el caso de SMP, una CPU quizás incremente * Device_Open mientras otra CPU está aquí, correcto * después de chequear. De todas formas, en la versión * 2.0 del núcleo esto no es un problema por que hay un * cierre que garantiza que sólamente una CPU estará en * el módulo del núcleo en un mismo instante. Esto es malo * en términos de rendimiento, por lo tanto la versión 2.2 * lo cambió. Desgraciadamente, no tengo acceso a un * equipo SMP para comprobar si funciona con SMP. */ Device_Open++; /* Inicializa el mensaje. */ sprintf(Message, "Si te lo dije una vez, te lo digo %d veces - %s", counter++, "Hola, mundo\n"); /* El único motivo por el que se nos permite hacer este * sprintf es porque la máxima longitud del mensaje * (asumiendo enteros de 32 bits - hasta 10 dígitos * con el signo menos) es menor que BUF_LEN, el cual es 80. * ¡¡TEN CUIDADO NO HAGAS DESBORDAMIENTO DE PILA EN LOS BUFFERS, * ESPECIALMENTE EN EL NÚCLEO!!! */ Message_Ptr = Message; /* Nos aseguramos de que el módulo no es borrado mientras * el fichero está abierto incrementando el contador de uso * (el número de referencias abiertas al módulo, si no es * cero rmmod fallará) */ MOD_INC_USE_COUNT; return SUCCESS; } /* Esta función es llamada cuando un proceso cierra el * fichero del dispositivo. No tiene un valor de retorno en * la versión 2.0.x porque no puede fallar (SIEMPRE debes de ser * capaz de cerrar un dispositivo). En la versión 2.2.x * está permitido que falle - pero no le dejaremos. */ #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) static int device_release(struct inode *inode, struct file *file) #else static void device_release(struct inode *inode, struct file *file) #endif { #ifdef DEBUG printk ("dispositivo_liberado(%p,%p)\n", inode, file); #endif /* Ahora estamos listos para la siguiente petición*/ Device_Open --; /* Decrementamos el contador de uso, en otro caso una vez que * hayas abierto el fichero no volverás a coger el módulo. */ MOD_DEC_USE_COUNT; #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) return 0; #endif } /* Esta función es llamada cuando un proceso que ya * ha abierto el fichero del dispositivo intenta leer de él. */ #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) static ssize_t device_read(struct file *file, char *buffer, /* El buffer a rellenar con los datos */ size_t length, /* La longitud del buffer */ loff_t *offset) /* Nuestro desplazamiento en el fichero */ #else static int device_read(struct inode *inode, struct file *file, char *buffer, /* El buffer para rellenar con * los datos */ int length) /* La longitud del buffer * (¡no debemos escribir más allá de él!) */ #endif { /* Número de bytes actualmente escritos en el buffer */ int bytes_read = 0; /* si estamos al final del mensaje, devolvemos 0 * (lo cual significa el final del fichero) */ if (*Message_Ptr == 0) return 0; /* Ponemos los datos en el buffer */ while (length && *Message_Ptr) { /* Porque el buffer está en el segmento de datos del usuario * y no en el segmento de datos del núcleo, la asignación * no funcionará. En vez de eso, tenemos que usar put_user, * el cual copia datos desde el segmento de datos del núcleo * al segmento de datos del usuario. */ put_user(*(Message_Ptr++), buffer++); length --; bytes_read ++; } #ifdef DEBUG printk ("%d bytes leidos, quedan %d\n", bytes_read, length); #endif /* Las funciones de lectura se supone que devuelven el * número de bytes realmente insertados en el buffer */ return bytes_read; } /* Se llama a esta función cuando alguien intenta escribir * en nuestro fichero de dispositivo - no soportado en este * ejemplo. */ #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) static ssize_t device_write(struct file *file, const char *buffer, /* El buffer */ size_t length, /* La longitud del buffer */ loff_t *offset) /* Nuestro desplazamiento en el fichero */ #else static int device_write(struct inode *inode, struct file *file, const char *buffer, int length) #endif { return -EINVAL; } /* Declaraciones del Módulo ***************************** */ /* El número mayor para el dispositivo. Esto es * global (bueno, estático, que en este contexto es global * dentro de este fichero) porque tiene que ser accesible * para el registro y para la liberación. */ static int Major; /* Esta estructura mantendrá las funciones que son llamadas * cuando un proceso hace algo al dispositivo que nosotros creamos. * Ya que un puntero a esta estructura se mantiene en * la tabla de dispositivos, no puede ser local a * init_module. NULL es para funciones no implementadas. */ struct file_operations Fops = { NULL, /* búsqueda */ device_read, device_write, NULL, /* readdir */ NULL, /* seleccionar */ NULL, /* ioctl */ NULL, /* mmap */ device_open, #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) NULL, /* borrar */ #endif device_release /* a.k.a. cerrar */ }; /* Inicializa el módulo - Registra el dispositivo de carácter */ int init_module() { /* Registra el dispositivo de carácter (por lo menos lo intenta) */ Major = module_register_chrdev(0, DEVICE_NAME, &Fops); /* Valores negativos significan un error */ if (Major < 0) { printk ("dispositivo %s falló con %d\n", "Lo siento, registrando el carácter", Major); return Major; } printk ("%s El número mayor del dispositivo es %d.\n", "El registro es un éxito.", Major); printk ("si quieres hablar con el controlador del dispositivo,\n"); printk ("tendrás que crear un fichero de dispositivo. \n"); printk ("Te sugerimos que uses:\n"); printk ("mknod <nombre> c %d <menor>\n", Major); printk ("Puedes probar diferentes números menores %s", "y ver que pasa.\n"); return 0; } /* Limpieza - liberamos el fichero correspondiente desde /proc */ void cleanup_module() { int ret; /* liberamos el dispositivo */ ret = module_unregister_chrdev(Major, DEVICE_NAME); /* Si hay un error, lo indicamos */ if (ret < 0) printk("Error en unregister_chrdev: %d\n", ret); }