kernel: start independent harts and exchange bounded mailbox requests
0b52e1efc00f9a65822213dfef2a998086e7988b3a69f7cc889f02d9995164e1
Verified: make -C kernel check; native checks and mailbox execution on 1, 2, and 8 harts pass.
1 parent
1279b225
kernel/Makefile
+9 -3
| 2 | 2 | EMU ?= $(or $(RAD_EMULATOR),emulator) |
|
| 3 | 3 | HOST_EMU ?= $(EMU) |
|
| 4 | 4 | COMPILER := ../bin/radiance.rv64.dev |
|
| 5 | 5 | COMPILE := $(HOST_EMU) -memory-size=385024 -data-size=348160 -stack-size=512 -run $(COMPILER) |
|
| 6 | 6 | MODULES := $(wildcard core/*.rad) |
|
| 7 | - | CORE_ASM := arch/atomic.ras arch/context.ras arch/clock.ras arch/mmio.ras arch/physical.ras |
|
| 7 | + | CORE_ASM := arch/atomic.ras arch/context.ras arch/clock.ras arch/mmio.ras arch/physical.ras arch/smp.ras |
|
| 8 | 8 | CORE := -pkg core -mod core.rad $(addprefix -mod ,$(MODULES) $(CORE_ASM)) |
|
| 9 | 9 | CHECK_MODULES := $(wildcard check/*.rad) |
|
| 10 | 10 | USER_INPUTS := user.rad $(wildcard user/*.rad user/*/*.rad) core/abi.rad |
|
| 11 | 11 | USER_BASE := -pkg abi -mod core/abi.rad -pkg user -mod user.rad -mod user/sys.rad -pkg probe -mod user/probe.rad |
|
| 12 | 12 | BASE_IMAGES := sample_root sample_control sample_scalars sample_memory sample_overflow sample_events sample_alias sample_instance |
| 47 | 47 | $(COMPILE) $(BASE_CATALOG) $(CORE) -pkg interrupt -start arch/entry.ras -mod interrupt.rad -mod check/interrupt.ras -entry interrupt -o $@ |
|
| 48 | 48 | ||
| 49 | 49 | native.rv64: native.rad native/instances.rad arch/entry.ras core.rad $(MODULES) $(CORE_ASM) build/baseline/images.rad user/sys.ras $(COMPILER) |
|
| 50 | 50 | $(COMPILE) $(BASE_CATALOG) $(CORE) -pkg native -start arch/entry.ras -mod native.rad -mod native/instances.rad -entry native -o $@ |
|
| 51 | 51 | ||
| 52 | - | check: all check.rv64 context.rv64 interrupt.rv64 native.rv64 |
|
| 52 | + | parallel.rv64: parallel.rad parallel/mailboxes.rad arch/start.ras core.rad $(MODULES) $(CORE_ASM) build/baseline/images.rad user/sys.ras $(COMPILER) |
|
| 53 | + | $(COMPILE) $(BASE_CATALOG) $(CORE) -pkg parallel -start arch/start.ras -mod parallel.rad -mod parallel/mailboxes.rad -entry parallel -o $@ |
|
| 54 | + | ||
| 55 | + | check: all check.rv64 context.rv64 interrupt.rv64 native.rv64 parallel.rv64 |
|
| 53 | 56 | $(HOST_EMU) -run check.rv64 |
|
| 54 | 57 | $(MACHINE) -run kernel.rv64 |
|
| 55 | 58 | $(MACHINE) -max-steps=1000000 -run context.rv64 |
|
| 56 | 59 | $(MACHINE) -harts=2 -irq=3 -irq-at=4000000 -uart-rx=52 -uart-rx-at=8000000 -run interrupt.rv64 |
|
| 57 | 60 | $(MACHINE) -run native.rv64 |
|
| 61 | + | $(MACHINE) -harts=1 -run parallel.rv64 |
|
| 62 | + | $(MACHINE) -harts=2 -run parallel.rv64 |
|
| 63 | + | $(MACHINE) -harts=8 -run parallel.rv64 |
|
| 58 | 64 | ||
| 59 | 65 | clean: |
|
| 60 | 66 | rm -rf build |
|
| 61 | - | rm -f kernel.rv64 check.rv64 context.rv64 interrupt.rv64 native.rv64 |
|
| 67 | + | rm -f kernel.rv64 check.rv64 context.rv64 interrupt.rv64 native.rv64 parallel.rv64 |
kernel/NOTES.md
+19 -2
| 1 | 1 | # Kernel implementation decisions |
|
| 2 | 2 | ||
| 3 | 3 | The specification at https://radiant.computer/system/kernel takes precedence |
|
| 4 | 4 | for fixed call numbers, handle layout, rights, and object behavior. These notes |
|
| 5 | - | record the contracts established through step 20 of the 22-step plan. |
|
| 5 | + | record the contracts established through step 21 of the 22-step plan. |
|
| 6 | 6 | ||
| 7 | 7 | ## Source and trust boundary |
|
| 8 | 8 | ||
| 9 | 9 | - Kernel mechanisms use freestanding Radiance; RAS owns machine entry, register |
|
| 10 | 10 | state, atomics, and MMIO. Hosted checks exercise the same mechanism modules. |
| 294 | 294 | scheduling of these effects is separate from their validation. |
|
| 295 | 295 | - Public Page counts, Interrupt numbers, and Events capacities use u32. Public |
|
| 296 | 296 | domain identifiers use u16, with Pending, Active, and Dead lifecycle values. |
|
| 297 | 297 | The register transport uses u64 words. |
|
| 298 | 298 | ||
| 299 | + | ## Hart startup and remote transport |
|
| 300 | + | ||
| 301 | + | - Each online hart enters M-mode on a private 64 KiB stack in reserved image |
|
| 302 | + | memory. Hart zero release-publishes shared state; secondary harts acquire it |
|
| 303 | + | before use. Each hart synchronizes local instruction fetch. |
|
| 304 | + | - One FIFO ticket lock serializes shared kernel mechanisms. Its Once guard must |
|
| 305 | + | be released exactly once. Machine interrupts remain disabled while it is held. |
|
| 306 | + | Wrapping u32 tickets stay distinct with at most eight callers. |
|
| 307 | + | - Each hart has eight ordinary FIFO request slots. Exact kind, domain incarnation, |
|
| 308 | + | and context identity form the coalescing key. A full queue of distinct requests |
|
| 309 | + | returns Busy without disturbing order. Transport itself grants no authority. |
|
| 310 | + | - CLINT publication uses firmware-selected banks and full memory/I/O fences. |
|
| 311 | + | Queue drain and software-interrupt acknowledgement share the publication lock. |
|
| 312 | + | - Machine probes cover ticket wraparound, mailbox capacity, coherent increments, |
|
| 313 | + | and software-interrupt wakeup after payload publication on 1, 2, and 8 harts. |
|
| 314 | + | RV64A checks run in machine mode, not the hosted emulator. |
|
| 315 | + | ||
| 299 | 316 | ## Validation |
|
| 300 | 317 | ||
| 301 | 318 | Use the current machine-capable sibling emulator. Set `RAD_EMULATOR`, pass |
|
| 302 | 319 | `EMU` to the kernel Make invocation, or put `emulator` on PATH. The kernel build |
|
| 303 | 320 | checks compiler dependencies. From the repository root, run: |
|
| 304 | 321 | ||
| 305 | 322 | ```sh |
|
| 306 | 323 | make -C kernel check |
|
| 307 | 324 | ``` |
|
| 308 | 325 | ||
| 309 | - | Run hosted direct-call coverage for argument and authority errors, metadata, query errors, and execution/lifecycle effects. Keep the existing native, context, IRQ, activation, and lifecycle checks. |
|
| 326 | + | Run machine mailbox/startup probes on 1, 2, and 8 harts, including wraparound, full queues, coherent shared increments, and CLINT wakeup after payload publication. |
|
| 310 | 327 | ||
| 311 | 328 | Run the context reservation probe with an emulator that retains LR/SC |
|
| 312 | 329 | reservations across traps. This checks the kernel's reservation invalidation. |
kernel/arch/smp.ras
added
+43 -0
| 1 | + | // RV64A FIFO tickets and machine-mode CLINT software interrupts. |
|
| 2 | + | .text; |
|
| 3 | + | .export @"core::smp::ticket"; |
|
| 4 | + | @"core::smp::ticket" |
|
| 5 | + | li %t0 1; |
|
| 6 | + | amoadd.w.aqrl %a0 %t0 0(%a0); |
|
| 7 | + | // AMO word results are sign-extended by RV64; the Radiance u32 ABI is not. |
|
| 8 | + | slli %a0 %a0 32; |
|
| 9 | + | srli %a0 %a0 32; |
|
| 10 | + | ret; |
|
| 11 | + | ||
| 12 | + | .export @"core::smp::hart"; |
|
| 13 | + | @"core::smp::hart" |
|
| 14 | + | csrr %a0 mhartid; |
|
| 15 | + | slli %a0 %a0 32; |
|
| 16 | + | srli %a0 %a0 32; |
|
| 17 | + | ret; |
|
| 18 | + | ||
| 19 | + | .export @"core::smp::kick"; |
|
| 20 | + | @"core::smp::kick" |
|
| 21 | + | slli %a1 %a1 2; |
|
| 22 | + | add %a0 %a0 %a1; |
|
| 23 | + | li %t0 1; |
|
| 24 | + | // Full iorw,iorw ordering publishes RAM before asserting MSIP. |
|
| 25 | + | fence; |
|
| 26 | + | sw %t0 0(%a0); |
|
| 27 | + | fence; |
|
| 28 | + | ret; |
|
| 29 | + | ||
| 30 | + | .export @"core::smp::acknowledge"; |
|
| 31 | + | @"core::smp::acknowledge" |
|
| 32 | + | slli %a1 %a1 2; |
|
| 33 | + | add %a0 %a0 %a1; |
|
| 34 | + | // Full iorw,iorw ordering completes the MSIP clear before queue handling. |
|
| 35 | + | fence; |
|
| 36 | + | sw %zero 0(%a0); |
|
| 37 | + | fence; |
|
| 38 | + | ret; |
|
| 39 | + | ||
| 40 | + | .export @"core::smp::syncInstructions"; |
|
| 41 | + | @"core::smp::syncInstructions" |
|
| 42 | + | fence.i; |
|
| 43 | + | ret; |
kernel/arch/start.ras
added
+37 -0
| 1 | + | // M-mode entry on each hart with private 64 KiB stacks in reserved image RAM. |
|
| 2 | + | .text; |
|
| 3 | + | csrw mie %zero; |
|
| 4 | + | csrr %t0 mstatus; |
|
| 5 | + | li %t1 -9; |
|
| 6 | + | and %t0 %t0 %t1; |
|
| 7 | + | csrw mstatus %t0; |
|
| 8 | + | la %t0 @bootFault; |
|
| 9 | + | csrw mtvec %t0; |
|
| 10 | + | csrr %a0 mhartid; |
|
| 11 | + | li %t0 8; |
|
| 12 | + | bgeu %a0 %t0 @bootFault; |
|
| 13 | + | la %sp @hartStacks; |
|
| 14 | + | addi %t0 %a0 1; |
|
| 15 | + | slli %t0 %t0 16; |
|
| 16 | + | add %sp %sp %t0; |
|
| 17 | + | call @"::default"; |
|
| 18 | + | csrr %t0 mhartid; |
|
| 19 | + | bnez %t0 @bootIdle; |
|
| 20 | + | li %t1 0x5555; |
|
| 21 | + | j @bootFinish; |
|
| 22 | + | @bootFault |
|
| 23 | + | csrr %t1 mcause; |
|
| 24 | + | slli %t1 %t1 16; |
|
| 25 | + | li %t0 0x3333; |
|
| 26 | + | or %t1 %t1 %t0; |
|
| 27 | + | @bootFinish |
|
| 28 | + | li %t0 0x10001000; |
|
| 29 | + | sw %t1 0(%t0); |
|
| 30 | + | @bootIdle |
|
| 31 | + | wfi; |
|
| 32 | + | j @bootIdle; |
|
| 33 | + | ||
| 34 | + | .data; |
|
| 35 | + | .align 16; |
|
| 36 | + | @hartStacks |
|
| 37 | + | .space 524288; |
kernel/core.rad
+1 -0
| 25 | 25 | export mod mmio; |
|
| 26 | 26 | export mod physical; |
|
| 27 | 27 | export mod activation; |
|
| 28 | 28 | export mod lifecycle; |
|
| 29 | 29 | export mod calls; |
|
| 30 | + | export mod smp; |
kernel/core/smp.rad
added
+162 -0
| 1 | + | //! FIFO kernel serialization and bounded, identity-bearing remote messages. |
|
| 2 | + | //! One global Lock protects all kernel state and all Mailbox operations. User |
|
| 3 | + | //! architectural frames remain owned by their Running hart outside that lock. |
|
| 4 | + | ||
| 5 | + | use core::abi; |
|
| 6 | + | use core::atomic; |
|
| 7 | + | ||
| 8 | + | /// Maximum distinct requests retained by one hart's mailbox. |
|
| 9 | + | export constant CAPACITY: u32 = 8; |
|
| 10 | + | ||
| 11 | + | /// Naturally aligned FIFO ticket counters; never move or copy a published lock. |
|
| 12 | + | /// At most platform::MAX_HARTS callers may be outstanding, one per online hart. |
|
| 13 | + | export record Lock { |
|
| 14 | + | /// Next ticket to allocate, modulo 2^32; accessed atomically after publication. |
|
| 15 | + | next: u32, |
|
| 16 | + | /// Ticket currently admitted, modulo 2^32; accessed atomically after publication. |
|
| 17 | + | serving: u32, |
|
| 18 | + | } |
|
| 19 | + | ||
| 20 | + | /// Exact-use ownership of a held lock; consume only through release. |
|
| 21 | + | export union Guard: Once { |
|
| 22 | + | /// Lock address and acquired ticket; the lock must outlive this obligation. |
|
| 23 | + | Held { |
|
| 24 | + | /// Stable address of the acquired kernel lock. |
|
| 25 | + | lock: *mut Lock, |
|
| 26 | + | /// Ticket whose turn was observed with acquire ordering. |
|
| 27 | + | ticket: u32, |
|
| 28 | + | }, |
|
| 29 | + | } |
|
| 30 | + | ||
| 31 | + | /// Atomically allocate one wrapping ticket with acquire/release ordering. |
|
| 32 | + | /// The native RV64A implementation zero-extends the old word to the u32 ABI. |
|
| 33 | + | fn ticket(pointer: *mut u32) -> u32; |
|
| 34 | + | ||
| 35 | + | /// Initialize unshared storage once, before publishing its stable address. |
|
| 36 | + | /// Reinitializing a published lock or one with outstanding callers is forbidden. |
|
| 37 | + | export fn init(lock: *mut Lock) { |
|
| 38 | + | set lock.next = 0; |
|
| 39 | + | set lock.serving = 0; |
|
| 40 | + | } |
|
| 41 | + | ||
| 42 | + | /// Wait in ticket order and return the obligation to release this lock once. |
|
| 43 | + | /// The caller must keep machine interrupts disabled until release, must not |
|
| 44 | + | /// acquire recursively, and must keep the lock at a stable address. There may |
|
| 45 | + | /// be at most platform::MAX_HARTS outstanding callers, so u32 wrap is unambiguous. |
|
| 46 | + | export fn acquire(lock: *mut Lock) -> Guard { |
|
| 47 | + | let acquired = ticket(&mut lock.next); |
|
| 48 | + | while atomic::load(&lock.serving) <> acquired {} |
|
| 49 | + | return Guard::Held { lock, ticket: acquired }; |
|
| 50 | + | } |
|
| 51 | + | ||
| 52 | + | /// Consume a held lock and release-publish all kernel writes to the next ticket. |
|
| 53 | + | export fn release(guard: Guard) { |
|
| 54 | + | match guard { |
|
| 55 | + | case Guard::Held { lock, ticket: acquired } => { |
|
| 56 | + | atomic::store(&mut lock.serving, ((acquired as u64 + 1) & 0xffffffff) as u32); |
|
| 57 | + | }, |
|
| 58 | + | } |
|
| 59 | + | } |
|
| 60 | + | ||
| 61 | + | /// Remote mechanisms only; a request does not convey capability authority. |
|
| 62 | + | export union RequestKind: Copy { |
|
| 63 | + | /// Reconsider a waiting context after authorized event publication. |
|
| 64 | + | Wake, |
|
| 65 | + | /// Dispatch an explicitly authorized context with its assigned budget. |
|
| 66 | + | Run, |
|
| 67 | + | /// Stop the named context before lifecycle reclamation can proceed. |
|
| 68 | + | Stop, |
|
| 69 | + | } |
|
| 70 | + | ||
| 71 | + | /// A remote message, not authority: the receiver must recheck current domain |
|
| 72 | + | /// incarnation and context identity under the global lock before any mutation. |
|
| 73 | + | export record Request: Copy { |
|
| 74 | + | /// Mechanism requested of the receiving hart. |
|
| 75 | + | kind: RequestKind, |
|
| 76 | + | /// Complete target domain identity, including its incarnation. |
|
| 77 | + | domain: abi::Object, |
|
| 78 | + | /// Complete target context identity, including its incarnation. |
|
| 79 | + | context: u64, |
|
| 80 | + | } |
|
| 81 | + | ||
| 82 | + | /// Caller-owned FIFO storage; every access after initialization requires the |
|
| 83 | + | /// global kernel lock, including canPost. Keep admission and posting in the |
|
| 84 | + | /// same critical section when another state change depends on admission. |
|
| 85 | + | export record Mailbox { |
|
| 86 | + | /// Circular request storage; only count entries starting at head are live. |
|
| 87 | + | entries: [Request; CAPACITY], |
|
| 88 | + | /// Index of the oldest live request, always less than CAPACITY. |
|
| 89 | + | head: u32, |
|
| 90 | + | /// Number of live distinct requests, never greater than CAPACITY. |
|
| 91 | + | count: u32, |
|
| 92 | + | } |
|
| 93 | + | ||
| 94 | + | /// Initialize private mailbox storage before publication to any receiving hart. |
|
| 95 | + | export fn initMailbox(mailbox: *mut Mailbox) { |
|
| 96 | + | let empty = Request { |
|
| 97 | + | kind: RequestKind::Wake, |
|
| 98 | + | domain: abi::Object { kind: abi::Kind::Empty, index: 0, epoch: 0 }, |
|
| 99 | + | context: 0, |
|
| 100 | + | }; |
|
| 101 | + | set mailbox.entries = [empty; CAPACITY]; |
|
| 102 | + | set mailbox.head = 0; |
|
| 103 | + | set mailbox.count = 0; |
|
| 104 | + | } |
|
| 105 | + | ||
| 106 | + | /// Compare the entire message key, including both object and context incarnations. |
|
| 107 | + | fn identical(left: Request, right: Request) -> bool { |
|
| 108 | + | return left.kind == right.kind and left.domain.kind == right.domain.kind |
|
| 109 | + | and left.domain.index == right.domain.index and left.domain.epoch == right.domain.epoch |
|
| 110 | + | and left.context == right.context; |
|
| 111 | + | } |
|
| 112 | + | ||
| 113 | + | /// Search only live entries; the caller holds the global kernel lock. |
|
| 114 | + | fn contains(mailbox: *Mailbox, request: Request) -> bool { |
|
| 115 | + | for offset in 0..mailbox.count { |
|
| 116 | + | if identical(mailbox.entries[(mailbox.head + offset) % CAPACITY], request) { return true; } |
|
| 117 | + | } |
|
| 118 | + | return false; |
|
| 119 | + | } |
|
| 120 | + | ||
| 121 | + | /// Check admission without mutation while holding the global kernel lock. |
|
| 122 | + | /// An identical outstanding request is admissible even when the mailbox is full. |
|
| 123 | + | export fn canPost(mailbox: *Mailbox, request: Request) -> bool { |
|
| 124 | + | return mailbox.count < CAPACITY or contains(mailbox, request); |
|
| 125 | + | } |
|
| 126 | + | ||
| 127 | + | /// Append or coalesce under the global kernel lock. Identical requests retain |
|
| 128 | + | /// their original FIFO position. Busy means a full distinct request, and leaves |
|
| 129 | + | /// all mailbox state unchanged. The sender authorizes the operation separately. |
|
| 130 | + | export fn post(mailbox: *mut Mailbox, request: Request) throws (abi::Error) { |
|
| 131 | + | if contains(mailbox, request) { return; } |
|
| 132 | + | if mailbox.count == CAPACITY { throw abi::Error::Busy; } |
|
| 133 | + | set mailbox.entries[(mailbox.head + mailbox.count) % CAPACITY] = request; |
|
| 134 | + | set mailbox.count = mailbox.count + 1; |
|
| 135 | + | } |
|
| 136 | + | ||
| 137 | + | /// Remove the oldest request under the global kernel lock, or return nil. |
|
| 138 | + | /// Revalidate the message's current identities before performing its operation. |
|
| 139 | + | export fn take(mailbox: *mut Mailbox) -> ?Request { |
|
| 140 | + | if mailbox.count == 0 { return nil; } |
|
| 141 | + | let request = mailbox.entries[mailbox.head]; |
|
| 142 | + | set mailbox.head = (mailbox.head + 1) % CAPACITY; |
|
| 143 | + | set mailbox.count = mailbox.count - 1; |
|
| 144 | + | return request; |
|
| 145 | + | } |
|
| 146 | + | ||
| 147 | + | /// Read mhartid in machine mode; boot must validate it fits platform::MAX_HARTS. |
|
| 148 | + | export unsafe fn hart() -> u32; |
|
| 149 | + | ||
| 150 | + | /// Set a validated CLINT MSIP bank with full memory and I/O ordering. |
|
| 151 | + | /// clint is the physical controller base; bank is its FDT-selected bank index, |
|
| 152 | + | /// not necessarily a hardware hart ID. Publish the request before calling. |
|
| 153 | + | export unsafe fn kick(clint: u64, bank: u32); |
|
| 154 | + | ||
| 155 | + | /// Clear a validated CLINT MSIP bank with full memory and I/O ordering. |
|
| 156 | + | /// Use the same lock as posting to serialize acknowledgement with queue drain; |
|
| 157 | + | /// clearing after an unlocked empty observation can lose a concurrent kick. |
|
| 158 | + | export unsafe fn acknowledge(clint: u64, bank: u32); |
|
| 159 | + | ||
| 160 | + | /// Synchronize this hart's instruction fetch after observing published code. |
|
| 161 | + | /// Each executing hart must call this before entering newly published code. |
|
| 162 | + | export unsafe fn syncInstructions(); |
kernel/parallel.rad
added
+121 -0
| 1 | + | //! Machine proof of per-hart stacks, coherent ticket locking, and CLINT requests. |
|
| 2 | + | ||
| 3 | + | use core::abi; |
|
| 4 | + | use core::atomic; |
|
| 5 | + | use core::clock; |
|
| 6 | + | use core::cpu; |
|
| 7 | + | use core::fdt; |
|
| 8 | + | use core::platform; |
|
| 9 | + | use core::smp; |
|
| 10 | + | ||
| 11 | + | mod mailboxes; |
|
| 12 | + | ||
| 13 | + | /// Release-published platform initialization. |
|
| 14 | + | static READY: u32 = 0; |
|
| 15 | + | /// Single FIFO lock for all protected counters and request mailboxes. |
|
| 16 | + | static LOCK: smp::Lock = undefined; |
|
| 17 | + | /// Firmware-selected hardware routes, immutable after publication. |
|
| 18 | + | static PLATFORM: platform::Platform = undefined; |
|
| 19 | + | /// Protected counter incremented by every actual hardware hart. |
|
| 20 | + | static COUNTER: u32 = 0; |
|
| 21 | + | /// Per-hart completion publication for each phase. |
|
| 22 | + | static DONE: [u32; platform::MAX_HARTS] = undefined; |
|
| 23 | + | /// Ordinary coherent data published before each software interrupt. |
|
| 24 | + | static PAYLOAD: [u32; platform::MAX_HARTS] = undefined; |
|
| 25 | + | /// Values observed by the receiving harts. |
|
| 26 | + | static OBSERVED: [u32; platform::MAX_HARTS] = undefined; |
|
| 27 | + | /// Bounded remote queues with the same lock as acknowledgement. |
|
| 28 | + | static MAILBOXES: [smp::Mailbox; platform::MAX_HARTS] = undefined; |
|
| 29 | + | ||
| 30 | + | /// Resolve a hardware ID to its validated firmware routing record. |
|
| 31 | + | fn route(hart: u32) -> platform::Hart { |
|
| 32 | + | for i in 0..PLATFORM.hartCount { |
|
| 33 | + | if PLATFORM.harts[i].id == hart { return PLATFORM.harts[i]; } |
|
| 34 | + | } |
|
| 35 | + | panic "route: executing hart absent from firmware"; |
|
| 36 | + | } |
|
| 37 | + | ||
| 38 | + | /// Wait for all discovered harts, not absent array slots. |
|
| 39 | + | fn barrier(phase: u32) { |
|
| 40 | + | for i in 0..PLATFORM.hartCount { |
|
| 41 | + | let hart = PLATFORM.harts[i].id; |
|
| 42 | + | while atomic::load(&DONE[hart]) < phase {} |
|
| 43 | + | } |
|
| 44 | + | } |
|
| 45 | + | ||
| 46 | + | /// Execute the same kernel function independently on each online hart. |
|
| 47 | + | @default unsafe fn main(hardware: u64, description: *u8) -> u32 { |
|
| 48 | + | assert hardware < platform::MAX_HARTS as u64; |
|
| 49 | + | let hart = hardware as u32; |
|
| 50 | + | if hart == 0 { |
|
| 51 | + | let size = try! fdt::word(@sliceOf(description, 40), 4); |
|
| 52 | + | assert size >= 40 and size <= fdt::MAX_BYTES; |
|
| 53 | + | let mut tree: fdt::Tree = undefined; |
|
| 54 | + | try! fdt::decode(@sliceOf(description, size), &mut tree); |
|
| 55 | + | try! platform::discover(&tree, &mut PLATFORM); |
|
| 56 | + | smp::init(&mut LOCK); |
|
| 57 | + | set LOCK.next = 0xfffffffc; |
|
| 58 | + | set LOCK.serving = 0xfffffffc; |
|
| 59 | + | for i in 0..platform::MAX_HARTS { |
|
| 60 | + | atomic::store(&mut DONE[i], 0); |
|
| 61 | + | set PAYLOAD[i] = 0; |
|
| 62 | + | set OBSERVED[i] = 0; |
|
| 63 | + | smp::initMailbox(&mut MAILBOXES[i]); |
|
| 64 | + | } |
|
| 65 | + | mailboxes::run(); |
|
| 66 | + | atomic::store(&mut READY, 1); |
|
| 67 | + | } else { |
|
| 68 | + | while atomic::load(&READY) == 0 {} |
|
| 69 | + | } |
|
| 70 | + | let routing = route(hart); |
|
| 71 | + | assert smp::hart() == hart; |
|
| 72 | + | smp::syncInstructions(); |
|
| 73 | + | clock::arm(PLATFORM.clint.base, routing.clintIndex, 0xffffffffffffffff); |
|
| 74 | + | smp::acknowledge(PLATFORM.clint.base, routing.clintIndex); |
|
| 75 | + | for i in 0..256 { |
|
| 76 | + | let guard = smp::acquire(&mut LOCK); |
|
| 77 | + | set COUNTER += 1; |
|
| 78 | + | smp::release(guard); |
|
| 79 | + | } |
|
| 80 | + | atomic::store(&mut DONE[hart], 1); |
|
| 81 | + | if hart == 0 { |
|
| 82 | + | barrier(1); |
|
| 83 | + | assert COUNTER == PLATFORM.hartCount * 256; |
|
| 84 | + | for i in 0..PLATFORM.hartCount { |
|
| 85 | + | let recipient = PLATFORM.harts[i]; |
|
| 86 | + | if recipient.id == 0 { continue; } |
|
| 87 | + | let guard = smp::acquire(&mut LOCK); |
|
| 88 | + | set PAYLOAD[recipient.id] = 100 + recipient.id; |
|
| 89 | + | try! smp::post(&mut MAILBOXES[recipient.id], smp::Request { |
|
| 90 | + | kind: smp::RequestKind::Wake, |
|
| 91 | + | domain: abi::Object { kind: abi::Kind::Domain, index: 7, epoch: 11 }, |
|
| 92 | + | context: 0x100000000 | recipient.id as u64, |
|
| 93 | + | }); |
|
| 94 | + | smp::kick(PLATFORM.clint.base, recipient.clintIndex); |
|
| 95 | + | smp::release(guard); |
|
| 96 | + | } |
|
| 97 | + | atomic::store(&mut DONE[hart], 2); |
|
| 98 | + | barrier(2); |
|
| 99 | + | for i in 0..PLATFORM.hartCount { |
|
| 100 | + | let recipient = PLATFORM.harts[i].id; |
|
| 101 | + | if recipient <> 0 { assert OBSERVED[recipient] == 100 + recipient; } |
|
| 102 | + | } |
|
| 103 | + | } else { |
|
| 104 | + | let mut frame: cpu::Frame = undefined; |
|
| 105 | + | cpu::init(&mut frame, 0, 0, 0, 0); |
|
| 106 | + | clock::interrupts(8); |
|
| 107 | + | cpu::idle(&mut frame); |
|
| 108 | + | clock::interrupts(0); |
|
| 109 | + | assert frame.cause == 0x8000000000000003; |
|
| 110 | + | let guard = smp::acquire(&mut LOCK); |
|
| 111 | + | let request = smp::take(&mut MAILBOXES[hart]) else { panic "main: missing published request"; }; |
|
| 112 | + | assert request.kind == smp::RequestKind::Wake; |
|
| 113 | + | assert request.domain.index == 7 and request.domain.epoch == 11; |
|
| 114 | + | assert request.context == (0x100000000 | hart as u64); |
|
| 115 | + | set OBSERVED[hart] = PAYLOAD[hart]; |
|
| 116 | + | smp::acknowledge(PLATFORM.clint.base, routing.clintIndex); |
|
| 117 | + | smp::release(guard); |
|
| 118 | + | atomic::store(&mut DONE[hart], 2); |
|
| 119 | + | } |
|
| 120 | + | return 0; |
|
| 121 | + | } |
kernel/parallel/mailboxes.rad
added
+125 -0
| 1 | + | //! Real RV64A ticket wraparound and bounded remote-request queue contracts. |
|
| 2 | + | //! Machine checks use the kernel's atomic object code on a real emulated hart. |
|
| 3 | + | //! Queue checks hold a ticket guard for admission, publication, and consumption. |
|
| 4 | + | ||
| 5 | + | use core::abi; |
|
| 6 | + | use core::atomic; |
|
| 7 | + | use core::smp; |
|
| 8 | + | ||
| 9 | + | /// Make one valid domain message with an explicit full context identity. |
|
| 10 | + | fn request(kind: smp::RequestKind, epoch: u32, context: u64) -> smp::Request { |
|
| 11 | + | return smp::Request { |
|
| 12 | + | kind, domain: abi::Object { kind: abi::Kind::Domain, index: 7, epoch }, context, |
|
| 13 | + | }; |
|
| 14 | + | } |
|
| 15 | + | ||
| 16 | + | /// Reject a full distinct request without disturbing subsequent FIFO delivery. |
|
| 17 | + | fn busy(mailbox: *mut smp::Mailbox, message: smp::Request) { |
|
| 18 | + | assert not smp::canPost(mailbox, message); |
|
| 19 | + | try smp::post(mailbox, message) catch error { |
|
| 20 | + | assert error == abi::Error::Busy; |
|
| 21 | + | return; |
|
| 22 | + | }; |
|
| 23 | + | panic "busy: full mailbox accepted a distinct request"; |
|
| 24 | + | } |
|
| 25 | + | ||
| 26 | + | /// Consume and compare one whole message, including both incarnation fields. |
|
| 27 | + | fn expect(mailbox: *mut smp::Mailbox, expected: smp::Request) { |
|
| 28 | + | let actual = smp::take(mailbox) else { panic "expect: mailbox lost a request"; }; |
|
| 29 | + | assert actual.kind == expected.kind; |
|
| 30 | + | assert actual.domain.kind == expected.domain.kind; |
|
| 31 | + | assert actual.domain.index == expected.domain.index; |
|
| 32 | + | assert actual.domain.epoch == expected.domain.epoch; |
|
| 33 | + | assert actual.context == expected.context; |
|
| 34 | + | } |
|
| 35 | + | ||
| 36 | + | /// Exercise full-ring coalescing, failed admission, FIFO order, and slot reuse. |
|
| 37 | + | /// The caller holds the same kernel lock for admission, publication, and drain. |
|
| 38 | + | fn capacity(mailbox: *mut smp::Mailbox) { |
|
| 39 | + | for round in 0..2 { |
|
| 40 | + | let base = 0x100000000 + round as u64 * 32; |
|
| 41 | + | for i in 0..smp::CAPACITY { |
|
| 42 | + | let message = request(smp::RequestKind::Wake, 3, base + i as u64); |
|
| 43 | + | assert smp::canPost(mailbox, message); |
|
| 44 | + | try! smp::post(mailbox, message); |
|
| 45 | + | } |
|
| 46 | + | let duplicate = request(smp::RequestKind::Wake, 3, base + 3); |
|
| 47 | + | assert smp::canPost(mailbox, duplicate); |
|
| 48 | + | try! smp::post(mailbox, duplicate); |
|
| 49 | + | busy(mailbox, request(smp::RequestKind::Wake, 3, base + 99)); |
|
| 50 | + | for i in 0..4 { |
|
| 51 | + | expect(mailbox, request(smp::RequestKind::Wake, 3, base + i as u64)); |
|
| 52 | + | } |
|
| 53 | + | for i in smp::CAPACITY..smp::CAPACITY + 4 { |
|
| 54 | + | let message = request(smp::RequestKind::Wake, 3, base + i as u64); |
|
| 55 | + | assert smp::canPost(mailbox, message); |
|
| 56 | + | try! smp::post(mailbox, message); |
|
| 57 | + | } |
|
| 58 | + | let retained = request(smp::RequestKind::Wake, 3, base + 6); |
|
| 59 | + | assert smp::canPost(mailbox, retained); |
|
| 60 | + | try! smp::post(mailbox, retained); |
|
| 61 | + | busy(mailbox, request(smp::RequestKind::Stop, 3, base + 6)); |
|
| 62 | + | for i in 4..smp::CAPACITY + 4 { |
|
| 63 | + | expect(mailbox, request(smp::RequestKind::Wake, 3, base + i as u64)); |
|
| 64 | + | } |
|
| 65 | + | assert smp::take(mailbox) == nil; |
|
| 66 | + | } |
|
| 67 | + | } |
|
| 68 | + | ||
| 69 | + | /// Distinct mechanism, domain slot, domain epoch, and context epoch all survive. |
|
| 70 | + | /// Reposting each exact key must neither add entries nor reorder the first post. |
|
| 71 | + | fn identities(mailbox: *mut smp::Mailbox) { |
|
| 72 | + | let wake = request(smp::RequestKind::Wake, 3, 0x100000007); |
|
| 73 | + | let run = request(smp::RequestKind::Run, 3, 0x100000007); |
|
| 74 | + | let stop = request(smp::RequestKind::Stop, 3, 0x100000007); |
|
| 75 | + | let domainEpoch = request(smp::RequestKind::Wake, 4, 0x100000007); |
|
| 76 | + | let contextEpoch = request(smp::RequestKind::Wake, 3, 0x200000007); |
|
| 77 | + | let mut domainSlot = wake; |
|
| 78 | + | set domainSlot.domain.index = 8; |
|
| 79 | + | let mut objectKind = wake; |
|
| 80 | + | set objectKind.domain.kind = abi::Kind::Empty; |
|
| 81 | + | // Mailbox keys are transport data, not authorization or a live-identity test. |
|
| 82 | + | let messages = [wake, run, stop, domainEpoch, contextEpoch, domainSlot, objectKind]; |
|
| 83 | + | for i in 0..messages.len { try! smp::post(mailbox, messages[i]); } |
|
| 84 | + | for i in 0..messages.len { |
|
| 85 | + | assert smp::canPost(mailbox, messages[i]); |
|
| 86 | + | try! smp::post(mailbox, messages[i]); |
|
| 87 | + | } |
|
| 88 | + | for i in 0..messages.len { expect(mailbox, messages[i]); } |
|
| 89 | + | assert smp::take(mailbox) == nil; |
|
| 90 | + | // A consumed key is a new request, not a permanently retained coalescing bit. |
|
| 91 | + | try! smp::post(mailbox, wake); |
|
| 92 | + | expect(mailbox, wake); |
|
| 93 | + | assert smp::take(mailbox) == nil; |
|
| 94 | + | } |
|
| 95 | + | ||
| 96 | + | /// Acquire and release real tickets across 0xffffffff, including sign-bit values. |
|
| 97 | + | /// A missing AMO zero-extension would leave acquire spinning at the first ticket. |
|
| 98 | + | fn wraparound(lock: *mut smp::Lock) { |
|
| 99 | + | // The lock is unshared and has no callers when its starting counters are seeded. |
|
| 100 | + | atomic::store(&mut lock.next, 0xfffffffe); |
|
| 101 | + | atomic::store(&mut lock.serving, 0xfffffffe); |
|
| 102 | + | for i in 0..4 { |
|
| 103 | + | let current = ((0xfffffffe + i as u64) & 0xffffffff) as u32; |
|
| 104 | + | let next = ((current as u64 + 1) & 0xffffffff) as u32; |
|
| 105 | + | let guard = smp::acquire(lock); |
|
| 106 | + | assert atomic::load(&lock.serving) == current; |
|
| 107 | + | assert atomic::load(&lock.next) == next; |
|
| 108 | + | smp::release(guard); |
|
| 109 | + | assert atomic::load(&lock.serving) == next; |
|
| 110 | + | } |
|
| 111 | + | } |
|
| 112 | + | ||
| 113 | + | /// Exercise queue behavior under a real ticket guard and wrapping exact-use release. |
|
| 114 | + | export fn run() { |
|
| 115 | + | let mut lock: smp::Lock = undefined; |
|
| 116 | + | let mut mailbox: smp::Mailbox = undefined; |
|
| 117 | + | smp::init(&mut lock); |
|
| 118 | + | smp::initMailbox(&mut mailbox); |
|
| 119 | + | wraparound(&mut lock); |
|
| 120 | + | let guard = smp::acquire(&mut lock); |
|
| 121 | + | assert smp::take(&mut mailbox) == nil; |
|
| 122 | + | capacity(&mut mailbox); |
|
| 123 | + | identities(&mut mailbox); |
|
| 124 | + | smp::release(guard); |
|
| 125 | + | } |