diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-21 21:17:44 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-21 21:17:44 +0200 |
| commit | 71b6445b048456bf198d8b622570db95f4665cb9 (patch) | |
| tree | 808e73fa95709ed9c2d409aecaf275fd70365293 /rsnix/src/sched.rs | |
| parent | input: total keyboard routing + per-terminal discipline (ADR 0006 ph1) (diff) | |
| download | rs-dos-71b6445b048456bf198d8b622570db95f4665cb9.tar.gz rs-dos-71b6445b048456bf198d8b622570db95f4665cb9.zip | |
exec: PE32+ loader, versioned ABI, first external binaries (v0.6.0)
rsapi: the kernel<->binary ABI crate (no_std, no deps). Versioned
Exports table (magic + version header), EntryPoint = extern "C" on
x86_64-unknown-uefi (Win64: RCX/RDX/R8), Line/Arg/StreamResult types.
Binaries link only this crate.
rsnix::load: PE32+ parser — DOS/COFF/optional headers, section copy,
BSS zero-fill, .reloc base relocations (DIR64) via rva_to_file mapping
(the reloc directory is an RVA, not a file offset). Images are
allocated at LOAD_BASE=192MiB inside the page allocator's conventional
span and identity-mapped user+writable+nx.
rsnix::exec: resolution name -> /disk/<UID>/bin/<kind>/<name>.efi per
mounted volume; spawn reserves a sched slot, stores a per-task ExecCtx
(exports + argv block + image) before launch_at installs the frame —
no adopt race. Trampoline calls entry(&Exports, argc, argv), posts
TaskMsg::User(exit_code) to the shell. abort_current notifies the
shell with u64::MAX when an exec'd binary dies, so a faulting command
cannot leave the shell blocked forever.
sched: reserve_slot/launch_at split out of spawn; task_exit made pub.
shell: exec_external runs before non-frozen built-ins (ADR 0006
resolution rule); frozen core (help/clear/echo/halt/poweroff/exit/
rescan) is exempt. Exports v1 wires put_line to the terminal and
get_line to EOF; stream injection lands in Phase 3. ^C while waiting
kills the command task.
bins/{hello,boom}: gate binaries. hello prints through the exports
table; boom faults on 0x400000000000 to prove containment. The image
entry point IS the ABI entry (efi_main carries the Exports signature).
Version 0.6.0: exec exists end-to-end.
Gates verified in QEMU: 'hello' loads from \\bin\\cli\\hello.efi, calls
put_line across the ABI, exits 0, shell unblocks; 'boom' faults, task
dies gracefully with full report, shell survives, jobs shows the corpse;
built-in fallback works when the file is absent; test-disk PASS.
Diffstat (limited to '')
| -rw-r--r-- | rsnix/src/sched.rs | 46 |
1 files changed, 45 insertions, 1 deletions
diff --git a/rsnix/src/sched.rs b/rsnix/src/sched.rs index 0c4938e..107dba6 100644 --- a/rsnix/src/sched.rs +++ b/rsnix/src/sched.rs | |||
| @@ -396,6 +396,31 @@ pub fn spawn(entry: fn(), name: &str) -> Option<usize> { | |||
| 396 | if ok { Some(id) } else { None } | 396 | if ok { Some(id) } else { None } |
| 397 | } | 397 | } |
| 398 | 398 | ||
| 399 | /// Reserve an empty task slot without installing a frame. The slot is | ||
| 400 | /// marked Ready-with-no-frame is NOT valid to schedule — callers must | ||
| 401 | /// follow with `launch_at` before yielding (exec does both back-to-back | ||
| 402 | /// under the preemption lock window of the caller). | ||
| 403 | pub fn reserve_slot() -> Option<usize> { | ||
| 404 | lock(); | ||
| 405 | let id = unsafe { | ||
| 406 | let mut slot = None; | ||
| 407 | for i in 2..MAX_TASKS { | ||
| 408 | if TASKS[i].state == TaskState::Done && TASKS[i].stack != 0 { | ||
| 409 | reap(i); | ||
| 410 | slot = Some(i); | ||
| 411 | break; | ||
| 412 | } | ||
| 413 | if TASKS[i].state == TaskState::Done && TASKS[i].stack == 0 && TASKS[i].name_len == 0 { | ||
| 414 | slot = Some(i); | ||
| 415 | break; | ||
| 416 | } | ||
| 417 | } | ||
| 418 | slot | ||
| 419 | }; | ||
| 420 | unlock(); | ||
| 421 | id | ||
| 422 | } | ||
| 423 | |||
| 399 | unsafe fn reap(id: usize) { | 424 | unsafe fn reap(id: usize) { |
| 400 | unsafe { | 425 | unsafe { |
| 401 | let t = &mut TASKS[id]; | 426 | let t = &mut TASKS[id]; |
| @@ -408,6 +433,19 @@ unsafe fn reap(id: usize) { | |||
| 408 | } | 433 | } |
| 409 | } | 434 | } |
| 410 | 435 | ||
| 436 | /// Install the initial frame for a reserved slot and mark it Ready. | ||
| 437 | /// The slot must have come from `reserve_slot` and must not have been | ||
| 438 | /// scheduled in between (exec stores its context between the two calls). | ||
| 439 | pub fn launch_at(id: usize, entry: fn(), name: &str) -> bool { | ||
| 440 | if id >= MAX_TASKS { | ||
| 441 | return false; | ||
| 442 | } | ||
| 443 | lock(); | ||
| 444 | let ok = spawn_at(id, entry, name); | ||
| 445 | unlock(); | ||
| 446 | ok | ||
| 447 | } | ||
| 448 | |||
| 411 | fn spawn_at(id: usize, entry: fn(), name: &str) -> bool { | 449 | fn spawn_at(id: usize, entry: fn(), name: &str) -> bool { |
| 412 | let Some(base) = memory::alloc_pages(STACK_PAGES) else { | 450 | let Some(base) = memory::alloc_pages(STACK_PAGES) else { |
| 413 | return false; | 451 | return false; |
| @@ -483,7 +521,7 @@ unsafe extern "C" fn trampoline(entry: usize) -> ! { | |||
| 483 | task_exit() | 521 | task_exit() |
| 484 | } | 522 | } |
| 485 | 523 | ||
| 486 | fn task_exit() -> ! { | 524 | pub fn task_exit() -> ! { |
| 487 | lock(); | 525 | lock(); |
| 488 | unsafe { | 526 | unsafe { |
| 489 | TASKS[CURRENT].state = TaskState::Done; | 527 | TASKS[CURRENT].state = TaskState::Done; |
| @@ -648,6 +686,12 @@ pub fn abort_current(msg: &str) -> ! { | |||
| 648 | TASKS[id].stack, | 686 | TASKS[id].stack, |
| 649 | TASKS[id].guard | 687 | TASKS[id].guard |
| 650 | ); | 688 | ); |
| 689 | // An exec'd binary died: unblock the waiting spawner with a | ||
| 690 | // completion message (exit code -1 encoded as u64::MAX). The | ||
| 691 | // shell's queue is the only waiter; best-effort by design. | ||
| 692 | if id >= 2 { | ||
| 693 | let _ = push_msg(SHELL_TID, TaskMsg::User(u64::MAX)); | ||
| 694 | } | ||
| 651 | PREEMPT_DEPTH.store(0, Ordering::Relaxed); | 695 | PREEMPT_DEPTH.store(0, Ordering::Relaxed); |
| 652 | } | 696 | } |
| 653 | yield_int(); | 697 | yield_int(); |
