Introducción a la programación de módulos en Linux

Objetivo de la guía

El objetivo es que el alumno se familiarice con la programación interna en el sistema operativo Linux, entendiendo como tal, aquélla 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.

Dada la disponibilidad de documentación de calidad y de libre acceso sobre esta temática, más que plantear un tutorial sobre este tipo de programación, vamos a proponer una serie de ejemplos y ejercicios y nos vamos a basar en el libro referencia sobre esta temática:

Primera sesión: Aspectos básicos sobre el desarrollo de módulos

El contenido de esta sección está vinculado con el capítulo número 2 del libro recomendado, y permitirá familiarizarse con la estructura básica de un módulo, así como sobre la compilación y carga del mismo. Se recomienda que el lector revise el contenido de este capítulo.

El primer módulo

El primer ejemplo que trataremos, y el primero que aparece en el libro, se denomina hello.c:
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");
static int hello_init(void)
{
        printk(KERN_ALERT "Hello, world\n");
        return 0;
}
static void hello_exit(void)
{
        printk(KERN_ALERT "Goodbye, cruel world\n");
}
module_init(hello_init);
module_exit(hello_exit);

Nótese el uso de las macros module_init y module_exit para definir las funciones que se ejecutarán en la carga y descarga del módulo, respectivamente. Obsérvese, asimismo, el uso de la función printk: el printf del núcleo.

En cuanto a las funciones especificadas para ser invocadas en la carga y descarga del módulo, aunque no es obligatorio, es recomendable usar, respectivamente, las etiquetas __init y __exit, que indican permiten optimizar la gestión del módulo (un fragmento de código etiquetado con __init puede descargarse después de completarse la carga, mientras que uno marcado con __exit puede eliminarse si el módulo nunca va a descargarse).


static int __init hello_init(void)

static void __exit hello_exit(void)

Compilación y carga de un módulo

Para compilar este fichero, crearemos, tal como proponen en el libro citado, el siguiente fichero Makefile (NOTA: no olvide incluir los tabuladores):
# 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

Con este fichero Makefile, bastaría con ejecutar el mandato make para compilar el módulo. Para ese paso no es necesario ser super-usuario, pero sí para la carga del módulo:
root# insmod ./hello.ko
Para comprobar si el módulo se ha cargado, puede usarse el mandato lsmod y para ver si ha escrito el mensaje de bienvenida puede usar dmesg.

Por último, para descargar el módulo debe usar el siguiente mandato:

root# rmmod hello
Con lsmod y dmesg puede comprobar si la descarga se ha realizado satisfactoriamente.

Tenga 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

Paso de parámetros a un módulo

Dentro del módulo, se define una variable global con las características requeridas por la propia funcionalidad del módulo:
        static int variable = 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 (el valor 0 indica que no podrá ser consultada ni modificada desde fuera del núcleo; en caso de que se le hubieran otorgado permisos, se habría creado una entrada en el directorio /sys para permitir su acceso según los permisos especificados):
        module_param(variable, int, 0);
Para poder pasarle ese parámetro al módulo, basta con especificarlo al cargarlo:
root# insmod ./hello.ko variable=5
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, obsérvese 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 y puede verse cuando se usa el mandato modinfo sobre el módulo. Recapitulando:

        static int variable = 1;
        module_param(variable, int, 0);
        MODULE_PARM_DESC(variable, "este parámetro indica...");
A continuación, se incluye el segundo ejemplo planteado en el libro (hellop.c) que muestra el uso de parámetros.
#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 hello_init(void)
{
	int i;
	for (i = 0; i < howmany; i++)
		printk(KERN_ALERT "(%d) Hello, %s\n", i, whom);
	return 0;
}

static void hello_exit(void)
{
	printk(KERN_ALERT "Goodbye, cruel world\n");
}

module_init(hello_init);
module_exit(hello_exit);

Ejercicio propuesto: visualización de la jerarquía de procesos

Se plantea desarrollar un módulo que al cargarse imprima en la consola información sobre algunos de los procesos existentes en un determinado momento en un sistema Linux.

Para facilitar el desarrollo de este módulo, se plantea una serie de fases de carácter incremental.

Nota aclaratoria previa

Por simplicidad, durante el desarrollo del módulo, se van a obviar todos los problemas relacionados con la sincronización. Por ese motivo, dicho módulo presenta condiciones de carrera cuando se ejecuta en un multiprocesador o, incluso, en un sistema monoprocesador donde Linux esté configurado como un núcleo expulsivo (opción Preemptible Kernel en el menú correspondiente del make config). El lector interesado puede revisar el capítulo dedicado a la sincronización (capítulo 5) del libro recomendado.

Primera versión: información del proceso actual

Se plantea programar una primera versión tal que al cargarse el módulo imprima en la consola (printk) el identificador de proceso, su estado y el nombre de programa asociado al proceso actual. Esta información está definida en <linux/sched.h>:

Segunda versión: información de los antecesores del proceso actual

En esta segunda versión, después de la información del proceso actual, se imprimirá esa misma información del padre (campo parent). Justo, a continuación, se puede generalizar siguiendo la cadena de sucesivos padres hasta el proceso inicial, que se caracteriza porque el campo parent apunta a sí mismo.

Tercera versión: información de los antecesores de un proceso dado

La tercera versión del módulo recibe como parámetro en el momento de su carga un identificador de proceso (pid) y, siguiendo la funcionalidad desarrollada previamente, muestra la información de ese proceso, de su padre, de su abuelo, y así sucesivamente hasta el proceso inicial. En caso de recibir como parámetro un 0, se imprimirá esa misma información pero tomando como base el proceso actual (o sea, se comportaría igual que la versión anterior). Al final de esta sección se explica cómo se manejan los parámetros de un módulo.

Esta nueva versión requiere poder obtener la estructura task_struct del proceso cuyo identificador ha sido recibido como parámetro. En versiones anteriores de Linux existían funciones que proporcionaban directamente esta funcionalidad requerida (find_task_by_pid y find_task_by_vpid). Sin embargo, en las versiones actuales, debido a la complejidad asociada a la gestión de los identificadores de proceso, hay que usar varias funciones para lograr este objetivo.

Para obtener el BCP a partir del PID, se puede usar primero la función find_vpid, que obtiene una estructura de tipo struct pid a partir del valor numérico recibido y, a continuación, aplicar la función pid_task especificando como segundo parámetro la constante PIDTYPE_PID.

Cuarta versión: información de hijos de un proceso dado

La cuarta versión del módulo plantea imprimir también la jerarquía de procesos descendente; es decir, dado un proceso, imprimir la información de todos sus hijos.

El módulo imprimirá la información descendente sólo cuando reciba como parámetro un número negativo, correspondiendo el valor absoluto del mismo con el identificador del proceso del que se pretende visualizar sus procesos hijos.

Para acceder a los procesos hijos, existe en el BCP un campo children que se corresponde con una lista de todos los hijos del proceso, que usa el tipo de lista genérica "oficial" de Linux denominado list_head.

En la quinta sección del capítulo 11 del libro Linux Device Drivers, se explica en detalle y con ejemplos cómo se manejan estas listas.

Quinta versión: información de todos los sucesores de un proceso dado

Esta versión final va a imprimir toda la jerarquía descendente de un proceso, es decir, se debe imprimir también los hijos de los hijos, y así sucesivamente hasta llegar a las "hojas" del árbol de procesos.