Implementierung · Kapitel 6–7 der Arbeit

Von der Theorie zur laufenden Hardware

Cache-Kohärenz aktivieren, SpinLocks in reinem Assembler bauen, und am Ende beweisen, dass es wirklich funktioniert — mit Tests, die es für dieses Interface vorher schlicht nicht gab.

01

Cache-Kohärenz aktivieren

Drei Schritte, in dieser Reihenfolge — jeder baut auf dem vorherigen auf.

1. SCU aktivieren

Ein einziges Bit im SCU Control Register, memory-mapped, gilt sofort für beide Kerne.

2. Jede CPU tritt der SMP-Kohärenz bei

Über das Auxiliary Control Register (ACTLR), pro Kern einzeln:

mrc p15, 0, r0, c1, c0, 1   ; Read ACTLR
orr r0, r0, #0x00000040    ; Set bit 6: SMP Cache Coherency
mcr p15, 0, r0, c1, c0, 1   ; Write ACTLR

3. Speicher als shareable markieren

Die SCU hält nur Speicher kohärent, der in der Translation Table auch als shareablemarkiert ist. Jeder der 4096 Einträge bekommt dafür Bit 16 gesetzt:

int i = 0;
for (; i < 4096; i++) {
    u32* section = (((u32*) &hal_mmu_page_directory_base)+i);
    *section |= (1 << 16);   // Shareable-Bit
}

Direkt danach müssen die TLBs invalidiert werden — sonst liefert die Hardware für bereits gecachte Übersetzungen weiterhin die alten, nicht-shareable Attribute zurück.

02

SpinLocks in Assembler

Keine Bibliothek, kein Compiler-Intrinsic — die komplette Lock/Unlock-Logik ist handgeschriebener ARM-Assembler, Befehl für Befehl gegen echte Nebenläufigkeit abgesichert.

Lock

slock:
    ldrex   r1, [r0]      ; Lock-Wert lesen, exclusive monitor setzen
    cmp     r1, #0x0      ; frei?
    WFENE                 ; wenn belegt: schlafen, bis sev weckt
    bne     slock         ; nochmal von vorn versuchen
    mov     r1, #1        ; r1 = "belegt"
    strex   r2, r1, [r0]  ; versuche zu schreiben, r2 = Erfolg?
    cmp     r2, #0x0
    bne     slock         ; fehlgeschlagen -> nochmal
    DMB                   ; Barriere VOR Zugriff auf den geschützten Bereich
    bx      lr

WFE (Wait For Event) ist der eigentliche Clou: statt im `busy-wait` die CPU-Zeit und den Speicherbus mit endlosen ldrex-Versuchen zu belasten, legt sich die wartende CPU schlafen und wacht erst durch ein SEV (Set Event) der lockhaltenden CPU wieder auf.

Unlock

sunlock:
    mov   r1, #0x0   ; "frei"
    DMB              ; alle Zugriffe auf den Bereich abgeschlossen
    str   r1, [r0]
    DSB              ; sicherstellen, dass der store sichtbar ist
    SEV              ; wartende CPUs aufwecken
    bx    lr
Fallstrick 1 — der stille Deadlock

Laut ARM-Dokumentation bleibt die Zieladresse unverändert, wenn ldrex fehlschlägt. In der Praxis kann strex trotzdem "fehlgeschlagen" zurückgeben, obwohl der Wert tatsächlich geschrieben wurde — die CPU denkt, sie hält den Lock nicht, während eine andere CPU ihn nie wieder freigibt. Ergebnis: eine Endlosschleife, die wie ein Hänger aussieht, aber ein Speicher-Konfigurationsfehler ist. Die korrekte Konfiguration (shareable + write-back + write-allocate) behebt das vollständig.

Fallstrick 2 — DSB zwischen ldrex und strex

