web-dev-qa-db-fra.com

WRITE_ONCE dans les listes de noyau Linux

Je lis implémentation du noyau linux de la liste chaînée doublée. Je ne comprends pas l'utilisation de la macro WRITE_ONCE(x, val). Il est défini comme suit dans compiler.h:

#define WRITE_ONCE(x, val) x=(val)

Il est utilisé sept fois dans le fichier, tel que

static inline void __list_add(struct list_head *new,
                  struct list_head *prev,
                  struct list_head *next)
{
    next->prev = new;
    new->next = next;
    new->prev = prev;
    WRITE_ONCE(prev->next, new);
}

J'ai lu qu'il est utilisé pour éviter les conditions de course.

J'ai deux questions:
1/Je pensais que la macro était remplacée par du code au moment de la compilation. Alors, comment ce code diffère-t-il du suivant? Comment cette macro peut éviter les conditions de course?

static inline void __list_add(struct list_head *new,
                  struct list_head *prev,
                  struct list_head *next)
{
    next->prev = new;
    new->next = next;
    new->prev = prev;
    prev->next = new;
}

2/Comment savoir quand l'utiliser? Par exemple, il est utilisé pour __lst_add() mais pas pour __lst_splice():

static inline void __list_splice(const struct list_head *list,
                 struct list_head *prev,
                 struct list_head *next)
{
    struct list_head *first = list->next;
    struct list_head *last = list->prev;

    first->prev = prev;
    prev->next = first;

    last->next = next;
    next->prev = last;
}

éditer:
Voici un message de validation concernant ce fichier et WRITE_ONCE, Mais cela ne m'aide pas à comprendre quoi que ce soit ...

list: Utilisez WRITE_ONCE () lors de l'initialisation des structures list_head
. Cette validation ajoute donc WRITE_ONCE () aux magasins de pointeurs de cette fonction qui pourraient affecter le pointeur -> suivant de la tête.

36
Gaut

La première définition à laquelle vous vous référez fait partie du validateur de verrouillage du noya , alias "lockdep". WRITE_ONCE (Et d'autres) n'ont pas besoin d'un traitement spécial, mais la raison pour laquelle fait l'objet d'une autre question.

La définition pertinente serait ici , et un commentaire très concis indique que leur objectif est:

Empêchez le compilateur de fusionner ou de récupérer des lectures ou des écritures.

...

Veiller à ce que le compilateur ne plie pas, ne broche pas ou ne mutile pas les accès qui ne nécessitent pas de commande ou qui interagissent avec une barrière de mémoire explicite ou une instruction atomique qui fournit la commande requise.

Mais que signifient ces mots?


Le problème

Le problème est en réalité pluriel:

  1. Lecture/écriture "déchirure": remplacement d'un seul accès mémoire par de nombreux plus petits. GCC peut (et fait!) Dans certaines situations remplacer quelque chose comme p = 0x01020304; Par deux instructions de stockage immédiat sur 16 bits - au lieu de placer vraisemblablement la constante dans un registre puis un accès à la mémoire, et ainsi de suite. WRITE_ONCE Nous permettrait de dire à GCC, "ne fais pas ça", comme ceci: WRITE_ONCE(p, 0x01020304);

  2. Les compilateurs C ont cessé de garantir qu'un accès Word est atomique. Tout programme qui n'est pas sans course peut être mal compilé avec des résultats spectaculaires. Non seulement cela, mais un compilateur peut décider de ne pas garder certaines valeurs dans les registres à l'intérieur d'une boucle, conduisant à plusieurs références qui peuvent gâcher du code comme ceci:

 for (;;) {
 owner = lock-> owner; 
 if (owner &&! mutex_spin_on_owner (lock, owner)) 
 break; 
/* ... */
} 
  1. En l'absence d'accès de "balisage" à la mémoire partagée, nous ne pouvons pas détecter automatiquement les accès involontaires de ce type. Les outils automatisés qui essaient de trouver de tels bogues ne peuvent pas les distinguer des accès intentionnellement racés.

La solution

Nous commençons par noter que le noyau Linux demande à être construit avec GCC. Ainsi, il n'y a qu'un seul compilateur dont nous devons nous occuper avec la solution, et nous pouvons utiliser son documentation comme seul guide.

Pour une solution générique, nous devons gérer les accès à la mémoire de toutes tailles. Nous avons tous les différents types de largeurs spécifiques et tout le reste. Nous notons également que nous n'avons pas besoin de marquer spécifiquement les accès à la mémoire qui sont déjà dans des sections critiques ( pourquoi pas? ).

Pour les tailles de 1, 2, 4 et 8 octets, il existe des types appropriés et volatile interdit spécifiquement à GCC d'appliquer l'optimisation dont nous avons parlé dans (1), ainsi que de prendre soin de autres cas (dernier point sous "BARRIÈRES COMPILATEURS"). Il interdit également à GCC de mal compiler la boucle dans (2), car cela déplacerait l'accès volatile sur un point de séquence, ce qui est interdit par la norme C. Linux tilise ce que nous appelons un "accès volatile" (voir ci-dessous) au lieu de marquer un objet comme volatile. Nous pourrions résoudre notre problème en marquant l'objet spécifique comme volatile, mais ce n'est (presque?) Jamais un bon choix. Il y a beaucoupraisons cela pourrait être dangereux.

Voici comment un accès volatile (écriture) est implémenté dans le noyau pour un type large 8 bits:

 * (volatile __u8_alias_t *) p = * (__ u8_alias_t *) res; 

Supposons que nous ne sachions pas exactement ce que volatile fait - et découvrir ce n'est pas facile! ( check out # 5) - une autre façon d'y parvenir serait de placer des barrières mémoire: c'est exactement ce que fait Linux dans le cas où la taille est autre que 1,2,4 ou 8, en recourant à memcpy et placer des barrières de mémoire avant et après l'appel. Les barrières de mémoire résolvent également facilement le problème (2), mais entraînent de lourdes pénalités de performances.

J'espère avoir couvert un aperçu sans me plonger dans les interprétations de la norme C, mais si vous le souhaitez, je pourrais prendre le temps de le faire.

31
Michael Foukarakis