Control de Concurrencia en Controladores de Dispositivos Linux

Control de Concurrencia en Controladores de Dispositivos Linux

La concurrencia surge cuando múltiples unidades de ejecución acceden simultáneamente a recursos compartidos, como variables globales, estructuras de datos o hardware. Este acceso simultáneo puede llevar a condiciones de carrera, donde el resultado de la ejecución depende del orden impredecible de acceso. Para mitigar esto, es crucial garantizar el acceso exclusivo a los recursos compartidos, protegiendo así las secciones críticas del código mediante mecanismos de exclusión mutua.

Las causas comunes de concurrencia en el kernel de Linux incluyen:

  • Multihilo: Dado que Linux es un sistema multitarea, múltiples hilos pueden ejecutarse concurrentemente.
  • Preemption (Interrupción): A partir de la versión 2.6, el kernel de Linux soporta preemption, permitiendo que el planificador interrumpa un hilo en ejecución para ejecutar otro.
  • Manejo de Interrupciones: Las interrupciones de hardware pueden ocurrir en cualquier momento, interrumpiendo la ejecución normal del kernel.
  • SMP (Multiprocesamiento Simétrico): En sistemas con múltiples núcleos de CPU, los accesos concurrentes entre núcleos son posibles.
  1. Enmascaramiento de Interrupciones Las CPUs proporcionan la capacidad de enmascarar (deshabilitar) y desenmascarar (habilitar) interrupciones. Esto previene que los manejadores de interrupciones interrumpan la ruta de ejecución del kernel, evitando ciertas condiciones de carrera. Al enmascarar interrupciones, se previene la concurrencia entre interrupciones y procesos, y también entre procesos preemptados, ya que muchas operaciones del kernel dependen de interrupciones.

local_irq_disable(); // Enmascara interrupciones en la CPU local
// ... sección crítica ...
critical_section; // Código que accede a recursos compartidos
// ...
local_irq_enable(); // Habilita interrupciones en la CPU local


Las secciones críticas protegidas por enmascaramiento de interrupciones deben ser lo más cortas posible. Es importante notar que este método solo enmascara interrupciones en la CPU local.

  1. Operaciones Atómicas Las operaciones atómicas garantizan que las modificaciones a ciertos tipos de datos se realicen de forma exclusiva, indivisible. El kernel de Linux proporciona funciones para operaciones atómicas sobre enteros y bits.

2.1 Operaciones Atómicas sobre Enteros Se utilizan variables de tipo atomic_t, definidas como:


typedef struct {
    int counter;
} atomic_t;


Funciones clave:

  • atomic_set(v, i): Establece el valor de la variable atómica v a i.
  • atomic_read(v): Lee y devuelve el valor de la variable atómica v.
  • atomic_add(i, v): Suma i al valor de v.
  • atomic_sub(i, v): Resta i al valor de v.
  • atomic_inc(v): Incrementa v en 1.
  • atomic_dec(v): Decrementa v en 1.
  • atomic_add_return(i, v): Suma i a v y devuelve el nuevo valor.
  • atomic_sub_return(i, v): Resta i de v y devuelve el nuevo valor.
  • atomic_dec_and_test(v): Decrementa v en 1 y devuelve verdadero si el resultado es 0.
  • atomic_inc_and_test(v): Incrementa v en 1 y devuelve verdadero si el resultado es 0.
  • atomic_sub_and_test(i, v): Resta i de v y devuelve verdadero si el resultado es 0.
  • atomic_add_negative(i, v): Suma i a v y devuelve verdadero si el resultado es negativo. Ejemplo de uso para asegurar que un dispositivo solo sea abierto por un proceso:

static atomic_t dev_available = ATOMIC_INIT(1); // Inicializada a 1 (disponible)

static int device_open(struct inode *inode, struct file *filp) {
    if (!atomic_dec_and_test(&dev_available)) {
        // El dispositivo no está disponible
        atomic_inc(&dev_available); // Restaurar el contador
        return -EBUSY;
    }
    // Dispositivo abierto exitosamente
    return 0;
}

static int device_release(struct inode *inode, struct file *filp) {
    atomic_inc(&dev_available); // Marcar el dispositivo como disponible
    return 0;
}


