Jak zaimplementować krytyczne sekcje na ARM Cortex A9

15

Przenoszę trochę starszego kodu z rdzenia ARM926 na CortexA9. Ten kod jest niemetalowy i nie zawiera systemu operacyjnego ani standardowych bibliotek, wszystkie niestandardowe. Mam awarię, która wydaje się być związana ze stanem wyścigu, któremu należy zapobiegać przez krytyczne dzielenie kodu.

Chcę uzyskać informacje zwrotne na temat mojego podejścia, aby sprawdzić, czy moje krytyczne sekcje mogą nie zostać poprawnie zaimplementowane dla tego procesora. Korzystam z GCC. Podejrzewam, że jest jakiś subtelny błąd.

Czy istnieje również biblioteka typu open source, która zawiera te typy prymitywów dla ARM (a nawet dobrą lekką bibliotekę 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_))

Kod jest używany w następujący sposób:

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

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

ARM_INT_UNLOCK(key);

Ideą „klucza” jest umożliwienie zagnieżdżenia sekcji krytycznych, które są używane na początku i na końcu funkcji w celu utworzenia funkcji ponownego wydania.

Dzięki!

CodePoet
źródło
1
zapoznaj się z infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dht0008a/... nie rób tego w osadzonym asm btw. uczyń to funkcją, tak jak artykuł.
Jason Hu
Nic nie wiem o ARM, ale spodziewam się, że w przypadku muteksu (lub dowolnej funkcji synchronizacji między wątkami lub synchronizacji między procesami) powinieneś używać clobber „pamięci”, aby upewnić się, że a) wszystkie wartości pamięci aktualnie buforowane w rejestrach zostaną opróżnione powrót do pamięci przed wykonaniem asm ib) wszelkie wartości w pamięci, które są dostępne po ponownym załadowaniu asm. Zauważ, że wykonanie połączenia (zgodnie z zaleceniami HuStmpHrrr) powinno w sposób dorozumiany wykonać ten clobber za Ciebie.
Ponadto, chociaż nadal nie mówię o ARM, twoje ograniczenia dla 'key_' nie wyglądają poprawnie. Ponieważ mówisz, że jest to przeznaczone do ponownego wejścia, zadeklarowanie go jako „= r” w zamku wydaje się podejrzane. „=” oznacza, że ​​zamierzasz go zastąpić, a istniejąca wartość jest nieistotna. Bardziej prawdopodobne jest, że zamierzasz użyć „+”, aby wskazać zamiar zaktualizowania istniejącej wartości. I znowu dla odblokowania, umieszczenie go jako danych wejściowych mówi gcc, że nie zamierzasz go zmieniać, ale jeśli się nie mylę, to robisz (zmień). Zgaduję, że to również powinno być wymienione jako wynik „+”.
1
+1 za kodowanie w zestawie dla tak wysokiej specyfikacji rdzenia. W każdym razie, czy można to powiązać z trybami uprawnień?
Dzarda
Jestem pewien, że będziesz musiał użyć ldrexi strexzrobić to poprawnie. Oto strona internetowa pokazująca, jak używać ldrexi strexwdrażać blokadę.

Odpowiedzi:

14

Najtrudniejszą częścią obsługi sekcji krytycznej bez systemu operacyjnego nie jest tworzenie muteksu, ale raczej zastanawianie się, co powinno się stać, jeśli kod chce użyć zasobu, który nie jest obecnie dostępny. Instrukcje wykluczające ładowanie i instrukcje warunkowe wyłączające przechowywanie sprawiają, że dość łatwo jest utworzyć funkcję „zamiany”, która podając wskaźnik na liczbę całkowitą, atomowo zapisze nową wartość, ale zwróci to, co zawierała wskazana liczba całkowita:

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

Biorąc pod uwagę powyższą funkcję, można łatwo wprowadzić muteks za pomocą czegoś takiego

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

