lunes, 10 de marzo de 2014

Usar /proc desde un módulo del kernel Linux

Ya he dedicado un artículo a cómo programar un módulo para el kernel. Hoy vamos a dar una vuelta más de tuerca y vamos a ver cómo usar /proc desde un módulo del kernel Linux. Esto nos permitirá tener un medio de comunicación desde nuestros módulos del kernel con el exterior.



¿Qué es el directorio /proc?

Seguramente te has preguntado alguna vez qué es ese misterioso directorio /proc que hay en tu sistema de ficheros (/proc filesystem). Originariamente, este directorio estaba pensado para ofrecer información rápida y de fácil acceso sobre los procesos del sistema. En la actualidad, su misión ha ido evolucionando y ahora tiene otras utilidades.
Lo primero que debemos saber es que el directorio /proc no es un directorio real de tu sistema de ficheros, sino que es un directorio virtual que sólo se encuentra en memoria (ni se te ocurra incluirlo en tus copias de seguridad). ¿De dónde salen entonces todos estos archivos y directorios? La respuesta es: directamente del Kernel.
El sistema de archivos /proc es pues una forma de comunicarnos directamente con el Kernel, ya que nos da información en tiempo real del estado en que se encuentra, sus parámetros de configuración y otras informaciones ofrecidas por los módulos que se encuentan cargados en memoria. Pero es un canal de doble sentido, ya que podemos escribir en algunos de estos archivos para modificar ciertos parámetros de funcionamiento del kernel.
Veámoslo con un sencillo ejemplo. Con el siguiente comando podemos obtener el nombre del host de nuestra máquina:

cat /proc/sys/kernel/hostname

Pero además, podemos cambiar el nombre del host con el siguiente comando:

echo supermachine.com > /proc/sys/kernel/hostname

Evidentemente cuando reiniciemos la máquina nuestro host volverá a tener su nombre original, pero nos sirve como ejemplo.
Otro clásico es el siguiente, que nos permite activar el reenvío de paquetes TCP/IP para que Linux actúe como un router:

echo 1 > /proc/sys/net/ipv4/ip_forward

Algunos directorios y archivos interesantes son:
/proc/cpuinfo Información sobre el microprocesador.
/proc/devices Controladores de dispositivos cargados en memoria.
/proc/interrupts Información sobre las interrupciones.
/proc/loadavg Carga del sistema en tiempo real.
/proc/meminfo Información sobre la memoria RAM y Swap.
/proc/modules Modulos del kernel cargados en memoria.
/proc/net Información sobre el estado de la red.
/proc/stat Datos estadísticos sobre el estado del sistema.
/proc/uptime Número de segundos que lleva el sistema funcionando.
/proc/version Indica la versión del núcleo

Por supuesto hay bastantes más archivos y directorios interesantes, pero son tantos que los dejo para que los investigues y descubras tú mismo.
Para los programadores del kernel, este directorio tiene bastante interés, ya que ofrece una manera sencilla de comunicar nuestro módulo con el resto del mundo, pero esto ya lo veremos en otro artículo.



Cómo usar /proc desde un módulo del kernel Linux

Para crear una entrada (ya sea un archivo o un directorio) en el filesystem /proc (procfs) nos valemos de la función create_proc_entry():

struct proc_dir_entry *create_proc_entry(const char *name, umode_t mode, struct proc_dir_entry *parent)

El primer parámetro es el nombre del archivo o directorio, el segundo es el modo (permisos y tipo) y finalmente el padre (si no tiene será NULL). La función devuelve una estructura llamada proc_dir_entry, que tiene la siguiente definición en proc_fs.h.

struct proc_dir_entry {
    unsigned int low_ino;
    umode_t mode;
    nlink_t nlink;
    kuid_t uid;
    kgid_t gid;
    loff_t size;
    const struct inode_operations *proc_iops;
    const struct file_operations *proc_fops;
    struct proc_dir_entry *next, *parent, *subdir;
    void *data;
    read_proc_t *read_proc;
    write_proc_t *write_proc;
    atomic_t count;         /* use count */
    int pde_users;  
    struct completion *pde_unload_completion;
    struct list_head pde_openers;   
    spinlock_t pde_unload_lock; 
    u8 namelen;
    char name[];
};

Por ahora sólo vamos a usar los campos read_proc y write_proc, que son punteros a la función encargada de la lectura y la escritura en el fichero. Sin más preámbulos vamos a presentar un ejemplo completo que seguidamente pasaremos a analizar. Lo situaremos en el directorio ~kerneldev/src/modulo2 y lo llamaremos hello_proc.c.

#include <linux/module.h>  /* importar función copy_from_user() */
#include <asm/uaccess.h> 
#include <linux/proc_fs.h> /* importar función find_task_by_pid_ns() */
#include <linux/sched.h> 

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Modulo Hola proc");
MODULE_AUTHOR("Alberto Garcia");

/* nombre del archivo: /proc/helloproc */
#define PROC_ENTRY "helloproc"
#define MAX_LEN 7

pid_t pid = 0;

/* Muestra el nombre el pid introducido */
/* La información se almacena en memoria (empezando en *buf) */
static int read_proc(char *buf, char **start, off_t off, 
                     int count, int *eof, void *data) 
{
    struct task_struct *task = NULL;
    unsigned len = 0;

    if (pid) {
        task = pid_task(find_vpid(pid), PIDTYPE_PID);
        if (!task)
            return -EINVAL;

        len += snprintf(buf + len, count - len, 
               "Datos sobre el proceso %u\n", pid);
        len += snprintf(buf + len, count - len, 
               " Padre: %u\n", task->parent->pid);

        return (len);
    }

    printk(KERN_INFO "No se ha indicado un pid\n");
    return 0;
}

