Mécanismes de communication et de synchronisation
Introduction une application temps réel est typiquement constituée de plusieurs tâches exécutées de façon concurrente ces tâches ne sont pas indépendantes besoin d'échanger des données besoin de synchroniser les moments où les données sont échangées
Introduction outils de coordination famille des sémaphores sémaphores binaire et à comptage spinlocks mutexes variables conditionnelles famille des signaux signaux POSIX.1 événements POSIX.4 outils de communication famille des files de messages files de messages pipes FIFOs mémoire partagée
Outils de coordination but : protéger l’accès à des variables partagées dans un environnement avec préemption exemple : gestion d’un stock un thread lit dans un registre la valeur courante V0 du stock et la décrémente un autre thread préempte le premier avant, lit la valeur courante (toujours V0) , la décrémente et met à jour le registre qui contient alors V0-1 le premier thread reprend son exécution et met à jour le registre en écrivant V0-1, au lieu de V0-2
Les différents types de sémaphores sémaphores binaires valeur : 0 ou 1 ressource globale sans notion de propriété peut être relâché par une tâche quelconque, même si elle n'en n'a pas fait l'acquisition Disponible (Available) Indisponible (Unavailable) Acquérir (valeur = 0) Relâcher (valeur = 1) valeur initiale = 1 initiale = 0 ● un feu que l'on ne peut franchir que quand il est vert et qui repasse immédiatement au rouge dès qu'il a été franchi
Les différents types de sémaphores sémaphores à compte valeur initiale positive ou nulle décrémentée à chaque fois que l'acquisition est accordée, incrémentée quand le sémaphore est relâché quand la valeur devient nulle, la demande d'acquisition est bloquante ressource globale Acquérir (valeur = valeur - 1) Acquérir (valeur = 0) Disponible (Available) Indisponible (Unavailable) le feu ne repasse au rouge que quand il a été franchi par un certain nombre de véhicules valeur initiale = 0 valeur initiale > 0 ● ● Relâcher (valeur = valeur + 1) Relâcher (valeur = 1)
Les différents types de sémaphores mutex sémaphores binaires améliorés améliorations autour des notions de propriété, de verrouillage récursif, de protection contre la destruction et les inversions de priorité l'implémentation des améliorations est facultative les détails d'implémentation peuvent varier Acquérir (valeur = 1) Acquérir (récursif) (valeur = valeur + 1) Déverrouillé (Unlocked) Verrouillé (Locked) valeur initiale = 0 on demande la permission pour utiliser la route. Quand la permission est accordée, personne d'autre ne peut emprunter cette route ● Relâcher (récursif) (valeur = valeur - 1) Relâcher (valeur = 0)
Les différents types de sémaphores notion de propriété d'un mutex la tâche qui a fait l'acquisition d'un mutex en est propriétaire. Elle seule peut le relâcher à comparer aux sémaphores verrouillage récursif la tâche qui possède le mutex peut en redemander l'acquisition permet de résoudre des problèmes de deadlocks potentiels entre routines d'une même tâche protection contre la destruction la tâche qui possède un mutex est protégée contre la destruction par une autre tâche problèmes liés à l'inversion de priorité quand une tâche de haute priorité attend l'accès à une ressource possédée par une tâche de basse priorité
Sémaphores POSIX.4 création destruction acquisition libération libération de toutes les tâches en attente (flush)
Sémaphores POSIX.4 sémaphores #include <semaphore.h> existent sous deux formes : sémaphores nommés sémaphores basés sur la mémoire mécanisme de gestion analogue à celui d'un système de fichiers granularité plus fine que les sémaphores nommés gestion plus complexe
Sémaphores POSIX.4 sémaphores nommés crée le sémaphore sem_t sem_open(const char *sem_name, int oflags, mode_t creation_mode, unsigned int initial_val); crée le sémaphore il est recommandé de fournir un nom "compatible" avec un nom de fichier, et commençant par un un "/", et n'en contenant pas d'autre oflags réfère aux protections en lecture/écriture, ou à la volonté de créer un nouveau sémaphore (O_CREAT et O_EXCL) int sem_close (sem_t *sem_id); int sem_unlink(const char *sem_name);
Sémaphores POSIX.4 sémaphores basés sur la mémoire int sem_init(sem_t sem_location, int pshared, unsigned_int initial_value); crée le sémaphore sem_location est une place mémoire, éventuellement en zone partagée pshared indique la visibilité des sémaphores (un ou plusieurs processes) int sem_destroy(sem_t sem_location);
Sémaphores POSIX.4 opérations sur les sémaphores mêmes primitives pour les deux types de sémaphores int sem_post(sem_t *sem_id); int sem_wait(sem_t *sem_id); int sem_trywait(sem_t *sem_id); int sem_getvalue(sem_t sem_id, int *value); les problèmes liés à des inversions de priorité sont pris en compte par le système d'exploitation si la constante _POSIX_PRIORITY_SCHEDULING est vraie (/usr/include/unistd.h) une opération sem_wait effectuée dans un thread Xenomai en mode secondaire le fait passer en mode primaire
Mutexes les propriétés du mutex sont décrites par des attributs création/destruction des attributs int pthread_mutexattr_init(pthread_mutexattr_t *attr); int pthread_mutexattr_destroy(pthread_mutexattr_t *attr); les attributs sont ensuite modifiés par des primitives int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type); int pthread_mutexattr_setprotocol(pthread_mutexattr_t *attr, int protocol); int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared); les valeurs des attributs peuvent être récupérées par int pthread_mutexattr_gettype(pthread_mutexattr_t *attr, int *type); int pthread_mutexattr_getprotocol(pthread_mutexattr_t *attr, int *protocol); int pthread_mutexattr_getpshared(pthread_mutexattr_t *attr, int *pshared);
Mutexes types de mutexes : protocoles : partage des mutexes : PTHREAD_MUTEX_NORMAL PTHREAD_MUTEX_ERRORCHECK PTHREAD_MUTEX_RECURSIVE protocoles : PTHREAD_PRIO_NONE PTHREAD_PRIO_INHERIT (héritage de priorité, supporté par Xenomai) PTHREAD_PRIO_PROTECT (priorité plafond, non supporté) partage des mutexes : PTHREAD_PROCESS_PRIVATE PTHREAD_PROCESS_SHARED Linux ne supporte que l'attribut "type" Xenomai supporte les 3 attributs (par défaut PTHREAD_MUTEX_NORMAL, PTHREAD_PRIO_NONE et PTHREAD_PROCESS_PRIVATE)
Mutexes création du mutex int pthread_mutex_init(pthread_mutex_t *mutexid, pthread_mutexattr_t *attr); un mutex peut être également initialisé par les initialiseurs pthread_mutex_t fmutex = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t rmutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP; pthread_mutex_t emutex = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP; non supporté par Xenomai destruction du mutex int pthread_mutex_destroy(pthread_mutex_t *mutexid,); ne peut se faire que sur un mutex libre (EBUSY)
Mutexes opérations sur les mutex int pthread_mutex_lock(pthread_mutex_t *mutexid); int pthread_mutex_trylock(pthread_mutex_t *mutexid); int pthread_mutex_unlock(pthread_mutex_t *mutexid); int pthread_mutex_timedlock(pthread_mutes_t mutexid, const struct *abstime); pthread_mutex_lock n’est pas un point d’annulation en cas d’appel multiple à pthread_mutex_lock par le même thread pour un mutex « normal » on est en situation de deadlock si le verrouillage est récursif, il faut autant de unlock que de lock si on a un mutex de diagnostic (PTHREAD_MUTEX_ERRORCHECK), retour d’un erreur (EDEADLOCK)
Mutexes et Xenomai quand une opération sur les mutexes (tentatives de lock ou unlock) est effectuée sur un thread Xenomai en mode secondaire, il est automatiquement transformé en mode primaire
Barrières Mécanisme simple d'attente d'un nombre prédéfini de threads initialisation d'une barrière en spécifiant le nombre de threads qui doivent être en attente avant que la barrière soit levée pthread_barrier_t barriere; pthread_barrier_init(&barriere, NULL, NB_THREADS); les threads qiu appellent pthread_barrier_wait sont bloqués jusqu'à ce qu'il y ait NB_THREADS threads en attente pthread_barrier_wait(&barriere);
Variables conditions synchronisation des threads sur la satisfaction d'une condition : la vérification d'un prédicat sur une donnée partagée deux opérations de base pour les threads participants : signaler la condition, quand le prédicat est vérifié attendre la condition (le thread est bloqué jusqu'à ce qu'un autre thread signale la condition) toutes les opérations se font sous la protection d'un mutex pour éviter une situation de compétition (race condition) entre les threads qui attendent la condition et ceux qui la signalent
Variables conditions les propriétés sont décrites par des attributs initialisation/destruction des attributs int pthread_condattr_init(pthread_condattr_t *attr); int pthread_condattr_destroy(pthread_condattr_t *attr); les attributs sont modifiés par les primitives int pthread_condattr_setclock (pthread_condattr_t attr, clockid_t clk_id); définit l’horloge utilisée pour pthread_cond_timedwait int pthread_condattr_setpshared (pthread_condattr_t attr, int pshared); variable condition accessible seulement aux threads du process (PTHREAD_PROCESS_PRIVATE) ou à tous (PTHREAD_PROCESS_SHARED) les valeurs des attributs peuvent être récupérées par int pthread_condattr_getclock (pthread_condattr_t attr, clockid_t *clk_id); int pthread_condattr_getpshared (pthread_condattr_t attr, int *pshared); seul Xenomai supporte ces attributs (pas Linux)
Variables conditions création (non supporté par Xenomai) destruction int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *attr); pthread_cond_t cond = PTHREAD_COND_INITIALIZER; (non supporté par Xenomai) destruction int pthread_cond_destroy(pthread_cond_t *cond);
Variables conditions utilisation des variables conditions la variable conditionnelle est un outil pour signaler la satisfaction de la condition l'attente doit se faire sous la protection d'un mutex int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime); l'appel est bloquant et le mutex est automatiquement relâché de façon atomique
Variables conditions utilisation des variables conditions la signalisation que la condition est satisfaite se fait également sous la protection du mutex int pthread_cond_signal(pthread_cond_t *cond); int pthread_cond_broadcast( pthread_cond_t *cond); le signal est fugace : il n'est pas mémorisé et une condition signalée ne sera pas prise en compte par un thread qui se mettrait en attente après le signalement il faut libérer le mutex après le signalement. Les threads en attente du mutex entrent alors en compétition pour le prendre
Thread attendant la condition Thread signalant la condition Variables conditions Thread attendant la condition Thread signalant la condition appel de pthread_mutex_lock() : blocage du mutex associé à la condition test de la non satisfaction de la condition appel de pthread_cond_wait() : déblocage du mutex attente... appel de pthread_mutex_lock () modification/test de la condition appel de pthread_cond_signal() : réveil du thread en attente dans pthread_cond_wait() : tentative de récupération du mutex appel de pthread_mutex_unlock() : déblocage du thread en attente fin de pthread_cond_wait() appel de pthread_mutex_unlock() : retour à la situation initiale
Variables conditions et Xenomai quand une opération sur les variables conditions (tentatives d'attente ou de signalisation) est effectuée sur un thread Xenomai en mode secondaire, il est automatiquement transformé en mode primaire
Exemple (1) #include <stdio.h> #include <pthread.h> #define NUM_THREADS 3 #define TCOUNT 10 #define COUNT_THRES 12 int count = 0; int thread_ids[3] = {0,1,2}; pthread_mutex_t count_lock=PTHREAD_MUTEX_INITIALIZER; pthread_cond_t count_hit_threshold=PTHREAD_COND_INITIALIZER; main(void) { int i; pthread_t threads[3]; pthread_create(&threads[0], NULL, inc_count, (void *)&thread_ids[0]); pthread_create(&threads[1], NULL, inc_count, (void *)&thread_ids[1]); pthread_create(&threads[2], NULL, watch_count, (void *)&thread_ids[2]); for (i = 0; i < NUM_THREADS; i++) { pthread_join(threads[i], NULL); } return 0;
Exemple (2) void *watch_count(void *idp) { int i=0; int *my_id = idp; printf("watch_count(): thread %d\n", *my_id); pthread_mutex_lock(&count_lock); while (count < COUNT_THRES) { pthread_cond_wait(&count_hit_threshold, &count_lock); printf("watch_count(): thread %d, count %d\n", *my_id, count); } pthread_mutex_unlock(&count_lock); return(NULL);
Exemple (3) void *inc_count(void *idp) { int i=0; int *my_id = idp; for (i=0; i<TCOUNT; i++) { pthread_mutex_lock(&count_lock); count++; printf("inc_counter(): thread %d, count = %d, unlocking mutex\n", *my_id, count); if (count == COUNT_THRES) { printf("inc_count(): Thread %d, count %d\n", *my_id, count); pthread_cond_signal(&count_hit_threshold); } pthread_mutex_unlock(&count_lock); return(NULL);
spinlocks dans le cas de programmation noyau et que le temps d'attente de la ressource est court un sémaphore classique serait très inefficace attente active (ne relâche pas la CPU) utile uniquement dans un environnement multiprocesseur primitives : int pthread_spin_init(pthread_spinlock_t *lock, int pshared); int pthread_spin_destroy(pthread_spinlock_t *lock); int pthread_spin_lock(pthread_spinlock_t *lock); int pthread_spin_trylock(pthread_spinlock_t *lock); int pthread_spin_unlock(pthread_spinlock_t *lock);
Quelques fonctions utiles exécution unique d'une fonction : pthread_once quand plusieurs threads instancient une même fonction (code), il peut arriver que l'on veuille qu'un seul de ces threads exécute une fonction particulière du code par exemple ouvrir un fichier, ou initialiser un mutex il n'est pas toujours possible de le faire avant la création des threads (dans la fonction main, par exemple) le mécanisme proposé par pthread_once permet d'implémenter ceci très facilement de façon atomique initialisation d'une variable de type pthread_once_t pthread_once_t var = PTHREAD_ONCE_INIT exécution de func par le premier thread qui parvient à l'instruction pthread_once (&var, func)
Quelques fonctions utiles exemple pthread_once_t variable_de controle= PTHREAD_ONCE_INIT; pthread_mutex_t mutex; pthread_t tid[10]; void initialisation_mutex() { /* fonctions pour initialiser le système */ pthread_mutex_init(&mutex, NULL); ... } void corps_du_thread () { ... pthread_once (&variable_de_controle, initialisation_mutex); ... } main () { int i; ... for (i=0, i<10,i++) { pthread_create(tid[i], NULL, corps_du_thread,); } ... }
Quelques fonctions utiles données spécifiques des threads pour stocker de façon permanente des données spécifiques à chaque thread pas facile de prévoir des variables globales spécifiques associées à chacune des instances d'un thread si on peut ne pas connaître à priori le nombre d'instances qui vont s'exécuter par exemple si on veut compter combien de fois chaque thread va demander l'acquisition d'un mutex on va associer à la donnée spécifique une clé qui sera ensuite utilisée pour accéder à la donnée création d'une clé : pthread_key_create(pthread_key_t key_id, void (*destr (void *));
Quelques fonctions utiles association d'une donnée à la clé pthread_setspecific(pthread_key_t key_id, const void *data); accès à une donnée void * pthread_getspecific(pthread_key_t) retourne un pointeur sur la donnée spécifique du thread associée à la clé destructeur le mécanisme de référence par clé fait appel à des pointeurs pour stocker les données spécifiques en mémoire, d'où un risque potentiel de fuite la fonction destr passée dans pthread_key_create va être exécutée à la fin du thread pour pouvoir éventuellement libérer la zone mémoire occupée
outils de communication but : échange d’informations entre différentes tâches files de messages mémoire partagée en utilisant éventuellement des moyens de synchronisation peuvent aussi servir de moyen de synchronisation files de messages avec opérations d’accès bloquantes
les files de messages définition : un objet de type tampon à travers lequel les tâches peuvent envoyer et recevoir des messages à des fins de communication ou de synchronisation Mémoire (espace système ou privé) Bloc de contrôle de la file QCB nom ou identificateur Tâche 1 Tâche 2 ... Tâche 1 Tâche 2 ... Longueur maximale du message message Liste d'attente des tâches émettrices Liste d'attente des tâches réceptrices Longueur de la queue
les files de messages états des files de messages les effets liés à la lecture d'une file vide ou à l'écriture dans une file pleine varient selon l'implémentation erreur blocage Message arrivé (nmsg = nmsg + 1) création de la file (nmsg = 0) Message arrivé (nmsg = 1) Message arrivé (nmsg = lfile) Vide Non vide Pleine Message reçu (nmsg = 0) Message reçu (nmsg = lfile - 1) Message reçu (nmsg = nmsg - 1)
les files de messages opérations sur les files de messages création protections définies au moment de la conception et uniquement sur le mode d'accès (lecture et/ou écriture) destruction débloquage des tâches en attente perte des messages dans la file
les files de messages opérations sur les files de messages écriture mode FIFO, LIFO et/ou basé sur une priorité du message différentes politiques de blocage d'écriture dans une file pleine peut dépendre de l'origine du message (tâche ou ISR) lecture différentes politiques de blocage de lecture dans une file vide politique de destruction automatique ou non du message lu broadcast
les files de messages opérations sur les files de messages obtenir de l'information sur les caractéristiques modifier les caractéristiques
files de messages POSIX.4 création mqd_t mq_open(const char *mq_name, int oflags, mode_t cr_mode, struct mq_attr *attr); nom "à la système de fichier" (cf sémaphores) oflags réfère aux protections en lecture/écriture, à la politique de blocage (O_NONBLOCK) ou à la volonté de créer une nouvelle file (O_CREAT et O_EXCL) cr_mode définit la protection sur le nom de la file attr contient les informations physiques de la file (nombre maximum de messages, longueur maximum d'un message) un thread Xenomai primaire sera basculé en mode secondaire
files de messages POSIX.4 #include <mqueue.h> structure mq_attr struct mq_attr { long int mq_flags; /* Message queue flags. */ long int mq_maxmsg; /* Maximum number of messages. */ long int mq_msgsize; /* Maximum message size. */ long int mq_curmsgs; /* Number of messages currently queued. */ }; mode #include <sys/stat.h> protection (I_IRUSR, I_IWUSR, ...)
files de messages POSIX.4 destruction int mq_close(mqd_t mq_id); fermeture de la file mq_unlink(const char *mq_name); un thread Xenomai primaire bascule en mode secondaire demande d'information int mq_getattr(mqd_t mqid, struct mq_attr *attr); modification des propriétés int mq_setattr(mqd_t mqid, const struct mq_attr *new_attr, struct mq_attr *old_attr); ne permet de modifier que la politique de blocage
files de messages POSIX.4 écriture int mq_send(mqd_t mqid, const char *msgbuf, size_t msqsize, unsigned int prio); différents niveaux de priorité pour le message, compris entre MQ_PRIO_MAX et MQ_PRIO_MIN (définis dans <linux/mqueue.h> les messages sont insérés dans la file suivant la priorité, en mode FIFO à niveau égal de priorité pas d'effet sur la priorité de la tâche de lecture lecture int mq_receive(mqd_t mqid, char *msgbuf, size_t mqsize, unsigned int *prio); lecture du message de tête de la file (donc le plus prioritaire)
files de messages POSIX.4 il existe des versions temporisées des fonctions d'écriture et de lecture int mq_timedsend(mqd_t mqid, const char *msgbuf, size_t msqsize, unsigned int prio, constant struct timespec *abs_timeout); nt mq_timedreceive(mqd_t mqid, char *msgbuf, size_t mqsize, unsigned int *prio constant struct timespec *abs_timeout); un thread Xenomai secondaire basculera en mode primaire à l'appel d'une fonction de lecture ou d'écriture
files de messages POSIX.4 demande de notification int mq_notify(mqd_t mqid, const struc sigevent *specs); un événement POSIX.4 destiné à la tâche le demandant sera généré automatiquement si on écrit dans la file alors qu'elle est vide les caractéristiques de l'événement sont décrites dans la structure sigevent une seule tâche peut faire la demande de notification la demande ne peut se faire que si aucun thread n'est bloqué sur une opération de lecture sur la file il faut "réarmer" la demande de notification après exécution un thread Xenomai secondaire basculera én mode primaire
files de messages POSIX.4 Comportement sur fork et exec analogue à celui des systèmes de fichier : file héritée au clonage par fork file fermée sur exit et _exit comportement différent sur exec : la file est fermée (alors qu'un fichier reste ouvert). Il faut donc refaire un appel à mq_open
Mémoire partagée moyen de communication très efficace entre tâches implémenté naturellement pour les threads ne passe pas par le système d'exploitation (donc rapide) mais dangereux accès concurrent à une même ressource nécessite l'utilisation des outils de synchronisation (mutex et/ou sémaphores) seul moyen par lequel des outils de synchronisation anonymes de POSIX (mutexes, variables conditionnelles, sémaphores) peuvent être partagés entre des modules exécutés dans l'espace noyau et des processes utilisateurs, ou entre des processus utilisateurs différents
Mémoire partagée POSIX.4 complexe... parce qu'elle utilise la possibilité de projeter en mémoire des fichiers mécanisme très puissant de partage et de sauvegarde de données, même entre machines distantes (il suffit qu'elles partagent un même disque) deux étapes : ouvrir (créer) l'objet de mémoire partagée (shm_open) utiliser le descripteur retourné pour projeter cet objet dans l'espace mémoie de la tâche (mmap) tous les appels à des fonctions manipulant la mémoire partagée font basculer des threads Xenomai primaires en mode secondaire
Mémoire partagée POSIX.4 #include <sys/mman.h> création d'un segment de mémoire partagée int shm_open(const char *name, int oflag, mode_t mode); retourne un descripteur analogue à celui d'un fichier, avec les mêmes restrictions que pour les sémaphores et les files de messages oflag et mode sont équivalents à ceux utilisés par open modification de la taille du segment int ftruncate (int fd, off_t size); la taille du segment est nulle à la création ftruncate est une fonction "généraliste" qui agit également sur les fichiers, pour augmenter ou diminuer la taille
Mémoire partagée POSIX.4 fermeture et destruction close(int fd); shm_unlink(const char *name);
Mémoire partagée POSIX.4 projection de la mémoire partagée caddr_t mmap(caddr_t addr, size_t len,int prot, int flag,int fildes, off_t offset); projection dans l'espace d'adressage de l'objet associé au descripteur fildes des len octets à partir du décalage offset mmap permet de projeter d'autres objets que des segments de mémoire partagée (par exemple, des fichiers “classiques”) addr est une proposition d'adresse. L'adresse effectivement choisie par le système est retournée par mmap (fd = mmap(...) contiendra l'adresse effective) il est recommandé de prendre des multiples de PAGESIZE pour len int munmap (void * addr, size_t len); annulation de la projection
Mémoire partagée POSIX.4 projection de la mémoire partagée caddr_t mmap(caddr_t addr, size_t len,int prot, int flag,int fildes, off_t offset); Mémoire len offset len Stockage
Verrouillage de la mémoire verrouillage de pages mémoire essentiel pour les processes temps réels car la pagination n'est pas un comportement prédictible int mlock (const *start_addr, size_t length); int mlockall (int type); type permet de préciser quelles pages on désire verrouiller, en faisant un OR de MCL_CURRENT : les pages actuellement possédées MCL_FUTURE : les pages qui seront allouées dans le futur (y compris la pile) int munlock (const *start_addr, size_t length); int munlockall (void); sous Linux, on peut verrouiller au maximum la moitié de la mémoire totale le verrouillage n’est pas hérité au cours d’un fork
Modules