Mise en oeuvre: signaux, gestion du temps et multi-activités

Transcription

Mise en oeuvre: signaux, gestion du temps et multi-activités
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
Chapitre 3
Mise en œuvre :
signaux, gestion du temps
et multi-activités
77
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
GENERALITES SUR LES SIGNAUX
Un signal est une information de contrôle (requête) transmise à un processus par un
autre processus ou par le matériel durant l'exécution.
Dans la gestion classique des signaux non temps réel, la nature de cette information
est très simple : une valeur entière positive constitue le signal. Par contre, le
mécanisme de transmission est très sophistiqué. En effet, un signal peut être
dynamiquement associé à une fonction d'un programme (que l'on appelle
généralement, le gestionnaire du signal ou fonction associée ou encore fonction de
déroutement). Dans ce cas, lorsque le signal est pris en compte par le processus
destinataire, l'exécution du traitement en cours est alors suspendue et le contrôle
est donné à la fonction de déroutement. Si la fonction effectue un retour, le
contrôle est redonnée au traitement qui avait été interrompu par la réception du
signal.
Pour les signaux temps réels, des données supplémentaires sont transmises d’un
processus expéditeur à un processus destinataires. De plus, toutes les occurrences
des signaux seront prises en compte.
Nous étudierons la gestion classique puis la gestion des signaux temps réel.
78
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
GENERALITES SUR LES SIGNAUX
Selon la manière dont le signal est émis, on distingue l'interruption matérielle
(signal émis par le matériel – un périphérique interne ou externe) de l'interruption
logicielle (signal émis par un autre processus).
Un signal peut être ignoré. Par défaut, s'il n'est ni ignoré, ni associé explicitement à
une fonction, la réception du signal provoque la terminaison du processus
destinataire (exécution de l'appel système exit()). L'envoi d'un signal peut par
exemple être provoqué par l'utilisateur au moyen de caractères spéciaux tapés
depuis le clavier du terminal. Il est important de distinguer, dans un environnement
multitâches, trois phases dans la production et le traitement d'un signal :
• l'envoi d'un signal (production d'un signal), qui est l'initialisation d'un signal
associé à un identificateur de processus destinataire,
• sa notification, qui est le positionnement dans le processus destinataire d'un
identificateur de réception,
• sa délivrance qui est la prise en compte du signal par le processus destinataire et
le lancement de la fonction associée (appel d'une fonction de déroutement,
terminaison anormale etc..., et la réinitialisation de l'indicateur (blocage/déblocage)
au cas où un signal interviendrait pendant l'exécution de la fonction associée).
79
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
GESTION CLASSIQUE DES SIGNAUX (sous linux)
• Production d’un signal
Les signaux sont produits par certaines conditions d'erreur, telles que des violations
d'espace mémoire, des erreurs arithmétiques émises par le processeur ou des
instructions illicites. Ils sont générés par l'interpréteur de commandes et les
gestionnaires de périphériques (terminaux, timer, clavier, carte E/S, liaison série,...).
Ils peuvent aussi être explicitement envoyés depuis un processus vers un autre et
constituent alors un moyen de synchronisation pour la transmission d'informations.
L'interface de programmation est à chaque fois la même. La fonction utilisée pour
envoyer un signal d'un processus vers un autre processus (de même propriétaire)
est la fonction kill() :
#include <sys/type.h>
#include <signal.h>
int kill( pid_t pid , int signal );
Les noms des signaux classiques sont définis dans le fichier d'en-tête standard
signal.h. Les valeurs numériques des signaux peuvent changer d'une
implémentation du système à un autre. La norme spécifie simplement des macroconstantes associées (#define). Elles commencent toutes génériquement par le
préfixe SIG.
80
ENSP Strasbourg (Edition 2009-2010)
Les Systèmes Temps Réels - Ch. DOIGNON
GESTION CLASSIQUE DES SIGNAUX (sous linux et systèmes Unix)
Noms des signaux (POSIX.1)
SIGHUP
SIGINT
SIGQUIT
SIGILL
SIGABRT
SIGFPE
SIGKILL
SIGSEGV
SIGPIPE
SIGALRM
SIGTERM
SIGUSR1/SIGIO
SIGUSR2/SIGIO
SIGCHLD
SIGSTOP
SIGCONT
SIGTSTP
SIGTTIN
SIGTTOU
Description
connexion interrompue (modem, ligne série)
ou terminaison d'un processus
interruption interactive (terminal)
terminaison interactive (terminal)
instruction illégale
terminaison anormale d'un processus (abort())
erreur arithmétique : division par zéro, dépassement (processeur) ,
signal provenant de (kill()) (non déroutable)
adresse mémoire incorrecte (MMU)
tube détruit (écriture dans le tube sans lecteur)
alarme: expiration d'un délai (alarm(),pause()) ou du timer
signal de terminaison
premier signal réservé pour les applications (kill())
second signal réservé par les applications (kill())
processus enfant stoppé ou terminé
arrêt (suspension) d'un processus (non déroutable)
reprise d'un processus (après suspension)
suspension d'un processus (provenant d'un terminal)
lecture du terminal pour un processus d'arrière-plan
écriture vers le terminal pour un processus d'arrière-plan
81
ENSP Strasbourg (Edition 2009-2010)
Les Systèmes Temps Réels - Ch. DOIGNON
GESTION CLASSIQUE DES SIGNAUX (sous linux)
• Délivrance d’un signal
On dit qu'un signal a été délivré lorsque le processus destinataire l'a pris en compte.
Lorsqu'un signal est produit, sa délivrance n'est pas immédiate. En particulier, il est
possible que le processus destinataire bloque temporairement la délivrance de
certains signaux (il est possible de choisir lesquels). Lorsqu'un signal est envoyé à un
processus qui le bloque, ce signal est dit pendant.
Le mécanisme de blocage de signaux permet d'empêcher temporairement la
délivrance d'un signal, sans pour autant le perdre. Il peut s'avérer très utile,
notamment lors de l'exécution d'une fonction de déroutement de signal, de bloquer
la délivrance d'une nouvelle occurrence de ce signal pendant son traitement. Il
apparaît donc indispensable de pouvoir traiter de façon atomique (sans interruption
possible) l'activation ou la désactivation d'un ensemble de signaux. Pour cela, la
norme POSIX propose un type de données appelé sigset_t pour représenter cet
ensemble de signaux :
#define _SIGSET_NWORDS 32
typedef struct{
unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;
typedef
__sigset_t
sigset_t;
82
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
GESTION CLASSIQUE DES SIGNAUX (sous linux)
• Délivrance d’un signal (suite)
Cette structure est manipulable à partir de cinq fonctions :
#include <signal.h>
int
int
int
int
int
sigemptyset( sigset_t *ens );
sigfillset( sigset_t *ens );
sigaddset( sigset_t *ens , int num_signal );
sigdelset( sigset_t *ens , int num_signal );
sigismember( sigset_t *ens , int num_signal );
Si ens est un objet de type sigset_t, l'appel de la fonction sigemptyset(&ens)
initialise l'ensemble ens de telle sorte qu'il ne contienne aucun des signaux de la
norme POSIX, et l'appel de la fonction sigfillset(&ens) de sorte qu'il les
contienne tous. L'une ou l'autre de ces fonctions doit être appelée au moins une fois
pour initialiser avant sa première utilisation, un objet de type sigset_t.
sigaddset(&ens,num_signal) et sigdelset(&ens,num_signal) ont des
fonctions qui respectivement ajoutent et enlèvent à l'ensemble ens le signal spécifié
par
le
numéro
du
signal
num_signal.
Enfin,
la
fonction
sigismember(&ens,num_signal) teste l'appartenance d'un signal à l'ensemble
ens; elle retourne 1 si le signal est dans ens, 0 sinon. Les autres fonctions retournent
0 en cas de succès et -1 en cas d'échec.
83
ENSP Strasbourg (Edition 2009-2010)
Les Systèmes Temps Réels - Ch. DOIGNON
GESTION CLASSIQUE DES SIGNAUX (sous linux)
• Configuration pour la délivrance
Un comportement spécifique est associé à chaque signal, indiquant la manière dont
il doit être délivré. Ce comportement est représenté par une structure de données
struct sigaction contenant (entre autres) les champs suivants :
struct sigaction
{
void
sigset_t
unsigned long
};
(*_sa_handler)(int); /* signaux classiques */
sa_mask;
sa_flags;
• void (*_sa_handler)(int signal) : ce champ décrit la fonction
associée à la délivrance du signal classique(fonction de déroutement) :
SIG_IGN pour ignorer le signal, SIG_DFL pour le traitement par défaut
(constantes symboliques), ou un pointeur vers une fonction de déroutement
du signal. Dans ce dernier cas, lorsque la fonction de déroutement est appelée,
elle reçoit en paramètre le signal délivré.
84
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
GESTION CLASSIQUE DES SIGNAUX (sous linux)
• Configuration pour la délivrance (suite)
• sigset_t sa_mask : ce champ (masque de signaux) permet de spécifier un
ensemble de signaux devant être bloqués durant l'exécution de la fonction de
déroutement du signal. La valeur de ce champ remplace temporairement le
masque de signaux du processus en cours.
• sa_flags : ce champ (drapeaux) permet de spécifier le comportement du
signal par rapport à l'environnement du processus (par exemple pour ne pas
prendre en compte le signal délivré à la terminaison d'un processus enfant).
La fonction qui positionne la valeur de ces champs avec le signal spécifié s'appelle
sigaction().
Les signaux interceptés à l'aide de fonctions de déroutement définis par
sigaction()ne sont pas par défaut réinstallés (après exécution de la fonction
associée). Ainsi, le champ sa_flags doit être paramétré avec la valeur symbolique
SA_ONESHOT afin de retrouver le comportement par défaut (SIG_DFL) du signal
avant le déroutement.
85
ENSP Strasbourg (Edition 2009-2010)
Les Systèmes Temps Réels - Ch. DOIGNON
GESTION CLASSIQUE DES SIGNAUX (sous linux)
• Remarque : lorsqu'un signal est ignoré, il est délivré et écarté. Lorsqu'un signal est
bloqué, il est mis en attente, durant le temps du blocage. Dans ce cas, l'arrivée
d'une nouvelle occurrence du signal peut, selon les implémentations du système
d'exploitation, être empilée ou écartée. Ce comportement n'est pas spécifié dans la
norme POSIX.
Un processus peut consulter et/ou modifier sa signalerie au moyen des fonctions
sigaction() et sigprocmask() :
int sigaction( int num_signal ,
struct
struct sigaction *ancienne_action );
sigaction
*nouvelle_action
,
La fonction sigaction() permet de consulter et/ou modifier le comportement
associé à la délivrance d'un signal spécifié. Le comportement par défaut des signaux
SIGKILL et SIGSTOP ne peut être modifié. Si le résultat de l'exécution de
sigaction() est d'ignorer un signal pendant, ce signal est écarté. Le paramètre
nouvelle_action décrit le comportement que l'on souhaite associer à la
délivrance du signal spécifié. Avant sa modification, le comportement courant est
sauvegardé dans la structure référencée par ancienne_action, sauf si ce dernier
est égal à à la constante symbolique NULL.
86
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
BLOCAGE DES SIGNAUX (sous linux)
int sigprocmask( int methode , sigset_t *nouvel_ens , sigset_t
ancien_ens );
La fonction sigprocmask() permet de consulter et/ou modifier le masque de
signaux. Le traitement dépend de la valeur du paramètre methode, qui peut être :
• SIG_SETMASK : le masque de signaux reçoit l'ensemble spécifié par le paramètre
nouvel_ens, qui est utilisé directement comme masque de blocage des signaux.
• SIG_BLOCK : on ajoute la liste des signaux contenus dans le paramètre
nouvel_ens au masque de blocage des signaux. Cela revient à bloquer tous les
signaux (sauf SIGKILL et SIGSTOP) spécifiés par l'ensemble nouvel_ens sans
changer le comportement des autres signaux. Il s’agit d’une addition au masque en
cours.
• SIG_UNBLOCK : on retire les signaux contenus dans nouvel_ens au masque de
blocage des signaux. Cela revient à débloquer tous les signaux spécifiés par
nouvel_ens sans changer le comportement des autres signaux.
Si le paramètre ancien_ens est différent de NULL, il reçoit une copie de la valeur
du masque avant sa modification. Il est possible d'obtenir la valeur courante du
masque de signaux, sans le modifier, en utilisant la valeur NULL dans nouvel_ens.
87
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
EXEMPLE (sous linux)
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void FonctionDeroutement( int sig )
{
printf("OUILLE ! - Signal reçu %d\n",sig);
return;
}
int main( void )
{
struct sigaction act;
act.sa_handler = FonctionDeroutement;
sigemptyset( &act.sa_mask );
/* act.sa_flags = SA_ONESHOT */
/* si decommenté, alors le comportement par défaut est réinstallé, pour le signal
concerné, à l’issue de l’invocation du gestionnaire du signal */
sigaction( SIGINT , &act , NULL );
while ( 1 )
{usleep(2000000L);/* on endort le processus pendant 2 s environ */
printf("Hello world !\n");}
exit(0);
}
88
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
EXPLICATIONS DE L’EXEMPLE (sous linux)
L'exécution de ce programme entraîne l'affichage d'un message lors de l'appui sur
Ctrl+C dans la mesure où act gère le signal SIGINT à plusieurs reprises. Pour mettre
fin au programme, il faut appuyer sur Ctrl+\ générant le signal SIGQUIT par défaut,
via le terminal.
ATTENTE DES SIGNAUX SUR FIN DE PROCESSUS (sous linux)
Un cas particulier est l'attente de la fin d'un processus enfant (envoie du signal
SIGCHLD par le processus enfant). Il est possible de faire attendre un processus
parent jusqu'à la terminaison d'un processus enfant en appelant les fonctions
wait() ou waitpid() :
#include <sys/type.h>
#include <sys/wait.h>
pid_t wait( int *stat_loc );
pid_t waitpid( pid_t pid , int *stat_loc , int options );
L'argument pid permet de définir le processus fils à attendre. Les informations de
sortie du processus seront inscrites à l'adresse spécifiée par *stat_loc. L'option
permet de savoir de quelle manière le processus enfant a été terminé.
89
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
MONITORING
Un processus peut savoir s'il possède des signaux pendants (en attente de
délivrance) au moyen de la fonction sigpending() :
int sigpending( sigset_t *ens );
Cette fonction recopie le masque de notification dans le paramètre ens (ensemble
de signaux) et retourne 0 en cas de succès et -1 en cas d'erreur.
ENDORMIR UN PROCESSUS : Un processus peut être endormi par un appel aux
fonctions sleep() ou usleep().
unsigned int sleep (unsigned int secondes );
void
usleep(unsigned int microsecondes );
Le processus ainsi endormi est réveillé soit à la fin du temps indiqué soit à l'arrivée
d'un signal.
ATTENTE D’UN SIGNAL : Un processus peut se mettre en attente de la délivrance
d'un signal quelconque ou particulier au moyen de la fonction pause() :
int pause( void );
La fonction pause() endort le processus jusqu'à réception d'un signal provoquant
la terminaison du programme ou l'appel d'une fonction de déroutement.
90
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
PROBLEMATIQUE DE L’ ATTENTE D’UN SIGNAL : le problème qui se pose souvent
est d’encadrer correctement pause(), de façon à éviter de perdre des signaux.
Imaginons que SIGUSR1 dispose d’un gestionnaire faisant passer à 0 une variable
globale appelée attente. On désire bloquer l’exécution du programme jusqu’à ce
que cette variable ait changé. Une première version – naïve – de ce programme
serait celle-ci :
attente = 1;
while (attente != 0) pause();
La présence de la boucle while() est justifiée par le fait qu’il se peut que l’appelsystème pause() soit interrompu par un autre signal qui ne modifie pas la variable
attente. Le problème principal est que le signal peut arriver entre l’instruction du
test (attente != 0) et l’appel pause(). Si le signal modifie la variable attente
à ce moment-là, et si le programme ne reçoit plus d’autres signaux, le processus
restera bloqué indéfiniment dans pause().
Pour éviter cette situation, on pourrait vouloir bloquer le signal temporairement à
l’aide de sigprocmask() ainsi :
91
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
PROBLEMATIQUE DE L’ ATTENTE D’UN SIGNAL (suite) :
sigset_t ensemble, ancien;
sigemptyset( &ensemble );
sigaddset( &ensemble , SIGUSR1 );
sigprocmask( SIG_BLOCK , &ensemble , &ancien );
attente = 1;
while (attente != 0){
sigprocmask( SIG_UNBLOCK , &ensemble , NULL );
pause();
sigprocmask( SIG_BLOCK , &ensemble , NULL );
/* traitement des autres signaux s’il y a lieu */
}
sigprocmask( SIG_SETMASK , &ancien , NULL );
Malheureusement, un signal bloqué en attente est délivré avant le retour de
sigprocmask(), qui le débloque. Un blocage du processus dans pause() est
donc toujours possible. Tant que les deux opérations (modifier le masque de
signaux et attente) sont réalisées sans être sûr qu’une interruption survienne entre
elles, il y aura toujours un risque de blocage. La solution est d’employer l’appelsystème sigsuspend() qui permet de manière atomique de modifier le masque
de signaux ET de bloquer en attente. Lorsqu’un signal non bloqué survient
sigsuspend() restitue le masque original avant de se terminer.
92
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
PROBLEMATIQUE DE L’ ATTENTE D’UN SIGNAL (fin) :
int sigsuspend( const sigset_t *ensemble );
L’ensemble transmis est celui des signaux à bloquer, pas celui des signaux attendus.
sigset_t ensemble, ancien;
sigemptyset( &ensemble );
sigaddset( &ensemble , SIGUSR1 );
sigprocmask( SIG_BLOCK , &ensemble , &ancien );
if ( sigismember( &ancien , SIGUSR1 )) {
sigdelset( &ancien , SIGUSR1 ):
sigusr1_dans_masque = 1;
}
attente = 1;
while (attente != 0){
sigsuspend( &ancien );
/* puis traitement pour les éventuels autres signaux */
}
if ( sigusr1_dans_masque )
sigaddset( &ancien , SIGUSR1 );
sigprocmask( SIG_SETMASK , &ancien , NULL );
On remarque qu’il est pris soin de restituer l’ancien masque de blocage des signaux
en sortie de routine, et qu’en transmettant cet ancien masque à sigsuspend(),
l’arrivée d’autres signaux que SIGUSR1 est permise.
93
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
GESTION DES SIGNAUX CLASSIQUES (synthèse) :
Nous avons vu qu’avec une gestion correcte des blocages des signaux, il est possible
d’accéder à n’importe quel type de données globales. Comme un signal non bloqué
peut survenir alors qu’un gestionnaire de signal est en exécution, le champ
sa_mask de la structure sigaction doit être manipulé avec soin.
Par ailleurs, comme la plupart des fonctions des bibliothèques ne sont pas
réentrantes, les seules opérations à effectuer dans un gestionnaire de signal sont :
- consulter/modifier des variables globales de type sig_atomic_t (défini dans
signal.h. Il s’agit d’un type entier que le processeur peut traiter de manière
atomique. Il faut de plus utiliser l’indicateur volatile pour signaler au compilateur
qu’elle peut être modifiée à tout moment, et pour qu’il ne se livre pas à des
optimisations (comme conserver la variable dans un des registres). Dans ce cas, le
gestionnaire ne fait que positionner l’état d’une variable globale, qui est ensuite
consultée dans le corps du programme.
- effectuer des appels-systèmes réentrants. Il existe une liste, définie par la norme
Posix.1, des appels-systèmes réentrants qui peuvent être invoqués depuis un
gestionnaire de signal. Le fait d’être réentrante permet à une fonction d’être
utilisable sans danger dans un programme multithread, mais la réciproque n’est pas
toujours vraie, comme pour malloc() qui est utilisable au sein de programmes
multithread mais ne doit pas être invoquée dans un gestionnaire de signal.
94
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
GESTION DES SIGNAUX TEMPS REELS
Les signaux temps réels présentent les particularités suivantes vis-à-vis des signaux
classiques :
• nombre plus important de signaux utilisateurs,
• empilement es occurrences des signaux bloqués,
• délivrance prioritaire des signaux,
• informations supplémentaires fournies au gestionnaire de signal.
Les signaux temps réel n’ont pas de noms spécifiques, contrairement aux signaux
classiques et s’étendent de SIGRTMIN à SIGRTMAX compris (SIGRTMAXSIGRTMIN=32 sous Linux). Linux par exemple associe à chaque signal temps réel
une file d’attente qui lui permet de mémoriser l’ensemble de toutes les
occurrences. Chaque occurrence présente dans la file d’attente donne lieu à une
délivrance spécifique. Le système Linux peut empiler jusqu’à 1024 signaux.
Lorsque plusieurs signaux temps réel doivent être délivrés à un processus, le noyau
délivre toujours les signaux temps réel de plus petit numéro. Ceci permet
d’attribuer un ordre de priorité entre les signaux temps réel.
Note : Sous Linux, pour vérifier l’existence de ces signaux en cas de portage d’une
application, on peut tester à la compilation la présence de la constante symbolique
_POSIX_REALTIME_SIGNALS dans le fichier <unistd.h>
95
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
GESTION DES SIGNAUX TEMPS REELS
Envoi d’un signal temps réel
Pour pouvoir fournir des informations supplémentaire au gestionnaire de signal, il
faut utiliser l’appel-système sigqueue() qui garantit que le signal sera empilé au
lieu de kill() (qui peut néanmoins envoyer un signal temps réel). La syntaxe est
la suivante :
#include <signal.h>
#include <unistd.h>
int sigqueue( pid_t pid , int num_sig , const union sigval info );
L’argument pid désigne le processus concerné destinataire du signal et l’argument
num_sig identifie le signal envoyé. Le troisième argument , info, contient
l’information supplémentaire associée au signal. C’est une valeur de type union
sigval (de la structure siginfo transmise au gestionnaire de signal – voir après)
qui peut prendre deux formes :
• un entier int si le champ sigval_int de l’union est utilisé par lors de l’appel au
gestionnaire du signal,
• un pointeur void * si le champ sigval_ptr de l’union est utilisé
96
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
GESTION DES SIGNAUX TEMPS REELS
Syntaxe de l’appel au gestionnaire
Du fait de la transmission d’informations supplémentaires associées au signal temps
réel, l’attachement et la définition d’un gestionnaire de signal s’effectuent
différemment que dans le cas d’un signal classique. Le gestionnaire prend la forme
suivante (le dernier paramètre n’est pas défini par POSIX et n’est donc pas utilisé):
void gestionnaire( int num_sig , struct siginfo *info , void *rien )
La structure de type struct siginfo contient notamment les champs suivants
conforment à la norme POSIX (les autres ne sont pas évoqués ici) :
• int si_signo, le numéro du signal,
• int si_code, qui indique l’origine du signal (si envoyé par le noyau - SI_KERNEL
- , par un appel-système kill() - SI_USER - , par l’appel-système sigqueue() SI_QUEUE - lors de la terminaison d’une opération d’entrée/sortie asynchrone SI_ASYNCIO - , à l’expiration d’une temporisation temps-réel - SI_TIMER - ,….),
• int si_value.sigval_int correspond au champ sigval_int de l’appel à
sigqueue(),
• void *si_value.sigval_ptr correspond au champ sigval_ptr de l’appel à
sigqueue().
97
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
GESTION DES SIGNAUX TEMPS REELS
Attachement du gestionnaire au signal
L’attachement du gestionnaire de signal temps réel à un signal s’effectue aussi avec
sigaction() et la structure struct sigaction étendue et contenant (entre
autres) les champs suivants :
struct sigaction
{
union {
sighandler_t void (*sa_handler)(int); /* signaux classiques */
void (*sa_sigaction)(int, struct siginfo * , void *);
/* signaux temps réel */
};
sigset_t
sa_mask;
unsigned long
sa_flags;
};
De plus, le champ sa_flags doit prendre la valeur SA_SIGINFO pour les signaux
temps réel.
98
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
EXEMPLE DE GESTION DES SIGNAUX TEMPS REELS
#define MON_SIG_TR (SIGRTMIN+3)
void gestion_TR( int , struct siginfo * , void * );
main() {
union sigval val;
struct sigaction act_TR;
pid = fork()
if (pid == 0 ){
act_TR.sa_sigaction = gestion_TR;
sigemptyset( &act_TR.sa_mask);
act_TR.sa_flags = SA_SIGINFO;
sigaction( MON_SIG_TR , &act_TR , NULL );
pause();
exit(0);}
else
{
printf(’’Introduisez un entier au clavier\n’’);
scanf(’’%d’’,&val.sigval_int);
printf(’’%d’’,val.sigval_int);
sigqueue( pid , MON_SIG_TR , val );
wait();
exit(0);}
}
void gestion_TR( int numero , struct siginfo *info , void *rien )
{
printf(’’signal temps reel recu , %d , %d\n’’, numero , info->si_signo);
printf(’’entier recu %d\n’’, info->si_value.sigval_int);
/* il est préférable de ne pas employer les fonctions E/S avec buffer dans un
gestionnaire ….*/
}
99
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
GESTION DU TEMPS
Mesure du temps CPU : Chaque processus possède dans son contexte une mesure
du temps CPU qu'il a consommé, et du temps consommé cumulé de tous ses
processus enfants terminés. Ces informations sont décrites par une structure de
données appelée struct tms dont les champs sont les suivants ;
tms_utime : temps CPU utilisateur (exécution d’instructions du programme utilisateur),
tms_stime : temps CPU système (exécution des instructions système - mode noyau),
tms_cutime : temps CPU utilisateur (comsommé des enfants terminés),
tms_cstime : temps CPU système (consommé des enfants terminés).
Tous les champs sont de type clock_t (équivalent à unsigned long) et la
fonction times() qui a la syntaxe suivante :
#include <sys/times.h>
clock_t times( struct tms *t );
permet d'obtenir ces informations. Cette fonction retourne un temps absolu
exprimé à partir d'une origine de temps arbitraire (il faut donc l'appeler deux fois
afin d'estimer une différence de temps). Tous les temps sont exprimés en unité
d'horloge. Le nombre d'unités d'horloge par seconde est la valeur symbolique
CLK_TCK (égale à 100 ou à 1000):
100
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
GESTION DU TEMPS
Exemple :
#include
#include
#include
#include
<sys/times.h>
<stdio.h>
<stdlib.h>
<time.h>
int main( void
{
struct tms
long
unsigned long
)
t1,t2;
i;
et1,et2;
void ConsommeDuTemps( long n )
{
void *p=NULL;
if ( n > 65535 ) return;
p = malloc( 1024*n );
if ( p ) free( p );
return;
}
et1 = times( &t1 );
for ( i = 0 ; i < 200000L ; i++ )
ConsommeDuTemps(i);
et2 = times( &t2 );
printf("\n clicks par sec : %ld\n",CLK_TCK);
printf("\n\tnombre de top utilisateur :
%ld",t2.tms_utime-t1.tms_utime);
printf("\n\tnombre de top systeme :
%ld'',t2.tms_stime-t1.tms_stime);
printf("\n\tnombre de top d'horloge (temps reel) :
%ld",et2-et1);
exit(0);
}
101
ENSP Strasbourg (Edition 2009-2010)
Les Systèmes Temps Réels - Ch. DOIGNON
GESTION DU TEMPS
Il peut arriver cependant que l'on ait besoin de dater les événements avec une
précision meilleure que 1/CLK_TCK. Pour cela, il existe un appel-système fournissant
une meilleur résolution, gettimeofday() , dont la syntaxe est :
#include <sys/time.h>
int gettimeofday( struct timeval * , struct timezone * );
struct timeval {
long tv_sec;
long tv_usec;
};
/* secondes */
/* microsecondes */
et fournit le nombre de secondes et le nombre de microsecondes depuis le dernier
changement de la valeur du champ tv_sec.
Le second argument de cette fonction n'est plus utilisé.
102
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
GESTION DU TEMPS
Exemple (sur l’allocation de la mémoire) :
void ConsommeDuTemps( long n )
{
void *p=NULL;
if ( n > 65535 ) return;
p = (void *)malloc( 1024L*n );
if ( p ) free( p );
return;
}
int main( void )
{
long
i;
gettimeofday( &timev1 , NULL );
for ( i = 0 ; i < 10 ; i++ ) ConsommeDuTemps( 65535L );
gettimeofday( &timev2 , NULL );
fprintf(stdout,"gettimeofday() : %6ld us\n",timev2.tv_usec-timev1.tv_usec);
/* display 000125 us on my computer */
exit(0);
}
103
ENSP Strasbourg (Edition 2009-2010)
Les Systèmes Temps Réels - Ch. DOIGNON
GESTION DU TEMPS
Exemple (sur le tri par sélection) :
#define TAILLE
#define SWAP(x,y,t)
16
((t)=(x),(x)=(y),(y)=(t))
struct timeval timev1,timev2;
long
t0[TAILLE]={ 25,36,2,9,51,12,18,42,
23,6,28,13,44,38,21,5};
void SortBySelection( long *t , long left ,
long right )
{
long i,j,tmp,min;
if ( left >= right ) return;
for ( i = left ; i < right ; i++ )
{
min = i;
for ( j = i+1 ; j <= right ; j++ )
{
if ( t[j] < t[min] ) min = j;
}
SWAP(t[i],t[min],tmp);
}
return;
}
int main( void )
{
long
i;
for ( i = 0 ; i < TAILLE ; i++ )
printf("%2ld - ",t0[i]);
printf("\n");
gettimeofday( &timev1 , NULL );
SortBySelection( t0 , 0 , TAILLE-1 );
gettimeofday( &timev2 , NULL );
for ( i = 0 ; i < TAILLE ; i++ )
printf("%2ld - ",t0[i]);
printf("\n");
fprintf(stdout,"gettimeofday(): %6ld us\n",
timev2.tv_usec-timev1.tv_usec);
/* display 4 us on my computer */
exit(0);
}
104
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
GESTION DU TEMPS
La norme POSIX met à disposition pour chaque processus trois compteurs, chacun
décrémentant de manière autonome. quand un compteur vient à expiration
(passage à zéro), un signal (SIGALRM ou SIGVTALRM) est envoyé au processus.
Selon le mode de fonctionnement de ce compteur, celui-ci peut redémarrer. La
programmation de ces compteurs est effectuée par l'initialisation des structures de
données itimerval et timeval et par l'emploi des fonctions getitimer() et
setitimer() :
#include <sys/time.h>
struct timeval {
long tv_sec;
/* secondes */
long tv_usec; /* microsecondes */
};
struct itimerval {
struct timeval it_interval;/* prochaine valeur */
struct timeval it_value;
/* valeur actuelle */
};
int getitimer( int compteur , struct itimerval *valeur );
int setitimer( int compteur , struct itimerval *valeur , struct itimerval
*anc_valeur );
105
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
GESTION DU TEMPS
Le paramètre compteur spécifie le compteur à utiliser :
- ITIMER_REAL : ce compteur décrémente en temps réel et fourni le signal
SIGALRM à l'expiration,
- ITIMER_VIRTUAL : ce compteur décrémente seulement quand le processus est
en exécution et délivre le signal SIGVTALRM à l'expiration (non POSIX),
- ITIMER_PROF : ce compteur décrémente seulement quand le processus est en
exécution, en mode noyau. Couplé avec le compteur ITIMER_VIRTUAL, ce
compteur est utilisé habituellement pour connaitre le temps d'exécution en mode
utilisateur et en mode noyau. Le signal SIGPROF est délivré à l'expiration (non
POSIX).
La fonction getitimer() permet de connaitre la valeur d'un compteur à un
moment donné et remplit la structure à l'adresse indiquée par valeur. La fonction
setitimer() permet d'initialiser un compteur avec le contenu de la structure
commençant à l'adresse valeur et de sauvegarder éventuellement l'ancienne valeur
à l'adresse indiquée par anc_valeur (si différente de NULL).
106
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
GESTION DU TEMPS
Exemple (initialiser le compteur temps réel pour l’envoi d’un signal toutes les 10 ms, après 2 s) :
#include <sys/time.h>
struct timerval t;
int main( void )
{
t.it_interval.tv_sec = 0;
t.it_interval.tv_usec = 10000;
t.it_value.tv_sec = 2;
t.it_value.tv_usec = 0;
setitimer( ITIMER_REAL , &t , NULL );
.....
exit(0);
}
107
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
GESTION DU TEMPS
Remarques :
• Puisque le signal SIGALRM est délivré à l'expiration du comptage, on peut associer
la délivrance du signal à l'appel d'une fonction de déroutement à intervalles
réguliers.
• Il est formellement déconseillé d'utiliser simultanément les appels système
setitimer() , sleep() et la programmation de l'alarme avec la fonction
alarm() car elles utilisent le même signal SIGALRM.
• Par défaut, à la délivrance des signaux SIGALRM, SIGVTALRM et SIGPROF, le
processus se termine.
Exercice : Concevoir un programme simple qui, s'appuyant sur le timer logiciel
ITIMER_REAL, déclenche toutes les 100 ms, l'exécution d'une fonction associée,
dont le rôle est de sauvegarder dans un tableau d'entiers (global), les valeurs
successives d'un incrément d'une boucle exécutée dans la fonction principale. A
l'issue de la boucle, les valeurs du tableau seront sauvegardées dans un fichier.
108
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
INTRODUCTION AU MULTITHREADING
Le multithreading est l’alternative à la programmation multiprocessus; il offre un
parallélisme plus léger à gérer pour le système. Le processus léger (ou thread)
constitue une extension du modèle traditionnel de processus, appelé en la
circonstance processus lourd. Un processus classique est constitué d’un espace
d’adressage avec un seul fil d’exécution, ce fil d’exécution étant représenté par une
valeur de compteur ordinal et une pile d’exécution.
Dans ce contexte, l’extension consiste à admettre plusieurs fils d’exécution
indépendants dans un même espace d’adressage, chacun de ces fils (ou threads ou
activités) étant caractérisé par une valeur de compteur ordinal propre et une pile
d’exécution privée. Un thread est donc une entité d’exécution, rattachée à un
processus, et chargée d’exécuter une partie du code du processus.
Le principal avantage lié à la notion de processus léger est un allègement des
opérations de commutations de contextes : en effet lorsque le processus est
attribué d’un fil d’exécution à un autre, comme les deux fils appartiennent au même
espace d’adressage, la commutation consiste alors en pratique à seulement changer
de pile et de valeur de compteur ordinal, le contexte mémoire restant le même. De
plus, l’opération de création dynamique d’un nouveau fil d’exécution est
sensiblement plus courte que celle correspondant à la création dynamique d’un
nouveau processus (fork()) puisqu’elle ne nécessite pas la duplication de l’espace
d’adressage du processus père.
109
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
Ci-dessus : Processus classique (une seule activité)
Ci-contre : Processus composé de deux activités
110
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
INTRODUCTION AU MULTITHREADING
Comme les processus légers au sein d’un même processus partagent le même
espace d’adressage (en partie), il s’ensuit des problèmes de partage de ressources à
gérer, mais des mécanismes spécifiques de synchronisation sont proposés.
Les processus légers peuvent être implémentés à deux niveaux différents, soit au
niveau utilisateur, soit au niveau noyau, ce qui en fait une entité tout à fait adaptée
pour représenter une tâche dans un système temps réel.
Implémentation au niveau utilisateur
Dans ce cas, le noyau ordonnance les processus comme s’ils étaient composés d’un
seul fil d’exécution. Le noyau ignore donc les différents fils d’exécution d’un même
processus. Un exécutif système gère l’interface avec le noyau en prenant en charge
la gestion des threads et en cachant ceux-ci du noyau. Cet exécutif (bibliothèque)
est donc responsable de la commutation des threads au sein d’un même processus,
lorsque ceux-ci en font explicitement la demande (non préemption).
L’avantage est que la commutation des threads d’un même processus s’effectue au
niveau utilisateur. L’inconvénient majeur est qu’au sein d’un même processus, un
thread peut monopoliser le processeur. Comme le noyau ne connait pas les threads,
un thread bloqué (suite à une demande de ressource ne pouvant pas être
immédiatement satisfaite) bloque l’ensemble des threads du processus.
111
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
INTRODUCTION AU MULTITHREADING
Implémentation au niveau noyau
Lorsque l’implémentation des threads est effectuée au niveau du noyau, alors ce
dernier connait l’existence de tous les threads au sein d’un processus et il attribue le
processeur à chacun des threads de manière indépendante. Chaque descripteur de
processus contient alors une table des threads qui le composent avec pour chacun
d’eux la sauvegarde de son contexte.
Avec cette approche, on évite le blocage de tout un processus à partir du moment
où l’un de ses threads est bloqué. Par contre, les commutations de contexte étant
gérées par le noyau, celles-ci sont un peu plus longues.
Multiprogrammation avec les fils d’exécution (sous Linux)
Les threads d’un même processus partageant le même espace d’adressage peuvent
communiquer entre eux sans faire appel au noyau (d’où un gain de temps par
rapport à la communication entre processus par IPC ou Sockets, par exemple). Cet
aspect fait apparaître la nécessité de synchroniser l’accès aux ressources pour éviter
la corruption des données. Pour parvenir à cela, il est nécessaire d’utiliser des
verrous (ou FUTEX – Fast Userlevel muTEX), qui rendent possibles tous les
mécanismes de synchronisation dans l’espace utilisateur.
112
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
INTRODUCTION AU MULTITHREADING
Multiprogrammation avec les fils d’exécution (sous Linux)
Note : La bibliothèque LinuxThread utilise les signaux temps réel SIGRTMIN,
SIGRTMIN+1 et SIGRTMIN+2 pour des besoins internes. Si on désire une
notification par signal temps réel, il faut nécessairement employer un numéro
supérieur ou égal à SIGRTMIN+3.
L’ensemble des primitives gérant les threads est compilé et rassemblé dans une
bibliothèque NPTL (Native POSIX Thread Library) dont l’en-tête se nomme
pthread.h sous Linux (LinuxThreads). A la compilation, la constante _REENTRANT
doit être incluse (-D_REENTRANT) et l’édition des liens nécessite alors d’intégrer
cette bibliothèque par l’ajout de l’option –lpthread.
Chaque thread est identifié de manière unique au sein d’une application par un
type pthread_t. La primitive pthread_self() permet à chaque thread de
connaître son propre identifiant.
La fonction pthread_equal() permet de comparer deux identifiants de threads :
int pthread_equal( pthread_t thread1 , pthread_t thread2 );
Cette fonction renvoie une valeur non nulle s’ils sont égaux.
113
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
INTRODUCTION AU MULTITHREADING
Multiprogrammation avec les fils d’exécution (sous Linux)
Création d’un thread : La fonction pthread_create() permet la création d’un
nouveau thread. Celle-ci donne naissance à un nouveau fil d’exécution, qui va
démarrer en invoquant la routine dont le nom est passé en argument. Lorsque cette
routine se termine, le thread est éliminé. Cette routine fonctionne donc un peu
comme la fonction main() des programmes en C. Pour cette raison, le fil
d’exécution original du processus est nommé thread principal (main thread).
int pthread_create( pthread_t *thread , pthread_attr_t
*(*fonction)( void *argument) , void *argument );
att
,
void
Le premier argument est un pointeur qui sera initialisé par la routine avec
l’identifiant du nouveau thread. Le second argument correspond aux attributs dont
on désire doter le nouveau thread. Si la valeur NULL est transmise, le thread reçoit
alors des attributs standards. Les autres attributs seront détaillés par la suite.
Le troisième argument est un pointeur représentant la fonction principale du
nouveau thread. Celle-ci est invoquée dès la création du thread et reçoit en
argument le pointeur passé en dernière position. Le type de l’argument étant
void*, on pourra le transformer en n’importe quel autre type pour passer un
argument au thread.
pthread_create() renvoie 0 si le thread a été créé.
114
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
INTRODUCTION AU MULTITHREADING
Multiprogrammation avec les fils d’exécution (sous Linux)
Note : Le nombre de threads simultanés est limité par la constante
PTHREAD_THREADS_MAX (1024 avec LinuxThreads).
Terminaison d’un thread : Lorsque la fonction principale d’un thread se termine,
celui-ci est éliminé et la fonction doit renvoyer une valeur de type void* qui pourra
être récupérée dans un autre fil d’exécution. Il est possible aussi de mettre fin à un
thread en invoquant directement la fonction pthread_exit() avec un pointeur
de type void* passé en argument :
void pthread_exit( void *argument );
Pour récupérer la valeur de l’argument d’un thread terminé, il faut utiliser la
fonction pthread_join(). Celle-ci suspend l’exécution du thread appelant jusqu’à
la terminaison du thread indiqué en argument.
int pthread_join( pthread_t thread , void **retour );
Cette fonction remplit alors le contenu de l’adresse passée en second argument
avec la valeur retournée par le thread terminé. pthread_join() peut échouer si
le thread attendu n’existe pas, s’il est détaché ou si un risque de blocage se
présente.
115
ENSP Strasbourg (Edition 2009-2010)
Les Systèmes Temps Réels - Ch. DOIGNON
EXEMPLE
#include
#include
#include
#include
#include
<pthread.h>
<stdio.h>
<stdlib.h>
<string.h>
<unistd>
#define NB_THREADS
5
void *fn_thread( void * );
static int compteur;
int main(void ) {
pthread_t thread[NB_THREADS];
int
i, ret;
for ( i=0 ; i < NB_THREADS ; i++ )
if ((ret = pthread_create( &thread[i] , NULL , fn_thread , (void *)i)) != 0 )
fprintf(stderr , ‘’%s’’ , strerror(ret));
exit(1); }
while ( compteur < 40 ) {
fprintf( stdout , ‘’main : compteur = %d\n’’, compteur);
sleep(1); }
for ( i=0 ; i < NB_THREADS ; i++ ) pthread_join( thread[i] , NULL );
return (0);}
void *fn_thread( void *num )
int numero = (int)(num);
{
{
while ( compteur < 40 )
{
usleep( numero * 100000 );
compteur++;
fprintf( stdout , ‘’Thread %d : compteur = %d\n’’, numero, compteur );
pthread_exit( NULL );
}
}
116
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
INTRODUCTION AU MULTITHREADING
Multiprogrammation avec les fils d’exécution (sous Linux)
Cette application montre l’enchevêtrement des différents threads et est donc mal
conçue, car les threads modifient la même variable globale sans se préoccuper les
uns des autres. C’est justement l’essence même de la programmation multithread
d’éviter ce genre de situation.
Détachement d’un thread : Lorsqu’un thread ne renvoie pas de valeur et qu’il n’a
pas besoin d’être attendu par un autre thread, on peut employer la fonction
pthread_detach() qui lui permet de disparaître du système quand il se termine.
Cela autorise la libération immédiate des ressources privées du thread (pile et
variables automatiques).
int pthread_detach( pthread_t thread );
Un thread peut très bien invoquer pthread_detach() à propos d’un autre thread
de l’application. Contrairement au processus, il n’y a pas de notion de hiérarchie
chez les threads ni d’autorisations particulières pour modifier les paramètres d’un
autre fil d’exécution. Cette fonction échoue alors si le thread que l’on veut détacher
n’existe pas (et donc en particulier s’il est déjà détaché).
117
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
INTRODUCTION AU MULTITHREADING
Multiprogrammation avec les fils d’exécution (sous Linux)
Attributs d’un thread : Chaque thread est doté d’un certain nombre d’attributs
regroupés dans une structure de données nommée pthread_attr_t. Lorsque les
attributs par défaut ne sont pas suffisants, il faut passer l’adresse d’un objet de type
pthread_attr_t qui aura été configuré préalablement. Pour cela il faut invoquer
la fonction pthread_attr_init() :
int pthread_attr_init( pthread_att_t * attributs );
Une fois les attributs initialisés, les fonctions pthread_attr_getXXX() et
pthread_attr_setXXX() sont utilisées pour consulter ou modifier les champs
de la structure de données des attributs. Le XXX indique l’attribut concerné.
Quand une variable contenant les attributs n’est plus nécessaire, elle peut être
détruite en employant la fonction pthread_attr_destroy(), qui peut libérer des
données dynamiques internes :
int pthread_attr_destroy( pthread_att_t * attributs );
118
ENSP Strasbourg (Edition 2009-2010)
Les Systèmes Temps Réels - Ch. DOIGNON
INTRODUCTION AU MULTITHREADING
Multiprogrammation avec les fils d’exécution (sous Linux)
Attributs d’un thread (suite) : Les attributs stackaddr et stacksize permettent de
configurer la pile utilisée par un thread. Il peut parfois être nécessaire de réclamer
une pile de dimension plus grande que celle qui est fournie par défaut si le thread à
créer fait un large usage de fonctions récursives, par exemple. Les fonctions
int pthread_attr_getstackaddr(
**valeur );
const
pthread_attr_t
*attributs
,
void
int pthread_attr_setstackaddr( pthread_attr_t *attributs , void *valeur );
int pthread_attr_getstacksize(
*valeur );
const
pthread_attr_t
*attributs
,
size_t
int pthread_attr_setstacksize( pthread_attr_t *attributs , size_t valeur );
sont
disponibles
dans
la
bibliothèque si les constantes symboliques
_POSIX_THREAD_ATTR_STACKADDR et _POSIX_THREAD_ATTR_STACKSIZE sont
définies dans le fichier à en-tête unistd.h. La valeur de la dimension minimale de la
pile est accessible par consultation de la valeur de la constante symbolique
PTHREAD_STACK_MIN (16 Ko le plus souvent sur PC).
119
ENSP Strasbourg (Edition 2009-2010)
Les Systèmes Temps Réels - Ch. DOIGNON
Multiprogrammation avec les fils d’exécution (sous Linux)
Attributs d’un thread (suite) : Les attributs schedpolicy, schedparam, scope et
inheritsched concernent l’ordonnancement des threads. Ils sont disponibles si la
constante symbolique _POSIX_THREAD_PRIORITY_SCHEDULING a été définie
int pthread_attr_getschedpolicy(
*valeur );
const
pthread_attr_t
*attributs
,
int
int pthread_attr_setschedpolicy( pthread_attr_t *attributs , int valeur );
L’attribut schedpolicy correspond à la méthode d’ordonnancement employée pour le
thread. Les valeurs possibles sont :
• SCHED_OTHER : ordonnancement classique
• SCHED_RR : séquencement temps réel avec la stratégie du tourniquet,
• SCHED_FIFO : ordonnancement temps réel FIFO.
L’attribut schedparam contient la priorité du processus et peut être consulté ou
modifié respectivement à l’aide des fonctions suivantes :
int pthread_attr_getschedparam( const pthread_attr_t *attributs , struct
sched_param *param );
int pthread_attr_setschedparam( pthread_attr_t *attributs , const struct
sched_param *param );
120
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
INTRODUCTION AU MULTITHREADING
Multiprogrammation avec les fils d’exécution (sous Linux)
Attributs d’un thread (suite) : L’attribut schedparam qui ne concerne que les
ordonnancements temps réel (RR et FIFO) ainsi que l’attribut schedpolicy peuvent
être consultés ou modifiés durant l’exécution du thread à l’aide des fonctions
int pthread_setschedparam( pthread_t thread , int ordonnancement , const
struct sched_param *param );
int pthread_getschedparam( pthread_t thread , int *ordonnancement , struct
sched_param *param );
L’attribut scope n’est pas vraiment configurable. Il sert dans les implémentations
hybrides reposant en partie sur un ordonnancement par le noyau (c’est le cas sous
Linux) et en partie sur une bibliothèque dans l’espace utilisateur. Dans ce cas, cet
attribut peut prendre l’une des valeurs suivantes :
• PTHREAD_SCOPE_SYSTEM : cette valeur réclame un ordonnancement du thread en
concurrence avec tous les processus du système. Le séquenceur utilisé est alors celui
du noyau.
• PTHREAD_SCOPE_PROCESS : cet ordonnancement oppose les threads les uns au
autres, au sein d’un même processus (non supporté pour les threads Linux).
121
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
INTRODUCTION AU MULTITHREADING
Multiprogrammation avec les fils d’exécution (sous Linux)
Attributs d’un thread (suite) : L’attribut scope est donc relatif aux priorités des threads
: dans un cas vis-à-vis du système, et dans l’autre cas, interne au processus dont ils
dépendent.
int pthread_attr_getscope( const pthread_attr_t *attributs , int *valeur );
int pthread_attr_setscope( pthread_attr_t *attributs , int valeur );
L’attribut inheritsched signale si le thread dispose de sa propre configuration
d’ordonnancement, comme c’est le cas par défaut, ou si les attributs schedparam et
schedpolicy sont ignorés, au profit de l’ordonnancement du thread qui l’a créé. Les
valeurs de cet attribut peuvent être :
• PTHREAD_EXPLICIT_SCHED : l’ordonnancement est spécifique au thread créé
(par défaut),
•
PTHREAD_INHERIT_SCHED : l’ordonnancement est hérité du thread créateur.
122
ENSP Strasbourg (Edition 2009-2010)
Les Systèmes Temps Réels - Ch. DOIGNON
INTRODUCTION AU MULTITHREADING
Multiprogrammation avec les fils d’exécution (sous Linux)
Attributs d’un thread (fin) : Les fonctions associées à cet attribut sont :
int pthread_attr_getinheritsched(
*valeur );
const
pthread_attr_t
*attributs
,
int
int pthread_attr_setinheritsched( pthread_attr_t *attributs , int valeur );
Annulation d’un thread : un thread peut vouloir annuler un autre thread. Une
demande d’annulation est envoyée, et sera prise en compte ou non, en fonction de la
configuration du thread récepteur. Le thread récepteur peut accepter la requête, la
refuser ou la repousser plus loin dans son exécution (point d’annulation). Pour
envoyer une demande d’annulation, on emploie la fonction :
int pthread_cancel( pthread_t thread );
La fonction pthread_setcancelstate() permet de configurer le comportement du
thread récepteur vis-à-vis d’une requête d’annulation à venir :
int pthread_setcancelstate( int etat_annulation , int *ancien_etat );
123
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
INTRODUCTION AU MULTITHREADING
Multiprogrammation avec les fils d’exécution (sous Linux)
Déroulement et annulation d’un thread (suite) : les valeurs possibles des états sont :
• PTHREAD_CANCEL_ENABLE : le thread acceptera les requêtes d’annulation (par défaut),
• PTHREAD_CANCEL_DISABLE : le thread ne tiendra pas compte des demandes
d’annulation.
Les requêtes d’annulation ne sont pas mémorisées, ce qui fait qu’un thread
désactivant temporairement les requêtes d’annulation sur une zone de code critique
ne se terminera pas lorsqu’il autorisera de nouveau les annulations, même si
plusieurs demandes sont parvenues pendant ce laps de temps.
Il existe un moyen d’empêcher l’annulation de se produire intempestivement, tout en
acceptant les requêtes; il s’agit d’un mécanisme de synchronisation qui est fondé sur
un retardement des annulations jusqu’à atteindre des emplacements bien définis du
code. Le thread récepteur ainsi configuré ne se terminera pas dès réception d’une
annulation (pas forcément), mais continuera de s’exécuter jusqu’à atteindre ce point
d’annulation. Pour configurer ce comportement, on emploie la fonction
int pthread_setcanceltype( int type_annulation , int *ancien_type );
124
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
INTRODUCTION AU MULTITHREADING
Multiprogrammation avec les fils d’exécution (sous Linux)
Déroulement et annulation d’un thread (fin) : avec pthread_setcanceltype(), le
type d’annulation peut correspondre à l’une des valeurs suivantes :
• PTHREAD_CANCEL_DEFERRED : le thread ne se terminera qu’en atteignant un point
d’annulation (par défaut),
• PTHREAD_CANCEL_ASYNCHRONOUS : l’annulation prendra effet dès réception de la
requête.
La norme POSIX définit quatre fonctions qui constituent des points d’annulation,
c’est-à-dire des fonctions avec lesquelles un thread est susceptible de se terminer :
• pthread_cond_wait() et pthread_cond_timedwait() : en attente qu’une
condition soit remplie, par exemple une condition de déblocage,
• pthread_join(),
• void pthread_testcancel( void )
Pour cette dernière fonction, le thread peut se terminer si une demande d’annulation
est en cours. On voit donc que dans le cas d’un état PTHREAD_CANCEL_DEFERRED,
un thread ne sera jamais interrompu au milieu d’un calcul ou dans une boucle de
manipulation de données. On pourra donc répartir des appels
pthread_testcancel() dans ce genre de code, aux endroits où on est sûr qu’une
annulation ne présente aucun danger.
125
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
SYNCHRONISATION DES TACHES
Dans un système multitâches, les tâches peuvent avoir besoin de communiquer entre
elles pour échanger des données. Cet échange de données peut se faire par le biais
d’une zone de mémoire partagée, par le biais d’un fichier ou encore en utilisant les
outils de communication du système d’exploitation. Dans tous les cas, les tâches ne
sont plus indépendantes et elles effectuent des accès concurrents aux ressources.
Plus généralement, une ressource désigne toute entité dont a besoin une tâche pour
s’exécuter. La ressource peut être matérielle comme le processeur ou un
périphérique ou elle peut être logicielle comme une variable. Une ressource est aussi
caractérisée par un état qui définit si la ressource est libre ou occupée et par son
nombre de points d’accès, c’est-à-dire le nombre de tâches pouvant y accéder en
même temps. On distingue alors la notion de ressource critique qui correspond à une
ressource ne pouvant être accédée que par une seule tâche à la fois.
L’utilisation d’une ressource par une tâche s’effectue en trois étapes. La première
étape correspond à l’étape d’attribution de la ressource à la tâche qui consiste à
donner la ressource à la tâche qui la demande. Une fois que la tâche a pu obtenir la
ressource, elle occupe la ressource durant un certain temps (deuxième étape) puis
rend la ressource; c’est la dernière étape de restitution de la ressource.
Les phases d’attribution et de restitution de la ressource doivent assurer que la
ressource est utilisée conformément à son nombre de points d’accès.
126
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
SYNCHRONISATION DES TACHES
Par ailleurs, l’étape d’attribution de la ressource peut se révéler bloquante pour la
tâche qui l’effectue si tous les points d’accès de la ressource sont occupés. Au
contraire, l’étape de restitution d’une ressource par une tâche peut entrainer le
déblocage d’une autre tâche en attente d’accès à cette ressource.
Plusieurs schémas de synchronisation entre tâches ont été définis afin de garantir
une bonne utilisation des ressources par les tâches et d’une manière plus générale
une communication entre tâches cohérente et sans perte de données. Un des
schémas les plus répandus pour les ressources critiques est celui de l’exclusion
mutuelle. Le code d’utilisation (durant la phase d’occupation) d’une ressource
critique est appelée section critique.
Accès à une ressource critique par exclusion mutuelle : ce mécanisme de
synchronisation a pour rôle de garantir qu’une ressource critique ne peut être
manipulée que par une seule tâche à la fois. Pour ce faire, la section critique est
précédée d’un prélude et suivie d’un postlude qui assurent l’exclusion mutuelle. Le
prélude prend la forme d’une protection et le postlude celui d’une fin de protection.
Quant à la section critique, elle doit offrir les trois propriétés suivantes :
• exclusion mutuelle,
• attente bornée : la tâche demandant l’accès à la ressource critique doit obtenir
satisfaction au bout d’un temps borné,
127
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
SYNCHRONISATION DES TACHES
Accès à une ressource critique par exclusion mutuelle (suite)
• bon déroulement : lorsque la ressource critique est inoccupée et qu’une ou
plusieurs tâches sont attentes pour entrer dans celle-ci, le choix de la tâche entrant
finalement en section critique ne relève que des tâches en attente, ce choix devant se
faire dans un laps de temps borné. Par ailleurs, une tâche s’exécutant en dehors de la
section critique ne peut pas bloquer l’accès à la ressource critique associée.
Algorithme de Peterson : c’est une solution entièrement logicielle au problème de
l’accès à une section critique. En particulier, elle ne requiert pas l’aide du système
d’exploitation. Pour l’appréhender, considérons deux tâches i et j. La structure de la
tâche i étant la suivante :
/* prelude */
while { Drapeau[i] = VRAI ; tour = j;
while (Drapeau[j] & tour == i );
Section critique
/* postlude */
Drapeau[i] = FAUX;
}
128
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
SYNCHRONISATION DES TACHES
Accès à une ressource critique par exclusion mutuelle (suite)
• explications : La variable Drapeau permet à chaque tâche d’indiquer sa volonté
d’entrer en section critique (donc d’accéder à la ressource critique) en positionnant
celle-ci à VRAI. La variable tour indique quelle tâche a le droit d’entrer en section
critique. Ce sont deux variables globales. On peut vérifier que les trois propriétés
précédentes sont vérifiées :
• exclusion mutuelle : si deux tâches veulent entrer en section critique, alors
Drapeau[i]=Drapeau[j]=VRAI. Seule la tâche pour laquelle tour contient sa
valeur peut entrer en section critique. Si une des tâches est déjà en section
critique (par exemple i) et que l’autre tâche , j, veut y entrer à son tour, alors la
variable tour vaut forcément i et la tâche j est bloquée.
• attente bornée : si une des deux tâches (i par exemple) veut entrer en section
critique et se trouve mise en attente, alors cela veut dire que la section critique
est occupée par l’autre tâche (j). Lorsque la tâche j sort de la section critique,
elle positionne son Drapeau à FAUX. Si cette même tâche revient
immédiatement demander l’entrée de la section critique, elle met son Drapeau
à VRAI mais également la variable tour à i. Comme la tâche i en attente dans la
boucle ne modifie pas la variable tour, la tâche i entre en section critique,
après une attente qui au maximum est égale à une entrée de la tâche j en
section critique.
129
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
SYNCHRONISATION DES TACHES
Accès à une ressource critique par exclusion mutuelle (suite)
• bon déroulement : une tâche en dehors de la section critique et ne désirant
pas y pénétrer positionne son Drapeau à FAUX. Elle ne peut alors pas bloquer
l’entrée de la section critique. Si deux tâches veulent entrer en même temps en
section critique, toutes deux positionnent leur Drapeau à VRAI et chacune
commute le tour pour le donner à l’autre. Ainsi, pour au moins une des deux
tâches, la condition du while est fausse et cette tâche entre en section critique.
Réalisation avec des MUTEX (sous Linux)
Les mutex et les variables de conditions sont deux outils de synchronisation
permettant de réaliser l’exclusion mutuelle entre threads.
Un mutex est une variable de type pthread_mutex_t servant de verrou pour
protéger l’accès à des zones de code ou de données. Ce verrou peut prendre deux
états, disponible ou verrouillé, et il ne peut être acquis que par un seul thread à la
fois. Un thread demandant à verrouiller un mutex déjà acquis par un autre thread est
mis en attente.
• Initialisation d’un mutex : l’initialisation d’un mutex s’effectue à l’aide de la
constante PTHREAD_MUTEX_INITIALIZER :
pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;
130
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
SYNCHRONISATION DES TACHES
Réalisation avec des MUTEX (sous Linux)
• Verrouillage d’un mutex :le verrouillage d’un mutex s’effectue en appelant la
primitive pthread_mutex_lock() dont le prototype est :
int pthread_mutex_lock( pthread_mutex_t * mut );
Si le mutex est libre alors il est attribué au mutex appelant, sinon le thread appelant
est mis en attente jusqu’à la libération du mutex. La primitive
pthread_mutex_trylock() effectue également le verrouillage d’un mutex.
Cependant, elle échoue (erreur EBUSY retournée) lorsque le mutex est déjà verrouillé
plutôt que de bloquer le thread appelant :
int pthread_mutex_trylock( pthread_mutex_t * mut );
• Libération d’un mutex : la libération d’un mutex s’effectue en appelant la primitive
int pthread_mutex_unlock( pthread_mutex_t * mut );
• Destruction d’un mutex : la destruction d’un mutex s’effectue à l’aide de la primitive
int pthread_mutex_destroy( pthread_mutex_t * mut );
131
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
EXEMPLE
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int i;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *addition( void *inutile ) {
/* verrouillage du mutex */
pthread_mutex_lock( &mutex );
i = i+10;
printf(‘’hello, thread fils %d\n’’,i);
i = i+20;
printf(‘’hello, thread fils %d\n’’,i);
/* liberation du mutex */
pthread_mutex_lock( &mutex );
}
int main( void ) {
pthread_t num_thread;
i = 0;
pthread_create( &num_thread, NULL, addition , NULL );
pthread_mutex_lock( &mutex ); /* verrouillage du mutex */
i = i+1000;
printf(‘’hello, thread principal %d\n’’,i);
i = i+2000;
printf(‘’hello, thread principal %d\n’’,i);
pthread_mutex_unlock( &mutex ); /* liberation du mutex */
pthread_join( num_thread , NULL ); /* sync le th principal sur fin du th fils */
pthread_mutex_destroy( &mutex ); /* destruction du mutex */
}
132
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
SYNCHRONISATION DES TACHES
Réalisation avec des variables conditions (et mutex - sous Linux)
Les variables conditions sont des objets de type pthread_cond_t permettant à une
tâche de se mettre en attente d’un événement provenant d’un autre thread, comme
par exemple la libération d’un mutex.
La variable condition est associée à deux opérations, l’une permettant l’attente d’une
condition, l’autre permettant de signaler que la condition est remplie. Elle est
toujours associée à un mutex qui la protège. Son utilisation suit le schéma suivant :
• Thread en attente de la condition :
1. Initialisation de la condition et du mutex associé,
2. Blocage du mutex et mise en attente sur la condition,
3. Libération du mutex
•
Thread signalant la condition :
1. Exécution jusqu’à réaliser la condition attendue,
2. Blocage du mutex associé et signalisation de la condition remplie,
3. Libération du mutex.
La primitive effectuant la mise en attente libère atomiquement le mutex associé à la
condition, permettant au thread signalant la condition de l’acquérir à son tour. Le
verrou est de nouveau acquis par le thread en attente, une fois la condition attendue
signalée.
133
ENSP Strasbourg (Edition 2009-2010)
Les Systèmes Temps Réels - Ch. DOIGNON
SYNCHRONISATION DES TACHES
Réalisation avec des variables conditions (et mutex - sous Linux)
Précisément, l’étape 2 pour le thread attendant la condition se déroule comme suit :
• blocage du mutex,
• mise en attente sur la condition et déblocage du mutex,
• condition signalée, réveille du thread et blocage du mutex, une fois celui-ci libéré
par le thread signalant la condition.
• Initialisation d’une variable condition : elle s’effectue à l’aide de la constante
PTHREAD_COND_INITIALIZER ou en employant pthread_cond_init():
pthread_mutex_t condition = PTHREAD_COND_INITIALIZER;
• Mise en attente sur une condition : la primitive pthread_cond_wait() permet à
un thread de se mettre en attente sur une condition :
pthread_cond_wait(
*mutex );
pthread_cond_t
*condition,
pthread_mutex_t
• Signalisation d’une condition : la primitive pthread_cond_signal() permet à un
thread de signaler une condition remplie :
pthread_cond_signal( pthread_cond_t *condition );
134
ENSP Strasbourg (Edition 2009-2010)
Les Systèmes Temps Réels - Ch. DOIGNON
SYNCHRONISATION DES TACHES
Réalisation avec des variables conditions (et mutex - sous Linux)
• Destruction d’une variable condition : la destruction s’effectue à l’aide de la
primitive pthread_cond_destroy() :
pthread_cond_destroy( pthread_cond_t *condition );
Thread attendant la condition
Thread signalant la condition
• Appel de pthread_mutex_lock(): blocage
du mutex associé à la condition.
• Appel de pthread_mutex_lock(): blocage du
mutex associé à la condition
• Appel de pthread_cond_wait(): déblocage
du mutex.
• …attente …
•.
•.
• Dans pthread_cond_wait(), tentative de
récupérer le mutex. Blocage du thread.
•.
•.
• Fin de pthread_cond_wait().
• Appel de pthread_mutex_unlock() pour
revenir à l’état initial.
•.
•.
• Appel de pthread_mutex_lock() sur le mutex.
• Appel de pthread_cond_signal(), qui réveille
l’autre thread.
•.
•.
• Appel de pthread_mutex_unlock(). Le mutex
étant libéré, l’autre thread se débloque.
135
ENSP Strasbourg (Edition 2009-2010)
Les Systèmes Temps Réels - Ch. DOIGNON
EXEMPLE
#include
#include
#include
#include
<stdio.h>
<stdlib.h>
<pthread.h>
<unistd.h>
pthread_cond_t condition_alarme
= PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex_alarme
= PTHREAD_MUTEX_INITIALIZER;
static int
aleatoire( int );
static void *thread_alarme(void * );
static void *thread_temperature( void * );
static void *
thread_alarme(void *inutile )
{
while (1){
pthread_mutex_lock(&mutex_alarme);
pthread_cond_wait(&condition_alarme,
&mutex_alarme);
pthread_mutex_unlock(&mutex_alarme);
fprint(stdout,’’ALARME\n’’);
}
return(NULL);
}
int main( void ) {
pthread_t thr[2];
pthread_create( &thr[0], NULL, thread_temperature , NULL );
pthread_create( &thr[1], NULL, thread_alarme , NULL );
pthread_exit( NULL );}
static void *thread_temperature(void *inutile ) {
int temperature = 20;
while (1){temperature += aleatoire(5)-2;
fprintf(stdout,‘’Temperature : %d \n’’,temperature);
if ((temperature < 16) || (temperature > 24)) {
pthread_mutex_lock(&mutex_alarme);
pthread_cond_signal(&condition_alarme);
pthread_mutex_unlock(&mutex_alarme);}
sleep(1);
}
return(NULL);}
136
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
SEMAPHORES POSIX (sous Linux – POSIX)
La bibliothèque LinuxThreads implémente un mécanisme de synchronisation qui
appartient en fait à la norme POSIX.1b (temps réel) : les sémaphores.
Un sémaphore est une variable de type sem_t servant à limiter l’accès à une
ressource, le plus souvent une portion de code. L’initialisation d’un sémaphore, dans
l’utilisation avec les threads pour des applications temps réel, se fait grâce à la
fonction sem_init(), et sa libération en employant sem_destroy() :
int sem_init( sem_t *semaphore, int partage, unsigned int val );
int sem_destroy( sem_t *semaphore );
Le deuxième argument de sem_init() indique si le sémaphore est réservé au
processus appelant ou s’il doit être partagé entre plusieurs processus. La version
actuelle de la bibliothèque LinuxThreads ne permet pas le partage en dehors des
threads du même processus, donc cette valeur est toujours nulle. Le troisième
argument représente la valeur initiale du sémaphore. Cette valeur est inscrite dans
un compteur qui est décrémenté chaque fois qu’un thread pénètre dans la ressource
logicielle, et incrémenté à chaque sortie de la ressource. L’entrée dans une portion de
code protégée par un sémaphore ne peut se faire que si le compteur est strictement
positif. Ainsi, la valeur initiale du compteur représente le nombre maximum de
threads simultanément tolérés.
Ces fonctionnalités sont déclarées dans le fichier à en-tête sémaphore.h si la
constante symbolique _POSIX_SEMAPHORES est définie dans unistd.h.
137
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
SEMAPHORES POSIX (sous Linux – POSIX)
Lorsqu’un thread désire entrer dans la portion de code critique, il appelle la fonction
sem_wait() qui attend que le compteur du sémaphore soit supérieur à zéro, et le
décrémente avant de revenir. La vérification de la valeur du compteur et sa
décrémentation sont liées de manière atomique, évitant ainsi tout problème de
concurrence d’accès. Cette fonction est un point d’annulation pour les threads :
int sem_wait( sem_t * semaphore );
En sortie de la portion (de code) critique, le thread invoque la fonction sem_post()
qui incrémente le compteur (postlude) :
int sem_post( sem_t *semaphore );
Il existe une fonction, sem_trywait() , fonctionnant comme sem_wait() mais
qui ne bloque pas si le compteur n’est pas supérieur à 0.
int sem_trywait( sem_t *semaphore );
On peut aussi consulter directement la valeur du compteur d’un sémaphore en
appelant sem_getvalue( ) qui stocke l’état actuel dans la variable du second
argument de sem_getvalue():
int sem_trywait( sem_t *semaphore , int *value );
138
ENSP Strasbourg (Edition 2009-2010)
Les Systèmes Temps Réels - Ch. DOIGNON
EXEMPLE
#include
#include
#include
#include
#include
<stdio.h>
<stdlib.h>
<pthread.h>
<semaphore.h>
<unistd.h>
sem_t semaphore;
int main( void ) {
int i;
pthread_t thread;
sem_init(&semaphore,0,3);
for (i=0; i < 10; i++ ) {
pthread_create( &thread, NULL,
routine_thread, (void *)(i));
pthread_exit( NULL );
}
static int aleatoire( int maximum );
static void *routine_thread( void *num_thread );
void *routine_thread( void *num_thread )
{
int i;
for (i=0; i<2 ; i++ ) {
sem_wait(&semaphore);
printf(’’Thread %d dans portion critique \n’’,(int)(num_thread));
sleep(aleatoire(4));
printf(’’Thread %d sort de la portion critique \n’’,(int)(num_thread));
sem_post(&semaphore);
sleep(aleatoire(4));
}
return(NULL);
}
139
Les Systèmes Temps Réels - Ch. DOIGNON
ENSP Strasbourg (Edition 2009-2010)
140

Documents pareils