Manejo de E/S Bloqueante y No Bloqueante en Controladores de Dispositivos Linux

En el desarrollo de controladores para Linux, la gestión de las operaciones de entrada y salida (I/O) es fundamental para determinar cómo interactúan los procesos con el hardware. Los dispositivos de caracteres, que procesan datos de forma secuencial, suelen implementar dos semánticas principales: bloqueo y no bloqueo.

Conceptos Fundamentales de E/S

Cuando un proceso de usuario solicita una operación de lectura o escritura a través de llamadas al sistema como read() o write(), el controlador puede responder de dos maneras si el recurso no está disponible de inmediato:

  • Operación Bloqueante: El proceso se suspende y entra en estado de espera (dormido) hasta que la condición se cumpla. Durante este tiempo, el proceso es retirado de la cola de ejecución del planificador, liberando ciclos de CPU.
  • Operación No Bloqueante: Si el recurso no está listo, la llamada regresa inmediatamente con un error (generalmente -EAGAIN o -EWOULDBLOCK). El proceso puede decidir reintentar la operación (polling) o realizar otras tareas.

Implementación en el Espacio de Usuario

Un programa puede elegir el comportamiento mediante el flag O_NONBLOCK al abrir el dispositvio. A continuación, se muestra una comparación de cómo cambia el flujo lógico:


/* Ejemplo 1: Acceso bloqueante convencional */
int fd_bloque;
char entrada;
fd_bloque = open("/dev/sensor_datos", O_RDONLY);
// La ejecución se detiene aquí hasta que haya datos
read(fd_bloque, &entrada, 1);
printf("Dato recibido: %c\n", entrada);

/* Ejemplo 2: Acceso no bloqueante con bucle de espera */
int fd_no_bloque;
char dato;
fd_no_bloque = open("/dev/sensor_datos", O_RDONLY | O_NONBLOCK);
while (read(fd_no_bloque, &dato, 1) != 1) {
    // Realizar otras operaciones o simplemente esperar
    usleep(100);
}
printf("Dato recibido tras polling: %c\n", dato);

Colas de Espera (Wait Queues)

Para implementar el bloqueo en el kernel, se utilizan las Wait Queues. Estas estructuras actúan como una lista de procesos que esperan un evento específico, permitiendo que el controlador despierte a los procesos cuando el hardware cambia de estado (por ejemplo, al recibir una interrupción).

Operaciones Esenciales

Primero, se debe definir e inicializar la cabecera de la cola de espera:


wait_queue_head_t cola_lectura;
init_waitqueue_head(&cola_lectura);
// O de forma estática:
DECLARE_WAIT_QUEUE_HEAD(cola_escritura);

Para poner un proceso a dormir hasta que se cumpla una condición, se suelen utilizar macros de alto nivel:


/* Bloquea el proceso hasta que 'condicion' sea verdadera */
wait_event_interruptible(cola_lectura, condicion);

Cuando el recurso queda disponible (por ejemplo, en el manejador de interrupciones), se despiertan los procesos:


wake_up_interruptible(&cola_lectura);

Patrón de Diseño en el Driver

El siguiente fragmento ilustra la lógica típica dentro de una función read o write del controlador para gestionar el bloqueo manualmente, lo cual es útil para entender el cambio de estado del proceso:


static ssize_t dev_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) {
    struct mi_dispositivo *dev = file->private_data;
    DECLARE_WAITQUEUE(espera, current);

    add_wait_queue(&dev->cola_espera, &espera);
    
    while (!recurso_disponible(dev)) {
        if (file->f_flags & O_NONBLOCK) {
            remove_wait_queue(&dev->cola_espera, &espera);
            return -EAGAIN;
        }
        
        // Cambiar estado a TASK_INTERRUPTIBLE para permitir señales
        __set_current_state(TASK_INTERRUPTIBLE);
        schedule(); // Ceder la CPU

        if (signal_pending(current)) {
            remove_wait_queue(&dev->cola_espera, &espera);
            set_current_state(TASK_RUNNING);
            return -ERESTARTSYS;
        }
    }
    
    // Procesar los datos...
    remove_wait_queue(&dev->cola_espera, &espera);
    set_current_state(TASK_RUNNING);
    return bytes_leidos;
}

