Kabosu - Creando cosas

Logo de la página. Gato esférico con colores verdoso.

Paellux (parte 2)

Publicado: 2024-11-15

Etiquetas: Linux, Proyectos, Reto de la Paella


En el artículo anterior comentaba que había estado compilando el kernel y añadiendo un mensaje en sus logs. En este voy a contar cómo modifiqué más el kernel para crear un directorio de recetas en el sistema de ficheros del kernel /proc.

Como parte del reto, me propuse no mirar nada de documentación y hacerlo todo mediante prueba y error. Buscando código similar en el propio kernel usando mis dotes de investigación.

Buscando ejemplos

Lo primero que hice fue buscar algún código de ejemplo para saber cómo se añaden entradas en /proc. Me metí en el propio /proc para identificar algún candidato con el que iniciar mi búsqueda. Debía ser algo relativamente único para que al buscar con grep no me saliesen montones de resultados.

Me fijé en /proc/dynamic_debug. Esa cadena era larga y debería un buen punto de partida pero no, había 83 resultados en el código. Parece que muchos módulos incluyen dynamic_debug.h. Aún así me puse a leer ese fichero e intentar entender. Allí había referencias a debugfs y proc_fs. Primero pensé que sería lo mismo pero luego ya descubría que, al parecer, son cosas diferentes. Supuse que \proc estaría relacionado con proc_fs.h así que me centré en include/linux/proc_fs.h.

Ojeando include/linux/proc_fs.h

Este fichero contiene un montón de funciones para crear y manejar cosas en /proc. Encontré una estructura llamada proc_dir_entry y una función proc_mkdir que parecía crear los directorios. Esas fueron mis siguientes pistas.

Buscando con grep, efectivamente, muchos módulos del kernel usaban proc_mkdir para crear los diferentes directorios de /proc. Utilicé drivers/pci/proc.c como punto de partida para intentar aprender cómo se va creando los diferentes elementos.

A por drivers/pci/proc.c

En este fichero se crea el directorio /proc/bus/pci. Dentro de él se añaden un fichero para cada dispositivo PCI del sistema. El punto de entrada del proceso parecía ser la función pci_proc_init que se encuentra al final.

static int __init pci_proc_init(void)
{
	struct pci_dev *dev = NULL;
	proc_bus_pci_dir = proc_mkdir("bus/pci", NULL);
	proc_create_seq("devices", 0, proc_bus_pci_dir,
		    &proc_bus_pci_devices_op);
	proc_initialized = 1;
	for_each_pci_dev(dev)
		pci_proc_attach_device(dev);

	return 0;
}

Esta función llama a proc_mkdir("bus/pci", NULL) que con toda seguridad crea el directorio /proc/bus/pci. La llamada proc_create_seq todavía no tengo muy claro qué hace. Yo no la usé y mi código funcionó bien. Al final del fichero se llama a pci_proc_attach_device para cada dispositivo PCI. Esta función era más larga pero la mayor parte del código era manejo de errores. Por lo que entendí, la línea importante es la siguiente:

	e = proc_create_data(name, S_IFREG | S_IRUGO | S_IWUSR, bus->procdir,
			     &proc_bus_pci_ops, dev);

Supuse que proc_create_data añade un nuevo elemento llamado name al directorio guardado en bus->procdir. El segundo parámetro parecían los permisos. &proc_bus_pci_ops es una estructura que contiene punteros a funciones casi seguro que se llaman cuando el fichero se lee o escribe.

static const struct proc_ops proc_bus_pci_ops = {
	.proc_lseek	= proc_bus_pci_lseek,
	.proc_read	= proc_bus_pci_read,
	.proc_write	= proc_bus_pci_write,
    ...
}

El parámetro dev no tenía muy claro para qué servía. Yo pasé NULL y me quedé tan pancho.

Creando mi directorio de recetas

Con lo descubierto examinando el fichero anterior tuve bastante claro que había que hacer. Añadí una nueva función llamada init_recipes que creaba el directorio /proc/recipes y luego dentro él definía el fichero paella. La función la definí en el mismo fichero drivers/pci/proc.c para no calentarme mucho la cabeza.

