Comunicación entre espacio de usuario y kernel en Linux: transferencia de datos con drivers de dispositivo

El driver actúa como puente entre la capa de usuario y el kernel. No es posible intercambiar datos directamente entre ambos espacios; por ello, este artículo explica cómo un driver permite que la programación en espacio de usuario interactúe con el kernel para transferir información.

Conceptos previos

  • El nodo de dispositivo es el punto de interacción entre la capa de aplicación (usuario) y el kernel.
  • Se usan estructuras predefinidas, por ejemplo, la función del sistema open corresponde al puntero open de la estructura struct file_operations.
  • La estructura de operaciones de archivo se rellana con los punteros que se necesitan; los demás pueden omitirse sin provocar fallos (retornan 0).

Este artículo se centra en cómo escribir datos desde la capa de usuario hacia el kernel y cómo devolver datos desde el kernel al usuario mediante read.

Preparación de la plantilla del driver

Copiamos el driver anterior testFileOpts y lo renombramos:

cd ~/work/drive/
ls
cp -arf 003_testFileOpts 004_testReadWrite
cd 004_testReadWrite/
make clean
ls
mv testFileOpts.c testReadWrite.c
vi Makefile 
ls

Modificamos el Makefile para cambiar el nombre del módulo (obj-m). Luego editamos el archivo testReadWrite.c para registrar un dispositivo misceláneo:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>

int my_open(struct inode *pInode, struct file *pFile)
{
    printk("my_open ejecutado\n");
    return 0;
}

int my_release(struct inode *pInode, struct file *pFile)
{
    printk("my_release ejecutado\n");
    return 0;
}

ssize_t my_read(struct file *pFile, char __user *pUser, size_t size, loff_t *pLofft)
{
    printk("my_read ejecutado\n");
    return 0;
}

ssize_t my_write(struct file *pFile, const char __user *pUser, size_t size, loff_t *pLofft)
{
    printk("my_write ejecutado\n");
    return 0;
}

struct file_operations my_fops = {
    .owner = THIS_MODULE,
    .open = my_open,
    .release = my_release,
    .read = my_read,
    .write = my_write,
};

struct miscdevice my_misc = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "mi_dispositivo_rw",
    .fops = &my_fops,
};

static int __init mi_init(void)
{
    int res;
    printk("Inicio del driver de prueba\n");
    res = misc_register(&my_misc);
    if (res < 0) {
        printk("Error al registrar dispositivo\n");
        return -1;
    }
    return 0;
}

static void __exit mi_exit(void)
{
    misc_deregister(&my_misc);
    printk("Driver eliminado\n");
}

MODULE_LICENSE("GPL");
module_init(mi_init);
module_exit(mi_exit);

Funciones del kernel para transferencia de datos

El kernel y el espacio de usuario no pueden intercambiar datos directamente; se usan copy_to_user y copy_from_user (archivo de cabecera <linux/uaccess.h>).

  • copy_from_user(void *to, const void __user *from, unsigned long n): copia datos desde el espacio de usuario al kernel.
  • copy_to_user(void __user *to, const void *from, unsigned long n): copia datos desde el kernel al espacio de usuario.

Ejemplo completo con transferencia de datos

Añadimos un búfer interno y las llamadas a las funciones de copia.

1. Incluir cabecera y definir buffer estático

#include <linux/uaccess.h>
static char internal_buf[256] = {0};

2. Inicializar el buffer en open

int my_open(struct inode *pInode, struct file *pFile)
{
    printk("my_open: inicializando buffer\n");
    memcpy(internal_buf, "buffer inicializado", sizeof("buffer inicializado"));
    return 0;
}

3. Implementar read usando copy_to_user

ssize_t my_read(struct file *pFile, char __user *pUser, size_t size, loff_t *pLofft)
{
    size_t len = strlen(internal_buf);
    printk("my_read: copiando al usuario %zu bytes\n", len);
    if (copy_to_user(pUser, internal_buf, len)) {
        printk("Error en copy_to_user\n");
        return -EFAULT;
    }
    return len;
}

4. Implementar write usando copy_from_user

ssize_t my_write(struct file *pFile, const char __user *pUser, size_t size, loff_t *pLofft)
{
    printk("my_write: recibiendo %zu bytes desde usuario\n", size);
    if (copy_from_user(internal_buf, pUser, size)) {
        printk("Error en copy_from_user\n");
        return -EFAULT;
    }
    return size;
}

5. Programa de prueba en espacio de usuario

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main(void)
{
    int fd = -1;
    char buf[64] = {0};
    int ret;

    fd = open("/dev/mi_dispositivo_rw", O_RDWR);
    if (fd < 0) {
        perror("open");
        return 1;
    }
    printf("Dispositivo abierto\n");

    // Leer
    ret = read(fd, buf, sizeof(buf)-1);
    if (ret < 0) {
        perror("read");
        close(fd);
        return 1;
    }
    printf("Leido: %s\n", buf);

    // Escribir nuevo contenido
    strcpy(buf, "Saludos desde el usuario");
    ret = write(fd, buf, strlen(buf)+1);
    if (ret < 0) {
        perror("write");
        close(fd);
        return 1;
    }
    printf("Escrito: %s\n", buf);

    // Volver a leer
    memset(buf, 0, sizeof(buf));
    ret = read(fd, buf, sizeof(buf)-1);
    if (ret < 0) {
        perror("read");
        close(fd);
        return 1;
    }
    printf("Despues de escribir: %s\n", buf);

    close(fd);
    return 0;
}

6. Compilar el driver y cargarlo

make
sudo insmod testReadWrite.ko

7. Compilar el programa de prueba y ejecutar

gcc prueba.c -o prueba
sudo ./prueba

La salida esperada muestra que la lectura inicial devuelve el texto de inicialización, luego se escribe un nuevo mensaje y la segunda lectura lo confirma.

Problemas comunes

Lectura no coincide con lo esperado

Posibles causas: no se copió la longitud correcta (usar strlen o el tamaño exacto del buffer), o el buffer no se inicializó adecuadamente. Verificar que copy_to_user reciba el tamaño apropiado.

Etiquetas: linux driver miscdevice copy_to_user copy_from_user

Publicado el 7-22 07:05