POSIX Semaphores

Ce forum est dédié à apprendre le développement de programmes user mode sur Linux

Moderator: Rick

Post Reply
Hydraxx
Site Admin
Posts: 114
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

POSIX Semaphores

Post by Hydraxx »

POSIX Semaphores

1. Introduction

Les sémaphores POSIX sont des mécanismes de synchronisation permettant à plusieurs threads ou plusieurs processus de coordonner l'accès à une ressource partagée.

Un sémaphore contient une valeur entière non négative.

Les opérations principales consistent à :
  • décrémenter cette valeur avec sem_wait() ;
  • l'incrémenter avec sem_post().
Quand la valeur est égale à 0, un thread ou un processus qui appelle sem_wait() doit attendre qu'une autre entité exécute sem_post().

POSIX définit deux grandes catégories :
  • les sémaphores nommés ;
  • les sémaphores non nommés.

2. Vue d'ensemble

Sémaphores nommés

Un sémaphore nommé possède un nom.

Plusieurs processus indépendants peuvent donc ouvrir le même sémaphore avec :

Code: Select all

sem_open()
Les principales fonctions sont :

Code: Select all

sem_open()
sem_close()
sem_unlink()

Sémaphores non nommés

Un sémaphore non nommé est stocké directement dans une zone mémoire.

Il est initialisé avec :

Code: Select all

sem_init()
et détruit avec :

Code: Select all

sem_destroy()
Il peut être utilisé :
  • entre plusieurs threads ;
  • entre plusieurs processus si la mémoire contenant le sémaphore est réellement partagée.

3. Fonctionnement général d'un sémaphore

Un sémaphore possède une valeur.

Exemple :

Code: Select all

1
Si un thread appelle :

Code: Select all

sem_wait()
la valeur devient :

Code: Select all

1 -> 0
Le thread peut continuer.

Si un autre thread appelle ensuite :

Code: Select all

sem_wait()
alors que la valeur vaut déjà 0, il est bloqué.

Lorsque le premier thread appelle :

Code: Select all

sem_post()
la valeur peut redevenir disponible et un thread en attente peut être réveillé.


4. Différence avec les sémaphores System V

Les sémaphores POSIX et System V remplissent globalement le même rôle, mais leur interface diffère.

System V

Les fonctions principales sont :

Code: Select all

semget()
semop()
semctl()
System V peut gérer des ensembles contenant plusieurs sémaphores.

Une opération semop() peut également agir sur plusieurs sémaphores avec une structure sembuf.


POSIX

POSIX manipule généralement un seul sémaphore à la fois.

Les opérations principales sont :

Code: Select all

sem_wait()
sem_post()
Cette interface est plus simple.


5. Les sémaphores nommés

Les sémaphores nommés permettent à plusieurs processus de partager un sémaphore grâce à son nom.

Ils utilisent principalement :
  • sem_open() ;
  • sem_close() ;
  • sem_unlink().

6. Création d'un sémaphore nommé

sem_open()

Prototype conceptuel :

Code: Select all

sem_t *sem_open(
    const char *name,
    int oflag,
    ...
);
Avec O_CREAT, deux paramètres supplémentaires sont utilisés :

Code: Select all

mode_t mode
unsigned int value
Exemple :

Code: Select all

sem_t *sem = sem_open(
    "/inside_sem",
    O_CREAT,
    0666,
    1
);
Le dernier argument définit la valeur initiale du sémaphore.


7. Valeur de retour de sem_open()

En cas de succès :

Code: Select all

sem_open()
retourne un pointeur vers un objet :

Code: Select all

sem_t *
En cas d'erreur, la fonction retourne :

Code: Select all

SEM_FAILED
Exemple :

Code: Select all

sem_t *sem = sem_open("/inside_sem", O_CREAT, 0666, 1);

if (sem == SEM_FAILED) {
    perror("sem_open");
}

8. Nom d'un sémaphore POSIX

Un sémaphore nommé possède un nom POSIX.

Exemple :

Code: Select all

"/inside_sem"
Sous Linux, ce nom commence normalement par :

Code: Select all

/
Plusieurs processus peuvent donc faire :

