Come implementare sezioni critiche su ARM Cortex A9


15

Sto trasferendo alcuni codici legacy da un core ARM926 a CortexA9. Questo codice è baremetal e non include un sistema operativo o librerie standard, tutte personalizzate. Sto riscontrando un errore che sembra essere correlato a una condizione di competizione che dovrebbe essere prevenuta dal sezionamento critico del codice.

Voglio un feedback sul mio approccio per vedere se le mie sezioni critiche potrebbero non essere implementate correttamente per questa CPU. Sto usando GCC. Ho il sospetto che ci sia qualche errore sottile.

Inoltre, esiste una libreria opensource che ha questi tipi di primitive per ARM (o anche una buona libreria spinlock / semephore leggera)?

#define ARM_INT_KEY_TYPE            unsigned int
#define ARM_INT_LOCK(key_)   \
asm volatile(\
    "mrs %[key], cpsr\n\t"\
    "orr r1, %[key], #0xC0\n\t"\
    "msr cpsr_c, r1\n\t" : [key]"=r"(key_) :: "r1", "cc" );

#define ARM_INT_UNLOCK(key_) asm volatile ("MSR cpsr_c,%0" : : "r" (key_))

Il codice viene utilizzato come segue:

/* lock interrupts */
ARM_INT_KEY_TYPE key;
ARM_INT_LOCK(key);

<access registers, shared globals, etc...>

ARM_INT_UNLOCK(key);

L'idea del "tasto" è consentire sezioni critiche nidificate, che vengono utilizzate all'inizio e alla fine delle funzioni per creare funzioni rientranti.

Grazie!


1
si prega di fare riferimento a infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dht0008a/… non farlo in embedded asm btw. renderlo una funzione come fa l'articolo.
Jason Hu,

Non so nulla di ARM, ma mi aspetto che per mutex (o qualsiasi funzione di sincronizzazione cross-thread o cross-process), dovresti usare il clobber "memory" per assicurarti che a) tutti i valori di memoria correntemente memorizzati nella cache nei registri vengano cancellati torna in memoria prima di eseguire l'asb eb) tutti i valori in memoria a cui si accede dopo l'asm viene ricaricato. Nota che eseguire una chiamata (come HuStmpHrrr consiglia) dovrebbe implicitamente eseguire questo clobber per te.

Inoltre, mentre ancora non parlo ARM, i tuoi vincoli per "key_" non sembrano corretti. Dal momento che dici che questo è destinato a essere utilizzato per il rientro, dichiararlo come "= r" nel lucchetto sembra sospetto. '=' significa che si intende sovrascriverlo e il valore esistente non è importante. Sembra più probabile che tu abbia intenzione di usare '+' per indicare la tua intenzione di aggiornare il valore esistente. E ancora per sbloccare, elencandolo come input dice a gcc che non hai intenzione di cambiarlo, ma se non sbaglio, lo fai (cambialo). Immagino che questo dovrebbe anche essere elencato come output '+'.

1
+1 per la codifica in assembly per un core con specifiche così elevate. Ad ogni modo, questo potrebbe essere correlato alle modalità di privilegio?
Dzarda,

Sono abbastanza sicuro che dovrai usarlo ldrexe strexfarlo correttamente. Ecco una pagina web che mostra come utilizzare ldrexe streximplementare uno spinlock.

Risposte:


14

La parte più difficile della gestione di una sezione critica senza un sistema operativo non è in realtà la creazione del mutex, ma piuttosto capire cosa dovrebbe accadere se il codice vuole usare una risorsa che non è attualmente disponibile. Le istruzioni esclusive di carico e esclusive di archivio condizionale rendono abbastanza facile creare una funzione di "scambio" che, dato un puntatore a un numero intero, memorizzerà atomicamente un nuovo valore ma restituirà ciò che l'intero puntato aveva contenuto:

int32_t atomic_swap(int32_t *dest, int32_t new_value)
{
  int32_t old_value;
  do
  {
    old_value = __LDREXW(&dest);
  } while(__STREXW(new_value,&dest);
  return old_value;
}

Data una funzione come quella sopra, si può facilmente inserire un mutex tramite qualcosa del genere

if (atomic_swap(&mutex, 1)==0)
{
   ... do stuff in mutex ... ;
   mutex = 0; // Leave mutex
}
else
{ 
  ... couldn't get mutex...
}

In assenza di un sistema operativo, la principale difficoltà risiede spesso nel codice "Impossibile ottenere il mutex". Se si verifica un interrupt quando una risorsa protetta da mutex è occupata, potrebbe essere necessario avere il codice di gestione degli interrupt impostato un flag e salvare alcune informazioni per indicare ciò che voleva fare e quindi avere un codice simile a quello principale che acquisisce il mutex controlla ogni volta che rilascerà il mutex per vedere se un interrupt voleva fare qualcosa mentre il mutex era tenuto e, in tal caso, eseguire l'azione per conto dell'interrupt.

Sebbene sia possibile evitare problemi con gli interrupt che desiderano utilizzare risorse protette da mutex semplicemente disabilitando gli interrupt (e in effetti, disabilitare gli interrupt può eliminare la necessità di qualsiasi altro tipo di mutex), in generale è auspicabile evitare di disabilitare gli interrupt più a lungo del necessario.

Un utile compromesso può essere l'uso di un flag come descritto sopra, ma avere il codice della linea principale che sta per rilasciare gli interrupt di disabilitazione del mutex e controllare il suddetto flag appena prima di farlo (riattivare gli interrupt dopo aver rilasciato il mutex). Un tale approccio non richiede di lasciare gli interrupt disabilitati molto a lungo, ma proteggerà dalla possibilità che se il codice della linea principale verifica la bandiera dell'interrupt dopo aver rilasciato il mutex, c'è il pericolo che tra il tempo in cui vede la bandiera e il tempo in cui agisce su di esso, potrebbe essere preceduto da un altro codice che acquisisce e rilascia il mutex e agisce sulla bandiera di interruzione; se il codice della linea principale non verifica il flag dell'interrupt dopo aver rilasciato il mutex,

In ogni caso, la cosa più importante sarà avere un mezzo con cui il codice che tenta di utilizzare una risorsa protetta da mutex quando non è disponibile avrà un modo per ripetere il suo tentativo una volta rilasciata la risorsa.


7

Questo è un modo pesante per fare sezioni critiche; disabilitare gli interrupt. Potrebbe non funzionare se il sistema ha / gestisce i guasti dei dati. Aumenterà anche la latenza degli interrupt. L'irqflags.h Linux ha alcune macro che gestiscono questo. Le istruzioni cpsiee cpsidpotrebbero essere utili; Tuttavia, non salvano lo stato e non consentono l'annidamento. cpsnon utilizza un registro.

Per la serie Cortex-A , ldrex/strexsono più efficienti e possono funzionare per formare un mutex per la sezione critica oppure possono essere utilizzati con algoritmi senza blocco per sbarazzarsi della sezione critica.

In un certo senso, ldrex/strexsembra un ARMv5 swp. Tuttavia, sono molto più complessi da implementare nella pratica. È necessaria una cache funzionante e la memoria di destinazione delle ldrex/strexnecessità deve trovarsi nella cache. La documentazione ARM sull'argomento ldrex/strexè piuttosto nebulosa in quanto vogliono che i meccanismi funzionino su CPU non Cortex-A. Tuttavia, per Cortex-A il meccanismo per mantenere sincronizzata la cache della CPU locale con le altre CPU è lo stesso utilizzato per implementare le ldrex/strexistruzioni. Per la serie Cortex-A il granito di riserva (dimensione della ldrex/strexmemoria riservata) è uguale a una riga della cache; è inoltre necessario allineare la memoria alla riga della cache se si intende modificare più valori, ad esempio con un elenco doppiamente collegato.

Ho il sospetto che ci sia qualche errore sottile.

mrs %[key], cpsr
orr r1, %[key], #0xC0  ; context switch here?
msr cpsr_c, r1

È necessario assicurarsi che la sequenza non possa mai essere anticipata . Altrimenti, potresti ottenere due variabili chiave con gli interrupt abilitati e il rilascio del blocco sarà errato. È possibile utilizzare l' swpistruzione con la memoria chiave per garantire la coerenza su ARMv5, ma questa istruzione è obsoleta su Cortex-A in ldrex/strexquanto funziona meglio per i sistemi multi-CPU.

Tutto questo dipende dal tipo di pianificazione del tuo sistema. Sembra che tu abbia solo linee principali e interruzioni. Spesso hai bisogno delle primitive della sezione critica per avere alcuni agganci allo scheduler a seconda dei livelli (sistema / spazio utente / ecc.) Con cui vuoi che la sezione critica funzioni.

Inoltre, esiste una libreria opensource che ha questi tipi di primitive per ARM (o anche una buona libreria spinlock / semephore leggera)?

Questo è difficile da scrivere in modo portatile. Vale a dire, tali librerie possono esistere per determinate versioni di CPU ARM e per sistemi operativi specifici.


2

Vedo diversi potenziali problemi con quelle sezioni critiche. Ci sono avvertimenti e soluzioni a tutti questi, ma come riassunto:

  • Non c'è nulla che impedisca al compilatore di spostare il codice attraverso queste macro, per ottimizzazione o altri motivi casuali.
  • Salvano e ripristinano alcune parti dello stato del processore che il compilatore si aspetta che l'assemblaggio in linea lasci da solo (se non diversamente indicato).
  • Non c'è nulla che impedisca che si verifichi un interrupt nel mezzo della sequenza e che cambi lo stato tra quando viene letto e quando viene scritto.

Prima di tutto, hai sicuramente bisogno di alcune barriere di memoria del compilatore . GCC li implementa come clobbers . Fondamentalmente, questo è un modo per dire al compilatore "No, non puoi spostare gli accessi alla memoria attraverso questo pezzo di assembly inline perché potrebbe influenzare il risultato degli accessi alla memoria." In particolare, è necessario sia "memory"e "cc"clobbers, sia sul iniziano e macro finali. Ciò impedirà che altre cose (come le chiamate di funzione) vengano riordinate anche rispetto all'assembly inline, poiché il compilatore sa che potrebbero avere accessi alla memoria. Ho visto GCC per ARM tenere lo stato nei registri dei codici delle condizioni in tutto l'assemblaggio in linea con "memory"i "cc"clobber , quindi sicuramente hai bisogno del clobber.

In secondo luogo, queste sezioni critiche stanno salvando e ripristinando molto più del semplice abilitare gli interrupt. In particolare, stanno salvando e ripristinando la maggior parte del CPSR (Current Program Status Register) (il collegamento è per Cortex-R4 perché non sono riuscito a trovare un diagramma carino per un A9, ma dovrebbe essere identico). Esistono sottili restrizioni su quali parti di stato possano effettivamente essere modificate, ma qui è più che necessario.

Tra le altre cose, questo include i codici delle condizioni (in cui i risultati di istruzioni come cmpsono memorizzati in modo che le successive istruzioni condizionali possano agire sul risultato). Il compilatore sarà sicuramente confuso da questo. Questo è facilmente risolvibile usando il "cc"clobber come menzionato sopra. Tuttavia, questo farà fallire il codice ogni volta, quindi non sembra quello con cui stai riscontrando problemi. Un po 'come una bomba a orologeria, però, in quanto modificare casualmente un altro codice potrebbe causare al compilatore di fare qualcosa di leggermente diverso, che verrà rotto da questo.

Questo tenterà anche di salvare / ripristinare i bit IT, che vengono utilizzati per implementare l'esecuzione condizionale Thumb . Nota che se non esegui mai il codice Thumb, questo non ha importanza. Non ho mai capito come l'assemblaggio inline di GCC gestisca i bit IT, a parte la conclusione che non lo fa, il che significa che il compilatore non deve mai mettere l'assemblaggio inline in un blocco IT e si aspetta sempre che l'assembly termini al di fuori di un blocco IT. Non ho mai visto GCC generare codice in violazione di questi presupposti, e ho fatto un assemblaggio in linea abbastanza intricato con una forte ottimizzazione, quindi sono ragionevolmente sicuro che lo siano. Ciò significa che probabilmente non tenterà effettivamente di modificare i bit IT, nel qual caso tutto va bene. Il tentativo di modificare questi bit è classificato come "architettonicamente imprevedibile", quindi potrebbe fare tutti i tipi di cose cattive, ma probabilmente non farà nulla.

L'ultima categoria di bit che verranno salvati / ripristinati (oltre a quelli per disabilitare effettivamente gli interrupt) sono i bit della modalità. Questi probabilmente non cambieranno, quindi probabilmente non importerà, ma se si dispone di un codice che modifica deliberatamente le modalità, queste sezioni di interruzione potrebbero causare problemi. Il passaggio dalla modalità privilegiata a quella utente è l'unico caso che mi aspetto.

In terzo luogo, non c'è nulla che impedisca a un interrupt di cambiare altre parti del CPSR tra MRSe MSRin ARM_INT_LOCK. Eventuali modifiche di questo tipo potrebbero essere sovrascritte. Nella maggior parte dei sistemi ragionevoli, gli interrupt asincroni non cambiano lo stato del codice che stanno interrompendo (incluso CPSR). Se lo fanno, diventa molto difficile ragionare su cosa farà il codice. Tuttavia, è possibile (la modifica del bit di disabilitazione FIQ mi sembra molto probabile), quindi dovresti considerare se il tuo sistema lo fa.

Ecco come li implementerei in un modo che affronti tutti i potenziali problemi che ho sottolineato:

#define ARM_INT_KEY_TYPE            unsigned int
#define ARM_INT_LOCK(key_)   \
asm volatile(\
    "mrs %[key], cpsr\n\t"\
    "ands %[key], %[key], #0xC0\n\t"\
    "cpsid if\n\t" : [key]"=r"(key_) :: "memory", "cc" );
#define ARM_INT_UNLOCK(key_) asm volatile (\
    "tst %[key], #0x40\n\t"\
    "beq 0f\n\t"\
    "cpsie f\n\t"\
    "0: tst %[key], #0x80\n\t"\
    "beq 1f\n\t"\
    "cpsie i\n\t"
    "1:\n\t" :: [key]"r" (key_) : "memory", "cc")