2.2 Operaciones Atómicas sobre Bits Estas operaciones actúan sobre bits individuales dentro de un valor unsigned long.

  • set_bit(nr, addr): Establece el bit nr en la dirección addr a 1.
  • clear_bit(nr, addr): Establece el bit nr en la dirección addr a 0.
  • change_bit(nr, addr): Invierte el estado del bit nr en la dirección addr.
  • test_bit(nr, addr): Devuelve el valor del bit nr en la dirección addr.
  • test_and_set_bit(nr, addr): Establece el bit nr a 1 y devuelve su valor original.
  • test_and_clear_bit(nr, addr): Establece el bit nr a 0 y devuelve su valor original.
  • test_and_change_bit(nr, addr): Invierte el bit nr y devuelve su valor original.
  1. Spinlocks (Cerraduras de Giro) Un spinlock es un mecanismo de exclusión mutua que utiliza un bucle de espera (spinning) para adquirir el bloqueo. Cuando un hilo intenta adquirir un spinlock que ya está en posesión, el hilo se queda en un bucle, comprobando repetidamente el estado del bloqueo hasta que se libera. Esto es eficiente para secciones críticas muy cortas, ya que evita el costo de la conmutación de contexto asociada con el sueño.

3.1 API de Spinlocks

  • spinlock_t lock;: Define una variable de tipo spinlock.
  • spin_lock_init(&lock);: Inicializa el spinlock.
  • spin_lock(&lock);: Adquiere el spinlock. Si no está disponible, el hilo espera en un bucle.
  • spin_trylock(&lock);: Intenta adquirir el spinlock. Devuelve verdadero si se adquiere, falso en caso contrario, sin esperar.
  • spin_unlock(&lock);: Libera el spinlock.

3.2 Variantes de Spinlocks Para manejar interrupciones y procesos en el contexto de SMP, existen variantes:

  • spin_lock_irq(lock): Adquiere el spinlock y deshabilita las interrupciones locales.
  • spin_unlock_irq(lock): Libera el spinlock y habilita las interrupciones locales.
  • spin_lock_irqsave(lock, flags): Adquiere el spinlock, deshabilita las interrupciones locales y guarda el estado de las interrupciones en flags.
  • spin_unlock_irqrestore(lock, flags): Libera el spinlock y restaura el estado de las interrupciones.
  • spin_lock_bh(lock): Adquiere el spinlock y deshabilita el procesamiento de bottom halves (subrutinas diferidas).
  • spin_unlock_bh(lock): Libera el spinlock y habilita el procesamianto de bottom halves. Precauciones con Spinlocks:
  • No son adecuados para secciones críticas largas debido al consumo de CPU en espera.
  • Pueden causar deadlock si se intentan adquirir recursivamente.
  • No se deben llamar funciones que puedan causar bloqueo (como copy_from_user, kmalloc con GFP_KERNEL) mientras se mantiene un spinlock. Ejemplo de uso:

static DEFINE_SPINLOCK(my_lock); // Define e inicializa un spinlock

void critical_operation(void) {
    unsigned long flags;
    spin_lock_irqsave(&my_lock, flags); // Adquirir spinlock, deshabilitar interrupciones
    // ... sección crítica ...
    spin_unlock_irqrestore(&my_lock, flags); // Liberar spinlock, restaurar interrupciones
}


3.3 Read-Write Spinlocks (Spinlocks de Lectura-Escritura) rwlock_t permite que múltiples lectores accedan concurrentemente a un recurso, pero solo un escritor puede acceder a la vez, y no puede haber lectores cuando un escritor tiene el bloqueo.

  • read_lock(&rwlock) / read_unlock(&rwlock)
  • write_lock(&rwlock) / write_unlock(&rwlock)
  • Variantes _irqsave, _irq, _bh disponibles.

