Comment implémenter des sections critiques sur ARM Cortex A9

15

Je porte du code hérité d'un noyau ARM926 vers CortexA9. Ce code est baremetal et ne comprend pas de système d'exploitation ni de bibliothèques standard, toutes personnalisées. J'ai un échec qui semble être lié à une condition de concurrence critique qui devrait être évitée par une section critique du code.

Je veux des commentaires sur mon approche pour voir si mes sections critiques peuvent ne pas être correctement implémentées pour ce CPU. J'utilise GCC. Je soupçonne qu'il y a une erreur subtile.

De plus, existe-t-il une bibliothèque open source qui possède ces types de primitives pour ARM (ou même une bonne bibliothèque légère de spinlock / semephore)?

#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_))

Le code est utilisé comme suit:

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

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

ARM_INT_UNLOCK(key);

L'idée de la "clé" est d'autoriser les sections critiques imbriquées, et celles-ci sont utilisées au début et à la fin des fonctions pour créer des fonctions réentrantes.

Merci!

CodePoet
la source
1
veuillez vous référer à infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dht0008a/… ne le faites pas dans asm btw intégré. en faire une fonction comme le fait l'article.
Jason Hu
Je ne sais rien d'ARM, mais je m'attendrais à ce que pour mutex (ou toute fonction de synchronisation entre threads ou processus croisés), vous devriez utiliser le clobber "memory" pour vous assurer que a) toutes les valeurs de mémoire actuellement mises en cache dans les registres soient vidées revenir à la mémoire avant d' exécuter l'asm et b) toutes les valeurs en mémoire auxquelles on accède après le rechargement de l'asm. Notez que l'exécution d'un appel (comme le recommande HuStmpHrrr) devrait implicitement effectuer ce clobber pour vous.
De plus, bien que je ne parle toujours pas ARM, vos contraintes pour 'key_' ne semblent pas correctes. Puisque vous dites que cela est destiné à être utilisé pour la rentrée, le déclarer comme "= r" dans le verrou semble suspect. '=' signifie que vous avez l'intention de l'écraser et que la valeur existante n'a pas d'importance. Il semble plus probable que vous ayez l'intention d'utiliser «+» pour indiquer votre intention de mettre à jour la valeur existante. Et encore une fois pour le déverrouillage, le répertorier en tant qu'entrée indique à gcc que vous n'avez pas l'intention de le changer, mais si je ne me trompe pas, vous le faites (changez-le). Je suppose que cela devrait également être répertorié comme une sortie «+».
1
+1 pour le codage en assemblage pour un noyau de spécifications aussi élevées. Quoi qu'il en soit, cela pourrait-il être lié aux modes de privilège?
Dzarda
Je suis presque sûr que vous devrez l'utiliser ldrexet strexle faire correctement. Voici une page Web vous montrant comment utiliser ldrexet streximplémenter un verrou tournant.

Réponses:

14

La partie la plus difficile de la gestion d'une section critique sans système d'exploitation n'est pas réellement la création du mutex, mais plutôt de déterminer ce qui devrait se produire si le code veut utiliser une ressource qui n'est pas actuellement disponible. Les instructions load-exclusive et conditional-store-exclusive facilitent la création d'une fonction "swap" qui, étant donné un pointeur sur un entier, stockera atomiquement une nouvelle valeur mais renverra ce que l'entier pointé contenait:

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

Étant donné une fonction comme celle ci-dessus, on peut facilement entrer dans un mutex via quelque chose comme

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

En l'absence d'un système d'exploitation, la principale difficulté réside souvent dans le code "impossible d'obtenir le mutex". Si une interruption se produit lorsqu'une ressource protégée par mutex est occupée, il peut être nécessaire que le code de gestion des interruptions définisse un indicateur et enregistre certaines informations pour indiquer ce qu'il voulait faire, puis avoir un code de type principal qui acquiert le mutex vérifie chaque fois qu'il va libérer le mutex pour voir si une interruption voulait faire quelque chose pendant que le mutex était maintenu et, si c'est le cas, effectuer l'action au nom de l'interruption.

Bien qu'il soit possible d'éviter les problèmes d'interruptions souhaitant utiliser des ressources protégées par mutex en désactivant simplement les interruptions (et en effet, la désactivation des interruptions peut éliminer le besoin de tout autre type de mutex), en général, il est souhaitable d'éviter de désactiver les interruptions plus longtemps que nécessaire.

Un compromis utile peut être d'utiliser un indicateur comme décrit ci-dessus, mais d'avoir le code de la ligne principale qui va libérer les interruptions de désactivation du mutex et vérifier l'indicateur susmentionné juste avant de le faire (réactiver les interruptions après la libération du mutex). Une telle approche ne nécessite pas de laisser les interruptions désactivées très longtemps, mais évitera que si le code de la ligne principale teste le drapeau de l'interruption après avoir relâché le mutex, il y ait un danger qu'entre le moment où il voit le drapeau et le moment où il agit sur lui, il pourrait être préempté par un autre code qui acquiert et libère le mutex et agit sur l'indicateur d'interruption; si le code de la ligne principale ne teste pas l'indicateur d'interruption après avoir libéré le mutex,

