Ir al contenido

Mecanismos de concurrencia de UNIX y del kernel Linux

🎯 Al terminar podrás

  1. Clasificar los mecanismos de UNIX en «pasan datos» (pipes, mensajes, memoria compartida) y «disparan acciones» (semáforos, señales).
  2. Predecir qué hace semop según el signo de sem_op, y por qué las señales no se encolan.
  3. Elegir entre atómicas, spinlock (y su variante), semáforo del kernel y RCU para un escenario, y justificarlo.
  4. Explicar para qué sirve una barrera de memoria con un ejemplo de reordenamiento.

Antes necesitas: Atómicas y spinlocks · Lectores–escritores

  • Debate elige una alternativa para el escenario y defiéndela
  • Feynman explícalo en voz alta sin mirar y compara con la lista

Pipes, mensajes y memoria compartida sirven para comunicar datos. Semáforos y señales sirven para disparar acciones en otros procesos.

MecanismoCómo funcionaBloqueo / exclusión mutua
Pipebuffer circular FIFO de tamaño fijo: productor–consumidor entre dos procesos. Sin nombre: solo entre procesos emparentados (fork). Con nombre (FIFO): cualquier procesoescribir con el buffer lleno bloquea; leer más de lo que hay bloquea. El SO garantiza la exclusión mutua
Mensajesbloques de bytes con tipo, enviados con msgsnd y recibidos con msgrcv. Cada proceso tiene una cola, como un buzónenviar a una cola llena bloquea; leer de una vacía bloquea. Si se pide un tipo que no está, no bloquea. Se puede recibir en orden FIFO o por tipo
Memoria compartidaun bloque de memoria virtual común; se lee y escribe con las instrucciones normales. Es lo más rápidopermisos de solo lectura o lectura/escritura, pero la exclusión mutua la ponen los procesos (con semáforos, por ejemplo)
Semáforos (System V)generalizan semWait/semSignal: vienen en conjuntos, semop aplica una lista de operaciones de forma atómica, y los incrementos pueden ser mayores que 1se ve abajo
Señalesaviso de un evento asíncrono, como una interrupción por software sin prioridades. Cada señal es un bit en la tabla de procesos, así que no se encolan: dos SIGINT seguidas cuentan como unase procesan al despertar el proceso o al volver de una syscall: acción por defecto, handler o ignorar

Las operaciones de semop, según el valor de sem_op:

sem_opQué hace el kernel
> 0suma ese valor y despierta a todos los que esperan que el semáforo aumente
= 0si el valor es 0, sigue con la siguiente operación; si no, el proceso espera a que valga 0
< 0 y |sem_op| ≤ valorresta; si queda en 0, despierta a los que esperaban el 0
< 0 y |sem_op| > valorel proceso se suspende hasta que el valor aumente

Cada semáforo guarda:

  • su valor;
  • el PID del último proceso que lo operó;
  • cuántos procesos esperan que aumente;
  • cuántos esperan que valga 0.

Señales de SVR4 que conviene conocer (tabla 6.1):

SeñalSignificado
SIGHUP (1)se cortó la terminal
SIGINT (2)interrupción (Ctrl+C)
SIGQUIT (3)salir con core dump
SIGILL (4)instrucción ilegal
SIGFPE (8)error de punto flotante
SIGKILL (9)matar: no se puede atrapar
SIGSEGV (11)acceso fuera del espacio de direcciones: lo que vimos en memcpy2
SIGPIPE (13)escribir en un pipe sin lectores
SIGALRM (14)alarma
SIGTERM (15)terminar
SIGCHLD (18)un hijo terminó

Más detalle en syscalls y señales.

Enteras, sobre un tipo especial atomic_t:

FunciónQué hace
ATOMIC_INIT(i)inicializa a i al declarar
atomic_read, atomic_setleer y asignar
atomic_add, atomic_sub, atomic_inc, atomic_decsumar y restar
atomic_sub_and_test, atomic_dec_and_test, atomic_inc_and_testoperar y devolver 1 si el resultado es 0 (sirve para contadores de referencias: «¿fui el último en soltarlo?»)
atomic_add_negativesumar y devolver 1 si queda negativo (se usa para implementar semáforos)

¿Por qué un tipo aparte? Según Love:

  1. Las operaciones atómicas nunca se usan, por error, sobre variables desprotegidas.
  2. Esas variables no se pueden tocar con operaciones no atómicas.
  3. El compilador no optimiza mal los accesos.
  4. Esconde las diferencias entre arquitecturas.

De bits (sobre cualquier dirección): set_bit, clear_bit, change_bit, test_and_set_bit, test_and_clear_bit, test_and_change_bit y test_bit. test_and_set_bit es el TAS del parcial (atómicas).

Es la técnica más común del kernel. Quien no consigue el lock gira (busy waiting), así que conviene cuando la espera es muy corta, del orden de menos de dos cambios de contexto.

VarianteÚsala cuando…
spin_lock (plain)la sección crítica no la ejecutan handlers de interrupción, o las interrupciones ya están deshabilitadas
spin_lock_irqlas interrupciones están siempre habilitadas: las deshabilita en la CPU local
spin_lock_irqsaveno sabes si están habilitadas: guarda el estado y lo restaura al soltar
spin_lock_bhla sección comparte datos con una bottom half (la parte diferida de una interrupción): la deshabilita
spin_trylock, spin_is_lockedintentar sin girar, o consultar