W przypadku braku systemu operacyjnego główna trudność często polega na kodzie „nie można uzyskać muteksu”. Jeśli wystąpi przerwanie, gdy zasób chroniony przez muteks jest zajęty, może być konieczne ustawienie flagi obsługującej przerwanie i zapisanie niektórych informacji wskazujących, co chce zrobić, a następnie dowolnego kodu głównego, który pobierze mutex sprawdza, ilekroć ma zamiar zwolnić muteks, aby zobaczyć, czy przerwanie chce coś zrobić, gdy muteks był trzymany, a jeśli tak, wykonaj akcję w imieniu przerwania.

Chociaż możliwe jest uniknięcie problemów z przerwaniami, które chcą korzystać z zasobów chronionych przez mutex, po prostu wyłączając przerwania (i faktycznie, wyłączenie przerwania może wyeliminować potrzebę jakiegokolwiek innego rodzaju muteksu), ogólnie pożądane jest unikanie wyłączania przerwania dłużej niż to konieczne.

Przydatnym kompromisem może być użycie flagi, jak opisano powyżej, ale mieć kod linii głównej, który ma zamiar zwolnić przerwania wyłączania muteksu i sprawdzić wyżej wspomnianą flagę tuż przed tym (ponownie włączyć przerwania po zwolnieniu muteksu). Takie podejście nie wymaga pozostawienia wyłączonych przerwań bardzo długo, ale chroni przed możliwością, że jeśli kod linii głównej przetestuje flagę przerwania po zwolnieniu muteksu, istnieje niebezpieczeństwo, że między czasem, w którym zobaczy flagę, a czasem działa na nie, może zostać zablokowany przez inny kod, który pobiera i zwalnia muteks oraz działa na flagę przerwania; jeśli kod linii głównej nie testuje flagi przerwania po zwolnieniu muteksu,

W każdym razie najważniejsze będzie posiadanie środków, dzięki którym kod, który spróbuje użyć zasobu chronionego przez muteks, gdy jest niedostępny, będzie mógł powtórzyć próbę po uwolnieniu zasobu.

supercat
źródło
7

Jest to ciężki sposób wykonywania krytycznych sekcji; wyłącz przerwania. Może nie działać, jeśli twój system ma / obsługuje błędy danych. Zwiększy również opóźnienie przerwań. Linux irqflags.h ma pewne makra, które zajmują się tego. ThecpsieI cpsidinstrukcje może użyteczne; Nie zapisują jednak stanu i nie pozwalają na zagnieżdżanie. cpsnie używa rejestru.

Dla Cortex-A szeregowo, ldrex/strexsą bardziej wydajne i mogą pracować w celu utworzenia muteksu do części krytycznej lub mogą być stosowane lock-wolny algorytmów pozbyć sekcji krytycznej.

W pewnym sensie ldrex/strexwydają się ARMv5 swp. Są jednak o wiele bardziej skomplikowane do wdrożenia w praktyce. Potrzebujesz działającej pamięci podręcznej i docelowej pamięci ldrex/strexpotrzebnej w pamięci podręcznej. Dokumentacja ARM na temat ldrex/strexjest raczej mglista, ponieważ chcą mechanizmów działających na procesorach innych niż Cortex-A. Jednak w przypadku Cortex-A mechanizm utrzymywania lokalnej pamięci podręcznej procesora w synchronizacji z innymi procesorami jest taki sam, jak w przypadku implementacji ldrex/strexinstrukcji. W przypadku serii Cortex-A rezerwa graniczna (wielkość ldrex/strexzarezerwowanej pamięci) jest taka sama jak linia pamięci podręcznej; musisz również wyrównać pamięć do linii pamięci podręcznej, jeśli zamierzasz zmodyfikować wiele wartości, na przykład z podwójnie połączoną listą.

Podejrzewam, że jest jakiś subtelny błąd.

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