Dans tous les cas, le plus important sera d'avoir un moyen par lequel le code qui essaie d'utiliser une ressource protégée par mutex lorsqu'elle n'est pas disponible aura un moyen de répéter sa tentative une fois la ressource libérée.

supercat
la source
7

C'est une manière lourde de faire des sections critiques; désactiver les interruptions. Cela peut ne pas fonctionner si votre système a / gère des erreurs de données. Cela augmentera également la latence d'interruption. Le Linux irqflags.h a quelques macros qui gèrent cela. Les instructions cpsieet cpsidpeuvent être utiles; Cependant, ils ne sauvegardent pas l'état et ne permettent pas l'imbrication. cpsn'utilise pas de registre.

Pour la série Cortex-A , ils ldrex/strexsont plus efficaces et peuvent fonctionner pour former un mutex pour la section critique ou ils peuvent être utilisés avec des algorithmes sans verrouillage pour se débarrasser de la section critique.

Dans un certain sens, ldrex/strexcela ressemble à un ARMv5 swp. Cependant, ils sont beaucoup plus complexes à mettre en œuvre dans la pratique. Vous avez besoin d'un cache de travail et la mémoire cible des ldrex/strexbesoins doit être dans le cache. La documentation ARM sur le ldrex/strexest plutôt nébuleuse car ils veulent que les mécanismes fonctionnent sur les processeurs non Cortex-A. Cependant, pour le Cortex-A, le mécanisme de synchronisation du cache du processeur local avec les autres processeurs est le même que celui utilisé pour implémenter les ldrex/strexinstructions. Pour la série Cortex-A, la réserve granulaire (taille de la ldrex/strexmémoire réservée) est identique à une ligne de cache; vous devez également aligner la mémoire sur la ligne de cache si vous avez l'intention de modifier plusieurs valeurs, comme avec une liste doublement liée.

Je soupçonne qu'il y a une erreur subtile.

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

Vous devez vous assurer que la séquence ne peut jamais être anticipée . Sinon, vous pouvez obtenir deux variables clés avec des interruptions activées et le déverrouillage sera incorrect. Vous pouvez utiliser l' swpinstruction avec la mémoire de clé pour assurer la cohérence sur l'ARMv5, mais cette instruction est déconseillée sur le Cortex-A en faveur de ldrex/strexcar elle fonctionne mieux pour les systèmes multi-CPU.

Tout cela dépend du type de planification de votre système. Il semble que vous n'ayez que des lignes principales et des interruptions. Vous avez souvent besoin des primitives de section critique pour avoir des liens avec le planificateur en fonction des niveaux (système / espace utilisateur / etc.) avec lesquels vous souhaitez que la section critique fonctionne.

De plus, existe-t-il une bibliothèque open source qui possède ces types de primitives pour ARM (ou même une bonne bibliothèque légère de spinlock / semephore)?

Il est difficile d'écrire de manière portable. C'est-à-dire que de telles bibliothèques peuvent exister pour certaines versions de CPU ARM et pour des OS spécifiques.

bruit insensé
la source
2

Je vois plusieurs problèmes potentiels avec ces sections critiques. Il y a des mises en garde et des solutions à tout cela, mais en résumé:

  • Rien n'empêche le compilateur de déplacer du code à travers ces macros, pour une optimisation ou pour d'autres raisons aléatoires.
  • Ils enregistrent et restaurent certaines parties de l'état du processeur que le compilateur s'attend à ce que l'assemblage en ligne laisse seul (sauf indication contraire).
  • Rien n'empêche une interruption de se produire au milieu de la séquence et de changer l'état entre sa lecture et son écriture.

Tout d'abord, vous avez certainement besoin de barrières de mémoire pour le compilateur . GCC les implémente en tant que clobbers . Fondamentalement, c'est une façon de dire au compilateur "Non, vous ne pouvez pas déplacer les accès à la mémoire à travers ce morceau d'assemblage en ligne car cela pourrait affecter le résultat des accès à la mémoire." Plus précisément, vous avez besoin des deux "memory"et des "cc"clobbers, sur les macros de début et de fin. Celles-ci empêcheront également d'autres choses (comme les appels de fonction) d'être réorganisées par rapport à l'assembly en ligne, car le compilateur sait qu'elles peuvent avoir des accès à la mémoire. J'ai vu GCC pour ARM tenir l'état dans les registres de code de condition à travers l'assemblage en ligne avec des "memory"clobbers, donc vous avez vraiment besoin du "cc"clobber.

