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); }