El objetivo es que el alumno se familiarice con la programación interna en el sistema operativo Linux, entendiendo como tal aquella orientada al desarrollo de componentes software (denominados módulos de núcleo de Linux) que se incorporarán al propio sistema operativo, ya sea de forma estática, en tiempo de compilación, o de manera dinámica, cuando el sistema operativo ya esté en ejecución.
Aunque los módulos de núcleo de Linux tienen diversos usos (como, por ejemplo, posibilitar la utilización de nuevos formatos de ejecutables), su principal aplicación es el desarrollo de manejadores de dispositivos, que es precisamente el objetivo de esta parte de la asignatura y lo que plantea esta práctica, que sirve además como un punto inicial para acometer el proyecto.
Este documento, además de especificar la funcionalidad del manejador que se pretende desarrollar, incluye la descripción de los fundamentos requeridos para afrontar la práctica de manera que se pueda realizar sin consultar ninguna documentación adicional. Téngase en cuenta que, dada la relación entre esta práctica y el proyecto, parte de la información que aparece en este documento aparece también en el enunciado del proyecto.
En cualquier caso, se propone como referencia este libro, un clásico dentro de este campo, aunque, dada su fecha de publicación, algo obsoleto.
# crea un fichero de 4M relleno de ceros dd if=/dev/zero of=nuevo bs=4096 count=1024 # ignoramos la salida y el error generados por make make > /dev/null 2>1La práctica plantea crear un dispositivo de caracteres denominado /dev/seq porque cuando se lee del mismo se obtiene una secuencia de bytes, siendo su comportamiento el siguiente:
#include <linux/init.h> #include <linux/module.h> #include <linux/moduleparam.h> MODULE_LICENSE("Dual BSD/GPL"); /* * A couple of parameters that can be passed in: how many times we say * hello, and to whom. */ static char *whom = "world"; static int howmany = 1; module_param(howmany, int, S_IRUGO); module_param(whom, charp, S_IRUGO); static int __init hello_init(void) { int i; for (i = 0; i < howmany; i++) printk(KERN_ALERT "(%d) Hello, %s\n", i, whom); return 0; } static void __exit hello_exit(void) { printk(KERN_ALERT "Goodbye, cruel world\n"); } module_init(hello_init); module_exit(hello_exit);
static int howmany = 1;A continuación, se debe usar la macro module_param(variable, tipo, permisos) para denotar que la variable definida previamente actuará como un parámetro, especificando su nombre, su tipo y los permisos de acceso a la misma desde fuera del núcleo.
module_param(howmany, int, S_IRUGO);En este ejemplo, se le ha otorgado permiso de lectura al usuario que active el módulo (U), a los de su grupo (G) y a los otros usuarios restantes (O).
Nótese que si no se especifica el parámetro en la carga del módulo, la variable contendrá el valor inicial definido en el propio programa.
Asimismo, téngase en cuenta que la macro MODULE_PARM_DESC permite asociar una descripción de texto a la variable. Dicha descripción sirve como documentación del módulo.
# If KERNELRELEASE is defined, we've been invoked from the # kernel build system and can use its language. ifneq ($(KERNELRELEASE),) obj-m := hello.o # Otherwise we were called directly from the command # line; invoke the kernel build system. else KERNELDIR ?= /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) default: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules endif
El mandato modinfo aplicado a ese fichero devuelve información sobre el módulo, entre la que se incluye sus parámetros, con su descripción en el caso de que se haya incluido esta usando MODULE_PARM_DESC.
modinfo ./hello.koEl próximo paso es la carga del módulo para lo que, por razones obvias, se requiere ser super-usuario. En el mandato de carga se especifican los parámetros del módulo:
sudo insmod ./hello.ko howmany=3 whom=amigoPara comprobar si el módulo se ha cargado, puede usarse el mandato lsmod, que lista todos los módulos activos en el sistema, y para ver si ha escrito el mensaje de bienvenida puede usar dmesg, que muestra el log del núcleo.
Estando cargado el módulo, se puede acceder a sus parámetros en el sistema de ficheros sys:
ls -l /sys/module/hello/parameters/ total 0 -r--r--r-- 1 root root 4096 oct 17 05:59 howmany -r--r--r-- 1 root root 4096 oct 17 05:59 whom
Por último, para descargar el módulo debe usar el siguiente mandato:
sudo rmmod helloTenga en cuenta que si el módulo está formado por varios ficheros, hay que incluir una entrada en el Makefile denominada nombre_del_módulo-y que especifique cuáles son esos ficheros. Así, por ejemplo, suponiendo que se pretende crear un módulo denominado hello a partir de dos ficheros de código fuente denominados hello1.c y hello2.c, habría que especificar:
obj-m := hello.o hello-y := hello1.o hello2.o
En esta primera fase, vamos a crear el dispositivo dentro de la función inicial del módulo. La creación de un dispositivo implica los siguientes pasos:
A continuación, se analiza cada uno de estos pasos.
Antes de dar de alta un dispositivo de caracteres, hay que reservar sus números major y minor asociados. Para esta práctica, dejaremos que el número major lo elija el propio sistema y recibiremos como parámetro el valor del minor (el parámetro se denominará minor), usando 0 como valor por defecto.
La función alloc_chrdev_region (definida en #include <linux/fs.h>) realiza esta labor devolviendo un número negativo en caso de error:
int alloc_chrdev_region(dev_t *devID, unsigned int firstminor, unsigned int count, char *name);
El tipo que representa un dispositivo de caracteres dentro de Linux es struct cdev (no confundir con el tipo dev_t, comentado previamente, que guarda un identificador de dispositivo; nótese, sin embargo, que, como es lógico, dentro del tipo struct cdev hay un campo denominado dev de tipo dev_t que almacena el identificador de ese dispositivo), que está definido en #include <linux/cdev.h>.
Se debe definir una variable de este tipo (o una estructura que la contenga) y usar la función cdev_init para dar valor inicial a sus campos. El primer parámetro será la dirección de esa variable que se pretende iniciar y el segundo una estructura de tipo struct file_operations que especifica las funciones de servicio del dispositivo:
void cdev_init(struct cdev *cdev, struct file_operations *fops);
A continuación, se muestra un ejemplo de uso de la estructura file_operations que especifica solamente las operaciones de apertura, cierre, lectura y escritura, que son las que se usarán en la práctica. En esta primera versión se debería incluir en cada una de ellas simplemente un printk identificándola que sirva para depuración.
static int seq_open(struct inode *inode, struct file *filp) { return 0; } static int seq_release(struct inode *inode, struct file *filp) { return 0; } static ssize_t seq_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) { return 0; } static ssize_t seq_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { return 0; } static struct file_operations seq_fops = { .owner = THIS_MODULE, .open = seq_open, .release = seq_release, .write = seq_write, .read = seq_read };Después de iniciar con cdev_init la estructura que representa al dispositivo, hay que asociarla con el identificador de dispositivo reservado previamente. Para ello, se usa la función cdev_add:
int cdev_add(struct cdev *dev, dev_t devID, unsigned int count);siendo el tercer parámetro igual a 1 en nuestro caso, puesto que queremos dar de alta un único dispositivo.
El primer paso que se debe llevar a cabo es la creación de una clase para el dispositivo gestionado por el manejador usando para ello la llamada class_create (todo dispositivo tiene que pertenecer a una clase) que retorna el identificador de la clase creada (NULL en caso de error):
#include <linux/device.h> struct class * class_create (struct module *owner, const char *name);Como primer parámetro se especificaría THIS_MODULE y en el segundo el nombre que se le quiere dar a esta clase de dispositivos (el que considere oportuno).
Después de crear la clase, hay que dar de alta el dispositivo asociándolo a esa clase. Para ello, se usa la función device_create:
struct device * device_create(struct class *class, struct device *parent, dev_t devID, void *drvdata, const char *fmt, ...)Para el ejemplo que nos ocupa, solo son relevantes los siguientes parámetros (para los demás se especificará un valor NULL):
Para obtener un valor aleatorio se puede usar la función get_random_bytes(void *buf, int nbytes); (linux/random.h), que devuelve en el primer parámetro el número aleatorio generado cuyo tamaño es especificado en el segundo parámetro.
Para realizar la copia de cada byte se dispone de la macro put_user que copia un dato a la dirección especificada por un puntero que hace referencia a la zona de memoria de usuario:
#include <asm/uaccess.h> int put_user(datum,ptr);En caso de éxito, esta función devuelve un 0. Si hay un fallo, que se deberá a que la dirección pasada como parámetro es inválida, devuelve el error -EFAULT, que será el valor que debe devolver este método en ese caso.
make > /dev/seq
# generamos f1 con 4 lecturas de 8 bytes dd if=/dev/seq bs=8 count=4 of=f1 # generamos f2 con 3 lecturas de 128 bytes dd if=/dev/seq bs=128 count=3 of=f2
strace -o traza1 dd if=/dev/seq bs=8 count=4 of=f1 strace -o traza2 dd if=/dev/seq bs=128 count=3 of=f2