Deuxièmement, ces sections critiques enregistrent et restaurent beaucoup plus que la simple activation des interruptions. Plus précisément, ils enregistrent et restaurent la plupart du CPSR (Current Program Status Register) (le lien est pour Cortex-R4 parce que je n'ai pas pu trouver un joli diagramme pour un A9, mais il devrait être identique). Il y a des restrictions subtiles autour desquelles des morceaux d'état peuvent réellement être modifiés, mais c'est plus que nécessaire ici.

Entre autres choses, cela inclut les codes de condition (où les résultats d'instructions comme cmpsont stockés afin que les instructions conditionnelles suivantes puissent agir sur le résultat). Le compilateur sera certainement confus par cela. Ceci est facilement résoluble en utilisant le "cc"clobber comme mentionné ci-dessus. Cependant, cela fera échouer le code à chaque fois, donc cela ne ressemble pas à ce que vous rencontrez des problèmes. Un peu comme une bombe à retardement, dans la mesure où la modification d'un autre code aléatoire pourrait amener le compilateur à faire quelque chose d'un peu différent qui sera rompu par cela.

Cela tentera également de sauvegarder / restaurer les bits informatiques, qui sont utilisés pour implémenter l'exécution conditionnelle Thumb . Notez que si vous n'exécutez jamais de code Thumb, cela n'a pas d'importance. Je n'ai jamais compris comment l'assemblage en ligne de GCC traite les bits informatiques, à part le conclure, ce qui signifie que le compilateur ne doit jamais mettre l'assemblage en ligne dans un bloc informatique et s'attend toujours à ce que l'assemblage se termine en dehors d'un bloc informatique. Je n'ai jamais vu GCC générer de code violant ces hypothèses, et j'ai fait un assemblage en ligne assez complexe avec une optimisation lourde, donc je suis raisonnablement sûr qu'ils tiennent. Cela signifie qu'il n'essaiera probablement pas de changer les bits informatiques, auquel cas tout va bien. Tenter de modifier ces bits est classé comme "imprévisible sur le plan architectural", il pourrait donc faire toutes sortes de mauvaises choses, mais ne ferait probablement rien du tout.

La dernière catégorie de bits qui sera enregistrée / restaurée (en plus de ceux qui désactivent réellement les interruptions) sont les bits de mode. Celles-ci ne changeront probablement pas, donc cela n'aura probablement pas d'importance, mais si vous avez du code qui modifie délibérément les modes, ces sections d'interruption pourraient causer des problèmes. Passer du mode privilégié au mode utilisateur est le seul cas de ce genre auquel je m'attendrais.

Troisièmement, il n'y a rien qui empêche une interruption de changer d' autres parties de CPSR entre le MRSet MSRdans ARM_INT_LOCK. Ces modifications pourraient être remplacées. Dans la plupart des systèmes raisonnables, les interruptions asynchrones ne modifient pas l'état du code qu'elles interrompent (y compris CPSR). S'ils le font, il devient très difficile de raisonner sur ce que le code fera. Cependant, cela est possible (le changement du bit de désactivation FIQ me semble le plus probable), vous devez donc vous demander si votre système le fait.

Voici comment je les implémenterais de manière à résoudre tous les problèmes potentiels que j'ai signalés:

#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")

Assurez-vous de compiler avec -mcpu=cortex-a9car au moins certaines versions de GCC (comme la mienne) utilisent par défaut un ancien processeur ARM qui ne prend pas en charge cpsieet cpsid.

J'ai utilisé andsau lieu de juste anden ARM_INT_LOCKdonc c'est une instruction 16 bits si elle est utilisée dans le code Thumb. Le "cc"clobber est de toute façon nécessaire, c'est donc strictement un avantage de performance / taille de code.

0et 1sont des étiquettes locales , pour référence.

Ceux-ci devraient être utilisables de la même manière que vos versions. Le ARM_INT_LOCKest aussi rapide / petit que votre original. Malheureusement, je n'ai pas pu trouver un moyen de le faire en ARM_INT_UNLOCKtoute sécurité en autant d'instructions.

Si votre système a des contraintes sur le moment où les IRQ et les FIQ sont désactivées, cela pourrait être simplifié. Par exemple, s'ils sont toujours désactivés ensemble, vous pouvez combiner en un cbz+ cpsie ifcomme ceci:

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

Alternativement, si vous ne vous souciez pas du tout des FIQ, il est similaire de simplement laisser complètement les activer / désactiver.

Si vous savez que rien d'autre ne change jamais les autres bits d'état du CPSR entre le verrouillage et le déverrouillage, vous pouvez également utiliser continuer avec quelque chose de très similaire à votre code d'origine, sauf avec les deux "memory"et les "cc"clobbers dans les deux ARM_INT_LOCKetARM_INT_UNLOCK

Brian Silverman
la source