/* Obtenemos el pid del proceso */
static ssize_t write_proc(struct file *file, const char __user * buff,
                          unsigned long len, void *data)
{
    char c[MAX_LEN + 1];

    if (len > MAX_LEN)
        return -EINVAL;

    if (copy_from_user(c, buff, len))
        return -EFAULT;

    c[len] = '\0';
    if (!sscanf(c, "%d\n", &pid))
        return -1;

    printk(KERN_INFO "Solicitud para pid %d\n", pid);

    return len;
}

/* Registrar la entrada en el fs /proc */
static int register_proc(void)
{
    struct proc_dir_entry *hello_entry;

    hello_entry = create_proc_entry(PROC_ENTRY, S_IFREG, NULL);
    if (!hello_entry)
        return -1;

    hello_entry->read_proc = read_proc;
    hello_entry->write_proc = write_proc;
    return 0;
}

/* eliminar la entrada en el fs /proc */
static void unregister_proc(void)
{
    remove_proc_entry(PROC_ENTRY, NULL);
}


static int __init hello_init(void)
{
    if (register_proc() == -1) {
        printk(KERN_INFO "No se ha podido crear la entrada en /proc\n");
        return -1; 
    }

    printk(KERN_INFO "Entrada creada en /proc.\n");
    return 0;
}

static void __exit hello_exit(void)
{
    unregister_proc();
    printk(KERN_INFO "Hasta otra.\n");
}

module_init(hello_init);
module_exit(hello_exit);

Para compilar este programa podemos usar el siguiente Makefile.

ifneq ($(KERNELRELEASE),)
    obj-m := hello_proc.o 
else
    KERNEL_VERSION = linux-3.5.0
    KERNELDIR := ../$(KERNEL_VERSION)
    PWD := $(shell pwd)
modules:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif

clean:
    rm -rf *.[oas] .*.flags *.ko .*.cmd .*.d .*.tmp \
        *.mod.c .tmp_versions Module.symvers

Tras compilarlo, obtendremos un módulo instalable con insmod, que crea un archivo llamado /proc/helloproc. El fichero soporta lectura y escritura. Su funcionamiento es el siguiente:
Escribimos sobre el fichero el pid de un proceso:

echo "927" > /proc/helloproc

Seguidamente podemos leer del fichero para obtener información sobre el proceso. En este caso, y por simplificar, sólo se mostrará el PID del proceso padre.

cat /proc/helloproc


Al igual que en el módulo creado en el artículo anterior, aquí usamos module_init y module_exit para indicar cuáles son las funciones encargadas de la carga y descarga del módulo. Al cargar el módulo en memoria se invoca la función register_proc(), que crea la entrada en el procfs usando create_proc_entry(). La constante S_IFREG definida en sys/stat.h, indica que queremos crear un fichero normal. Si quisiéramos crear un directorio debemos usar S_IFDIR.
Sobre la estructura devuelta por esta función indicamos cuáles son las funciones encargadas de la gestión de la lectura/escritura del fichero:

hello_entry->read_proc = read_proc;
hello_entry->write_proc = write_proc;

Así pues, read_proc() y write_proc() son las funciones en la que codificaremos lo que se debe hacer cuando se lee o escribe en el fichero.
La función write_proc() tiene la siguiente definición.

static ssize_t write_proc(struct file *file, const char __user * buff,
unsigned long len, void *data)

Por ahora, para no complicarlo más, nos basta saber que buff contiene los datos que se están escribiendo en el fichero, y len la longitud en bytes de dichos datos.
Hay que tener en cuenta que buff es un puntero a una zona de memoria del espacio de usuario, pero como estamos en el espacio del kernel, no podemos acceder de forma directa, por lo que hay que usar la función copy_from_user() para copiar el dato en una variable del espacio del kernel. Tras esto simplemente usamos la función sscanf() para poner el valor introducido en formato entero en la variable pid.
La función para la lectura tiene la siguiente definición.

static int read_proc(char *buf, char **start, off_t off, 
int count, int *eof, void *data)

Cuando se invoca la función read_proc, el kernel reserva una zona de memoria para almacenar la salida. En este caso es buf, el primer parámetro, donde escribiremos la información que queremos devolver. Usamos la función snprintf() para ir poniendo la información en buf y, finalmente, retornamos en número de bytes que vamos a devolver.
Vamos a pararnos en la siguiente línea:

task = pid_task(find_vpid(pid), PIDTYPE_PID);

Tras la ejecución, la variable task apuntará a una estructura de datos llamada task_struct con información del proceso cuyo PID es el almacenado en la variable pid. Esta estructura almacena mucha información sobre el proceso (cada proceso tiene asociado un task_struct). Aquí puedes encontrar más información sobre la estructura task_struct (además de otras).
Nosotros hemos usado el campo parent, que es un puntero al task_struct del proceso padre, y de ahí hemos sacado su PID con task->parent->pid.
Si estuviéramos codificando directamente sobre el código del kernel podríamos haber usado las funciones find_task_by_pid_ns() o find_task_by_vpid() para obtener el proceso a partir del PID, pero desde un módulo no podemos usarlas directamente a no ser que las exportemos desde el propio código del kernel con, por ejemplo, EXPORT_SYMBOL(find_task_by_pid_ns).
Antes de terminar nos resta comentar la función unregister_proc(), que es invocada al descargar el módulo de la memoria con rmmod, y que se encarga de eliminar el archivo del procfs mediante la función remove_proc_entry().
Ahora que somos capaces de crear un módulo y usar el procfs, el siguiente paso lógico es hablar sobre los controladores o device drivers y cómo programarlos, pero será ya en un próximo artículo.

No hay comentarios:

Publicar un comentario en la entrada