Implementierung · Kapitel 6–7 der Arbeit
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.
Drei Schritte, in dieser Reihenfolge — jeder baut auf dem vorherigen auf.
Ein einziges Bit im SCU Control Register, memory-mapped, gilt sofort für beide Kerne.
Ü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
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.
Keine Bibliothek, kein Compiler-Intrinsic — die komplette Lock/Unlock-Logik ist handgeschriebener ARM-Assembler, Befehl für Befehl gegen echte Nebenläufigkeit abgesichert.
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 lrWFE (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.
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 lrLaut 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.
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.
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.
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.
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.
| CPU | Test | Ergebnis |
|---|---|---|
| CPU_0 | SMP Bit set in ACTLR | PASS |
| CPU_1 | SCU activated | PASS |
| CPU_0 | [CACHE COHERENCY] Variable change 0→1 sichtbar | PASS [timeout: 775] |
| CPU_1 | SGI (RESCHEDULE) reached CPU_0 | PASS [timeout: 788] |
| CPU_0 | SGI (RESCHEDULE) reached CPU_1 | TIMED OUT [timeout: 0] |
| CPU_0 | SGI (TIMESLICE) reached CPU_1 | PASS [timeout: 721] |
| CPU_1 | [SL_3_1] Fehlgeschlagener Claim gibt 0 zurück | PASS |
| CPU_1 | [SL_7] Gesperrter Lock bleibt endlos gesperrt | PASS |
| CPU_1 | [SL_5] Ein CPU sieht den Lock der anderen | PASS [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.