static void init_recipes(void) {
    // Crea /proc/recipes
    proc_recipes_dir = proc_mkdir("recipes", NULL);

    // Crea /proc/recipes/paella
    struct proc_dir_entry *e = proc_create_data("paella", S_IFREG | S_IRUGO | S_IWUSR, proc_recipes_dir, &proc_recipes_paella_ops, NULL);
    proc_set_size(e, 0);
}

Usé los mismos argumentos que el código original excepto el nombre, que obviamente ahora era "paella" y la estructura que indicaba qué había que hacer llamada en mi caso proc_recipes_paella_ops.

static const struct proc_ops proc_recipes_paella_ops = {
	.proc_lseek	= proc_bus_pci_lseek,
	.proc_read	= proc_recipes_paella_read,
	.proc_write	= proc_bus_pci_write,
	.proc_ioctl	= proc_bus_pci_ioctl,
};

Necesitaba crear una función llamada proc_recipes_paella_read que se ejecutara cada vez que se leía el fichero. Mi intención era que esta función devolviese el texto de la receta. El resto de funciones las dejé como estaban porque no me interesaban demasiado. Mi función está basada en el ejemplo que hay en el mismo fichero aunque en el caso de drivers/pci/proc.c la lógica era un poco más complicada.

// Receta de la paella
static char* RECIPE_PAELLA = "Ingredientes:\n- arroz\n- pollo\n- conejo\n- judía verde plana\n- garrofó\n- caracoles\n- aceite de oliva virgen extra\n- pimentón dulce\n- tomate triturado\n- azafrán\n- romero\n- sal\n\nAquí va la receta de la paella.";


// Función que se ejecuta cuando alguien lee el fichero /proc/recipes/paella
static ssize_t proc_recipes_paella_read(struct file *file, char __user *buf,
				 size_t nbytes, loff_t *ppos)
{
	unsigned int pos = *ppos;
	unsigned int cnt;
	unsigned int size = strlen(RECIPE_PAELLA);

    // Calcular hasta qué posición de memoria hay que devolver
	if (pos >= size)
		return 0;
	if (nbytes >= size)
		nbytes = size;
	if (pos + nbytes > size)
		nbytes = size - pos;
	cnt = nbytes;

	if (!access_ok(buf, cnt))
		return -EINVAL;

    // Devuelve los bytes pedidos
	while (cnt > 0) {
		__put_user(RECIPE_PAELLA[pos], buf);
		buf++;
		pos++;
		cnt--;

	}

	*ppos = pos;
	return nbytes;
}

La función tenía que aceptar los siguientes argumentos:

Esto permite que los procesos que lean el fichero puedan pedir diferentes partes del mismo. En mi función lo primero que hago es calcular el trozo de memoria a devolver. Si ppos es mayor que el tamaño de la receta se termina la función directamente porque no vamos a poder copiar nada. Si se piden más bytes que el tamaño máxima se trunca el valor. También si hay una posición inicial distinta de 0 se comprueba que esa posición más el tamaño pedido no exceda del tamaño de la receta size. El bucle al final de la función simplemente copia el trozo de cadena que corresponda byte a byte.

Probando mis cambios

Compilé el kernel. Por suerte solo tardó unos 10 minutos. Luego lo copié a /boot y creé una nueva imagen initramfs. Todo eso lo explico en el artículo anterior.

Reinicié la máquina virtual y todo se cargó correctamente. Llegó el momento de la verdad. Fui a ver si mis recetas existían y ls las mostraba correctamente.

localhost:~$ ls /proc/recipes/
all i pebre     allioli         arroz al horno  fideua          horchata        paella

Ahora tocaba la prueba definitiva. No tenía ninguna confianza en que la función que había hecho para copiar la receta a memoria de usuario funcionase. Usando cat intenté leer la receta y, para mi sorpresa, ¡funcionó a la primera!