Ein Data Synchronisation Barrier mitten in der ldrex/strex-Sequenz klingt nach zusätzlicher Sicherheit — bricht aber den Exclusive Monitor, weil zwischenzeitlich ausgeführte Speicherzugriffe den Zustand zurücksetzen können. Der strex schlägt dannimmer fehl. Zwischen beiden Befehlen gehört so wenig Code wie möglich, keine zusätzliche Barriere.

03

Die komplette Boot-Sequenz des zweiten Kerns

  1. Stacks für alle Prozessor-Modi initialisieren
  2. GIC initialisieren: Group Register zurücksetzen, CPU-Interface + Distributor aktivieren, Priority Mask setzen, ISR/DSR für SGIs registrieren
  3. HIVECS im SCTLR aktivieren (Vektortabelle liegt bei 0xFFFF0000)
  4. Cache-Kohärenz initialisieren: Translation Table Base Register setzen, TLBs invalidieren, MMU aktivieren, I-/D-Cache aktivieren
  5. Instruction Synchronisation Barrier — erst danach geht die Kontrolle an den Kernel
04

Tests: Beweisen, nicht hoffen

eCos bringt eine eigene SMP-Testsuite mit — die prüft aber nur, ob Scheduler und Timeslicing korrekt arbeiten, nicht das HAL-SMP-Interface selbst. Schlagen diese Tests fehl, bleibt unklar, ob der Fehler im Interface oder im Kernel liegt. Also wurden eigene Tests gebaut, direkt gegen die Zielhardware, ohne externes Debug-Interface.

Beide Kerne durch denselben Korridor

Da Kommunikation zwischen den Kernen selbst Testgegenstand ist, laufen beide Kerne denselben Test gleichzeitig — synchronisiert über eine Tür-Funktion, die erst öffnet, wenn beide Kerne angekommen sind.

Test 1
CPU0 + CPU1
Test 2
CPU0 + CPU1
Test 3
CPU0 + CPU1
…
bis Test 42

Tests mit potenziell unendlichem Warten (z.B. auf einen SGI, der nie ankommt) laufen gegen ein herunterzählendes Timeout statt gegen die Realtime-Clock — die läuft beim Debuggen weiter, auch wenn der Prozessor selbst angehalten ist.

Das echte Ergebnis

40
Bestanden
2
Fehlgeschlagen
42
Gesamt
CPUTestErgebnis
CPU_0SMP Bit set in ACTLRPASS
CPU_1SCU activatedPASS
CPU_0[CACHE COHERENCY] Variable change 0→1 sichtbarPASS [timeout: 775]
CPU_1SGI (RESCHEDULE) reached CPU_0PASS [timeout: 788]
CPU_0SGI (RESCHEDULE) reached CPU_1TIMED OUT [timeout: 0]
CPU_0SGI (TIMESLICE) reached CPU_1PASS [timeout: 721]
CPU_1[SL_3_1] Fehlgeschlagener Claim gibt 0 zurückPASS
CPU_1[SL_7] Gesperrter Lock bleibt endlos gesperrtPASS
CPU_1[SL_5] Ein CPU sieht den Lock der anderenPASS [timeout: 9957]

Die "2 Fehlgeschlagen" in der Kopfzeile sind kein Hinweis auf zwei unabhängige Bugs: jeder Test läuft gleichzeitig auf beiden Kernen, taucht also normalerweise doppelt im Log auf — die Duplikate wurden aus der Ausgabe entfernt, bis auf genau diesen einen Fall. Es handelt sich um ein einziges Problem (Zeile 5), das durch die Duplikation als zwei Fails gezählt wird: dieSGI (RESCHEDULE)-Nachricht erreichte CPU_1 nicht innerhalb des Timeout-Fensters. Direkt am verbleibenden Timeout-Wert von 0 erkennbar, und durch Erhöhen des Timeouts behoben — kein Bug im Interface, sondern ein zu knapp bemessenes Zeitfenster. Jeder SpinLock-Test trägt zusätzlich einen Tag (SL_x_y), um bei einem Fehlschlag die genaue Stelle im mehrstufigen Testablauf sofort zu identifizieren.