Musisz upewnić się, że sekwencji nigdy nie można uprzedzić . W przeciwnym razie możesz uzyskać dwie kluczowe zmienne z włączonymi przerwaniami, a zwolnienie blokady będzie nieprawidłowe. Możesz użyć swpinstrukcji z kluczem pamięcią aby zapewnić spójność ARMv5, ale ta instrukcja jest przestarzała na Cortex-A na korzyść, ldrex/strexponieważ działa lepiej w systemach wieloprocesorowych.

Wszystko to zależy od tego, jaki rodzaj planowania ma Twój system. Wygląda na to, że masz tylko linie główne i zakłócenia. Często potrzebujesz sekcji krytycznej podstawowe aby mieć pewne zaczepienia w harmonogramie w zależności od poziomów (system / przestrzeń użytkownika / etc), z którymi ma pracować sekcja krytyczna.

Czy istnieje również biblioteka typu open source, która zawiera te typy prymitywów dla ARM (a nawet dobrą lekką bibliotekę spinlock / semephore)?

Trudno to pisać w przenośny sposób. Tzn. Takie biblioteki mogą istnieć dla niektórych wersji procesorów ARM i dla określonych systemów operacyjnych.

bezgłośny hałas
źródło
2

Widzę kilka potencjalnych problemów z tymi krytycznymi sekcjami. Istnieją pewne zastrzeżenia i rozwiązania dla nich wszystkich, ale jako podsumowanie:

  • Nic nie stoi na przeszkodzie, aby kompilator przenosił kod między tymi makrami w celu optymalizacji lub z innych przypadkowych powodów.
  • Zapisują i przywracają niektóre części stanu procesora, którego kompilator oczekuje, że zestaw wbudowany pozostawi w spokoju (chyba że powiedziano inaczej).
  • Nic nie stoi na przeszkodzie, aby przerwa pojawiła się w środku sekwencji i zmieniła stan między momentem odczytu a zapisaniem.

Po pierwsze, zdecydowanie potrzebujesz barier pamięci kompilatora . GCC implementuje je jako clobbers . Zasadniczo jest to sposób powiedzenia kompilatorowi: „Nie, nie można przenosić dostępu do pamięci przez ten element zestawu wbudowanego, ponieważ może to wpłynąć na wynik dostępu do pamięci”. W szczególności potrzebujesz zarówno "memory"i "cc"clobbers, zarówno na początku, jak i na końcu makra. Zapobiegną one zmianie kolejności innych elementów (np. Wywołań funkcji) w stosunku do zestawu wbudowanego, ponieważ kompilator wie, że mogą mieć dostęp do pamięci. Widziałem stan wstrzymania GCC dla ARM w rejestrach kodów stanu w zespole wbudowanym z "memory"clobberami, więc na pewno potrzebujesz "cc"clobbera.

Po drugie, te krytyczne sekcje oszczędzają i przywracają znacznie więcej niż tylko to, czy przerwania są włączone. W szczególności zapisują i przywracają większość CPSR (aktualny rejestr statusu programu) (link dotyczy Cortex-R4, ponieważ nie mogłem znaleźć ładnego schematu dla A9, ale powinien być identyczny). Istnieją subtelne ograniczenia, wokół których elementy stanu mogą być faktycznie modyfikowane, ale tutaj jest to więcej niż to konieczne.

Obejmuje to między innymi kody warunków (gdzie wyniki takich instrukcji cmpsą przechowywane, aby kolejne instrukcje warunkowe mogły działać na wynik). Kompilator na pewno się tym pomyli. Można to łatwo rozwiązać za pomocą "cc"Clobbera, jak wspomniano powyżej. Spowoduje to jednak, że kod zawiedzie za każdym razem, więc nie brzmi jak z problemami. Jednak nieco tykająca bomba zegarowa, modyfikując losowy inny kod, może spowodować, że kompilator zrobi coś nieco innego, co zostanie przez to zepsute.