3.4 Seqlocks (Bloqueos de Secuencia) seqlock_t optimiza el acceso de lectura, permitiendo que los lectores continúen incluso si un escritor está activo. Sin embargo, los lectores deben verificar si la lectura fue consistente; si un escritor modificó los datos durante la lectura, el lector debe reintentar la lectura completa.

  • Escritor: write_seqlock() / write_sequnlock()
  • Lector: read_seqbegin() / read_seqretry()
  1. Semáforos Los semáforos (struct semaphore) son mecanismos clásicos para sincronización y exclusión mutua. Su valor puede ser 0, 1 o n.
  • sema_init(sem, val): Inicializa el semáforo.
  • down(sem): Adquiere el semáforo, puede causar sueño (no usar en contexto de interrupción).
  • down_interruptible(sem): Similar a down, pero el sueño puede ser interrumpido por señales.
  • down_trylock(sem): Intenta adquirir el semáforo sin dormir (apropiado para contexto de interrupción).
  • up(sem): Libera el semáforo. Los semáforos son útiles para proteger secciones críticas que pueden implicar bloqueo, pero su uso para exclusión mutua está siendo reemplazado por mutexes en kernels más recientes.
  1. Mutexes (Mutexes) Los mutexes (struct mutex) son la forma moderna y preferida para la exclusión mutua en el kernel de Linux. Están diseñados para proteger recursos compartidos entre procesos.
  • mutex_init(&mutex): Inicializa el mutex.
  • mutex_lock(&mutex): Adquiere el mutex, puede causar sueño.
  • mutex_lock_interruptible(&mutex): Adquiere el mutex, el sueño puede ser interrumpido.
  • mutex_trylock(&mutex): Intenta adquirir el mutex sin dormir.
  • mutex_unlock(&mutex): Libera el mutex. Diferencias clave entre Mutexes y Spinlocks:
  • Contexto de Ejecución: Los mutexes operan a nivel de proceso y pueden dormir, lo que permite la conmutación de contexto. Los spinlocks operan en el contexto actual y no pueden dormir.
  • Secciones Críticas: Los mutexes son adecuados para secciones críticas largas que pueden implicar operaciones de bloqueo (ej. copy_from_user). Los spinlocks son para secciones críticas cortas donde el bloqueo no es deseable.
  • Rendimiento: Los spinlocks son más rápidos para secciones críticas muy cortas debido a la ausencia de conmutación de contexto. Los mutexes tienen una sobrecarga mayor pero son más flexibles.
  • Uso en Interrupciones: Los spinlocks (con variantes apropiadas) se usan en manejadores de interrupciones. Los mutexes no deben usarse en el contexto de interrupción, excepto con mutex_trylock.
  1. Completions (Señales de Finalización) Las struct completion se utilizan para la sincronización donde una unidad de ejecución necesita esperar a que otra complete una tarea.
  • init_completion(&c): Inicializa una variable de completion.
  • wait_for_completion(&c): El hilo actual espera hasta que la completion sea señalada.
  • complete(c): Señala la completion, despertando a un hilo en espera.
  • complete_all(c): Señala la completion, despertando a todos los hilos en espera.
  1. Ejemplo: Controlador GlobalMem con Mutex El siguiente código muestra cómo integrar un mutex en un controlador de dispositivo de memoria global (globalmem) para proteger las operaciones de lectura, escritura y ioctl, especialmente porque estas operaciones involucran llamadas potencialmente bloqueantes como copy_to_user y copy_from_user.

#include <linux>
#include <linux>
#include <linux>
#include <linux>
#include <linux>
#include <linux>
#include <linux> // Incluir cabecera para mutex

#define MEM_CLEAR           0x1
#define GLOBALMEM_MAJOR     230
#define GLOBALMEM_SIZE      0x1000

static int globalmem_major = GLOBALMEM_MAJOR;
module_param(globalmem_major, int, S_IRUGO);

struct globalmem_dev {
    struct cdev cdev;
    unsigned char mem[GLOBALMEM_SIZE];
    struct mutex dev_mutex; // Mutex para proteger el acceso al dispositivo
};

struct globalmem_dev *globalmem_devp;

static int globalmem_open(struct inode *inode, struct file *filp) {
    filp->private_data = globalmem_devp;
    return 0;
}

static int globalmem_release(struct inode *inode, struct file *filp) {
    return 0;
}

static long globalmem_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
    struct globalmem_dev *dev = filp->private_data;

    switch (cmd) {
    case MEM_CLEAR:
        mutex_lock(&dev->dev_mutex); // Adquirir mutex antes de la operación
        memset(dev->mem, 0, GLOBALMEM_SIZE);
        mutex_unlock(&dev->dev_mutex); // Liberar mutex
        printk(KERN_INFO "globalmem cleared\n");
        break;
    default:
        return -EINVAL;
    }
    return 0;
}

static ssize_t globalmem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos) {
    unsigned long p = *ppos;
    unsigned long count = size;
    int ret = 0;
    struct globalmem_dev *dev = filp->private_data;

    if (p >= GLOBALMEM_SIZE) return 0;
    if (count > GLOBALMEM_SIZE - p) count = GLOBALMEM_SIZE - p;

    mutex_lock(&dev->dev_mutex); // Adquirir mutex
    if (copy_to_user(buf, dev->mem + p, count)) {
        ret = -EFAULT;
    } else {
        *ppos += count;
        ret = count;
        printk(KERN_INFO "read %lu bytes from %lu\n", count, p);
    }
    mutex_unlock(&dev->dev_mutex); // Liberar mutex
    return ret;
}