Ejemplo Práctico: Dispositivo FIFO con Bloqueo

Imaginemos un dispositivo que emula un búfer circular (FIFO). Los lectores deben bloquearse si el búfer está vacío, y los escritores deben bloquearse si el búfer está lleno. Cuando un escritor añade datos, despierta a los lectores, y viceversa.


#include <linux/module.h>
#include <linux/fs.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/uaccess.h>
#include <linux/mutex.h>

#define BUFFER_SIZE 1024

struct fifo_driver {
    unsigned char datos[BUFFER_SIZE];
    size_t len_actual;
    struct mutex cerrojo;
    wait_queue_head_t r_cola;
    wait_queue_head_t w_cola;
    struct cdev cdev;
};

static ssize_t fifo_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) {
    struct fifo_driver *dev = filp->private_data;
    DECLARE_WAITQUEUE(wait, current);

    mutex_lock(&dev->cerrojo);
    add_wait_queue(&dev->r_cola, &wait);

    while (dev->len_actual == 0) {
        if (filp->f_flags & O_NONBLOCK) {
            mutex_unlock(&dev->cerrojo);
            remove_wait_queue(&dev->r_cola, &wait);
            return -EAGAIN;
        }
        __set_current_state(TASK_INTERRUPTIBLE);
        mutex_unlock(&dev->cerrojo);
        schedule();
        if (signal_pending(current)) {
            remove_wait_queue(&dev->r_cola, &wait);
            set_current_state(TASK_RUNNING);
            return -ERESTARTSYS;
        }
        mutex_lock(&dev->cerrojo);
    }

    // Calcular cantidad a leer
    if (count > dev->len_actual) count = dev->len_actual;

    if (copy_to_user(buf, dev->datos, count)) {
        mutex_unlock(&dev->cerrojo);
        remove_wait_queue(&dev->r_cola, &wait);
        set_current_state(TASK_RUNNING);
        return -EFAULT;
    }

    // Desplazar datos en el buffer
    memmove(dev->datos, dev->datos + count, dev->len_actual - count);
    dev->len_actual -= count;

    // Despertar a posibles escritores
    wake_up_interruptible(&dev->w_cola);

    mutex_unlock(&dev->cerrojo);
    remove_wait_queue(&dev->r_cola, &wait);
    set_current_state(TASK_RUNNING);
    return count;
}

static ssize_t fifo_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) {
    struct fifo_driver *dev = filp->private_data;
    DECLARE_WAITQUEUE(wait, current);

    mutex_lock(&dev->cerrojo);
    add_wait_queue(&dev->w_cola, &wait);

    while (dev->len_actual == BUFFER_SIZE) {
        if (filp->f_flags & O_NONBLOCK) {
            mutex_unlock(&dev->cerrojo);
            remove_wait_queue(&dev->w_cola, &wait);
            return -EAGAIN;
        }
        __set_current_state(TASK_INTERRUPTIBLE);
        mutex_unlock(&dev->cerrojo);
        schedule();
        if (signal_pending(current)) {
            remove_wait_queue(&dev->w_cola, &wait);
            set_current_state(TASK_RUNNING);
            return -ERESTARTSYS;
        }
        mutex_lock(&dev->cerrojo);
    }

    size_t espacio_libre = BUFFER_SIZE - dev->len_actual;
    if (count > espacio_libre) count = espacio_libre;

    if (copy_from_user(dev->datos + dev->len_actual, buf, count)) {
        mutex_unlock(&dev->cerrojo);
        remove_wait_queue(&dev->w_cola, &wait);
        set_current_state(TASK_RUNNING);
        return -EFAULT;
    }

    dev->len_actual += count;
    wake_up_interruptible(&dev->r_cola);

    mutex_unlock(&dev->cerrojo);
    remove_wait_queue(&dev->w_cola, &wait);
    set_current_state(TASK_RUNNING);
    return count;
}

Esta implementación garantiza que el acceso al búfer sea seguro mediante exclusión mutua (mutex) y que los procesos se sincronicen eficientemente utilizando las colas de espera, evitando el consumo innecesario de recursos de CPU.

Etiquetas: Linux Kernel Device Drivers Wait Queues I/O Blocking C Programming

Publicado el 7-12 06:13