FABRIC-3.10.8 MC-3b: waking a worker core

Each worker core sets up its own IPI receiver before going online (amd64
LAPIC + vector 0x22 with its own stub, aarch64 banked GIC SGI 0, riscv64
sie.SSIE); core 0 sends by ICR / GICD_SGIR / SBI IPI. Park lets exactly
one interrupt in without a lost-wake window. Boot self-test: IPI round
trips 3/3 and 'woke x3, IPIs x3' on all three ISAs; worker vCPU threads
0.0% on the host. 3-ISA acceptance clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-26 16:31:01 -04:00
co-authored by Claude Opus 5.5
parent d7745b49be
commit 71e0be96b0
24 changed files with 46016 additions and 8 deletions
+25
View File
@@ -1104,3 +1104,28 @@ The boot hart HSM chose (0) is the one OpenSBI names as `Boot HART ID`.
- **All three ISAs:** `SMP: core 1: IPI round trips 3/3`, then one idle-point line with **3 wakes, 3 IPIs**, and no later line in the run (no further wake).
- **The standard sequence unchanged:** K-10 hashes, G byte-identical, all ok `3`, 0 B idle growth, 0 order violations, 0 double claims.
- **Host `ps -L` on aarch64 and riscv64:** `CPU 1/TCG` stays at about 0% after boot.
### 7.8 MC-3b — executed (2026-09-26)
**Change**, as planned (§7.7), in these files:
- **Common** (`hal/smp.c`, `smp.h`, `percpu.h`): `arch_smp_secondary_init()` is called on the worker core before it marks itself online. `arch_smp_send_ipi()`, `sk_smp_ipi_received()`, the per-core `ipi_target` / `ipi_received`, the boot round-trip self-test, and the idle-point line `SMP: core N woke xW, IPIs xI`.
- **amd64:** `SK_IPI_VECTOR` 0x22 with its own stub `isr_stub34` (hidden, in the table; the default range is now 35–255). `isr_common_handler` counts and EOIs it. `apic_secondary_enable()` sets SIVR enable and TPR 0. Park is `sti; hlt; cli`; send is a fixed ICR IPI to the APIC id.
- **aarch64:** `apic_secondary_init_gic()` sets the banked SGI 0 priority and enable, then PMR and the CPU interface, and returns this core's mask from its banked `GICD_ITARGETSR0`. `apic_send_wake_sgi()` writes `GICD_SGIR`. `aarch64_irq_handler` counts INTIDs 0–15 and EOIs the full IAR value. Park is `wfi` masked, then a one-instant `daifclr`/`daifset`.
- **riscv64:** `sie.SSIE` on the worker hart. Sending is SBI IPI `send_ipi` (`0x735049`). The handler takes cause 1: clear `sip.SSIP`, then count. Park is `wfi` with SIE clear, then a one-instant `csrsi`/`csrci`.
**One change after the first aarch64 run** (`logs/20260926-154915`, kept as the record):
- That run gave `IPI round trips 3/3` but `woke x1, IPIs x3`. The self-test sent each next IPI as soon as the previous one was *counted*. Under TCG, core 1 was still inside its brief interrupt-open window, so IPIs 2 and 3 were taken there, and one return from `wfi` covered all three.
- Nothing was lost and nothing was spurious: wakes ≤ IPIs, which is the property that matters.
- The self-test now waits for the IPI count **and** the wake count before sending the next IPI, so each round trip is one IPI and one wake. amd64 was re-run on that final code.
**Evidence** (clean builds; the tree is byte-identical to the saved stage diff):
| ISA | log | SMP | host `ps -L` after O-A | standard sequence |
|---|---|---|---|---|
| amd64 | `logs/20260926-162111` | `IPI round trips 3/3`; one idle line `woke x3, IPIs x3`, none after | (threads unnamed on amd64) | K-10 hashes, G byte-identical, all ok `3`, 0 B idle, 0 order violations, 0 refusals, 0 `LOCK:` |
| aarch64 | `logs/20260926-160029` | `3/3`; `woke x3, IPIs x3`, none after | `CPU 1/TCG` **0.0%, 0:00** (`CPU 0` 64.4%) | same |
| riscv64 | `logs/20260926-161120` | `3/3`; `woke x3, IPIs x3`, none after | `CPU 1/TCG` **0.0%, 0:00** (`CPU 0` 65.3%) | same; 0 `timed out` |
The first amd64 run (`logs/20260926-153859`, before the self-test change) also gave `3/3` and `woke x3, IPIs x3`.
**MC-3b is complete.** Core 0 can wake any worker core, and a worker core wakes only for an IPI. Next: MC-3c, the per-core idle context and wake on work, where participants actually run on core 1.
+9
View File
@@ -60,6 +60,11 @@
* before, so this path was previously dormant. */
#define APIC_SPURIOUS_VECTOR 0xFF
/* amd64 (FABRIC-3.10.8 MC-3b): the inter-processor wake vector. Needs its
* own stub in isr.S -- vectors past 33 otherwise land in the default stub,
* which reports 255. */
#define SK_IPI_VECTOR 0x22
/* Initialize Local APIC (enables APIC, sets spurious vector) */
int apic_init(BootInfo *boot_info);
@@ -72,6 +77,10 @@ uint8_t apic_id(void);
* Returns 0 once delivered, -1 if still pending after the bound. */
int apic_send_ipi(uint8_t dest_apic_id, uint32_t icr_low);
/* amd64 (MC-3b): on a secondary core, enable its own LAPIC (SIVR, TPR 0)
* so it can take SK_IPI_VECTOR. Its timer LVT stays masked. */
void apic_secondary_enable(void);
/* Send End-of-Interrupt signal */
void apic_eoi(void);
+4
View File
@@ -73,6 +73,10 @@ typedef struct sk_percpu {
struct VM *release_after;
volatile uint32_t decision_due;
struct VM *entry_vm; /* the VM a first-entry trampoline starts as */
/* MC-3b (hal/smp.c): how to reach this core with an IPI (the APIC id,
* the GIC target mask, the hartid), and how many it has received. */
uint64_t ipi_target;
volatile uint64_t ipi_received;
} sk_percpu_t;
/* Installs the boot core's block in this core's per-CPU register. Call
+12
View File
@@ -64,4 +64,16 @@ void arch_smp_park_once(void);
* exception level, the privilege mode), as a short phrase for its line. */
const char *arch_smp_arrival(const sk_percpu_t *pc);
/* MC-3b. On the secondary core, before it reports online: set up its own
* interrupt receiver for the IPI only, and record in pc->ipi_target how
* to reach it. */
void arch_smp_secondary_init(sk_percpu_t *pc);
/* MC-3b. Send the wake IPI to the core `target` describes. */
void arch_smp_send_ipi(const sk_percpu_t *target);
/* MC-3b. Called by each ISA's interrupt handler when the IPI arrives: counts
* it in this core's block. The handler acknowledges it. */
void sk_smp_ipi_received(void);
#endif /* STARKERNEL_SMP_H */
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+33
View File
@@ -380,6 +380,39 @@ void apic_eoi_intid(uint32_t intid)
mmio_write32(s_gicc_base, GICC_EOIR, intid);
}
/* FABRIC-3.10.8 MC-3b: the wake SGI. */
#define GICD_SGIR 0xF00
#define GIC_WAKE_SGI 0u
/**
* @brief On a secondary core: its banked GIC state, for the wake SGI only.
*
* SGI/PPI enables and priorities (GICD_ISENABLER0, GICD_IPRIORITYR 0-31)
* and the whole CPU interface are banked per core, so each core sets its
* own; the distributor itself is already enabled by core 0's apic_init().
* Its timer PPI stays disabled.
*
* @return This core's CPU-interface target mask, read from its own banked
* GICD_ITARGETSR0 (byte 0 names the reading core).
*/
uint32_t apic_secondary_init_gic(void)
{
uint32_t val = mmio_read32(s_gicd_base, GICD_IPRIORITYR); /* SGIs 0-3 */
val = (val & ~0xFFu) | TIMER_PRIORITY; /* SGI 0 */
mmio_write32(s_gicd_base, GICD_IPRIORITYR, val);
mmio_write32(s_gicd_base, GICD_ISENABLER0, 1u << GIC_WAKE_SGI);
mmio_write32(s_gicc_base, GICC_PMR, PMR_ALLOW_ALL);
mmio_write32(s_gicc_base, GICC_CTLR, GICC_CTLR_ENABLE_GRP0);
return mmio_read32(s_gicd_base, GICD_ITARGETSR) & 0xFFu;
}
/** @brief Send the wake SGI to the cores in @p target_mask. */
void apic_send_wake_sgi(uint32_t target_mask)
{
__asm__ volatile ("dsb ishst" ::: "memory");
mmio_write32(s_gicd_base, GICD_SGIR, ((target_mask & 0xFFu) << 16) | GIC_WAKE_SGI);
}
/**
* @brief Signal End of Interrupt (common cross-ISA signature — see below).
*
+8
View File
@@ -12,6 +12,7 @@
#include "arch.h"
#include "console.h"
#include "starkernel/timer.h"
#include "starkernel/smp.h" /* FABRIC-3.10.8 MC-3b -- sk_smp_ipi_received() */
volatile const char *g_sk_fault_word = (void *)0;
@@ -93,6 +94,13 @@ void aarch64_irq_handler(void)
virtio_input_isr();
}
/* FABRIC-3.10.8 MC-3b: an SGI (INTID 0-15) is the wake IPI. The EOI
* below writes the whole IAR value back, as GICv2 requires for an SGI
* (it carries the sending core's id in bits 12:10). */
if ((intid & 0x3FFu) < 16u) {
sk_smp_ipi_received();
}
if (intid != GIC_INTID_SPURIOUS) {
apic_eoi_intid(intid);
}
+21 -1
View File
@@ -130,9 +130,29 @@ uint32_t arch_smp_enumerate(BootInfo *boot_info, uint64_t *hw_ids, uint32_t cap)
return n;
}
extern uint32_t apic_secondary_init_gic(void);
extern void apic_send_wake_sgi(uint32_t target_mask);
/* MC-3b: wfi with IRQs masked still wakes on a pending, enabled interrupt;
* only then are IRQs opened for one instant to take it -- so a wake SGI
* that arrives just before the wfi is never lost. */
void arch_smp_park_once(void)
{
__asm__ volatile ("msr daifset, #0xf\n\twfi" ::: "memory");
__asm__ volatile ("msr daifset, #0xf\n\t"
"wfi\n\t"
"msr daifclr, #2\n\t"
"isb\n\t"
"msr daifset, #2" ::: "memory");
}
void arch_smp_secondary_init(sk_percpu_t *pc)
{
pc->ipi_target = apic_secondary_init_gic();
}
void arch_smp_send_ipi(const sk_percpu_t *target)
{
apic_send_wake_sgi((uint32_t)target->ipi_target);
}
/* Clean [p, p+len) to the point of coherency: the core reads it with its
+6
View File
@@ -366,6 +366,12 @@ int apic_send_ipi(uint8_t dest_apic_id, uint32_t icr_low)
return -1; /* still pending: reported by the caller */
}
void apic_secondary_enable(void)
{
lapic_write(APIC_REG_TPR, 0);
lapic_write(APIC_REG_SIVR, 0x100u | APIC_SPURIOUS_VECTOR); /* bit 8: APIC enable */
}
/* ============================================================================
* APIC Timer
* ============================================================================ */
+9
View File
@@ -50,6 +50,7 @@
#include "apic.h"
#include "timer.h"
#include "starkernel/i8042.h"
#include "starkernel/smp.h" /* FABRIC-3.10.8 MC-3b -- sk_smp_ipi_received() */
/* Set by the FORTH dispatcher just before calling entry->func(vm).
* Printed on fault to identify which word was executing. */
@@ -346,6 +347,14 @@ void isr_common_handler(uint64_t vector,
return;
}
/* FABRIC-3.10.8 MC-3b: the wake IPI -- counted on the receiving core;
* the wake itself is the return from its halt. */
if (vector == SK_IPI_VECTOR) {
sk_smp_ipi_received();
apic_eoi();
return;
}
/* Handle i8042 keyboard interrupt (item 4.3.5) */
if (vector == I8042_KEYBOARD_VECTOR) {
i8042_handle_irq();
+14 -3
View File
@@ -25,6 +25,7 @@
.hidden isr_stub0
.hidden isr_stub32
.hidden isr_stub33
.hidden isr_stub34
.hidden isr_stub_default
.hidden isr_common_entry
@@ -197,7 +198,15 @@ isr_stub33:
push 33
jmp isr_common_entry
/* Default stub for other vectors (34-255) */
/* Stub for the inter-processor wake IPI, vector 0x22 = 34 (FABRIC-3.10.8
* MC-3b, SK_IPI_VECTOR) -- its own stub for the reason stub 33 has one. */
.global isr_stub34
isr_stub34:
push 0
push 34
jmp isr_common_entry
/* Default stub for other vectors (35-255) */
.global isr_stub_default
isr_stub_default:
push 0 /* fake error */
@@ -221,7 +230,9 @@ isr_stub_table:
.quad isr_stub32
/* Vector 33 = i8042 keyboard IRQ1 (item 4.3.5) */
.quad isr_stub33
/* Vectors 34-255 default to unknown stub */
.rept 222
/* Vector 34 = the wake IPI (MC-3b) */
.quad isr_stub34
/* Vectors 35-255 default to unknown stub */
.rept 221
.quad isr_stub_default
.endr
+14 -1
View File
@@ -102,9 +102,22 @@ uint32_t arch_smp_enumerate(BootInfo *boot_info, uint64_t *hw_ids, uint32_t cap)
uint64_t arch_smp_boot_hw_id(void) { return apic_id(); }
/* MC-3b: exactly one interrupt may end the halt -- `sti`'s one-instruction
* shadow covers the `hlt`, so a wake IPI can't slip in between and be lost. */
void arch_smp_park_once(void)
{
__asm__ volatile ("cli; hlt" ::: "memory");
__asm__ volatile ("sti; hlt; cli" ::: "memory");
}
void arch_smp_secondary_init(sk_percpu_t *pc)
{
apic_secondary_enable();
pc->ipi_target = pc->hw_id; /* the APIC id */
}
void arch_smp_send_ipi(const sk_percpu_t *target)
{
(void)apic_send_ipi((uint8_t)target->ipi_target, 0x00004000u | SK_IPI_VECTOR); /* fixed, assert */
}
const char *arch_smp_arrival(const sk_percpu_t *pc)
+7
View File
@@ -13,6 +13,7 @@
#include "console.h"
#include "starkernel/timer.h"
#include "starkernel/plic.h"
#include "starkernel/smp.h" /* FABRIC-3.10.8 MC-3b -- sk_smp_ipi_received() */
volatile const char *g_sk_fault_word = (void *)0;
@@ -35,6 +36,7 @@ extern void virtio_input_isr(void);
/* scause cause codes for supervisor-mode interrupts (RISC-V Privileged Spec
* §4.1.9, Table "Supervisor cause register values"). */
#define SCAUSE_INTERRUPT_BIT (1ULL << 63)
#define SCAUSE_S_SOFTWARE 1ULL /* FABRIC-3.10.8 MC-3b: the wake IPI */
#define SCAUSE_S_TIMER 5ULL
#define SCAUSE_S_EXTERNAL 9ULL
@@ -78,6 +80,11 @@ void riscv64_interrupt_handler(uint64_t scause)
if (cause == SCAUSE_S_TIMER) {
riscv64_timer_rearm();
heartbeat_tick();
} else if (cause == SCAUSE_S_SOFTWARE) {
/* FABRIC-3.10.8 MC-3b: the wake IPI (SBI send_ipi sets sip.SSIP);
* cleared here, counted on the receiving hart. */
__asm__ volatile ("csrc sip, %0" :: "r"(2ul) : "memory");
sk_smp_ipi_received();
} else if (cause == SCAUSE_S_EXTERNAL) {
uint32_t irq = plic_claim();
if (irq != 0) {
+21 -1
View File
@@ -190,9 +190,29 @@ uint32_t arch_smp_enumerate(BootInfo *boot_info, uint64_t *hw_ids, uint32_t cap)
return n;
}
#define SBI_EXT_IPI 0x735049ul
#define SBI_IPI_SEND 0
/* MC-3b: wfi with SIE clear still wakes on a pending, enabled interrupt
* (sie.SSIE); only then is SIE opened for one instant to take it -- so a
* wake IPI that arrives just before the wfi is never lost. */
void arch_smp_park_once(void)
{
__asm__ volatile ("csrci sstatus, 2\n\twfi" ::: "memory");
__asm__ volatile ("csrci sstatus, 2\n\t"
"wfi\n\t"
"csrsi sstatus, 2\n\t"
"csrci sstatus, 2" ::: "memory");
}
void arch_smp_secondary_init(sk_percpu_t *pc)
{
__asm__ volatile ("csrs sie, %0" :: "r"(2ul) : "memory"); /* SSIE only */
pc->ipi_target = pc->hw_id; /* the hartid */
}
void arch_smp_send_ipi(const sk_percpu_t *target)
{
(void)sbi_call(SBI_EXT_IPI, SBI_IPI_SEND, 1ul, (unsigned long)target->ipi_target, 0);
}
typedef struct rv_started { sk_percpu_t *pc; rv_boot_t *b; struct rv_started *next; } rv_started_t;
+44 -2
View File
@@ -57,6 +57,7 @@ void sk_smp_secondary_main(sk_percpu_t *pc)
/* The entry path set this core's per-CPU register before calling here;
* read it back so the boot core can check it names this block. */
pc->reg_readback = (uint64_t)(uintptr_t)sk_percpu();
arch_smp_secondary_init(pc); /* MC-3b: its IPI receiver, before it says so */
__atomic_store_n(&pc->online, 1u, __ATOMIC_RELEASE);
for (;;) {
arch_smp_park_once();
@@ -79,8 +80,41 @@ uint64_t arch_smp_boot_hw_id(void) { return 0; }
int arch_smp_start(BootInfo *b, sk_percpu_t *pc, void *st) { (void)b; (void)pc; (void)st; return -1; }
void arch_smp_park_once(void) { }
const char *arch_smp_arrival(const sk_percpu_t *pc) { (void)pc; return ""; }
void arch_smp_secondary_init(sk_percpu_t *pc) { (void)pc; }
void arch_smp_send_ipi(const sk_percpu_t *t) { (void)t; }
#endif
void sk_smp_ipi_received(void)
{
__atomic_add_fetch(&sk_percpu()->ipi_received, 1u, __ATOMIC_RELAXED);
}
/* MC-3b boot self-test: `rounds` IPIs to `pc`, one at a time. A round
* trip is the IPI counted AND the core back out of its park step (wake
* counted); only then is the next one sent. Waiting on the IPI count alone
* let TCG take the next IPIs inside the same brief interrupt-open window,
* one wake for three IPIs (first aarch64 run). Returns the round trips. */
static uint32_t ipi_round_trips(sk_percpu_t *pc, uint32_t rounds)
{
uint32_t r, ok = 0;
uint64_t hz = timer_tsc_hz();
for (r = 0; r < rounds; r++) {
uint64_t ipi0 = __atomic_load_n(&pc->ipi_received, __ATOMIC_ACQUIRE);
uint64_t wake0 = __atomic_load_n(&pc->park_wakes, __ATOMIC_ACQUIRE);
uint64_t t0 = heartbeat_read_counter();
uint64_t budget = hz ? (hz / 1000u) * 100u : 0; /* 100 ms */
uint64_t spin = 0x10000000u;
arch_smp_send_ipi(pc);
while (__atomic_load_n(&pc->ipi_received, __ATOMIC_ACQUIRE) == ipi0 ||
__atomic_load_n(&pc->park_wakes, __ATOMIC_ACQUIRE) == wake0) {
if (hz ? (heartbeat_read_counter() - t0 >= budget) : (--spin == 0)) break;
}
if (__atomic_load_n(&pc->ipi_received, __ATOMIC_ACQUIRE) != ipi0 &&
__atomic_load_n(&pc->park_wakes, __ATOMIC_ACQUIRE) != wake0) ok++;
}
return ok;
}
void sk_smp_init(BootInfo *boot_info)
{
char line[200], a[19], b[19];
@@ -182,6 +216,12 @@ void sk_smp_init(BootInfo *boot_info)
(unsigned long long)(hz ? waited / (hz / 1000000u ? hz / 1000000u : 1u) : 0));
}
console_println(line);
{
uint32_t back = ipi_round_trips(pc, 3u);
snprintf(line, sizeof line, "SMP: core %u: IPI round trips %u/3",
(unsigned)pc->core_id, (unsigned)back);
console_println(line);
}
}
g_core_count = next;
kfree(ids);
@@ -195,10 +235,12 @@ void sk_smp_report(void)
if (!g_cores || !g_wakes_reported) return;
for (i = 1; i < g_core_count; i++) {
uint64_t w = __atomic_load_n(&g_cores[i]->park_wakes, __ATOMIC_RELAXED);
uint64_t p = __atomic_load_n(&g_cores[i]->ipi_received, __ATOMIC_RELAXED);
if (w != g_wakes_reported[i]) {
g_wakes_reported[i] = w;
log_message(LOG_WARN, "SMP: core %u woke from park x%llu",
(unsigned)i, (unsigned long long)w);
/* MC-3b: a wake without an IPI behind it is the spurious one. */
log_message(LOG_WARN, "SMP: core %u woke x%llu, IPIs x%llu",
(unsigned)i, (unsigned long long)w, (unsigned long long)p);
}
}
}