localhost:~$ cat /proc/recipes/paella 
Ingredientes:
- arroz
- pollo
- conejo
- judía verde plana
- garrofó
- caracoles
- aceite de oliva virgen extra
- pimentón dulce
- tomate triturado
- azafrán
- romero
- sal

Aquí va la receta de la paella.

Había conseguido añadir algo nuevo (e inútil) al kernel de Linux por lo que di por concluido el reto de la paella.

Captura de pantalla de la máquina virtual mostrando el resultado

¿Lo hice bien?

Durante este "reto" me había propuesto no buscar en internet ni leer documentación pero ahora quería ver si mi solución era decente. Busqué un poco en redes por "procfs programming" y similares pero no encontré ningún tutorial que me diera confianza. También estuve leyendo algunas respuestas de StackOverflow y otros foros.

En principio mi solución era buena pero la gente parece usar proc_create_entry mientras que yo usé proc_create_data. No tengo ni idea de cuál es la diferencia entre las dos funciones. Tampoco me paré a investigar mucho más.

Código final

Este el código que me quedó al final. Es igual que el que he explicado más arriba pero añadí unas pocas entradas más /proc/recipes para que la captura de pantalla quedase un poco más bonita. Todos utilizan la función de la paella así que realmente si alguien intentase leer esos ficheros recibiría la misma receta todo el rato.

Para usar solo hay que añadir una llamada init_recipes(); dentro de la función pci_proc_init.

static char* RECIPE_PAELLA = "Ingredientes:\n- arroz\n- pollo\n- conejo\n- judía verde plana\n- garrofó\n- caracoles\n- aceite de oliva virgen extra\n- pimentón dulce\n- tomate triturado\n- azafrán\n- romero\n- sal\n\nAquí va la receta de la paella.";

static ssize_t proc_recipes_paella_read(struct file *file, char __user *buf,
				 size_t nbytes, loff_t *ppos)
{
	unsigned int pos = *ppos;
	unsigned int cnt;
	unsigned int size = strlen(RECIPE_PAELLA);

	if (pos >= size)
		return 0;
	if (nbytes >= size)
		nbytes = size;
	if (pos + nbytes > size)
		nbytes = size - pos;
	cnt = nbytes;

	if (!access_ok(buf, cnt))
		return -EINVAL;

	while (cnt > 0) {
		__put_user(RECIPE_PAELLA[pos], buf);
		buf++;
		pos++;
		cnt--;

	}

	*ppos = pos;
	return nbytes;
}


static const struct proc_ops proc_recipes_paella_ops = {
	.proc_lseek	= proc_bus_pci_lseek,
	.proc_read	= proc_recipes_paella_read,
	.proc_write	= proc_bus_pci_write,
	.proc_ioctl	= proc_bus_pci_ioctl,
};

static struct proc_dir_entry *proc_recipes_dir;


static void init_recipes(void) {
	proc_recipes_dir = proc_mkdir("recipes", NULL);
	struct proc_dir_entry *e = proc_create_data("paella", S_IFREG | S_IRUGO | S_IWUSR, proc_recipes_dir, &proc_recipes_paella_ops, NULL);
	proc_set_size(e, 0);

	proc_create_data("horchata", S_IFREG | S_IRUGO | S_IWUSR, proc_recipes_dir, &proc_recipes_paella_ops, NULL);
	proc_create_data("fideua", S_IFREG | S_IRUGO | S_IWUSR, proc_recipes_dir, &proc_recipes_paella_ops, NULL);
	proc_create_data("arroz al horno", S_IFREG | S_IRUGO | S_IWUSR, proc_recipes_dir, &proc_recipes_paella_ops, NULL);
	proc_create_data("allioli", S_IFREG | S_IRUGO | S_IWUSR, proc_recipes_dir, &proc_recipes_paella_ops, NULL);
	proc_create_data("all i pebre", S_IFREG | S_IRUGO | S_IWUSR, proc_recipes_dir, &proc_recipes_paella_ops, NULL);
}

Artículo siguiente: Soportando otros navegadores
Artículo anterior: Paellux (parte 1)