2012-08-10 13 views
12

Ho lavorato su un sistema operativo embedded per ARM, tuttavia ci sono alcune cose che non ho capito sull'architettura anche dopo aver fatto riferimento a ARMARM e alla sorgente di Linux.Operazioni atomiche in ARM

Operazioni atomiche.

ARM ARM indica che le istruzioni Load e Store sono atomiche e l'esecuzione è garantita completa prima dell'esecuzione del gestore di interrupt. Verificato da guardando

arch/arm/include/asm/atomic.h : 
    #define atomic_read(v) (*(volatile int *)&(v)->counter) 
    #define atomic_set(v,i) (((v)->counter) = (i)) 

Tuttavia, il problema arriva quando voglio manipolare questo valore atomicamente utilizzando le istruzioni della CPU (atomic_inc, atomic_dec, atomic_cmpxchg ecc ..) che utilizzano LDREX e STREX per ARMv7 (il mio obiettivo) .

ARMARM non dice nulla sugli interrupt che vengono bloccati in questa sezione quindi presumo che possa verificarsi un interrupt tra LDREX e STREX. La cosa che fa menzione riguarda il blocco del bus di memoria che credo sia utile solo per i sistemi MP dove ci possono essere più CPU che tentano di accedere allo stesso luogo allo stesso tempo. Ma per UP (e possibilmente MP), se un timer interrupt (o IPI per SMP) si attiva in questa piccola finestra di LDREX e STREX, il gestore di eccezioni esegue eventualmente modifiche al contesto della cpu e ritorna alla nuova attività, tuttavia la parte scioccante arriva ora , esegue 'CLREX' e quindi rimuove qualsiasi blocco esclusivo tenuto dal thread precedente. Quindi, quanto è migliore l'utilizzo di LDREX e STREX rispetto a LDR e STR per atomicità su un sistema UP?

Ho letto qualcosa su un monitor di blocco esclusivo, quindi ho una teoria possibile che quando il thread riprende ed esegue STREX, il monitor OS causa il fallimento di questa chiamata che può essere rilevata e il loop può essere eseguito utilizzando il nuovo valore nel processo (diramazione verso LDREX), sono qui?

risposta

9

Ok, ho ricevuto la risposta dal loro website.

Se un interruttore di contesto pianifica un processo dopo che il processo ha eseguito un Load-Exclusive ma prima esegue Store-Exclusive, Store-Exclusive restituisce un risultato falso negativo quando il processo riprende e la memoria non viene aggiornata. Ciò non influisce sulla funzionalità del programma, poiché il processo può riprovare immediatamente l'operazione.

8

L'idea alla base del/negozio esclusiva paradigma carico-linked è che se se il negozio segue molto presto dopo il carico, senza operazioni di memoria successivi, e se non altro ha toccato la posizione, il negozio è probabilmente per riuscire, ma se qualcos'altro ha toccato la posizione il negozio è certi fallire. Non vi è alcuna garanzia che i negozi non a volte falliscano senza una ragione apparente; se il tempo tra carico e deposito è ridotta al minimo, tuttavia, e non ci sono accessi di memoria tra loro, un ciclo come:

do 
{ 
    new_value = __LDREXW(dest) + 1; 
} while (__STREXW(new_value, dest)); 

possono generalmente essere invocata per riuscire in pochi tentativi.Se calcolare il nuovo valore in base al vecchio valore richiesto qualche elaborazione significativa, si dovrebbe riscrivere il ciclo come:

do 
{ 
    old_value = *dest; 

    new_value = complicated_function(old_value); 
} while (CompareAndStore(dest, new_value, old_value) != 0); 

... Assuming CompareAndStore is something like: 