Code: Select all

sem_open("/inside_sem", ...);
et accéder au même sémaphore.


9. Flags de sem_open()

Les flags importants sont notamment :
  • O_CREAT ;
  • O_EXCL.

O_CREAT

Crée le sémaphore s'il n'existe pas.

Exemple :

Code: Select all

sem_open("/inside_sem", O_CREAT, 0666, 1);

O_EXCL

Utilisé avec O_CREAT.

Si le sémaphore existe déjà, sem_open() échoue.

Exemple :

Code: Select all

sem_open(
    "/inside_sem",
    O_CREAT | O_EXCL,
    0666,
    1
);

10. Permissions

Lors de la création d'un sémaphore nommé, le paramètre mode définit les permissions.

Exemple :

Code: Select all

0666
Ces permissions fonctionnent de manière similaire aux permissions de fichiers Unix.

Elles sont affectées par le umask.


11. Valeur initiale

Le dernier argument de sem_open() définit la valeur initiale du sémaphore.

Exemple :

Code: Select all

sem_open(
    "/inside_sem",
    O_CREAT,
    0666,
    1
);
La valeur initiale est ici :

Code: Select all

1
Cela permet un comportement similaire à un verrou.

Un seul thread ou processus peut passer à la fois.


12. Ouvrir un sémaphore existant

Pour ouvrir un sémaphore existant :

Code: Select all

sem_t *sem = sem_open(
    "/inside_sem",
    0
);
Si le sémaphore existe, un pointeur valide est retourné.

Sinon :

Code: Select all

SEM_FAILED

13. Fermer un sémaphore nommé

sem_close()

Prototype :

Code: Select all

int sem_close(sem_t *sem);
Exemple :

Code: Select all

sem_close(sem);
sem_close() ferme la référence du processus.

Le sémaphore peut continuer d'exister si son nom n'a pas été supprimé.


14. Supprimer un sémaphore nommé

sem_unlink()

Prototype :

Code: Select all

int sem_unlink(const char *name);
Exemple :

Code: Select all

sem_unlink("/inside_sem");
Cette fonction supprime le nom du sémaphore.

Le sémaphore peut rester actif tant qu'il existe encore des références ouvertes.

Ce comportement ressemble à :

Code: Select all

unlink()
pour les fichiers Unix.


15. fork() et sémaphores nommés

Après :

Code: Select all

fork()
un processus fils hérite des références ouvertes par son parent.

Le père et le fils peuvent donc utiliser le même sémaphore.

Exemple conceptuel :

Code: Select all

sem_t *sem = sem_open("/inside_sem", O_CREAT, 0666, 1);

pid_t pid = fork();

if (pid == 0) {
    sem_wait(sem);
}

16. Opérations sur un sémaphore

Les principales opérations sont :
  • sem_wait() ;
  • sem_trywait() ;
  • sem_timedwait() ;
  • sem_post() ;
  • sem_getvalue().

17. sem_wait()

sem_wait() tente de décrémenter la valeur du sémaphore.

Prototype :

Code: Select all

int sem_wait(sem_t *sem);
Si la valeur est supérieure à 0 :
  • elle est décrémentée ;
  • l'appel retourne immédiatement.
Si la valeur vaut 0 :
  • le thread est bloqué ;
  • il attend qu'un autre thread ou processus appelle sem_post().

18. Exemple avec sem_wait()

Sémaphore initial :

Code: Select all

1
Thread A :

Code: Select all

sem_wait(sem);
Valeur :

Code: Select all

1 -> 0
Thread B :

Code: Select all

sem_wait(sem);
Comme la valeur vaut 0, B attend.

Thread A :

Code: Select all

sem_post(sem);
B peut alors reprendre.


19. Interruption de sem_wait()

sem_wait() peut être interrompu par un signal.

Dans ce cas, l'appel peut échouer avec :

Code: Select all

errno == EINTR
Un programme robuste peut donc relancer l'appel.

Exemple :

Code: Select all

while (sem_wait(sem) == -1) {
    if (errno != EINTR) {
        perror("sem_wait");
        break;
    }
}

20. sem_trywait()