static ssize_t globalmem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos) {
    unsigned long p = *ppos;
    unsigned long count = size;
    int ret = 0;
    struct globalmem_dev *dev = filp->private_data;

    if (p >= GLOBALMEM_SIZE) return 0;
    if (count > GLOBALMEM_SIZE - p) count = GLOBALMEM_SIZE - p;

    mutex_lock(&dev->dev_mutex); // Adquirir mutex
    if (copy_from_user(dev->mem + p, buf, count)) {
        ret = -EFAULT;
    } else {
        *ppos += count;
        ret = count;
        printk(KERN_INFO "wrote %lu bytes to %lu\n", count, p);
    }
    mutex_unlock(&dev->dev_mutex); // Liberar mutex
    return ret;
}

static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig) {
    // Implementación de llseek sin necesidad de protección de concurrencia
    // ya que filp->f_pos se protege implícitamente.
    loff_t ret = 0;
    switch (orig) {
        case 0: // SEEK_SET
            if (offset < 0 || offset > GLOBALMEM_SIZE) return -EINVAL;
            filp->f_pos = offset;
            break;
        case 1: // SEEK_CUR
            if ((filp->f_pos + offset) < 0 || (filp->f_pos + offset) > GLOBALMEM_SIZE) return -EINVAL;
            filp->f_pos += offset;
            break;
        case 2: // SEEK_END
             if (offset < 0 || (GLOBALMEM_SIZE + offset) < 0 ) return -EINVAL;
             filp->f_pos = GLOBALMEM_SIZE + offset;
             break;
        default:
            return -EINVAL;
    }
    ret = filp->f_pos;
    return ret;
}

static const struct file_operations globalmem_fops = {
    .owner = THIS_MODULE,
    .llseek = globalmem_llseek,
    .read = globalmem_read,
    .write = globalmem_write,
    .unlocked_ioctl = globalmem_ioctl,
    .open = globalmem_open,
    .release = globalmem_release,
};

static void globalmem_setup_cdev(struct globalmem_dev *dev, int index) {
    int err;
    dev_t devno = MKDEV(globalmem_major, index);
    cdev_init(&dev->cdev, &globalmem_fops);
    dev->cdev.owner = THIS_MODULE;
    err = cdev_add(&dev->cdev, devno, 1);
    if (err) printk(KERN_NOTICE "Error %d adding globalmem%d", err, index);
}

static int __init globalmem_init(void) {
    int ret;
    dev_t devno = MKDEV(globalmem_major, 0);

    if (globalmem_major)
        ret = register_chrdev_region(devno, 1, "globalmem");
    else {
        ret = alloc_chrdev_region(&devno, 0, 1, "globalmem");
        globalmem_major = MAJOR(devno);
    }
    if (ret < 0) return ret;

    globalmem_devp = kzalloc(sizeof(struct globalmem_dev), GFP_KERNEL);
    if (!globalmem_devp) {
        ret = -ENOMEM;
        goto fail_malloc;
    }
    globalmem_setup_cdev(globalmem_devp, 0);
    mutex_init(&globalmem_devp->dev_mutex); // Inicializar el mutex del dispositivo
    return 0;

fail_malloc:
    unregister_chrdev_region(devno, 1);
    return ret;
}
module_init(globalmem_init);

static void __exit globalmem_exit(void) {
    cdev_del(&globalmem_devp->cdev);
    kfree(globalmem_devp);
    unregister_chrdev_region(MKDEV(globalmem_major, 0), 1);
}
module_exit(globalmem_exit);

MODULE_AUTHOR("Adaptado");
MODULE_LICENSE("GPL v2");
</linux></linux></linux></linux></linux></linux></linux>

En resumen, la concurrencia es un desafío fundamental en el desarrollo de kernels. Las técnicas como el enmascaramiento de interrupciones, las operaciones atómicas, los spinlocks, los semáforos, los mutexes y las completions proporcionan los mecanismos necesarios para gestionar el acceso a recursos compartidos de forma segura y eficiente.

KVERS = $(shell uname -r)
obj-m += globalmem_mutex.o

build: kernel_modules
kernel_modules:
	make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules
clean:
	make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean

Compilación y prueba:


make
sudo insmod globalmem_mutex.ko
sudo mknod /dev/globalmem_test c 230 0
echo "Test data" | sudo tee /dev/globalmem_test
sudo cat /dev/globalmem_test
sudo rm /dev/globalmem_test
sudo rmmod globalmem_mutex


Etiquetas: Linux Kernel Device Drivers Concurrency Control mutex Spinlock

Publicado el 6-17 16:20