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().
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()
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()
Code: Select all
sem_destroy()
- 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
Code: Select all
sem_wait()
Code: Select all
1 -> 0
Si un autre thread appelle ensuite :
Code: Select all
sem_wait()
Lorsque le premier thread appelle :
Code: Select all
sem_post()
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()
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()
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,
...
);
Code: Select all
mode_t mode
unsigned int value
Code: Select all
sem_t *sem = sem_open(
"/inside_sem",
O_CREAT,
0666,
1
);
7. Valeur de retour de sem_open()
En cas de succès :
Code: Select all
sem_open()
Code: Select all
sem_t *
Code: Select all
SEM_FAILED
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"
Code: Select all
/
Code: Select all
sem_open("/inside_sem", ...);
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
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
);
Code: Select all
1
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
);
Sinon :
Code: Select all
SEM_FAILED
13. Fermer un sémaphore nommé
sem_close()
Prototype :
Code: Select all
int sem_close(sem_t *sem);
Code: Select all
sem_close(sem);
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);
Code: Select all
sem_unlink("/inside_sem");
Le sémaphore peut rester actif tant qu'il existe encore des références ouvertes.
Ce comportement ressemble à :
Code: Select all
unlink()
15. fork() et sémaphores nommés
Après :
Code: Select all
fork()
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);
- elle est décrémentée ;
- l'appel retourne immédiatement.
- 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
Code: Select all
sem_wait(sem);
Code: Select all
1 -> 0
Code: Select all
sem_wait(sem);
Thread A :
Code: Select all
sem_post(sem);
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
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 la valeur vaut 0, l'appel échoue immédiatement.
L'erreur typique est :
Code: Select all
EAGAIN
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
);
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");
}
}
23. sem_post()
sem_post() incrémente la valeur du sémaphore.
Prototype :
Code: Select all
int sem_post(sem_t *sem);
Code: Select all
sem_post(sem);
24. Utilisation classique pour une section critique
Exemple conceptuel :
Code: Select all
sem_wait(sem);
/* section critique */
sem_post(sem);
Code: Select all
1
25. Sémaphore de comptage
Un sémaphore peut aussi représenter plusieurs ressources disponibles.
Exemple :
Code: Select all
3
Chaque :
Code: Select all
sem_wait()
Chaque :
Code: Select all
sem_post()
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
);
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()
Code: Select all
sem_post()
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
);
- 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
Code: Select all
sem_t sem;
if (sem_init(&sem, 0, 1) == -1) {
perror("sem_init");
}
- 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
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);
}
Code: Select all
pshared = 1
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);
Code: Select all
sem_destroy(&sem);
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
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()
Code: Select all
sem_post()
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.
Code: Select all
sem_wait(&empty);
mutex_lock();
ajouter_element();
mutex_unlock();
sem_post(&full);
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
...
- 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()
- 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()
- 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()
- la vérification de la valeur ;
- la décrémentation si possible ;
- sinon la mise en attente.
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);
}
Entre :
Code: Select all
sem_getvalue()
Code: Select all
sem_wait()
Il faut directement utiliser :
Code: Select all
sem_wait()
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
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()
Code: Select all
sem_open()
45. Erreurs fréquentes
Oublier sem_post()
Exemple :
Code: Select all
sem_wait(sem);
/* erreur */
return;
Les autres threads risquent alors de rester bloqués.
Détruire trop tôt
Ne pas appeler :
Code: Select all
sem_destroy()
Mauvais pshared
Pour un sémaphore utilisé entre processus :
Code: Select all
pshared
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_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);
Code: Select all
sem_destroy(sem);
munmap(sem, sizeof(sem_t));
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.
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.
