futex(2) System Calls Manual futex(2)

futex – Verrouillage rapide en mode utilisateur

Bibliothèque C standard (libc, -lc)

#include <linux/futex.h>   /* Définition des constantes FUTEX_* */
#include <sys/syscall.h>   /* Définition des constantes SYS_* */
#include <unistd.h>
long syscall(SYS_futex, uint32_t *uaddr, int futex_op, uint32_t val,
             const struct timespec *timeout,   /* ou : uint32_t val2 */
             uint32_t *uaddr2, uint32_t val3);

Remarque : la glibc ne fournit pas de fonction autour de futex(), nécessitant l'utilisation de syscall(2).

L'appel système futex() offre une méthode pour attendre qu'une condition soit vraie. On l'utilise en général comme construction de blocage dans le contexte de la synchronisation de la mémoire partagée. Quand on utilise des futex, la majorité des opérations de synchronisation s'effectue dans l'espace utilisateur. Un programme de l'espace utilisateur n'utilise l'appel système futex() que lorsqu'il est probable qu'il doive se bloquer plus longtemps avant que la condition ne soit vraie. D'autres opérations futex() peuvent être utilisées pour réveiller des processus ou des threads qui attendent une condition en particulier.

Un futex est une valeur 32 bits — désignée ci-dessous comme « mot futex » —dont l'adresse est fournie à l'appel système futex() (les futex ont une taille de 32 bits sur toutes les plateformes, y compris les systèmes 64 bits). Toutes les opérations futex sont pilotées par cette valeur. Afin de partager un futex entre des processus, le futex est placé dans une zone de la mémoire partagée créée en utilisant (par exemple) mmap(2) ou shmat(2) (ainsi, le mot futex peut avoir plusieurs adresses virtuelles dans différents processus, mais ces adresses se rapportent toutes au même emplacement de la mémoire physique). Dans un programme multithreadé, il suffit de mettre le mot futex dans une variable globale partagée par tous les threads.

Lors de l'exécution d'une opération futex qui demande le blocage d'un thread, le noyau ne le bloquera que si le mot futex a une valeur fournie par le thread appelant (en tant qu'un des paramètres de l'appel futex()) correspondant à celle prévue du mot futex. Le chargement de la valeur du mot futex, la comparaison de cette valeur avec celle attendue et le blocage s'effectueront de manière atomique et seront entièrement organisés par rapport aux opérations qui sont effectuées en parallèle par d'autres threads sur le même mot futex. Ainsi, le mot futex est utilisé pour relier la synchronisation de l'espace utilisateur et l'implémentation du blocage par le noyau. Tout comme une opération compare-and-exchange atomique qui modifie potentiellement la mémoire partagée, le blocage par futex est une opération compare-and-block atomique.

