En el anterior artículo de la serie, nos introducíamos en los contexto de proceso e interrupciones, los timers y el RTC en Linux. Hoy vamos a hablar sobre la concurrencia en el Kernel de Linux.
Con la llegada de los procesadores de múltiples núcleos, cualquier sistema doméstico puede hacer uso del Multi Procesamiento Simétrico, SMP a partir de ahora (de Symmetric Multi Processing en Inglés).
Gracias al SPM y al Kernel prremptible disponemos de escenarios donde se generan múltiples hilos de ejecución. Estos hilos operan simultáneamente con estructuras del Kernel compartidas, por eso, el acceso a las mismas debe ser serializado.
Se hace necesario proteger los recursos utilizados por el Kernel para prevenir inconsistencias producidas por el acceso concurrente a los mismos.
Spinlocks y Mutex
Como ya hemos visto en otros artículos de la serie de multiprocesamiento en C++, una sección de código que accede a recursos compartidos entre threads se conoce como sección crítica.
Los spinlocks y los mutex son los dos mecanismos básicos utilizados para proteger las secciones críticas en el Kernel.
Spinlocks
Los spinlocks se aseguran de que solo un único thread entra en una sección crítica a la vez. Cualquier otro thread que quiera entrar en la sección crítica espera hasta que el thread que está dentro salga.
Los spinlocks son busy-wait, algo que hay que tener en cuenta. Su uso es bastante sencillo:
#include
/* Inicia el spinlock */
spinlock_t klock = SPIN_LOCK_UNLOCKED;
/* Adquiere el spinlock */
spin_lock(&klock);
/* ... aquí va la sección crítica ... */
/* Libera el spinlock */
spin_unlock(&klock);
Mutex
Los mutex echan los threads que esperan para adquirir una sección crítica a dormir en lugar de hacer una operación busy-wait. Como es (casi) siempre una mala idea consumir ciclos de la CPU en una operación busy-wait, los mutex son más adecuados que los spinlocks a la hora de proteger secciones críticas, sobre todo cuando los tiempos de espera sean largos.
En términos de mutex, cualquier cosa que tarde más que dos cambios de contexto está considerado "largo" puesto que el mutex debe cambiar de contexto al mandar el thread a dormir y otra vez al despertarlo.
El uso básico de los mutex es como sigue:
#include
/* Definimos el mutex de forma estática */
static DEFINE_MUTEX(kmutex);
/* Adquiere el mutex */
mutex_lock(&kmutex);
/* ... Código de la sección crítica ... */
/* Liberamos el mutex */
mutex_unlock(&kmutex);
En muchos casos es muy sencillo saber cuando usar un spinlock y cuando un mutex:
Si la sección crítica necesita echarse a dormir, la única opción es usar un mutex. No se puede planificar, interrumpir o dormir un thread después de adquirir un spinlock.
Puesto que los mutex ponen el thread a dormir, no queda más remedio que usar spinlocks dentro del contexto de interrupciones, por ejemplo en un manejador de interrupciones.
Algunas consideraciones históricas
La interfaz de los mutex reemplazó a la antigua interfaz de semáforos en la versión 2.6.16 del Kernel. Sin embargo, la interfaz de los semáforos aún sigue estando presente. Su uso es también muy sencillo:
#include
/* Declaración estática del semáforo /
static DEFINE_SEMAPHORE(ksem);
/ Adquiere el semáforo /
down(&ksem);
/ ... Código de la sección crítica ... /
/ Libera el semáforo */
up(&ksem);
Al igual que los semáforos en la programación concurrente en espacio de usuario, los semáforos a nivel de Kernel pueden definir un número máximo de threads que pueden acceder a la sección crítica, pero más de uno es raramente utilizado. Los semáforos utilizan spinlocks así que puedes aplicarles las mismas reglas de uso que a los susodichos.
Conclusión
Hoy hemos hecho una breve introducción a la programación concurrente en el Kernel de Linux para ir tomando contacto con las primitivas de sincronización. En el siguiente artículo, veremos casos prácticos de protección de la sección crítica en diferentes plataformas y configuraciones del núcleo.
En Genbeta Dev | Programación a pecho descubierto (Linux Kernel), Introducción al multiprocesamiento en C++