docs+code: spell out aspace/vaddr/paddr per coding standards

Expand the abbreviations flagged in docs/coding-standards.md (names spelled
out in full unless an acronym) across the kernel, runtime, ABI, tests, and
docs:

  aspace -> address_space  (AspaceRef -> AddressSpaceRef, retainAspace ->
           retainAddressSpace, loaded_aspace -> loaded_address_space, the
           liveAspaceCount/aspaceDestroyCount test hooks, etc.)
  vaddr  -> virtual_address
  paddr  -> physical_address

The kernel test case and its serial markers are renamed to match:
aspace-refcount -> address-space-refcount (kernel dispatch string and
test/qemu_test.py case name kept in sync). Prose in docs uses the natural
"address space"/"virtual address"; backticked field/identifier references
use the code spelling.

Also expand the bare "AS" abbreviation in three ABI comments and reframe the
set_thread_pointer ABI/handler docs to lead with the arch-neutral concept
(user-space TLS thread pointer; x86_64 IA32_FS_BASE, aarch64 TPIDR_EL0)
rather than x86 FS-first, matching scheduler.zig's existing framing.

Foreign ABI names preserved: the ELF p_vaddr field and mmap/mmio remain.

Verified: zig build, zig build test, and the full 25-case QEMU guardrail
suite all green.
This commit is contained in:
2026-07-21 01:45:47 +01:00
parent 6101e429ba
commit 28b3635979
18 changed files with 232 additions and 231 deletions
+48 -48
View File
@@ -78,7 +78,7 @@ pub const Task = struct {
ipc_wait_endpoint: ?*anyopaque = null,
// Physical root of this task's address space, or 0 for a kernel task (which
// runs on the shared kernel page tables). A user task carries its own.
aspace: u64 = 0,
address_space: u64 = 0,
user_ip: u64 = 0, // user-mode entry point (user task only)
user_sp: u64 = 0, // user-mode stack pointer (user task only)
user_arg: u64 = 0, // value delivered in the user's first argument register at first entry
@@ -95,8 +95,8 @@ pub const Task = struct {
// aarch64. Restored on every context switch to this task (docs/threading-plan.md M10).
thread_pointer: u64 = 0,
// The mmap / MMIO grant-arena cursors moved from Task to the per-address-space object
// (`AspaceRef`, below) so threads sharing one address space hand out disjoint grants
// — see aspaceMmapNextPtr / aspaceDeviceMapNextPtr (docs/threading-plan.md M7).
// (`AddressSpaceRef`, below) so threads sharing one address space hand out disjoint grants
// — see addressSpaceMmapNextPtr / addressSpaceDeviceMapNextPtr (docs/threading-plan.md M7).
// --- synchronous IPC (ipc_sync.zig) ---
// Per-process handle table: a small-int handle names a kernel capability object.
// Each entry tags its `kind` (an IPC endpoint or a shared-memory object) so the
@@ -107,9 +107,9 @@ pub const Task = struct {
// receive, cleared when it replies). A client, while blocked in Call, records
// its message + reply buffers here and its result lands in `ipc_status`.
ipc_client: ?*Task = null,
ipc_send_ptr: u64 = 0, // client: outgoing message (vaddr in this task's AS)
ipc_send_ptr: u64 = 0, // client: outgoing message (virtual_address in this task's address space)
ipc_send_len: u64 = 0,
ipc_reply_ptr: u64 = 0, // client: reply buffer (vaddr)
ipc_reply_ptr: u64 = 0, // client: reply buffer (virtual_address)
ipc_reply_cap: u64 = 0,
ipc_status: i64 = 0, // client: reply length / -errno, written by the replier
dma_map_next: u64 = 0, // bump pointer into this task's DMA arena (0 = unseeded)
@@ -159,9 +159,9 @@ var tasks = [_]Task{.{}} ** maximum_tasks;
// One live entry per address space; threads sharing an address space share this entry,
// so their mmap/mmio grants bump one cursor and never overlap (docs/threading-plan.md M7).
// `mmap_next`/`device_map_next` are 0 until process.zig seeds them to the arena base.
const AspaceRef = struct { root: u64 = 0, count: u32 = 0, mmap_next: u64 = 0, device_map_next: u64 = 0 };
var aspace_refs = [_]AspaceRef{.{}} ** maximum_tasks;
var aspace_destroy_count: u64 = 0;
const AddressSpaceRef = struct { root: u64 = 0, count: u32 = 0, mmap_next: u64 = 0, device_map_next: u64 = 0 };
var address_space_refs = [_]AddressSpaceRef{.{}} ** maximum_tasks;
var address_space_destroy_count: u64 = 0;
/// Total bytes of task **kernel** stacks currently allocated from the kernel heap —
/// incremented when a task is created, decremented when the reaper frees a dead task's
@@ -202,10 +202,10 @@ fn drainReapListLocked(pc: *PerCpu) void {
/// Take a reference to address space `root` (0 = a kernel task, which owns none).
/// Returns false only if the ref table is full — bounded by `maximum_tasks`, so in
/// practice it never is. Caller holds the kernel lock.
fn retainAspace(root: u64) bool {
fn retainAddressSpace(root: u64) bool {
if (root == 0) return true;
var free: ?*AspaceRef = null;
for (&aspace_refs) |*entry| {
var free: ?*AddressSpaceRef = null;
for (&address_space_refs) |*entry| {
if (entry.count != 0 and entry.root == root) {
entry.count += 1;
return true;
@@ -220,51 +220,51 @@ fn retainAspace(root: u64) bool {
/// Drop a reference to `root`; destroy the address space when the **last** one drops.
/// A `root` with no entry — never retained, e.g. a hand-built test space — is
/// destroyed directly, preserving the pre-refcount behaviour. Caller holds the lock.
fn releaseAspace(root: u64) void {
fn releaseAddressSpace(root: u64) void {
if (root == 0) return;
for (&aspace_refs) |*entry| {
for (&address_space_refs) |*entry| {
if (entry.count == 0 or entry.root != root) continue;
entry.count -= 1;
if (entry.count == 0) {
entry.root = 0;
architecture.destroyAddressSpace(root);
aspace_destroy_count += 1;
address_space_destroy_count += 1;
}
return;
}
architecture.destroyAddressSpace(root);
aspace_destroy_count += 1;
address_space_destroy_count += 1;
}
/// Test-observable: how many address spaces are live (entries with a nonzero count).
pub fn liveAspaceCount() u32 {
pub fn liveAddressSpaceCount() u32 {
var live: u32 = 0;
for (&aspace_refs) |*entry| {
for (&address_space_refs) |*entry| {
if (entry.count != 0) live += 1;
}
return live;
}
/// Test-observable: total address-space destructions since boot.
pub fn aspaceDestroyCount() u64 {
return aspace_destroy_count;
pub fn addressSpaceDestroyCount() u64 {
return address_space_destroy_count;
}
/// Pointer to the mmap grant-arena cursor for address space `root`, so the mmap syscall
/// can read-and-bump it. Per-address-space (not per-task), so sibling threads get
/// disjoint grants. **Caller holds the kernel lock** (the entry is stable while held).
/// Null only if `root` was never retained — which can't happen for a live user task.
pub fn aspaceMmapNextPtr(root: u64) ?*u64 {
for (&aspace_refs) |*entry| {
pub fn addressSpaceMmapNextPtr(root: u64) ?*u64 {
for (&address_space_refs) |*entry| {
if (entry.count != 0 and entry.root == root) return &entry.mmap_next;
}
return null;
}
/// Pointer to the MMIO grant-arena cursor for address space `root` (see
/// `aspaceMmapNextPtr`). Caller holds the kernel lock.
pub fn aspaceDeviceMapNextPtr(root: u64) ?*u64 {
for (&aspace_refs) |*entry| {
/// `addressSpaceMmapNextPtr`). Caller holds the kernel lock.
pub fn addressSpaceDeviceMapNextPtr(root: u64) ?*u64 {
for (&address_space_refs) |*entry| {
if (entry.count != 0 and entry.root == root) return &entry.device_map_next;
}
return null;
@@ -288,7 +288,7 @@ pub const PerCpu = struct {
hw_id: u32 = 0, // the core's hardware id (Local APIC id on x86_64)
index: u32 = 0, // dense 0-based core index
online: bool = false, // has this core finished bring-up?
loaded_aspace: u64 = 0, // the address-space root currently loaded on this core
loaded_address_space: u64 = 0, // the address-space root currently loaded on this core
loaded_thread_pointer: u64 = 0, // the TLS thread pointer currently loaded on this core (docs/threading-plan.md M10)
// Tasks pinned to this core (affinity == index), per priority level + bitmap.
pinned_head: [number_priorities]?*Task = .{null} ** number_priorities,
@@ -331,7 +331,7 @@ var preemption_enabled = true;
/// boot, before interrupts are enabled — so no lock is needed here.
pub fn init(boot_priority: Priority) void {
const pc = &cpus[0];
pc.* = .{ .index = 0, .online = true, .loaded_aspace = architecture.kernelPageTable() };
pc.* = .{ .index = 0, .online = true, .loaded_address_space = architecture.kernelPageTable() };
architecture.setCpuLocal(0, @intFromPtr(pc));
tasks[0] = .{ .id = 0, .state = .running, .priority = boot_priority };
pc.current = &tasks[0];
@@ -370,7 +370,7 @@ pub fn secondaryMain() callconv(.c) noreturn {
pc.current = t;
pc.idle = t;
pc.online = true;
pc.loaded_aspace = architecture.kernelPageTable(); // the AP adopted the kernel tables at bring-up
pc.loaded_address_space = architecture.kernelPageTable(); // the AP adopted the kernel tables at bring-up
sync.leave(flags);
architecture.enableInterrupts(); // the timer now preempts this idle context into work
@@ -456,7 +456,7 @@ pub fn spawnOn(entry: *const fn () void, priority: Priority, cpu: u32) bool {
return ok;
}
/// Spawn a **user** task: a task with its own address space (`aspace`) that starts
/// Spawn a **user** task: a task with its own address space (`address_space`) that starts
/// in user mode at `entry` on `user_sp`, recorded under `name` (its argv[0]).
/// `supervisor` is the id of the spawning process (0 = the kernel) — the kill
/// authority — and `exit_endpoint` (an *ipc.Endpoint whose reference the caller
@@ -465,14 +465,14 @@ pub fn spawnOn(entry: *const fn () void, priority: Priority, cpu: u32) bool {
/// lands in `user_task_trampoline`.
/// Returns the new process id, or null (creating nothing) if the table is full or
/// out of memory.
/// **Caller must hold the kernel lock** (the loader that builds `aspace` holds it
/// **Caller must hold the kernel lock** (the loader that builds `address_space` holds it
/// across the whole spawn, so the address space and the task appear atomically).
pub fn spawnUserLocked(aspace: u64, entry: u64, user_sp: u64, user_arg: u64, priority: Priority, task_name: []const u8, supervisor: u32, exit_endpoint: ?*anyopaque) ?u32 {
pub fn spawnUserLocked(address_space: u64, entry: u64, user_sp: u64, user_arg: u64, priority: Priority, task_name: []const u8, supervisor: u32, exit_endpoint: ?*anyopaque) ?u32 {
const t = freeSlot() orelse return null;
const stack = heap.allocator().alloc(u8, stack_size) catch return null;
// Take this task's reference to the address space before we commit the slot, so a
// failure here leaves nothing to unwind (the caller still owns the raw `aspace`).
if (!retainAspace(aspace)) {
// failure here leaves nothing to unwind (the caller still owns the raw `address_space`).
if (!retainAddressSpace(address_space)) {
heap.allocator().free(stack);
return null;
}
@@ -482,7 +482,7 @@ pub fn spawnUserLocked(aspace: u64, entry: u64, user_sp: u64, user_arg: u64, pri
.state = .ready,
.priority = priority,
.stack = stack,
.aspace = aspace,
.address_space = address_space,
.user_ip = entry,
.user_sp = user_sp,
.user_arg = user_arg,
@@ -561,7 +561,7 @@ fn schedule() void {
/// Make `next` this core's running task: publish its kernel stack (TSS.rsp0, so a
/// user-mode interrupt lands on a good stack) and its address space (only when
/// it differs from what's loaded — every page-table switch is a full TLB flush),
/// then switch registers/stacks. Kernel tasks (aspace == 0, no kstack_top used
/// then switch registers/stacks. Kernel tasks (address_space == 0, no kstack_top used
/// from user mode) resolve to the shared kernel page tables and skip the kernel-
/// stack write, so this is a no-op beyond the register switch for a pure-kernel
/// workload. The big kernel lock is held and interrupts are off throughout, so no
@@ -569,10 +569,10 @@ fn schedule() void {
/// `save_sp` receives the outgoing task's stack pointer.
fn switchTo(pc: *PerCpu, save_sp: *usize, next: *Task) void {
if (next.kstack_top != 0) architecture.setKernelStack(pc.index, next.kstack_top);
const want = if (next.aspace != 0) next.aspace else architecture.kernelPageTable();
if (want != pc.loaded_aspace) {
const want = if (next.address_space != 0) next.address_space else architecture.kernelPageTable();
if (want != pc.loaded_address_space) {
architecture.loadPageTable(want);
pc.loaded_aspace = want;
pc.loaded_address_space = want;
}
// Restore the next task's user TLS thread pointer — only on change, the same
// conditional-load discipline as CR3 above (docs/threading-plan.md M10).
@@ -635,12 +635,12 @@ pub fn futexWaitLocked(addr: u64, timeout_ms: u64) FutexResult {
}
/// Wake up to `count` tasks blocked in `futex_wait` on `addr` in address space
/// `aspace`. Precondition: the big kernel lock is held. Returns how many woke.
pub fn futexWakeLocked(aspace: u64, addr: u64, count: u32) u32 {
/// `address_space`. Precondition: the big kernel lock is held. Returns how many woke.
pub fn futexWakeLocked(address_space: u64, addr: u64, count: u32) u32 {
var woken: u32 = 0;
for (&tasks) |*t| {
if (woken >= count) break;
if (t.state == .blocked and t.aspace == aspace and t.futex_addr == addr) {
if (t.state == .blocked and t.address_space == address_space and t.futex_addr == addr) {
t.futex_addr = 0; // the "woken, not timed out" signal to futexWaitLocked
t.wake_at = 0;
t.state = .ready;
@@ -907,7 +907,7 @@ fn reapKillPendingLocked() void {
// task_trampoline, not switchTo's tail), its stack is still queued here. The dying
// task switched away before this tick, so it is off its stack — drain now (M8).
drainReapListLocked(pc);
if (cur.kill_pending and cur.aspace != 0 and !cur.in_system_call) {
if (cur.kill_pending and cur.address_space != 0 and !cur.in_system_call) {
if (terminate_current_hook) |hook| hook(); // noreturn
}
if (reap_task_hook) |hook| {
@@ -977,16 +977,16 @@ pub fn exitUser() noreturn {
pub fn exitUserLocked() noreturn {
const pc = thisCpu();
const dying = pc.current;
const as = dying.aspace;
const as = dying.address_space;
if (as != 0) {
const kroot = architecture.kernelPageTable();
architecture.loadPageTable(kroot); // off the process tables before freeing them
pc.loaded_aspace = kroot;
releaseAspace(as); // destroys only when this was the last task on the space
pc.loaded_address_space = kroot;
releaseAddressSpace(as); // destroys only when this was the last task on the space
}
dying.state = .reaping; // dead but its slot stays reserved until the stack is freed
wakeJoinersLocked(dying.id); // let any thread_join(dying.id) return (M9)
dying.aspace = 0;
dying.address_space = 0;
dying.kill_pending = false;
dying.in_system_call = false;
// Queue for reaping: the task we switch to (or the next tick) frees this stack (M8/M9).
@@ -1007,9 +1007,9 @@ pub fn exitUserLocked() noreturn {
/// task isn't running). The kernel stack is leaked, as in `exitUser` (no reaper
/// yet). Precondition: the big kernel lock is held.
pub fn destroyTaskLocked(t: *Task) void {
if (t.aspace != 0) releaseAspace(t.aspace); // destroys only on the last reference
if (t.address_space != 0) releaseAddressSpace(t.address_space); // destroys only on the last reference
reapStackLocked(t); // safe to free now: `t` is not running on any core (M8)
t.aspace = 0;
t.address_space = 0;
t.kill_pending = false;
t.in_system_call = false;
t.wake_at = 0;
@@ -1052,7 +1052,7 @@ pub fn enumerate(out: []abi.ProcessDescriptor) u64 {
/// Whether the running task is a user process (has its own address space).
pub fn currentIsUserProcess() bool {
return current().aspace != 0;
return current().address_space != 0;
}
pub fn currentId() u32 {