Une utilisation des futex consiste à implémenter des verrous. L'état du verrou (c'est-à-dire acquis ou non acquis) peut se représenter comme un drapeau auquel on a un accès atomique en mémoire partagée. En absence de conflit (uncontended case), un thread peut accéder et modifier l'état du verrou avec des instructions atomiques, par exemple le passer de manière atomique de l'état non acquis à acquis, en utilisant une instruction compare-and-exchange atomique (de telles instructions s'effectuent entièrement dans l'espace utilisateur et le noyau ne conserve aucune information sur l'état du verrou). D'un autre côté, un thread peut être incapable d'acquérir un verrou parce qu'il est déjà acquis par un autre thread. Il peut alors passer l'attribut du verrou en tant que mot futex, et la valeur représentant l'état acquis en tant que valeur attendue pour l'opération d'attente de futex(). Cette opération futex() bloquera si et seulement si le verrou est encore acquis (c'est-à-dire si la valeur du mot futex correspond toujours à « l'état acquis »). Lorsque le verrou est relâché, le thread doit d'abord réinitialiser l'état du verrou sur non acquis puis exécuter une opération futex qui réveille les threads bloqués par le drapeau de verrou utilisé en tant que mot futex (cela peut être mieux optimisé pour éviter les réveils inutiles). Voir futex(7) pour plus de détails sur la manière d'utiliser les futex.

Outre la fonctionnalité de base du futex consistant à attendre et à réveiller, d'autres opérations futex visent à gérer des cas d'utilisation plus complexes.

Remarquez qu'aucune initialisation ou destruction explicite n'est nécessaire pour utiliser les futex ; le noyau ne garde un futex (c'est-à-dire un artefact d'implémentation interne au noyau) que pendant que les opérations telles que FUTEX_WAIT, décrite ci-dessous, s'effectuent sur un mot futex en particulier.

Le paramètre uaddr pointe vers un mot futex. Sur toutes les plateformes, les futex sont des entiers de quatre octets qui doivent être alignés sur une limite de quatre octets. L'opération à effectuer sur le futex est indiquée dans le paramètre de futex_op ; val est une valeur dont la signification et l'objectif dépendent de futex_op.

Les autres paramètres (timeout, uaddr2 et val3) ne sont nécessaires que pour certaines opérations futex décrites ci-dessous. Si un de ces arguments n'est pas nécessaire, il est ignoré.

Pour plusieurs opérations de blocage, le paramètre timeout est un pointeur vers une structure timespec qui indique la durée maximale de l'opération. Toutefois, contrairement au prototype décrit ci-dessus, pour certaines opérations, les quatre octets les moins significatifs de ce paramètre sont utilisés comme un entier dont la signification est déterminée par l'opération. Pour ces opérations, le noyau diffuse la valeur timeout d'abord à unsigned long, puis à uint32_t, et dans le reste de cette page, ce paramètre est désigné par val2 quand il est interprété de cette manière.

Lorsqu'il est nécessaire, le paramètre uaddr2 est un pointeur vers un deuxième mot futex utilisé par l'opération.

L'interprétation du paramètre de l'entier final, val3, dépend de l'opération.

Le paramètre futex_op est en deux parties : une commande qui indique l'opération à effectuer et un bit ORed avec zéro ou plusieurs options qui changent le comportement de l'opération. Les options qui peuvent être incluses dans futex_op sont les suivantes :

Ce bit d'option peut être utilisé avec toutes les opérations futex. Il dit au noyau que le futex est un processus privé non partagé avec d'autres processus (c'est-à-dire qu'il n'est utilisé que pour la synchronisation entre les threads du même processus). Cela permet au noyau d'effectuer des optimisations de performance supplémentaires.
Par commodité, <linux/futex.h> définit un ensemble de constantes dont le suffixe est _PRIVATE et qui sont équivalentes à toutes les opérations listées ci-dessous mais avec l'attribut FUTEX_PRIVATE_FLAG ORed dans la valeur de la constante. On trouve ainsi FUTEX_WAIT_PRIVATE, FUTEX_WAKE_PRIVATE et ainsi de suite.
Ce bit d'option ne peut être utilisé qu'avec les opérations FUTEX_WAIT_BITSET, FUTEX_WAIT_REQUEUE_PI (depuis Linux 4.5), FUTEX_WAIT (depuis Linux 4.5) et FUTEX_LOCK_PI2 (depuis Linux 5.14).
Si cette option est positionnée, le noyau mesure le timeout par rapport à l'horloge CLOCK_REALTIME.
Si cette option n'est pas positionnée, le noyau mesure le timeout par rapport à l'horloge CLOCK_MONOTONIC.

L'opération indiquée dans futex_op prend une de ces valeurs :

Cette option teste que la valeur du mot futex vers laquelle pointe l'adresse uaddr contient toujours la valeur val attendue, et si tel est le cas, elle s'endort jusqu'à une opération FUTEX_WAKE sur le mot futex. Le chargement de la valeur du mot futex est un accès en mémoire atomique (c'est-à-dire qu'il utilise des instructions machine atomiques de l'architecture concernée). Ce chargement, la comparaison avec la valeur attendue et la mise en sommeil s'effectuent de manière atomique et sont totalement organisés selon les autres opérations futex sur le même mot futex. Si le thread commence à dormir, il est considéré comme en attente de ce mot futex. Si la valeur futex ne correspond pas à val, l'appel échoue immédiatement avec l'erreur EAGAIN.
Le but de la comparaison avec la valeur attendue est d'empêcher des réveils perdus. Si un autre thread a changé la valeur du mot futex après que le thread a décidé de se bloquer en se fondant sur la valeur d'avant, et si l'autre thread a effectué une opération FUTEX_WAKE (ou un réveil équivalent) après le changement de cette valeur et avant cette opération FUTEX_WAIT, le thread appelant observera cette valeur et ne commencera pas à dormir.
Si le timeout n'est pas NULL, la structure vers laquelle il pointe indique un délai d'attente (cet intervalle sera arrondi à la valeur supérieure à partir de la granularité de l'horloge système et il est garanti de ne pas expirer en avance). Le délai est mesuré par défaut par rapport à l'horloge CLOCK_MONOTONIC mais depuis Linux 4.5, l'horloge CLOCK_REALTIME peut être choisie en indiquant FUTEX_CLOCK_REALTIME dans futex_op. Si le timeout est NULL, l'appel se bloque indéfiniment.
Remarque : pour FUTEX_WAIT, le timeout est interprété comme une valeur relative. Cela diffère des autres opérations futex où le timeout est interprété comme une valeur absolue. Pour obtenir l'équivalent de FUTEX_WAIT, avec un délai absolu, utilisez FUTEX_WAIT_BITSET en indiquant val3 comme FUTEX_BITSET_MATCH_ANY.
Les paramètres uaddr2 et val3 sont ignorés.
Cette opération réveille jusqu'à val éléments en attente (comme dans FUTEX_WAIT) sur le mot futex à l'adresse uaddr. Généralement, val est indiqué soit sous la forme de 1 (réveil d'un seul élément en attente) soit avec INT_MAX (réveil de tous les éléments en attente). Vous n'avez aucune garantie quant aux éléments qui sont réveillés (par exemple un élément en attente dont la priorité d'ordonnancement élevée n'est pas garanti de se réveiller avant un autre d'une priorité plus basse).
Les paramètres timeout, uaddr2 et val3 sont ignorés.
Cette opération crée un descripteur de fichier associé au futex sur uaddr. L'appelant doit fermer le descripteur de fichier renvoyé après l'avoir utilisé. Quand un autre processus ou un autre thread effectue un FUTEX_WAKE sur le mot futex, le descripteur de fichier indique qu'il est accessible en lecture avec select(2), poll(2), et epoll(7)
Le descripteur de fichier peut être utilisé pour avoir des notifications asynchrones, si val n'est pas nul, puis, quand un autre processus ou un autre thread exécute FUTEX_WAKE, l'appelant recevra le numéro du signal passé à val.
Les paramètres timeout, uaddr2 et val3 sont ignorés.
Parce qu'il était de façon inhérente sujet à des situations de concurrence, FUTEX_FD a été supprimé de Linux 2.6.26 et les suivants.
Cette opération effectue la même chose que FUTEX_CMP_REQUEUE (voir ci-dessous), sauf qu'elle ne vérifie rien en utilisant la valeur dans val3 (le paramètre val3 est ignoré).
Cette opération vérifie d'abord si l'emplacement uaddr contient toujours la valeur val3. Si tel n'est pas le cas, l'opération échoue avec l'erreur EAGAIN. Si tel est le cas, l'opération réveille un maximum de val éléments en attente du futex sur uaddr. S'il y a plus de val éléments en attente, les autres sont supprimés de la file d'attente du futex source sur uaddr et ajoutés à la file d'attente du futex cible sur uaddr2. Le paramètre val2 indique une limite supérieure du nombre d'éléments remis en attente dans le futex sur uaddr2.
Le chargement à partir de uaddr est un accès atomique en mémoire (c'est-à-dire qu'il utilise les instructions machine atomiques de l'architecture concernée). Ce chargement, la comparaison avec val3 et la remise en attente d'éléments s'effectuent de manière atomique et sont totalement organisées par rapport aux autres opérations sur le même mot futex.
Les valeurs classiques qu'on indique à val sont 0 ou 1 (indiquer INT_MAX n'est pas utile car cela rendrait l'opération FUTEX_CMP_REQUEUE équivalente à FUTEX_WAKE). La valeur limite indiquée avec val2 est généralement 1 ou INT_MAX (indiquer 0 en paramètre n'est pas utile car cela rendrait l'opération FUTEX_CMP_REQUEUE équivalente à FUTEX_WAIT).
L'opération FUTEX_CMP_REQUEUE a été ajoutée pour remplacer l'ancienne FUTEX_REQUEUE. La différence est que la vérification de la valeur sur uaddr peut être utilisée pour s'assurer que la remise en attente ne se produit que sous certaines conditions, ce qui évite les conflits de mémoire (race conditions) dans certains cas d'utilisation.
FUTEX_REQUEUE et FUTEX_CMP_REQUEUE peuvent être utilisées pour éviter des réveils en troupeau (thundering herd) qui peuvent survenir quand on utilise FUTEX_WAKE dans des cas où tous les éléments en attente qu'on réveille doivent acquérir un autre futex. Imaginons le scénario suivant où plusieurs threads attendent en B, une file d'attente implémentée en utilisant un futex :

lock(A)
while (!check_value(V)) {
    unlock(A);
    block_on(B);
    lock(A);
};
unlock(A);

Si un thread qui se réveille utilisait FUTEX_WAKE, tous les éléments attendant en B se réveilleraient et essaieraient d'acquérir le verrou A. Cependant, réveiller tous ces threads de cette manière serait vain car tous les threads, sauf un, se bloqueraient immédiatement à nouveau via le verrou A. Au contraire, une remise dans la file d'attente ne réveille qu'un élément et déplace les autres sur le verrou A et quand celui réveillé déverrouille A, le suivant peut continuer.
Cette opération a été ajoutée pour prendre en charge certains cas d'utilisation de l'espace utilisateur où plus d'un futex à la fois doit être géré. L'exemple le plus frappant est l'implémentation de pthread_cond_signal(3), qui nécessite des opérations sur deux futex, une pour implémenter le mutex, l'autre pour utiliser dans l'implémentation de la file d'attente associée à la variable conditionnelle. FUTEX_WAKE_OP permet d'implémenter de tels cas sans augmenter le nombre de conflits et de changement de contexte.
L'opération FUTEX_ WAKE_OP revient à exécuter le code suivant de manière atomique et complètement organisé en fonction des opérations futex sur un des deux mots futex fournis :

uint32_t oldval = *(uint32_t *) uaddr2;
*(uint32_t *) uaddr2 = oldval op oparg;
futex(uaddr, FUTEX_WAKE, val, 0, 0, 0);
if (oldval cmp cmparg)
    futex(uaddr2, FUTEX_WAKE, val2, 0, 0, 0);

En d'autres termes, FUTEX_WAKE_OP fait ce qui suit :
  • sauvegarde la valeur d'origine du mot futex sur uaddr2 et effectue une opération pour modifier la valeur du futex sur uaddr2 ; il s'agit d'un accès en mémoire read-modify-write atomique (c'est-à-dire d'une utilisation des instructions machine atomiques liées à l'architecture concernée)
  • réveille un maximum de val éléments en attente sur le futex pour le mot futex sur uaddr ;
  • et selon les résultats d'un test de la valeur d'origine du mot futex sur uaddr2, réveille un maximum de val2 éléments en attente du mot futex sur le futex sur uaddr2.
L'opération et la comparaison qui doivent être effectuées sont encodées dans les bits du paramètre val3. Visuellement, l'encodage est :

+---+---+-----------+-----------+
|op |cmp|   oparg   |  cmparg   |
+---+---+-----------+-----------+
  4   4       12          12    <== # of bits

Exprimé en code, l'encodage est :

#define FUTEX_OP(op, oparg, cmp, cmparg) \
                (((op & 0xf) << 28) | \
                ((cmp & 0xf) << 24) | \
                ((oparg & 0xfff) << 12) | \
                (cmparg & 0xfff))

Dans ce qui précède, op et cmp sont chacun des codes listés ci-dessous. Les composants oparg et cmparg sont des valeurs numériques littérales, sauf les remarques ci-dessous.
Le composant op prend une de ces valeurs :

FUTEX_OP_SET        0  /* uaddr2 = oparg; */
FUTEX_OP_ADD        1  /* uaddr2 += oparg; */
FUTEX_OP_OR         2  /* uaddr2 |= oparg; */
FUTEX_OP_ANDN       3  /* uaddr2 &= ~oparg; */
FUTEX_OP_XOR        4  /* uaddr2 ^= oparg; */

En outre, comparer bit à bit (ORing) la valeur suivante dans op a pour conséquence que (1 << oparg) sera utilisé en tant qu'opérande :

FUTEX_OP_ARG_SHIFT  8  /* Utiliser (1 << oparg) comme opérande */

Le champ cmp prend une de ces valeurs :

FUTEX_OP_CMP_EQ     0  /* si (oldval == cmparg) réveiller */
FUTEX_OP_CMP_NE     1  /* si (oldval != cmparg) réveiller */
FUTEX_OP_CMP_LT     2  /* si (oldval < cmparg) réveiller */
FUTEX_OP_CMP_LE     3  /* si (oldval <= cmparg) réveiller */
FUTEX_OP_CMP_GT     4  /* si (oldval > cmparg) réveiller */
FUTEX_OP_CMP_GE     5  /* si (oldval >= cmparg) réveiller */

Le code de retour de FUTEX_WAKE_OP est la somme du nombre d'éléments en attente réveillés par le futex uaddr et du nombre d'éléments en attente réveillés sur le futex uaddr2.
Cette opération est équivalente à FUTEX_WAIT, sauf que val3 est utilisé pour fournir un masque de bit de 32 bits au noyau. Ce masque, où au moins un bit doit être positionné, est stocké dans la partie interne du noyau de l'élément en attente. Voir la description de FUTEX_WAKE_BITSET pour plus de détails.
Si timeout n'est pas NULL, la structure vers laquelle il pointe indique un délai absolu de l'opération d'attente. Si timeout est NULL, l'opération peut se bloquer indéfiniment.
L'argument uaddr2 est ignoré.
Cette opération est identique à FUTEX_WAKE, sauf que le paramètre val3 est utilisé pour fournir un masque de bit de 32 bits au noyau. Ce masque, où au moins un bit doit être positionné, est utilisé pour choisir les éléments en attente qui doivent être réveillés. Le choix se fait par une comparaison bit à bit AND du masque de bit « wait » (à savoir la valeur de val3) et par un masque de bit stocké dans la partie interne de l'élément en attente (le masque de bit « wait » positionné en utilisant FUTEX_WAIT_BITSET). Tous les éléments en attente pour lesquels le AND est positif sont réveillés ; les autres restent endormis.
L'effet de FUTEX_WAIT_BITSET et de FUTEX_WAKE_BITSET est de permettre un réveil sélectif parmi les éléments en attente bloqués sur le même futex. Cependant, remarquez que selon le cas, l'utilisation de cette fonction de mélange de masques de bit sur un futex peut être moins efficace que le fait d'avoir plusieurs futex, car elle a besoin que le noyau vérifie tous les éléments en attente sur un futex, y compris ceux non concernés par le réveil (à savoir qu'ils n'ont pas de bit pertinent positionné dans leur masque de bit « wait »).
La constante FUTEX_BITSET_MATCH_ANY, qui correspond à tous les positionnements 32 bits du masque, peut être utilisé en tant que val3 de FUTEX_WAIT_BITSET et de FUTEX_WAKE_BITSET. En dehors des différences dans la gestion du paramètre timeout, l'opération FUTEX_WAIT est équivalente à FUTEX_WAIT_BITSETval3 est indiqué en tant que FUTEX_BITSET_MATCH_ANY ; c'est-à-dire permettre le réveil par n'importe quel élément en attente). L’opération FUTEX_WAKE est équivalente à FUTEX_WAKE_BITSETval3 est indiqué en tant que FUTEX_BITSET_MATCH_ANY ; c'est-à-dire, réveiller n’importe quel élément en attente.
Les arguments uaddr2 et timeout sont ignorés.

Linux prend en charge l'héritage de priorité (priority inheritance, PI) des futex, afin de gérer des problèmes d'inversion des priorités qu'on peut rencontrer avec des verrous futex normaux. L'inversion des priorités est un problème qui survient quand une tâche de haute priorité est bloquée en attente d'acquérir un verrou que possède une tâche de basse priorité issue du processeur. Du coup, la tâche de priorité basse ne va pas relâcher le verrou et celle de haute priorité reste bloquée.

L'héritage de priorité est un mécanisme pour gérer le problème d'inversion des priorités. Avec ce mécanisme, quand une tâche à haute priorité est bloquée par un verrou possédé par une tâche à basse priorité, la priorité de la seconde est temporairement amenée au même niveau que celle à haute priorité, de sorte qu'elle ne soit pas doublée par une tâche de niveau intermédiaire et qu'elle puisse ainsi avancer pour relâcher le verrou. Pour fonctionner, l'héritage de priorité doit être transitif, ce qui signifie que si une tâche à haute priorité bloque sur le verrou d'une tâche à priorité intermédiaire (et ainsi de suite sur des chaînes de la taille de votre choix), les deux tâches (ou plus généralement toutes les tâches de la chaîne de verrous) voient leur niveau de priorité amené à celui de la tâche à haute priorité.

Du point de vue de l'espace utilisateur, le futex a conscience d'un PI en acceptant une réglementation (décrite ci-dessous) entre l'espace utilisateur et le noyau sur la valeur du mot futex, couplé à l'utilisation d'opérations futex PI décrites ci-dessous (contrairement aux autres opérations futex décrites ci-dessus, celles PI-futex sont conçues pour l'implémentation de mécanismes IPC très spécifiques).

Les opérations PI-futex décrites ci-dessous diffèrent des autres opérations dans le sens où elles imposent des règles dans l'utilisation de la valeur du mot futex :

  • Si le verrou n'est pas acquis, la valeur du mot futex doit être 0.
  • Si le verrou est acquis, la valeur du mot futex doit être l'ID du thread (TID ; voir gettid(2)) du thread propriétaire.
  • Si le verrou a un propriétaire et s'il y a des threads en concurrence pour le verrou, le bit FUTEX_WAITERS doit être positionné dans la valeur du mot futex ; autrement dit, cette valeur est :

FUTEX_WAITERS | TID

(Remarquez que cela n'est pas possible pour un mot futex PI d'être sans propriétaire ni FUTEX_WAITERS défini).

Avec cette règle, une application de l'espace utilisateur peut acquérir un verrou non acquis ou en relâcher un en utilisant des instructions atomiques dans l'espace utilisateur (comme une opération compare-and-swap telle que cmpxchg sur l'architecture x86). L'acquisition d'un verrou consiste simplement dans l'utilisation de compare-and-swap pour positionner la valeur du mot futex de manière atomique sur le TID de l'appelant si sa valeur précédente était 0. Relâcher un verrou exige d'utiliser compare-and-swap pour positionner la valeur du mot futex sur 0 si la valeur précédente était le TID prévu.

Si un futex est déjà acquis (c'est-à-dire qu'il a une valeur positive), les éléments en attente doivent utiliser l'opération FUTEX_LOCK_PI pour acquérir le verrou. Si d'autres threads attendent le verrou, le bit FUTEX_WAITERS est défini dans la valeur du futex ; dans ce cas le détenteur du verrou doit utiliser l'opération FUTEX_UNLOCK_PI pour relâcher le verrou.

Dans le cas où les appelants sont bloqués dans le noyau (c'est-à-dire qu'ils doivent effectuer un appel futex()), ils traitent directement avec ce qu'on appelle un RT-mutex, un mécanisme de verrouillage du noyau qui implémente la sémantique de l'héritage de priorité requis. Après que le RT-mutex est acquis, la valeur futex est mise à jour en fonction, avant que le thread appelant ne renvoie vers l'espace utilisateur.

Il est important de remarquer que le noyau mettra à jour la valeur du mot futex avant de renvoyer vers l'espace utilisateur (cela enlève la possibilité pour la valeur d'un mot futex de se terminer dans un état non valable, par exemple en ayant un propriétaire mais en ayant la valeur 0, ou en ayant des éléments en attente mais aucun bit FUTEX_WAITERS positionné).

Si un futex a un RT-mutex associé dans le noyau (c'est-à-dire qu'il y a des éléments en attente bloqués) et si le propriétaire du futex/RT-mutex meurt de manière inattendue, le noyau nettoie le RT-mutex et passe la main au prochain élément en attente. Cela implique, en retour, que la valeur dans l'espace utilisateur soit mise à jour en fonction. Pour dire que c'est nécessaire, le noyau positionne le bit FUTEX_OWNER_DIED dans le mot futex ainsi que dans l'ID du thread du nouveau propriétaire. L'espace utilisateur peut détecter cette situation par la présence du bit FUTEX_OWNER_DIED et il est alors responsable pour nettoyer l'espace laissé par le propriétaire mort.

Les PI futex sont utilisés en indiquant une des valeurs listées ci-dessous dans futex_op. Remarquez que les opérations de PI futex doivent être utilisées par paires et sont soumises à des exigences supplémentaires :

  • FUTEX_LOCK_PI, FUTEX_LOCK_PI2 et FUTEX_TRYLOCK_PI vont de pair avec FUTEX_UNLOCK_PI. FUTEX_UNLOCK_PI ne doit être appelé que sur un futex appartenant au thread appelant, tel que défini par les règles de la valeur, sans quoi on obtient l'erreur EPERM.
  • FUTEX_WAIT_REQUEUE_PI va de pair avec FUTEX_CMP_REQUEUE_PI. Elles doivent s'effectuer depuis un futex non-PI vers un PI futex distinct (sans quoi on obtient l'erreur EINVAL). De plus, val (le nombre d'éléments en attente à réveiller) doit être de 1 (sans quoi on obtient l'erreur EINVAL).

Les opérations PI futex sont comme suit :

Cette opération est utilisée après avoir essayé sans succès d'acquérir un verrou en utilisant une instruction atomique en mode utilisateur, car le mot futex a une valeur positive – en particulier parce qu'il contenait le TID (spécifique à l’espace de noms PID) du verrou propriétaire.
L'opération vérifie la valeur du mot futex sur l'adresse uaddr. Si la valeur est de 0, le noyau essaie de positionner de manière atomique la valeur du futex sur le TID de l'appelant. Si la valeur du mot futex est positive, le noyau positionne de manière atomique le bit FUTEX_WAITERS, qui signale au propriétaire du futex qu'il ne peut pas déverrouiller le futex dans l'espace utilisateur de manière atomique, en positionnant la valeur du futex à 0. Après cela, le noyau :
(1)
Essaie de trouver le thread associé au TID du propriétaire.
(2)
Crée ou réutilise l'état du noyau sur la base du propriétaire (s'il s'agit du premier élément en attente, il n'existe pas d'état du noyau pour ce futex, donc il est créé en verrouillant le RT-mutex et le propriétaire du futex devient propriétaire du RT-mutex). Si des éléments en attente existent, l'état existant est réutilisé.
(3)
Rattache l'élément en attente au futex (c'est-à-dire que l'élément est mis dans la file d'attente du RT-futex).
S'il existe plus d'un élément en attente, la mise dans la file d'un élément se fait par ordre de priorité descendant (pour des informations sur l'ordre des priorités, voir les points sur l'ordonnancement SCHED_DEADLINE, SCHED_FIFO et SCHED_RR dans sched(7)). Le propriétaire hérite soit de la bande passante de processeur de l'élément en attente (si l'élément est programmé sous la règle SCHED_DEADLINE ou SCHED_FIFO), soit de la priorité de l'élément en attente (s'il est programmé sous la règle SCHED_RR ou SCHED_FIFO). Cet héritage suit la chaîne de verrous dans les cas de verrous imbriqués et il effectue la détection des verrous morts (deadlocks).
Le paramètre timeout fournit un délai de tentative de verrouillage. Si timeout est positif, la structure vers laquelle il pointe indique un délai absolu mesuré en fonction de l'horloge CLOCK_REALTIME. Si timeout est NULL, l'opération se bloquera indéfiniment.
Les paramètres uaddr2, val et val3 sont ignorés.
Cette opération est la même que FUTEX_LOCK_PI, sauf que l'horloge par rapport à laquelle timeout est mesuré peut être sélectionnée. Par défaut, le délai (absolu) indiqué dans timeout est mesuré par rapport à l'horloge CLOCK_MONOTONIC mais si l'attribut FUTEX_CLOCK_REALTIME est indiqué dans futex_op, le délai est mesuré par rapport à l'horloge CLOCK_REALTIME.
L'opération essaie d'acquérir le verrou sur uaddr. Elle est appelée quand l'acquisition atomique dans l'espace utilisateur n'a pas réussi parce que le mot futex ne valait pas 0.
Du fait que le noyau accède à plus d'informations d'état que l'espace utilisateur, l'acquisition du verrou pourrait réussir si elle est effectuée par le noyau dans les cas où le mot futex (c'est-à-dire les informations d'état accessibles dans l'espace utilisateur) contient un état stable (FUTEX_WAITERS et/ou FUTEX_OWNER_DIED). Cela peut arriver quand le propriétaire du futex est mort. L'espace utilisateur ne peut pas gérer cette condition de manière "race-free", mais le noyau peut corriger cela et acquérir le futex.
Les paramètres uaddr2, val, timeout et val3 sont ignorés.
Cette opération réveille l'élément ayant la plus haute priorité et attendant un FUTEX_LOCK_PI ou un FUTEX_LOCK_PI2 à l'adresse indiquée par le paramètre uaddr.
Cela est appelé quand la valeur dans l'espace utilisateur sur uaddr ne peut pas être passée à 0 de manière atomique depuis un TID (du propriétaire).
Les paramètres uaddr2, val, timeout et val3 sont ignorés.
Cette opération est une variante PI-aware de FUTEX_CMP_REQUEUE. Elle remet en attente des éléments bloqués avec FUTEX_WAIT_REQUEUE_PI sur uaddr à partir d'un futex source non-PI (uaddr) vers un futex cible PI (uaddr2).
Comme avec FUTEX_CMP_REQUEUE, cette opération réveille un maximum de val éléments qui attendent le futex sur uaddr. Toutefois, pour FUTEX_CMP_REQUEUE_PI, val doit valoir 1 (puisque son but principal est d'éviter l’effet de troupeau (thundering herd). Les autres éléments sont supprimés de la file d'attente du futex source sur uaddr et ajoutés sur celle du futex cible sur uaddr2.
Les paramètres val2 et val3 ont le même objectif qu'avec FUTEX_CMP_REQUEUE.
Attendre un futex non-PI sur uaddr et se mettre potentiellement en attente (avec une opération FUTEX_CMP_REQUEUE_PI dans une autre tâche), d'un futex PI sur uaddr2. L'opération d'attente sur uaddr est la même que pour FUTEX_WAIT.
L'élément peut être retiré de la file d'attente sur uaddr sans être transféré sur uaddr2 à l’aide d’une opération FUTEX_WAKE dans une autre tâche. Dans ce cas, l'opération FUTEX_WAIT_REQUEUE_PI échoue avec l'erreur EAGAIN.
Si timeout n'est pas NULL, la structure vers laquelle il pointe indique un délai absolu de l'opération d'attente. Si timeout est NULL, l'opération peut se bloquer indéfiniment.
L'argument val3 est ignoré.
FUTEX_WAIT_REQUEUE_PI et FUTEX_CMP_REQUEUE_PI ont été ajoutés pour gérer un cas d'utilisation bien particulier : la prise en charge des variables conditionnelles de threads POSIX ayant connaissance de l'héritage de priorité. L'idée est que ces opérations devraient toujours aller par paires, afin de garantir que l'espace utilisateur et le noyau restent toujours synchronisés. Ainsi, dans l'opération FUTEX_WAIT_REQUEUE_PI, l'application dans l'espace utilisateur pré-indique la cible de la remise en attente qui va se faire dans l'opération FUTEX_CMP_REQUEUE_PI.

En cas d'erreur (en supposant que futex() a été appelé à l’aide de syscall(2)), toutes les opérations renvoient -1 et positionnent errno pour indiquer l'erreur.

En cas de succès, le code de retour dépend de l'opération, comme décrit dans la liste suivante :

Renvoie 0 si l'appelant a été réveillé. Remarquez qu'un réveil peut également résulter de l'utilisation de motifs d'utilisation classiques de futex dans du code non lié qui a pu utiliser l'emplacement mémoire du mot futex (par exemple des implémentations classiques basées sur futex de mutex Pthreads peuvent provoquer cela dans certaines conditions). Donc, les appelants devraient toujours, à titre conservatoire, supposer qu'un code de retour 0 peut signifier un faux réveil, et donc utiliser la valeur du mot futex (à savoir le schéma de synchronisation de l'espace utilisateur) pour décider de rester bloqués ou pas.
Renvoie le nombre de processus en attente qui ont été réveillés.
Renvoie le nouveau descripteur de fichier associé au futex.
Renvoie le nombre de processus en attente qui ont été réveillés.
Renvoie le nombre total d'éléments en attente réveillés ou remis dans la file du futex pour le mot futex sur uaddr2. Si cette valeur est supérieure à val, la différence devient le nombre d'éléments en attente remis dans la file du futex pour le mot futex sur uaddr2.
Renvoie le nombre total d'éléments en attente réveillés. Il s'agit de la somme des éléments réveillés sur les deux futex pour les mots futex sur uaddr et uaddr2.
Renvoie 0 si l'appelant a été réveillé. Voir FUTEX_WAIT sur la manière d'interpréter cela correctement en pratique.
Renvoie le nombre de processus en attente qui ont été réveillés.
Renvoie 0 si le futex a appliqué le verrou avec succès.
Renvoie 0 si le futex a appliqué le verrou avec succès.
Renvoie 0 si le futex a appliqué le verrou avec succès.
Renvoie 0 si le futex a correctement enlevé le verrou.
Renvoie le nombre total d'éléments en attente réveillés ou remis dans la file du futex pour le mot futex sur uaddr2. Si cette valeur est supérieure à val, la différence devient le nombre d'éléments en attente remis dans la file du futex pour le mot futex sur uaddr2.
Renvoie 0 si l'appelant a été mis dans la file d'attente avec succès au futex pour le mot futex sur uaddr2.

Pas d'accès en lecture à la mémoire d'un mot futex.
(FUTEX_WAIT, FUTEX_WAIT_BITSET, FUTEX_WAIT_REQUEUE_PI) La valeur vers laquelle pointait uaddr n'était pas égale à la valeur val attendue au moment de l'appel.
Remarque : sur Linux, les noms symboliques EAGAIN et EWOULDBLOCK (les deux apparaissent dans différents endroits du code futex du noyau) ont la même valeur.
(FUTEX_CMP_REQUEUE, FUTEX_CMP_REQUEUE_PI) La valeur vers laquelle pointait uaddr n'était pas égale à la valeur val3 attendue.
(FUTEX_LOCK_PI, FUTEX_LOCK_PI2, FUTEX_TRYLOCK_PI, FUTEX_CMP_REQUEUE_PI) L'ID du thread propriétaire du futex sur uaddr (pour FUTEX_CMP_REQUEUE_PI : uaddr2) est sur le point de se terminer, mais il n'a pas encore géré le nettoyage de l'état interne. Réessayez.
(FUTEX_LOCK_PI, FUTEX_LOCK_PI2, FUTEX_TRYLOCK_PI, FUTEX_CMP_REQUEUE_PI) Le mot futex sur uaddr est déjà verrouillé par l'appelant.
(FUTEX_CMP_REQUEUE_PI) Pendant qu'il remettait en attente un élément du PI futex pour le mot futex sur uaddr2, le noyau a détecté un verrou mort (deadlock).
Le paramètre d'un pointeur nécessaire (c'est-à-dire uaddr, uaddr2 ou timeout) ne pointait pas vers une adresse valable de l'espace utilisateur.
Une opération FUTEX_WAIT ou FUTEX_WAIT_BITSET a été interrompue par un signal (voir signal(7)). Dans Linux 2.6.22, cette erreur pouvait aussi être renvoyée pour un faux réveil ; depuis Linux 2.6.22, cela n'arrive plus.
L'opération dans futex_op fait partie de celles qui utilisent un délai, mais le paramètre timeout fourni n'était pas valable (tv_sec valait moins de 0 ou tv_nsec ne valait pas moins de 1 000 000 000).
L'opération indiquée dans futex_op utilise uaddr et/ou uaddr2 mais l'un d'eux ne pointe pas vers un objet valable — c'est-à-dire, l'adresse n'est pas alignée sur quatre octets.
(FUTEX_WAIT_BITSET, FUTEX_WAKE_BITSET) Le masque de bit fourni dans val3 vaut zéro.
(FUTEX_CMP_REQUEUE_PI) uaddr est égal à uaddr2 (c'est-à-dire qu'une remise en attente a été tentée sur le même futex).
(FUTEX_FD) Le numéro du signal fourni dans val n'est pas valable.
(FUTEX_WAKE, FUTEX_WAKE_OP, FUTEX_WAKE_BITSET, FUTEX_REQUEUE, FUTEX_CMP_REQUEUE) Le noyau a détecté une incohérence entre l'état de l'espace utilisateur sur uaddr et l'état du noyau — c'est-à-dire qu'il a détecté un élément qui attend dans FUTEX_LOCK_PI ou FUTEX_LOCK_PI2 sur uaddr.
(FUTEX_LOCK_PI, FUTEX_LOCK_PI2, FUTEX_TRYLOCK_PI, FUTEX_UNLOCK_PI) Le noyau a détecté une incohérence entre l'état de l'espace utilisateur sur uaddr et l'état du noyau. Cela indique soit une corruption d'état, soit que le noyau a trouvé un élément en attente sur uaddr qui attend aussi à l'aide de FUTEX_WAIT ou de FUTEX_WAIT_BITSET.
(FUTEX_CMP_REQUEUE_PI) Le noyau a détecté une incohérence entre l'état de l'espace utilisateur sur uaddr et l'état du noyau ; c'est-à-dire qu'il a détecté un élément qui attend via FUTEX_WAIT ou FUTEX_WAIT_BITSET sur uaddr2.
(FUTEX_CMP_REQUEUE_PI) Le noyau a détecté une incohérence entre l'état de l'espace utilisateur sur uaddr et l'état du noyau ; c'est-à-dire qu'il a détecté un élément qui attend à l'aide de FUTEX_WAIT ou de FUTEX_WAIT_BITESET sur uaddr.
(FUTEX_CMP_REQUEUE_PI) Le noyau a détecté une incohérence entre l'état de l'espace utilisateur sur uaddr et l'état du noyau ; c'est-à-dire qu'il a détecté un élément qui attend à l'aide de FUTEX_LOCK_PI ou de FUTEX_LOCK_PI2 (au lieu de FUTEX_WAIT_REQUEUE_PI).
(FUTEX_CMP_REQUEUE_PI) Tentative de remise dans la file d'un élément en attente vers un futex différent de celui indiqué avec l'appel FUTEX_WAIT_REQUEUE_PI correspondant pour cet élément.
(FUTEX_CMP_REQUEUE_PI) Le paramètre val ne vaut pas 1.
Argument incorrect.
(FUTEX_FD) La limite du nombre total de fichiers ouverts sur le système a été atteinte.
(FUTEX_LOCK_PI, FUTEX_LOCK_PI2, FUTEX_TRYLOCK_PI, FUTEX_CMP_REQUEUE_PI) Le noyau n'a pas pu allouer de la mémoire pour conserver les informations d'état.
Opération non valable indiquée dans futex_op.
L'option FUTEX_CLOCK_REALTIME était indiquée dans futex_op, mais l'opération qui l'accompagne n'est ni FUTEX_WAIT, ni FUTEX_WAIT_BITSET, ni FUTEX_WAIT_REQUEUE_PI, ni FUTEX_LOCK_PI2.
(FUTEX_LOCK_PI, FUTEX_LOCK_PI2, FUTEX_TRYLOCK_PI, FUTEX_UNLOCK_PI, FUTEX_CMP_REQUEUE_PI, FUTEX_WAIT_REQUEUE_PI) Une vérification pendant l'exécution a déterminé que l'opération n'est pas disponible. Les opérations PI-futex ne sont pas implémentées sur toutes les architectures et ne sont pas prises en charge sur certaines variantes de processeur.
(FUTEX_LOCK_PI, FUTEX_LOCK_PI2, FUTEX_TRYLOCK_PI, FUTEX_CMP_REQUEUE_PI) L'appelant n'est pas autorisé à se rattacher au futex sur uaddr (pour FUTEX_CMP_REQUEUE_PI : le futex sur uaddr2) (cela peut venir d'une corruption de l'état dans l'espace utilisateur).
(FUTEX_UNLOCK_PI) Le verrou représenté par le mot futex n'appartient pas à l'appelant.
(FUTEX_LOCK_PI, FUTEX_LOCK_PI2, FUTEX_TRYLOCK_PI, FUTEX_CMP_REQUEUE_PI) L'ID du thread dans le mot futex sur uaddr n'existe pas.
(FUTEX_CMP_REQUEUE_PI) L'ID du thread dans le mot futex sur uaddr2 n'existe pas.
L'opération de futex_op a utilisé un délai indiqué dans timeout et le délai a expiré avant la fin de l'opération.

Linux.

Linux 2.6.0.

La prise en charge initiale des futex a été ajoutée dans Linux 2.5.7 mais avec une sémantique différente de celle décrite ci‐dessus. Un appel système à 4 paramètres avec la sémantique décrite dans cette page a été ajouté dans Linux 2.5.40. Dans Linux 2.5.70, un cinquième paramètre a été ajouté. Un sixième paramètre a été ajouté dans Linux 2.6.7.

Le programme ci-dessous montre l'utilisation des futex dans un programme où un processus parent et un processus enfant utilisent une paire de futex située dans un tableau anonyme partagé pour synchroniser l'accès à une ressource partagée : le terminal. Les deux processus écrivent chacun un message nloops (un paramètre en ligne de commande qui vaut 5 par défaut s'il est absent) sur le terminal et ils utilisent un protocole de synchronisation pour garantir qu'ils alternent dans l'écriture des messages. Pendant l'exécution de ce programme, nous voyons un affichage comme suit :


$ ./futex_demo
Parent (18534) 0
Child  (18535) 0
Parent (18534) 1
Child  (18535) 1
Parent (18534) 2
Child  (18535) 2
Parent (18534) 3
Child  (18535) 3
Parent (18534) 4
Child  (18535) 4

/* futex_demo.c
   Usage: futex_demo [nloops]
                    (Default: 5)
   Montrer l'utilisation des futex dans un programme où le parent et
   l'enfant utilisent une paire de futex située dans un tableau anonyme
   partagé pour synchroniser l'accès à une ressource partagée : le
   terminal. Les processus écrivent chacun des messages 'num-loops'
   sur le terminal et ils utilisent un protocole de synchronisation qui
   garantit qu'ils alternent l'écriture des messages.
*/
#define _GNU_SOURCE
#include <err.h>
#include <errno.h>
#include <linux/futex.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <unistd.h>
static uint32_t *futex1, *futex2, *iaddr;
static int
futex(uint32_t *uaddr, int futex_op, uint32_t val,
      const struct timespec *timeout, uint32_t *uaddr2, uint32_t val3)
{
    return syscall(SYS_futex, uaddr, futex_op, val,
                   timeout, uaddr2, val3);
}
/* Acquérir le futex vers lequel pointe 'futexp' : attendre que sa
   valeur passe à 1 puis positionner la valeur sur 0. */
static void
fwait(uint32_t *futexp)
{
    long            s;
    const uint32_t  one = 1;
    /* atomic_compare_exchange_strong(ptr, oldval, newval)
       fait atomiquement comme :
           if (*ptr == *oldval)
               *ptr = newval;
       Il renvoie true si le test a montré true et *ptr a été mis à jour. */
    while (1) {
        /* Le futex est-il disponible ? */
        if (atomic_compare_exchange_strong(futexp, &one, 0))
            break;      /* Oui */
        /* Le futex n'est pas disponible ; attendre. */
        s = futex(futexp, FUTEX_WAIT, 0, NULL, NULL, 0);
        if (s == -1 && errno != EAGAIN)
            err(EXIT_FAILURE, "futex-FUTEX_WAIT");
    }
}
/* Relâcher le futex vers lequel pointe 'futexp' : si le futex a
  actuellement la valeur 0, positionner la valeur à 1 et réveiller tous les
  futex en attente pour que si le pair est bloqué dans fwait(), ça puisse
  continuer. */
static void
fpost(uint32_t *futexp)
{
    long            s;
    const uint32_t  zero = 0;
    /* atomic_compare_exchange_strong() a été décrit
       dans les commentaires ci-dessus. */
    if (atomic_compare_exchange_strong(futexp, &zero, 1)) {
        s = futex(futexp, FUTEX_WAKE, 1, NULL, NULL, 0);
        if (s  == -1)
            err(EXIT_FAILURE, "futex-FUTEX_WAKE");
    }
}
int
main(int argc, char *argv[])
{
    pid_t         childPid;
    unsigned int  nloops;
    setbuf(stdout, NULL);
    nloops = (argc > 1) ? atoi(argv[1]) : 5;
    /* Créer un tableau anonyme partagé qui gardera les futex.
       Comme les futex vont être partagés entre les processus, nous
        utilisons donc les opérations futex « shared » (donc pas celles
        dont le suffixe est "_PRIVATE") */
    iaddr = mmap(NULL, sizeof(*iaddr) * 2, PROT_READ | PROT_WRITE,
                 MAP_ANONYMOUS | MAP_SHARED, -1, 0);
    if (iaddr == MAP_FAILED)
        err(EXIT_FAILURE, "mmap");
    futex1 = &iaddr[0];
    futex2 = &iaddr[1];
    *futex1 = 0;        /* State: unavailable */
    *futex2 = 1;        /* State: available */
    /* Créer un processus enfant qui hérite du tableau anonyme
       partagé. */
    childPid = fork();
    if (childPid == -1)
        err(EXIT_FAILURE, "fork");
    if (childPid == 0) {        /* Child */
        for (unsigned int j = 0; j < nloops; j++) {
            fwait(futex1);
            printf("Child  (%jd) %u\n", (intmax_t) getpid(), j);
            fpost(futex2);
        }
        exit(EXIT_SUCCESS);
    }
    /* Le parent se retrouve ici. */
    for (unsigned int j = 0; j < nloops; j++) {
        fwait(futex2);
        printf("Parent (%jd) %u\n", (intmax_t) getpid(), j);
        fpost(futex1);
    }
    wait(NULL);
    exit(EXIT_SUCCESS);
}

get_robust_list(2), restart_syscall(2), pthread_mutexattr_getprotocol(3), futex(7), sched(7)

Les fichiers suivants des sources du noyau :

  • Documentation/pi-futex.txt
  • Documentation/futex-requeue-pi.txt
  • Documentation/locking/rt-mutex.txt
  • Documentation/locking/rt-mutex-design.txt
  • Documentation/robust-futex-ABI.txt

Franke, H., Russell, R., and Kirwood, M., 2002. Fuss, Futexes and Furwocks: Fast Userlevel Locking in Linux (à partir des actions d'Ottawa Linux Symposium 2002),
http://kernel.org/doc/ols/2002/ols2002-pages-479-495.pdf

Hart, D., 2009. A futex overview and update, http://lwn.net/Articles/360699/

Hart, D. et Guniguntala, D., 2009. Requeue-PI: Making Glibc Condvars PI-Aware (à partir des comptes rendus de l'atelier Real-Time Linux 2009), http://lwn.net/images/conf/rtlws11/papers/proc/p10.pdf

Drepper, U., 2011. Futexes Are Tricky, http://www.akkadia.org/drepper/futex.pdf

La bibliothèque d'exemples de futex, futex-*.tar.bz2 à
https://mirrors.kernel.org/pub/linux/kernel/people/rusty/

La traduction française de cette page de manuel a été créée par Christophe Blaess https://www.blaess.fr/christophe/, Stéphan Rafin <stephan.rafin@laposte.net>, Thierry Vignaud <tvignaud@mandriva.com>, François Micaux, Alain Portal <aportal@univ-montp2.fr>, Jean-Philippe Guérard <fevrier@tigreraye.org>, Jean-Luc Coulon (f5ibh) <jean-luc.coulon@wanadoo.fr>, Julien Cristau <jcristau@debian.org>, Thomas Huriaux <thomas.huriaux@gmail.com>, Nicolas François <nicolas.francois@centraliens.net>, Florentin Duneau <fduneau@gmail.com>, Simon Paillard <simon.paillard@resel.enst-bretagne.fr>, Denis Barbier <barbier@debian.org>, David Prévot <david@tilapin.org> et Jean-Philippe MENGUAL <jpmengual@debian.org>

Cette traduction est une documentation libre ; veuillez vous reporter à la GNU General Public License version 3 concernant les conditions de copie et de distribution. Il n'y a aucune RESPONSABILITÉ LÉGALE.

Si vous découvrez un bogue dans la traduction de cette page de manuel, veuillez envoyer un message à debian-l10n-french@lists.debian.org.

2 mai 2024 Pages du manuel de Linux 6.8