uint32_t CompareAndStore(uint32_t *dest, uint32_t new_value, uint_32 old_value) 
{ 
    do 
    { 
    if (__LDREXW(dest) != old_value) return 1; // Failure 
    } while(__STREXW(new_value, dest); 
    return 0; 
} 

Questo codice dovrà eseguire nuovamente il suo ciclo principale, se qualcosa cambia * dest, mentre il nuovo valore è stato calcolato , ma solo il piccolo anello dovrà essere eseguire nuovamente se il __STREXW non riesce per qualche altra ragione [che non è si spera troppo probabile, dato che ci saranno solo circa due istruzioni tra il __LDREXW e __STREXW]

Addendum Un esempio di una situazione in cui "calcolare un nuovo valore basato su vecchio" potrebbe essere complicato sarebbe uno dove i "valori" ar e efficacemente un riferimento a una struttura dati complessa. Il codice può recuperare il vecchio riferimento, ricavare una nuova struttura dati dal vecchio e quindi aggiornare il riferimento. Questo schema si presenta molto più spesso nei framework raccolti da garbage piuttosto che nella programmazione "bare metal", ma ci sono una varietà di modi in cui può verificarsi anche durante la programmazione di bare metal. Normalmente gli allocatori malloc/calloc non sono generalmente sicuri per i thread/interrupt-safe, ma spesso gli allocatori per strutture a dimensione fissa lo sono. Se uno ha un "pool" di un numero potere-di-due di strutture di dati (ad esempio 255), si potrebbe usare qualcosa come:

#define FOO_POOL_SIZE_SHIFT 8 
#define FOO_POOL_SIZE (1 << FOO_POOL_SIZE_SHIFT) 
#define FOO_POOL_SIZE_MASK (FOO_POOL_SIZE-1) 

void do_update(void) 
{ 
    // The foo_pool_alloc() method should return a slot number in the lower bits and 
    // some sort of counter value in the upper bits so that once some particular 
    // uint32_t value is returned, that same value will not be returned again unless 
    // there are at least (UINT_MAX)/(FOO_POOL_SIZE) intervening allocations (to avoid 
    // the possibility that while one task is performing its update, a second task 
    // changes the thing to a new one and releases the old one, and a third task gets 
    // given the newly-freed item and changes the thing to that, such that from the 
    // point of view of the first task, the thing never changed.) 

    uint32_t new_thing = foo_pool_alloc(); 
    uint32_t old_thing; 
    do 
    { 
    // Capture old reference 
    old_thing = foo_current_thing; 

    // Compute new thing based on old one 
    update_thing(&foo_pool[new_thing & FOO_POOL_SIZE_MASK], 
     &foo_pool[old_thing & FOO_POOL_SIZE_MASK); 
    } while(CompareAndSwap(&foo_current_thing, new_thing, old_thing) != 0); 
    foo_pool_free(old_thing); 
} 

Se non ci saranno spesso più thread/interrupt/tutto ciò che cercano di aggiornare la stessa cosa allo stesso tempo, questo approccio dovrebbe consentire di eseguire gli aggiornamenti in modo sicuro. Se tra le cose che potrebbero tentare di aggiornare lo stesso articolo esisterà una relazione di priorità, quella con priorità più alta è garantita per riuscire al suo primo tentativo, la successiva priorità più alta avrà esito positivo in ogni tentativo che non è preceduto da la più alta priorità, ecc. Se si stava utilizzando il blocco, l'attività con la priorità più alta che voleva eseguire l'aggiornamento avrebbe dovuto attendere il completamento dell'aggiornamento con priorità più bassa; usando il paradigma CompareAndSwap, il task con la priorità più alta non sarà influenzato da quello inferiore (ma farà sì che quello inferiore debba fare uno spreco di lavoro).

+0

Ho fatto esattamente la stessa cosa, ma la parte in cui è richiesto un calcolo significativo per il nuovo valore, mi lascia ancora. Usare il ciclo cmxchg ha senso perché quindi il monitor esclusivo non verrà cancellato da un interruttore di contesto, ma rifare il calcolo significativo richiede un sacco di spese generali poiché ho osservato che Street fallire senza ragioni apparenti (UP con IRQ mascherati in PSR) come menzionato nel tuo post. – sgupta

+0

@ user1075375: vedere appendice – supercat

+0

Questi (__LDREXW e __STREXW) sono intrinsecamente supportati nei compilatori Keil per processori a microcontrollore della serie Cortex-M, non generalmente disponibili per target ARM mainstream (ad esempio AArch64) e compilatori (ad esempio, gcc, llvm) destra? http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0552a/BABDEEJC.html – ahcox

Problemi correlati