En un uniprocesador el spinlock casi desaparece:

  • sin expropiación en el kernel, se elimina al compilar;
  • con expropiación, queda solo como «deshabilitar/habilitar interrupciones».

En un multiprocesador sí se compila el giro sobre la variable. El mismo código sirve para los dos casos.

Spinlock de lectores–escritores: un contador de lectores de 24 bits más un flag de desbloqueo.

ContadorFlagSignificado
01libre
00tomado por un escritor
n > 00tomado por n lectores
n > 01no válido

Favorece a los lectores: mientras haya uno, un escritor no puede entrar, y entran lectores nuevos aunque haya un escritor esperando. Es la «solución 1» de lectores–escritores, con riesgo de starvation del escritor.

Son internos al kernel: los programas de usuario no los pueden llamar, y son más eficientes que los de usuario. Hay tres tipos:

  • Binarios, que Linux llama MUTEX: init_MUTEX (valor 1) e init_MUTEX_LOCKED (valor 0).
  • De conteo: sema_init(sem, count).
  • De lectores–escritores: init_rwsem, down_read/up_read, down_write/up_write. Funcionan como semáforo de conteo para los lectores y como mutex para los escritores, y siempre duermen sin poder ser interrumpidos.

Las operaciones son down (= semWait) y up (= semSignal), con tres versiones de down:

FunciónComportamiento
downbloquea en sueño no interrumpible (estado D, como la E/S de disco)
down_interruptiblesueño interrumpible: si llega una señal, abandona y retorna -EINTR (útil en drivers)
down_trylockno bloquea: si no está disponible, retorna distinto de 0 (el sem_trywait del barbero)

El compilador y el procesador pueden reordenar lecturas y escrituras independientes. Por ejemplo, a = 1; b = 1; puede escribir b antes que a; en cambio, a = 1; b = a; no se reordena, porque hay una dependencia. Es justo lo que rompía Peterson en x86.

BarreraImpide reordenar…
rmb()lecturas a través de la barrera
wmb()escrituras a través de la barrera
mb()ambas
barrier()solo al compilador (el procesador sí puede reordenar). En x86, las escrituras no se reordenan entre sí
smp_rmb(), smp_wmb(), smp_mb()en multiprocesador equivalen a la barrera completa; en uniprocesador, solo a barrier()

Las barreras actúan sobre instrucciones de máquina: a = b es una lectura de b y una escritura de a.

Se integró al kernel en 2002 y se usa en redes, memoria y VFS. Los lectores no toman ningún lock.

  1. El recurso compartido se accede a través de un puntero.
  2. Lectores: rcu_read_lock(), luego p = rcu_dereference(ptr), usar p, y rcu_read_unlock().
  3. Escritor: copia el recurso, modifica la copia y publica el puntero nuevo con rcu_assign_pointer. Actualizar un puntero es atómico, así que cada lector ve la versión vieja o la nueva, nunca una mezcla.
  4. La versión vieja solo se libera cuando ningún lector la puede estar usando. Se espera con synchronize_rcu() (bloqueante) o se registra un callback con call_rcu().

RCU es ideal cuando hay muchas lecturas y pocas escrituras. Su API central son 6 funciones: rcu_read_lock, rcu_read_unlock, call_rcu, synchronize_rcu, rcu_assign_pointer y rcu_dereference.

Pregunta de análisis: ¿spinlock o semáforo en el kernel?
  • Spinlock: la sección crítica es muy corta, el código no puede dormir (handlers de interrupción, bottom halves), y hay varios procesadores. Usa spin_lock_irqsave si también lo toca un handler.
  • Semáforo / mutex del kernel: la espera puede ser larga (E/S, copiar desde el espacio de usuario) y el código corre en contexto de proceso, así que puede dormir. Usa down_interruptible si conviene poder cancelar con una señal.
  • RCU: estructuras que se leen todo el tiempo y casi nunca se modifican (tablas de rutas, listas de módulos).
  • Atómicas: contadores simples y flags.

Nunca se duerme con un spinlock tomado: los demás procesadores girarían todo ese tiempo, y en un uniprocesador sería un deadlock.

Autoevaluación

0/8 correctas · 0 respondidas
1. ¿Qué mecanismo de UNIX es el más rápido para compartir datos?
2. ¿Quién garantiza la exclusión mutua en la memoria compartida de UNIX?
3. Dos señales SIGUSR1 seguidas, antes de que el proceso las atienda…
4. En semop, sem_op = 0 significa…
5. ¿Qué spinlock usas si no sabes si las interrupciones están habilitadas?
6. down_interruptible, si llega una señal…
7. En RCU, los lectores…
8. barrier() impide reordenar…

📈 ¿Qué tan bien lo dominas?

Elige pensando en lo que hiciste, no en lo que leíste. Para marcar 3 tienes que haber resuelto un ejercicio nuevo sin pistas y haberlo justificado. Si solo lo leíste y sientes que «ya entendí», todavía es un 1.

Fuente ·Stallings 9e §6.7 (UNIX Concurrency Mechanisms, tabla 6.1) · Stallings 9e §6.8 (Linux Kernel Concurrency Mechanisms, tablas 6.2–6.5, RCU)