sem_trywait() est la version non bloquante de sem_wait().

Prototype :

Code: Select all

int sem_trywait(sem_t *sem);
Si le sémaphore est disponible, la fonction décrémente sa valeur.

Si la valeur vaut 0, l'appel échoue immédiatement.

L'erreur typique est :

Code: Select all

EAGAIN
Exemple :

Code: Select all

if (sem_trywait(sem) == -1) {
    if (errno == EAGAIN) {
        printf("Sémaphore indisponible\n");
    }
}

21. sem_timedwait()

sem_timedwait() fonctionne comme sem_wait(), mais avec une échéance.

Prototype conceptuel :

Code: Select all

int sem_timedwait(
    sem_t *sem,
    const struct timespec *abs_timeout
);
Si le sémaphore devient disponible avant l'échéance, l'opération réussit.

Sinon, la fonction échoue généralement avec :

Code: Select all

ETIMEDOUT

22. Exemple de timeout

Code: Select all

struct timespec ts;

clock_gettime(CLOCK_REALTIME, &ts);

ts.tv_sec += 5;

if (sem_timedwait(sem, &ts) == -1) {
    if (errno == ETIMEDOUT) {
        printf("Timeout\n");
    }
}
Le délai est exprimé comme une échéance absolue.


23. sem_post()

sem_post() incrémente la valeur du sémaphore.

Prototype :

Code: Select all

int sem_post(sem_t *sem);
Exemple :

Code: Select all

sem_post(sem);
Si des threads sont bloqués dans sem_wait(), l'un d'eux peut être réveillé.


24. Utilisation classique pour une section critique

Exemple conceptuel :

Code: Select all

sem_wait(sem);

/* section critique */

sem_post(sem);
Avec une valeur initiale de :

Code: Select all

1
le sémaphore agit comme un verrou binaire.


25. Sémaphore de comptage

Un sémaphore peut aussi représenter plusieurs ressources disponibles.

Exemple :

Code: Select all

3
Cela signifie que trois utilisateurs peuvent simultanément acquérir la ressource.

Chaque :

Code: Select all

sem_wait()
décrémente le compteur.

Chaque :

Code: Select all

sem_post()
l'incrémente.


26. sem_getvalue()

Cette fonction permet de récupérer la valeur courante du sémaphore.

Prototype :

Code: Select all

int sem_getvalue(
    sem_t *sem,
    int *sval
);
Exemple :

Code: Select all

int value;

if (sem_getvalue(sem, &value) == -1) {
    perror("sem_getvalue");
} else {
    printf("Valeur : %d\n", value);
}

27. Attention à sem_getvalue()

La valeur récupérée peut devenir obsolète immédiatement.

Dans un programme concurrent, un autre thread peut exécuter :

Code: Select all

sem_wait()
ou :

Code: Select all

sem_post()
juste après sem_getvalue().

Il ne faut donc pas utiliser cette valeur pour construire une logique de synchronisation fragile.

La synchronisation doit être basée sur les opérations atomiques du sémaphore elles-mêmes.


28. Sémaphores non nommés

Un sémaphore non nommé est stocké dans une zone mémoire fournie par le programme.

Il n'est pas créé dans un espace de noms global.

Les fonctions principales sont :

Code: Select all

sem_init()
sem_destroy()

29. sem_init()

Prototype :

Code: Select all

int sem_init(
    sem_t *sem,
    int pshared,
    unsigned int value
);
Les paramètres sont :
  • sem : adresse du sémaphore ;
  • pshared : indique le mode de partage ;
  • value : valeur initiale.

30. Sémaphore entre threads

Pour un sémaphore utilisé uniquement entre threads du même processus :

Code: Select all

pshared = 0
Exemple :

Code: Select all

sem_t sem;

if (sem_init(&sem, 0, 1) == -1) {
    perror("sem_init");
}
Le sémaphore peut être placé :
  • dans une variable globale ;
  • dans le heap ;
  • dans une structure partagée par les threads.

31. Sémaphore entre processus

Pour partager un sémaphore non nommé entre processus :

Code: Select all

pshared != 0
Mais cela ne suffit pas.