Spowoduje to również próbę zapisania / przywrócenia bitów IT, które są używane do implementacji warunkowego wykonywania Thumb . Pamiętaj, że jeśli nigdy nie wykonasz kodu Thumb, nie ma to znaczenia. Nigdy nie zorientowałem się, w jaki sposób wbudowany zestaw GCC radzi sobie z bitami IT, poza stwierdzeniem, że tak nie jest, co oznacza, że ​​kompilator nigdy nie może umieszczać wbudowanego zestawu w bloku IT i zawsze oczekuje, że zestaw skończy się poza blokiem IT. Nigdy nie widziałem, aby GCC generowało kod naruszający te założenia, i zrobiłem dość skomplikowane wstawianie z dużą optymalizacją, więc jestem pewien, że się utrzymują. Oznacza to, że prawdopodobnie nie będzie próbował zmienić bitów IT, w którym to przypadku wszystko jest w porządku. Próba modyfikacji tych bitów jest klasyfikowana jako „nieprzewidywalna architektonicznie”, więc może robić wszelkiego rodzaju złe rzeczy, ale prawdopodobnie nic nie zrobi.

Ostatnią kategorią bitów, które zostaną zapisane / przywrócone (oprócz tych, które faktycznie wyłączają przerwania) są bity trybu. Te prawdopodobnie się nie zmienią, więc prawdopodobnie nie będzie to miało znaczenia, ale jeśli masz jakiś kod, który celowo zmienia tryby, te sekcje przerwań mogą powodować problemy. Zmiana pomiędzy trybem uprzywilejowanym a trybem użytkownika jest jedynym przypadkiem zrobienia tego, czego się spodziewałam.

Po trzecie, nie ma nic zapobiegania przerwanie zmianę innych części CPSR pomiędzy MRSi MSRw ARM_INT_LOCK. Wszelkie takie zmiany mogą zostać zastąpione. W większości rozsądnych systemów asynchroniczne przerwania nie zmieniają stanu kodu, w którym są przerywane (w tym CPSR). Jeśli to zrobią, bardzo trudno będzie zrozumieć, co zrobi kod. Jest to jednak możliwe (zmiana bitu wyłączania FIQ wydaje mi się najbardziej prawdopodobna), więc powinieneś rozważyć, czy twój system to robi.

Oto, w jaki sposób wdrożyłbym je w sposób uwzględniający wszystkie potencjalne problemy, które wskazałem:

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

Upewnij się, że kompilujesz, -mcpu=cortex-a9ponieważ przynajmniej niektóre wersje GCC (takie jak moja) są domyślnie starsze na procesor ARM, który nie obsługuje cpsiei cpsid.

Użyłem andszamiast tylko andw, ARM_INT_LOCKwięc jest to 16-bitowa instrukcja, jeśli jest używana w kodzie Thumb. "cc"Clobber jest konieczne tak czy inaczej, więc jest to ściśle korzyść rozmiar wydajność / code.

0i 1są to lokalne etykiety , w celach informacyjnych.

Powinny być one użyteczne na wszystkie te same sposoby, co twoje wersje. ARM_INT_LOCKJest tak samo szybka / small jako oryginalne. Niestety nie udało mi się wymyślić sposobu na ARM_INT_UNLOCKbezpieczne wykonanie zadania w pobliżu tak niewielu instrukcji.

Jeśli twój system ma ograniczenia, kiedy IRQ i FIQ są wyłączone, można to uprościć. Na przykład, jeśli zawsze są one razem wyłączone, możesz połączyć w jeden cbz+ w cpsie ifnastępujący sposób:

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

Alternatywnie, jeśli w ogóle nie przejmujesz się FIQ, jest to podobne do porzucenia włączania / wyłączania ich całkowicie.

Jeśli wiesz, że nic innego nigdy nie zmienia żadnych innych bitów stanu w CPSR między blokadą a odblokowaniem, możesz również użyć kontynuacji z czymś bardzo podobnym do twojego oryginalnego kodu, z wyjątkiem zarówno "memory"i "cc"bloków w obu ARM_INT_LOCKiARM_INT_UNLOCK

Brian Silverman
źródło