← Concurrencia y Sincronización Bloque 5 · Semáforos · Tema 21 de 47

Tema 21

Semáforos POSIX

POSIX proporciona dos tipos de semáforos: los anónimos (sin nombre, compartidos entre hilos de un proceso) y los con nombre (identificados por un nombre, compartidos entre procesos). En este tema verás la API completa (sem_init, sem_wait, sem_post, sem_destroy, sem_open, sem_close, sem_unlink) con ejemplos que se ejecutan en Linux.

1. Los dos tipos de semáforos POSIX

POSIX define dos tipos de semáforos:

TipoCompartido entreCreaciónAPI
Anónimo (unnamed)Hilos de un mismo proceso (o procesos que comparten memoria).sem_initsem_wait, sem_post, sem_destroy
Con nombre (named)Cualquier proceso del sistema.sem_opensem_wait, sem_post, sem_close, sem_unlink

Los semáforos anónimos se usan para sincronizar hilos dentro de un proceso. Los semáforos con nombre se usan para sincronizar procesos distintos (comunicación entre procesos).

2. Semáforos anónimos

Un semáforo anónimo se declara como una variable de tipo sem_t y se inicializa con sem_init. Se destruye con sem_destroy cuando ya no se usa.

Los semáforos anónimos se almacenan en la memoria del proceso (en el stack o en el heap). Son compartidos entre los hilos del proceso porque comparten el espacio de direcciones.

3. La API de semáforos anónimos

FunciónDescripciónRetorno
sem_init(sem, pshared, value)Inicializa el semáforo con valor inicial. pshared=0 para hilos, pshared=1 para procesos.0 si OK, -1 si error.
sem_destroy(sem)Destruye el semáforo.0 si OK, -1 si error.
sem_wait(sem)Decrementa. Bloquea si es 0.0 si OK, -1 si error.
sem_post(sem)Incrementa. Despierta a un hilo bloqueado.0 si OK, -1 si error.
sem_trywait(sem)Intenta decrementar sin bloquear. Retorna EAGAIN si es 0.0 si OK, -1 si error.
sem_timedwait(sem, abstime)Intenta decrementar con timeout.0 si OK, -1 si error/timeout.
sem_getvalue(sem, *val)Obtiene el valor actual del semáforo.0 si OK, -1 si error.

4. Inicialización y destrucción

inicialización
#include <semaphore.h>

sem_t sem;

/* Inicializar con valor 3 para hilos del mismo proceso. */
sem_init(&sem, 0, 3);

/* ... usar el semáforo ... */

/* Destruir cuando ya no se use. */
sem_destroy(&sem);

El parámetro pshared indica si el semáforo se comparte entre procesos (pshared=1) o solo entre hilos (pshared=0). Para pshared=1, el semáforo debe estar en memoria compartida (shm).

El semáforo debe destruirse cuando ya no se usa. Destruir un semáforo con hilos bloqueados es comportamiento indefinido.

5. wait y post

wait y post
/* Consumir un permiso. */
sem_wait(&sem);  /* decrementa; bloquea si es 0 */

/* ... usar el recurso ... */

/* Liberar un permiso. */
sem_post(&sem);  /* incrementa; despierta a un hilo bloqueado */

sem_wait es atómica: decrementa el contador y, si es 0, bloquea el hilo. sem_post es atómica: incrementa el contador y despierta a un hilo bloqueado (si hay).

En Linux, sem_wait y sem_post se implementan con futexes (como los mutexes), por lo que son eficientes cuando no hay contención.

6. trywait y timedwait

sem_trywait intenta decrementar sin bloquear:

trywait
int ret = sem_trywait(&sem);
if (ret == 0) {
    /* Se adquirió un permiso. */
    sem_post(&sem);
} else if (errno == EAGAIN) {
    /* No hay permisos disponibles. */
    printf("No hay permisos\n");
}

sem_timedwait intenta decrementar con un timeout (tiempo absoluto):

timedwait
#include <time.h>

struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += 5;  /* timeout de 5 segundos */

int ret = sem_timedwait(&sem, &ts);
if (ret == 0) {
    /* Se adquirió un permiso. */
    sem_post(&sem);
} else if (errno == ETIMEDOUT) {
    /* Timeout. */
    printf("Timeout\n");
}

7. Semáforos con nombre

Los semáforos con nombre se identifican por un nombre (una cadena que empieza con /). Se crean con sem_open y se destruyen con sem_unlink:

FunciónDescripción
sem_open(name, oflag, mode, value)Crea o abre un semáforo con nombre.
sem_close(sem)Cierra el semáforo (libera recursos del proceso).
sem_unlink(name)Elimina el semáforo del sistema.
sem_wait(sem)Decrementa (igual que anónimo).
sem_post(sem)Incrementa (igual que anónimo).

Los semáforos con nombre se almacenan en el sistema de archivos (típicamente en /dev/shm/). Son compartidos entre cualquier proceso que conozca el nombre.

8. La API de semáforos con nombre

sem_open
#include <fcntl.h>    /* O_CREAT, O_EXCL */
#include <sys/stat.h> /* mode constants */
#include <semaphore.h>

