Comme nous l’avons vu dans l’épisode 5, les sémaphores sont des grappes de jetons qui permettent de protéger l’accès à des sections de code critiques. Critiques dans le sens où : si deux processus ou deux processus légers (threads) exécutent la même section de code en même temps alors la valeur des variables devient indéterminée.
Le cas typique est du code qui lit ou écrit des variables dans un segment de mémoire partagée. Si deux processus accèdent à la même variable dans un segment de mémoire partagée, même si l’un ne fait que lire le contenu de la variable et l’autre ne fait qu’y écrire, l’accès à la variable doit être protégé par un sémaphore.
Un sémaphore est un jeton, qu’on décrémente en réalisant une opération P (via un appel système au noyau) et qu’on incrémente en faisant une opération V (via un autre appel système au noyau).
Si un processus fait une opération P, la valeur du jeton (qu’on fixe initialement) est décrémentée. Tant que cette valeur n’atteint pas 0, rien d’autre ne se passe. Si par contre l’opération de décrémentation donne 0, alors le processus appelant cet appel système est bloqué par le noyau en attente qu’un autre processus fasse une opération V pour rendre un jeton.
Ainsi, un sémaphore initialisé à la valeur 1 garantit qu’un seul processus à la fois sera dans une section de code critique donnée. Et un sémaphore initialisé à la valeur N garantit qu’au plus N processus pourront se retrouver simultanément dans une section de code critique.
Vous l’avez compris, les sémaphores sont un mécanisme très puissant et indispensable, mais un mécanisme dangereux car il peut y avoir une perte de jeton. Les deux cas dans lesquels les programmeurs doivent impérativement éviter de tomber sont :
- le cas où un programme « oublie » de rendre le jeton lorsqu’il quitte la section de code critique. Dans ce cas, tous les autres processus qui attendent la libération du jeton pour y rentrer sont bloqués ad vitam
- Et le cas qu’on appelle « Etreinte fatale » (deadlock en anglais) qui est celui où un processus A disposant du jeton 1 demande le jeton 2 (non disponible car détenu par B), et où un processus B disposant du jeton 2 demande le jeton 1 (non disponible car détenu par A). Chaque processus attend la libération du jeton de l’autre.
Les sémaphores sont très souvent initialisés à 1. Dans ce cas on les appelle des mutex (exclusion mutuelle). Cependant, par commodité, les appels système permettant de les manipuler les manipulent par grappe de plusieurs sémaphores.
Travaux pratiques
Nous allons écrire un programme en C qui va créer deux processus fils qui vont chacun tenter d’exécuter une section de code critique. Pour l’exercice, la section de code critique consistera juste à compter jusqu’à 10 en 10 secondes.
Chacun des processus devra demander le jeton du sémaphore pour pouvoir entrer en section de code critique, et restera bloqué sur cette demande tant que le processus qui a le jeton ne l’a pas rendu.
Bien sûr notre code contient tout ce qu’il faut pour créer et initialiser sémaphore ainsi que pour demander et rendre le jeton.
Voici le code source de notre programme prog.c :
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/wait.h>
union type_argument
{
int valeur;
struct semid_ds *tampon;
unsigned short *tab_valeur;
} argument;
int sem;
int init_semaphore()
{
if((sem=semget(ftok("/home",1),1,IPC_CREAT | 0644))==-1) return(-1); // On tente de créer une grappe de sémaphores. Ramène -1 si le sémaphore existe déjà
argument.valeur=1;
semctl(sem,0,SETVAL,argument); // On initialise le sémaphore 0 de la grappe de sémaphore à la valeur 1 (mutex)
}
int get_semaphore()
{
if((sem=semget(ftok("/home",1),1,0644))==-1) return(-1); // On tente d'accéder à la grappe de sémaphores, qui doit déjà exister, sinon ramène -1
}
void prendre_jeton()
{
struct sembuf s;
s.sem_num=0;
s.sem_op=-1;
s.sem_flg=0;
semop(sem,&s,1); // Demande le jeton du sémaphore 0 de la grappe de sémaphores sem
}
void rendre_jeton()
{
struct sembuf s;
s.sem_num=0;
s.sem_op=1;
s.sem_flg=0;
semop(sem,&s,1); // Rend le jeton du sémaphore 0 de la grappe de sémaphores sem
}
void section_critique()
{
// Section de code on va considérer, pour l'exercice, qu'un seul processus à la fois peut se trouver
int i;
for(i=1;i<=10;i++)
{
printf("%d\n",i);
sleep(1);
}
}
int main()
{
int status;
if(init_semaphore()==-1)
{
if(get_semaphore()==-1)
{
printf("Impossible d'obtenir le sémaphore\n");
exit(1);
}
}
if(fork()==0)
{
// code du processus fils 1
printf("Ici le processus fils 1. Je réclame le jeton\n");
prendre_jeton(); // On réclame le jeton
printf("Ici le processus fils 1. J'ai obtenu le jeton\n");
section_critique();
printf("Ici le processus fils 1. Je rends le jeton\n");
rendre_jeton();
exit(0);
}
if(fork()==0)
{
// code du processus fils 2
printf("Ici le processus fils 2. Je réclame le jeton\n");
prendre_jeton(); // On réclame le jeton
printf("Ici le processus fils 2. J'ai obtenu le jeton\n");
section_critique();
printf("Ici le processus fils 2. Je rends le jeton\n");
rendre_jeton();
exit(0);
}
wait(&status);
wait(&status);
}
Nous allons voir ci-dessous l’exécution, mais avant il faut signaler que le shell Linux propose la commande ipcs pour visualiser les IPC (InterProcessus Communication), à savoir les files de messages, les segments de mémoire partagée, et les sémaphores.

La commande ipcrm permet de supprimer des sémaphores créés.
Voici ce que donne le résultat, sachant qu’il est impossible de savoir si le premier processus à avoir le jeton sera le processus fils 1 ou fils 2, puisqu’ils le demandent en même temps. Comme on peut le constater, le deuxième processus à qui sera attribué le jeton devra attendre que le premier processus rende le jeton.
Exécution du TP :

La fonction ftok utilisée par notre programme prend en paramètre un nom de fichier existant et permet de générer un code unique utilisable pour la génération de ressources IPC, comme l’explique la page correspondante du manuel :