L'objet sem_t doit aussi se trouver dans une mémoire réellement partagée.

Exemples :
  • mmap() avec MAP_SHARED ;
  • mémoire partagée POSIX ;
  • mémoire partagée System V.

32. Exemple avec mmap()

Code: Select all

sem_t *sem = mmap(
    NULL,
    sizeof(sem_t),
    PROT_READ | PROT_WRITE,
    MAP_SHARED | MAP_ANONYMOUS,
    -1,
    0
);

if (sem == MAP_FAILED) {
    perror("mmap");
    exit(EXIT_FAILURE);
}

if (sem_init(sem, 1, 1) == -1) {
    perror("sem_init");
    exit(EXIT_FAILURE);
}
Ici :

Code: Select all

pshared = 1
car le sémaphore doit être partagé entre processus.


33. Pourquoi la mémoire doit être partagée

Après fork(), chaque processus possède normalement son propre espace d'adressage.

Même si les adresses virtuelles semblent identiques, les pages peuvent devenir privées avec le mécanisme copy-on-write.

Pour qu'un sémaphore non nommé fonctionne entre processus, tous les processus doivent donc accéder au même objet mémoire physique partagé.

C'est pour cela qu'on utilise :

Code: Select all

MAP_SHARED

34. sem_destroy()

Cette fonction détruit un sémaphore non nommé.

Prototype :

Code: Select all

int sem_destroy(sem_t *sem);
Exemple :

Code: Select all

sem_destroy(&sem);
Il ne faut pas détruire un sémaphore alors que des threads ou processus sont encore bloqués dessus.

Le comportement pourrait être indéfini.


35. Exemple threads avec sémaphore

Code: Select all

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>

sem_t sem;
long counter = 0;

void *worker(void *arg)
{
    for (int i = 0; i < 100000; i++) {

        sem_wait(&sem);

        counter++;

        sem_post(&sem);
    }

    return NULL;
}

int main(void)
{
    pthread_t t1;
    pthread_t t2;

    if (sem_init(&sem, 0, 1) == -1) {
        perror("sem_init");
        return EXIT_FAILURE;
    }

    pthread_create(&t1, NULL, worker, NULL);
    pthread_create(&t2, NULL, worker, NULL);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    printf("Counter = %ld\n", counter);

    sem_destroy(&sem);

    return EXIT_SUCCESS;
}

36. Exemple processus avec sémaphore partagé

Code: Select all

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <semaphore.h>

struct Shared {
    sem_t sem;
    int counter;
};

int main(void)
{
    struct Shared *shared = mmap(
        NULL,
        sizeof(struct Shared),
        PROT_READ | PROT_WRITE,
        MAP_SHARED | MAP_ANONYMOUS,
        -1,
        0
    );

    if (shared == MAP_FAILED) {
        perror("mmap");
        return EXIT_FAILURE;
    }

    shared->counter = 0;

    if (sem_init(&shared->sem, 1, 1) == -1) {
        perror("sem_init");
        return EXIT_FAILURE;
    }

    pid_t pid = fork();

    if (pid == -1) {
        perror("fork");
        return EXIT_FAILURE;
    }

    if (pid == 0) {

        sem_wait(&shared->sem);

        shared->counter++;

        sem_post(&shared->sem);

        _exit(EXIT_SUCCESS);
    }

    sem_wait(&shared->sem);

    shared->counter++;

    sem_post(&shared->sem);

    wait(NULL);

    printf("Counter = %d\n", shared->counter);

    sem_destroy(&shared->sem);
    munmap(shared, sizeof(struct Shared));

    return EXIT_SUCCESS;
}

37. Sémaphore binaire et mutex

Un sémaphore initialisé à :

Code: Select all

1
peut fonctionner de manière proche d'un mutex.

Cependant, un mutex et un sémaphore ne sont pas exactement équivalents.

Un mutex possède généralement une notion de propriétaire.

Le thread qui verrouille un mutex est normalement celui qui doit le déverrouiller.

Un sémaphore ne possède pas cette notion stricte de propriétaire.

Un thread peut faire :

Code: Select all

sem_wait()
et un autre thread peut faire :