Assicurati di compilare -mcpu=cortex-a9perché almeno alcune versioni di GCC (come la mia) sono impostate su una CPU ARM precedente che non supporta cpsiee cpsid.

Ho usato andsinvece semplicemente andin, ARM_INT_LOCKquindi è un'istruzione a 16 bit se questo è usato nel codice Thumb. Il "cc"clobber è comunque necessario, quindi è strettamente un vantaggio in termini di prestazioni / dimensioni del codice.

0e 1sono etichette locali , per riferimento.

Questi dovrebbero essere utilizzabili allo stesso modo delle tue versioni. È ARM_INT_LOCKveloce / piccolo come quello originale. Sfortunatamente, non sono riuscito a trovare un modo per farlo in ARM_INT_UNLOCKsicurezza ovunque vicino a poche istruzioni.

Se il tuo sistema ha dei vincoli su quando IRQ e FIQ sono disabilitati, questo potrebbe essere semplificato. Ad esempio, se sono sempre disabilitati insieme, è possibile combinare in uno cbz+ in cpsie ifquesto modo:

#define ARM_INT_UNLOCK(key_) asm volatile (\
    "cbz %[key], 0f\n\t"\
    "cpsie if\n\t"\
    "0:\n\t" :: [key]"r" (key_) : "memory", "cc")

In alternativa, se non ti interessano affatto i FIQ, è simile a lasciarli abilitare / disabilitare completamente.

Se sai che nient'altro cambia mai nessuno degli altri bit di stato in CPSR tra il blocco e lo sblocco, puoi anche usare continue con qualcosa di molto simile al tuo codice originale, tranne con entrambi "memory"e "cc"clobber in entrambi ARM_INT_LOCKeARM_INT_UNLOCK


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.