WRITE_ONCE negli elenchi del kernel di Linux

WRITE_ONCE negli elenchi del kernel di Linux


Sto leggendo l'implementazione del kernel Linux dell'elenco collegato raddoppiato. Non capisco l'uso della macro WRITE_ONCE(x, val) . È definito come segue in compiler.h:


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

Viene utilizzato sette volte nel file, ad esempio


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);
}

Ho letto che viene utilizzato per evitare le condizioni di gara.


Ho due domande:

1/ Ho pensato che la macro fosse stata sostituita dal codice in fase di compilazione. Quindi, in che modo questo codice differisce dal seguente? In che modo questa macro può evitare le condizioni di gara?


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/ Come sapere quando dovremmo usarlo? Ad esempio, viene utilizzato per __lst_add() ma non per __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;
}

modificare:

Ecco un messaggio di commit relativo a questo file e WRITE_ONCE , ma non mi aiuta a capire niente...



Risposte:


La prima definizione a cui fai riferimento fa parte del validatore di blocco del kernel, noto anche come "lockdep". WRITE_ONCE (e altri) non hanno bisogno di un trattamento speciale, ma il motivo è oggetto di un'altra domanda.


La definizione pertinente sarebbe qui e un commento molto conciso afferma che il loro scopo è:



Ma cosa significano queste parole?



Il problema


Il problema è in realtà al plurale:



  1. Lettura/scrittura "strappo" :sostituzione di un singolo accesso alla memoria con molti più piccoli. GCC può (e lo fa!) in determinate situazioni sostituire qualcosa come p = 0x01020304; con due istruzioni di salvataggio immediate a 16 bit, invece di inserire presumibilmente la costante in un registro e quindi un accesso alla memoria e così via. WRITE_ONCE ci permetterebbe di dire a GCC "non farlo", in questo modo:WRITE_ONCE(p, 0x01020304);



  2. I compilatori C hanno smesso di garantire che l'accesso a una parola sia atomico. Qualsiasi programma che non sia privo di gara può essere compilato in modo errato con risultati spettacolari. Non solo, ma un compilatore può decidere di non mantenere determinati valori nei registri all'interno di un ciclo, portando a più riferimenti che possono rovinare il codice come questo:





for(;;) {
owner = lock->owner;
if (owner && !mutex_spin_on_owner(lock, owner))
break;
/* ... */
}


  1. In assenza di accessi "tagging" alla memoria condivisa, non possiamo rilevare automaticamente gli accessi non intenzionali di quel tipo. Gli strumenti automatizzati che cercano di trovare tali bug non possono distinguerli da accessi intenzionalmente audaci.



La soluzione


Iniziamo notando che il kernel Linux richiede di essere compilato con GCC. Quindi, c'è un solo compilatore di cui dobbiamo occuparci con la soluzione e possiamo usare la sua documentazione come unica guida.


Per una soluzione generica, dobbiamo gestire accessi alla memoria di tutte le dimensioni. Abbiamo tutti i vari tipi di larghezze specifiche e tutto il resto. Notiamo inoltre che non è necessario contrassegnare in modo specifico gli accessi alla memoria che si trovano già in sezioni critiche (perché no? ).


Per le dimensioni di 1, 2, 4 e 8 byte, esistono tipi appropriati e volatile in particolare impedisce a GCC di applicare l'ottimizzazione di cui al punto (1), nonché di occuparsi di altri casi (ultimo punto in "BARRIERE DEL COMPILATORE"). Impedisce inoltre a GCC di compilare erroneamente il ciclo in (2), perché sposterebbe il volatile accesso attraverso un punto della sequenza e questo non è consentito dallo standard C. Linux usa quello che chiamiamo "accesso volatile" (vedi sotto) invece di etichettare un oggetto come volatile. Potremmo risolviamo il nostro problema contrassegnando l'oggetto specifico come volatile , ma questa non è (quasi?) mai una buona scelta. Ci sono molte ragioni per cui potrebbe essere dannoso.


Ecco come viene implementato un accesso volatile (scrittura) nel kernel per un tipo a 8 bit:



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

Supponiamo di non sapere esattamente cosa volatile fa - e scoprirlo non è facile! (dai un'occhiata n. 5) - un altro modo per farlo sarebbe quello di posizionare barriere di memoria:questo è esattamente ciò che fa Linux nel caso in cui la dimensione sia diversa da 1,2,4 o 8, ricorrendo a memcpy e porre barriere di memoria prima di e dopo la chiamata. Anche le barriere di memoria risolvono facilmente il problema (2), ma comportano notevoli penalità in termini di prestazioni.


Spero di aver coperto una panoramica senza approfondire le interpretazioni dello standard C, ma se vuoi potrei prendermi il tempo per farlo.