Code: Select all

sem_post()
C'est utile pour certains modèles de synchronisation.


38. Sémaphores et producteur/consommateur

Les sémaphores sont particulièrement adaptés aux files producteur/consommateur.

On peut utiliser par exemple :
  • un sémaphore pour compter les cases libres ;
  • un sémaphore pour compter les cases occupées ;
  • un mutex pour protéger la structure de la file.
Exemple conceptuel :

Code: Select all

sem_wait(&empty);
mutex_lock();

ajouter_element();

mutex_unlock();
sem_post(&full);
Et côté consommateur :

Code: Select all

sem_wait(&full);
mutex_lock();

retirer_element();

mutex_unlock();
sem_post(&empty);

39. Différence avec un mutex

Mutex

Principalement utilisé pour protéger une section critique.

Il représente généralement :

Code: Select all

verrouillé / déverrouillé

Sémaphore

Peut compter plusieurs ressources.

Exemple :

Code: Select all

0
1
2
3
...
Il peut donc servir à :
  • limiter le nombre de threads simultanés ;
  • signaler qu'un événement est disponible ;
  • compter des ressources ;
  • synchroniser producteur et consommateur.

40. Comparaison POSIX et System V

POSIX

Interface :

Code: Select all

sem_open()
sem_init()
sem_wait()
sem_post()
sem_close()
sem_unlink()
sem_destroy()
Avantages :
  • interface simple ;
  • un objet sem_t représente directement un sémaphore ;
  • intégration facile avec pthreads ;
  • sémaphores non nommés faciles à placer dans une structure.

System V

Interface :

Code: Select all

semget()
semop()
semctl()
Avantages :
  • gestion d'ensembles de sémaphores ;
  • semop() peut appliquer plusieurs opérations en une seule fois ;
  • fonctionnalités historiques plus riches dans certains cas.

41. Atomicité

Les opérations importantes d'un sémaphore sont atomiques.

Cela signifie que deux threads ne peuvent pas modifier simultanément la valeur du sémaphore d'une manière incohérente.

Exemple :

Code: Select all

sem_wait()
effectue de manière atomique :
  • la vérification de la valeur ;
  • la décrémentation si possible ;
  • sinon la mise en attente.
Cette atomicité est essentielle pour éviter les races.


42. Pas besoin de tester la valeur avant sem_wait()

Il ne faut pas faire :

Code: Select all

sem_getvalue(sem, &value);

if (value > 0) {
    sem_wait(sem);
}
Cette logique contient une race condition.

Entre :

Code: Select all

sem_getvalue()
et :

Code: Select all

sem_wait()
un autre thread peut modifier le sémaphore.

Il faut directement utiliser :

Code: Select all

sem_wait()
ou :

Code: Select all

sem_trywait()

43. Limites des sémaphores

POSIX définit plusieurs limites liées aux sémaphores.

Une constante importante est :

Code: Select all

SEM_VALUE_MAX
Elle représente la valeur maximale pouvant être stockée dans un sémaphore.

La valeur réelle dépend de l'implémentation.


44. SEM_VALUE_MAX

On peut obtenir la valeur maximale avec les interfaces appropriées du système.

La valeur initiale passée à :

Code: Select all

sem_init()
ou :

Code: Select all

sem_open()
ne doit pas dépasser cette limite.


45. Erreurs fréquentes

Oublier sem_post()

Exemple :

Code: Select all

sem_wait(sem);

/* erreur */

return;
Le sémaphore peut rester définitivement acquis.

Les autres threads risquent alors de rester bloqués.


Détruire trop tôt

Ne pas appeler :

Code: Select all

sem_destroy()
pendant qu'un thread attend encore.


Mauvais pshared

Pour un sémaphore utilisé entre processus :

Code: Select all

pshared
doit être non nul.

Mais la mémoire doit également être partagée.


Oublier sem_unlink()

Un sémaphore nommé peut continuer d'exister après la fin du programme si son nom n'est jamais supprimé.


46. Ordre de nettoyage d'un sémaphore nommé

Un schéma courant est :

Code: Select all