/* Crear o abrir un semáforo con nombre. */
sem_t *sem = sem_open("/mi_semaforo", O_CREAT, 0644, 3);
if (sem == SEM_FAILED) {
    perror("sem_open");
    return 1;
}

/* Usar el semáforo. */
sem_wait(sem);
/* ... */
sem_post(sem);

/* Cerrar y eliminar. */
sem_close(sem);
sem_unlink("/mi_semaforo");

El nombre debe empezar con /. El flag O_CREAT crea el semáforo si no existe. El flag O_EXCL falla si ya existe.

9. Ejemplo: semáforo anónimo

sem_anonimo.c
#define _POSIX_C_SOURCE 200809L
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define NUM_HILOS 5
#define MAX_CONCURRENTES 2

sem_t sem;

void *trabajar(void *arg) {
    int id = *(int *)arg;
    printf("Hilo %d: esperando...\n", id);
    sem_wait(&sem);
    printf("Hilo %d: entrando\n", id);
    sleep(1);
    printf("Hilo %d: saliendo\n", id);
    sem_post(&sem);
    return NULL;
}

int main(void) {
    pthread_t hilos[NUM_HILOS];
    int ids[NUM_HILOS];
    sem_init(&sem, 0, MAX_CONCURRENTES);

    for (int i = 0; i < NUM_HILOS; i++) {
        ids[i] = i;
        pthread_create(&hilos[i], NULL, trabajar, &ids[i]);
    }
    for (int i = 0; i < NUM_HILOS; i++) {
        pthread_join(hilos[i], NULL);
    }
    sem_destroy(&sem);
    return 0;
}
terminal · compilar y ejecutar
$ gcc -std=c17 -Wall -Wextra -pthread -o sem_anonimo sem_anonimo.c
$ ./sem_anonimo
Hilo 0: esperando...
Hilo 0: entrando
Hilo 1: esperando...
Hilo 1: entrando
Hilo 2: esperando...
Hilo 3: esperando...
Hilo 4: esperando...
Hilo 0: saliendo
Hilo 2: entrando
Hilo 1: saliendo
Hilo 3: entrando
Hilo 2: saliendo
Hilo 4: entrando
Hilo 3: saliendo
Hilo 4: saliendo

10. Ejemplo: semáforo con nombre

sem_nombre.c (productor)
#define _POSIX_C_SOURCE 200809L
#include <fcntl.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void) {
    sem_t *sem = sem_open("/mi_sem", O_CREAT, 0644, 1);
    if (sem == SEM_FAILED) {
        perror("sem_open");
        return 1;
    }
    printf("Productor: adquiriendo semáforo...\n");
    sem_wait(sem);
    printf("Productor: en sección crítica\n");
    sleep(2);
    printf("Productor: liberando semáforo\n");
    sem_post(sem);
    sem_close(sem);
    sem_unlink("/mi_sem");
    return 0;
}

Se puede ejecutar el productor en una terminal y el consumidor en otra. El semáforo con nombre coordina los dos procesos.

11. Observar en Linux

terminal · observar semáforos
# Ver los semáforos con nombre en /dev/shm
$ ls -la /dev/shm/sem.*
-rw------- 1 user user 32 Oct 18 10:00 /dev/shm/sem.mi_sem

# Ver el valor de un semáforo (con sem_getvalue)
$ cat /proc/PID/fdinfo/FD

Los semáforos con nombre se almacenan en /dev/shm/ (memoria compartida). Se pueden ver con ls /dev/shm/sem.*.

12. Simulador interactivo

Este simulador muestra cómo funciona un semáforo anónimo con varios hilos:

Simulador · semáforo POSIX anónimo

HiloAcciónValor del semáforo

13. Resumen

14. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Qué es un semáforo anónimo?

Un semáforo sin nombre, compartido entre hilos del mismo proceso, creado con sem_init.

2. ¿Qué es un semáforo con nombre?

Un semáforo identificado por un nombre, compartido entre procesos, creado con sem_open.

3. ¿Para qué se usa pshared en sem_init?

Para indicar si el semáforo se comparte entre procesos (pshared=1) o solo entre hilos (pshared=0).

4. ¿Qué hace sem_trywait?

Intenta decrementar sin bloquear. Retorna EAGAIN si no hay permisos.

5. ¿Qué hace sem_timedwait?

Intenta decrementar con un timeout (tiempo absoluto).

6. ¿Dónde se almacenan los semáforos con nombre?

En /dev/shm/ (memoria compartida).

7. ¿Qué flags se usan con sem_open?

O_CREAT (crear si no existe) y O_EXCL (fallar si ya existe).

8. ¿Cómo se elimina un semáforo con nombre?

Con sem_unlink.

9. ¿Qué syscall usa sem_wait internamente?

futex (FUTEX_WAIT).

10. ¿Por qué los semáforos POSIX son eficientes?

Porque se implementan con futexes: sin contención, no entran al núcleo.

Para practicar

  1. Compila y ejecuta sem_anonimo.c.
  2. Crea un semáforo con nombre y úsalo entre dos procesos.
  3. Usa sem_trywait en un bucle y observa el comportamiento.
  4. Usa sem_timedwait con un timeout de 1 segundo.
  5. Observa los archivos en /dev/shm/ durante la ejecución.
  6. Investiga cómo se implementan los semáforos POSIX en glibc.