sem_close(sem);
sem_unlink("/inside_sem");
sem_close() ferme la référence locale.

sem_unlink() supprime le nom.

La suppression du nom peut aussi être effectuée plus tôt selon le modèle de conception souhaité.


47. Ordre de nettoyage d'un sémaphore non nommé

Pour un sémaphore local :

Code: Select all

sem_destroy(&sem);
Pour un sémaphore dans une zone mmap() :

Code: Select all

sem_destroy(sem);
munmap(sem, sizeof(sem_t));
Tous les utilisateurs doivent avoir terminé avant cette destruction.


48. Sémaphores POSIX et performances

Les sémaphores POSIX sont généralement efficaces.

Lorsque le sémaphore est disponible, certaines opérations peuvent être réalisées sans blocage coûteux.

Lorsque le thread doit attendre, le noyau peut l'endormir au lieu de faire une attente active.

Cela évite de consommer inutilement du CPU.


49. Sémaphores POSIX ou spinlock

Un sémaphore peut bloquer le thread.

Un spinlock effectue une attente active.

Un sémaphore est donc généralement préférable lorsque l'attente peut être longue.

Un spinlock est utile surtout lorsque :
  • l'attente est extrêmement courte ;
  • le contexte interdit de dormir ;
  • on travaille dans certaines parties du noyau.
En espace utilisateur, un sémaphore est généralement plus approprié pour une attente potentiellement longue.


50. Sémaphores POSIX ou condition variable

Une variable de condition sert à attendre qu'une condition liée à un état partagé devienne vraie.

Elle est normalement utilisée avec un mutex.

Un sémaphore possède directement un compteur.

Avec un sémaphore, une notification peut être mémorisée par l'incrémentation du compteur.

Avec une condition variable, un signal sans thread en attente peut être perdu si le programme ne mémorise pas la condition dans une variable partagée.


51. Points importants à retenir
  • POSIX fournit des sémaphores nommés et non nommés.
  • Les sémaphores nommés utilisent sem_open().
  • Les sémaphores non nommés utilisent sem_init().
  • sem_wait() décrémente le compteur ou bloque.
  • sem_trywait() n'attend jamais.
  • sem_timedwait() permet une attente limitée.
  • sem_post() incrémente le compteur.
  • sem_getvalue() permet d'observer la valeur courante, mais cette valeur peut devenir immédiatement obsolète.
  • sem_close() ferme un sémaphore nommé.
  • sem_unlink() supprime son nom.
  • sem_destroy() détruit un sémaphore non nommé.
  • pshared = 0 signifie partage entre threads du même processus.
  • pshared != 0 permet un partage entre processus si sem_t se trouve dans une mémoire partagée.
  • Un sémaphore initialisé à 1 peut servir de verrou binaire.
  • Un sémaphore initialisé à une valeur supérieure permet de compter plusieurs ressources.
  • Les opérations sem_wait() et sem_post() sont atomiques.

52. Résumé

Les sémaphores POSIX sont des primitives de synchronisation fondées sur un compteur entier non négatif.

La fonction sem_wait() tente de décrémenter ce compteur. Si la valeur est nulle, le thread est bloqué jusqu'à ce qu'une autre entité exécute sem_post().

POSIX fournit deux catégories de sémaphores.

Les sémaphores nommés sont accessibles grâce à un nom et sont manipulés avec sem_open(), sem_close() et sem_unlink().

Les sémaphores non nommés résident directement dans la mémoire et sont manipulés avec sem_init() et sem_destroy().

Un sémaphore non nommé peut être partagé entre threads ou entre processus. Dans ce dernier cas, il doit être situé dans une mémoire partagée et initialisé avec pshared différent de 0.

Les fonctions sem_trywait() et sem_timedwait() permettent d'éviter un blocage indéfini.

sem_getvalue() permet d'inspecter la valeur, mais cette information ne doit pas être utilisée pour remplacer les opérations atomiques de synchronisation.

Comparée à l'interface System V, l'interface POSIX est plus simple et particulièrement adaptée aux applications multithreadées et aux scénarios producteur/consommateur.

Who is online

Users browsing this forum: No registered users and 0 guests