M14b: DMA memory (dma_alloc / dma_free)
An HCD programs a bus-master engine: it needs a descriptor ring that is physically contiguous, at a physical address it knows, uncacheable, and pinned. mmap gives none of those. Add dma_alloc(len, flags) -> vaddr (rax), paddr (rdx) and dma_free(vaddr, len): grant contiguous, zeroed, pinned, strong-uncacheable memory in a per-process DMA arena (PML4[228]) and hand back both addresses. Pieces: pmm.allocContiguous(count, max_phys) finds a run of contiguous free frames below a cap (dma_below_4g for 32-bit engines); mapUserDmaInto maps them uncacheable (PCD|PWT) but WITHOUT device_grant, so unlike an MMIO grant these frames are real RAM and freeSubtree returns them on teardown — a driver that dies leaks nothing. dma_free is bounded to the DMA arena so it can never unmap the caller's stack/heap/MMIO. dma_write_combining is accepted but falls back to coherent (WC needs PAT programming). Runtime: runtime.dma.alloc/free (a two-return-value stub, like replyWait). New `dma` kernel test drives the mechanism directly — contiguity, the below-4G cap, coherent mapping, and reclaim-on-teardown (no leak). The thin syscall wrappers follow the tested mmap/mmio_map shape and land their first real use with the first DMA driver. Suite 38/38 plus host tests.
This commit is contained in:
+13
-1
@@ -138,6 +138,13 @@ If a class driver needs `mmio`, it has become an HCD and should be one.
|
|||||||
table fails `-ENOSPC` and does not half-deliver. This is the "open" primitive — a bus
|
table fails `-ENOSPC` and does not half-deliver. This is the "open" primitive — a bus
|
||||||
driver mints a per-device endpoint and hands it to a class driver. The runtime exposes
|
driver mints a per-device endpoint and hands it to a class driver. The runtime exposes
|
||||||
`callCap` and `replyWait(..., send_cap)`; no class driver consumes it yet.
|
`callCap` and `replyWait(..., send_cap)`; no class driver consumes it yet.
|
||||||
|
- **M14** — DMA memory + the memory-ordering layer. `/lib/mmio` gives drivers typed
|
||||||
|
volatile access and `mb`/`rmb`/`wmb` (per-arch); `dma_alloc`/`dma_free` grant
|
||||||
|
physically-contiguous, pinned, uncacheable, reclaim-on-teardown buffers with the
|
||||||
|
physical address exposed (`pmm.allocContiguous`, a DMA arena, `mapUserDmaInto`).
|
||||||
|
`dma_below_4g` caps the address for legacy engines; `dma_write_combining` is accepted
|
||||||
|
but falls back to coherent until PAT is programmed. hpet is refactored onto `/lib/mmio`;
|
||||||
|
no DMA driver consumes `dma_alloc` yet.
|
||||||
- **`system_spawn`** — a user-space supervisor starts a driver: `system_spawn(name)`
|
- **`system_spawn`** — a user-space supervisor starts a driver: `system_spawn(name)`
|
||||||
loads a binary bundled in the initial-ramdisk as a fresh ring-3 process. This is what
|
loads a binary bundled in the initial-ramdisk as a fresh ring-3 process. This is what
|
||||||
turned the device manager from "log the match" into "run the driver": the kernel now
|
turned the device manager from "log the match" into "run the driver": the kernel now
|
||||||
@@ -194,7 +201,12 @@ const dev_ep = ipc.callCap(h, // ... mint a per-device endpoint,
|
|||||||
// now dev_ep is a private channel to that one device
|
// now dev_ep is a private channel to that one device
|
||||||
```
|
```
|
||||||
|
|
||||||
## M14 — DMA memory and the memory-ordering contract, for HCDs
|
## M14 — DMA memory and the memory-ordering contract, for HCDs ✅ done
|
||||||
|
|
||||||
|
*Implemented: `/lib/mmio` (typed volatile access + `mb`/`rmb`/`wmb`, per-arch) and
|
||||||
|
`dma_alloc`/`dma_free` (contiguous, pinned, uncacheable, reclaim-on-teardown, physical
|
||||||
|
address exposed). `dma_write_combining` still falls back to coherent — real WC needs
|
||||||
|
PAT, a small follow-up. The rest of this section is the original design note.*
|
||||||
|
|
||||||
**The blocker.** An HCD is a DMA-engine programmer. It needs a descriptor ring the
|
**The blocker.** An HCD is a DMA-engine programmer. It needs a descriptor ring the
|
||||||
device can read, which means memory that is (a) physically contiguous, (b) at a
|
device can read, which means memory that is (a) physically contiguous, (b) at a
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
//! User-space DMA memory: `dma_alloc` / `dma_free`. A driver that programs a
|
||||||
|
//! bus-mastering engine needs a descriptor ring the device can read — memory that is
|
||||||
|
//! physically contiguous, at a physical address the driver knows, uncacheable, and
|
||||||
|
//! pinned. `mmap` gives none of those; this does. Pair it with the barriers in
|
||||||
|
//! `/lib/mmio` (fill the ring, `wmb()`, ring the doorbell). See docs/driver-model.md.
|
||||||
|
|
||||||
|
const abi = @import("abi");
|
||||||
|
const sc = @import("system-call.zig");
|
||||||
|
|
||||||
|
/// Allocation flags. `coherent` (uncacheable) is the portable default; the rest are
|
||||||
|
/// opt-in for specific hardware — see `abi`.
|
||||||
|
pub const coherent: usize = abi.dma_coherent;
|
||||||
|
pub const write_combining: usize = abi.dma_write_combining;
|
||||||
|
pub const below_4g: usize = abi.dma_below_4g;
|
||||||
|
|
||||||
|
/// A DMA allocation: the `virtual` address the CPU touches, and the `physical` address
|
||||||
|
/// to program into the device's descriptor-ring / base registers.
|
||||||
|
pub const Region = struct {
|
||||||
|
virtual: usize,
|
||||||
|
physical: usize,
|
||||||
|
};
|
||||||
|
|
||||||
|
inline fn failed(r: usize) bool {
|
||||||
|
return r > ~@as(usize, 0) - 4095;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allocate `len` bytes of DMA-capable memory with `flags` (e.g. `coherent`, or
|
||||||
|
/// `coherent | below_4g`). Returns the virtual/physical pair, or null on failure. Two
|
||||||
|
/// return values — the virtual address in rax, the physical address in rdx — so it
|
||||||
|
/// needs a hand-written stub.
|
||||||
|
pub fn alloc(len: usize, flags: usize) ?Region {
|
||||||
|
var rax: usize = undefined;
|
||||||
|
var rdx: usize = undefined; // out: physical address
|
||||||
|
asm volatile ("syscall"
|
||||||
|
: [rax] "={rax}" (rax),
|
||||||
|
[rdx] "={rdx}" (rdx),
|
||||||
|
: [n] "{rax}" (@intFromEnum(abi.SystemCall.dma_alloc)),
|
||||||
|
[a0] "{rdi}" (len),
|
||||||
|
[a1] "{rsi}" (flags),
|
||||||
|
: .{ .rcx = true, .r11 = true, .memory = true });
|
||||||
|
if (failed(rax)) return null;
|
||||||
|
return .{ .virtual = rax, .physical = rdx };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Release a region from a prior `alloc` (`virtual` and the same `len`).
|
||||||
|
pub fn free(virtual: usize, len: usize) void {
|
||||||
|
_ = sc.systemCall2(.dma_free, virtual, len);
|
||||||
|
}
|
||||||
@@ -20,6 +20,8 @@ pub const vfs_protocol = @import("vfs-protocol");
|
|||||||
/// C stdio: fopen/fread/fwrite/fseek/ftell/fclose over unistd.
|
/// C stdio: fopen/fread/fwrite/fseek/ftell/fclose over unistd.
|
||||||
/// Device access for drivers: enumerate/claim/mmioMap.
|
/// Device access for drivers: enumerate/claim/mmioMap.
|
||||||
pub const device = @import("device.zig");
|
pub const device = @import("device.zig");
|
||||||
|
/// DMA-capable memory for drivers: contiguous, pinned, uncacheable buffers.
|
||||||
|
pub const dma = @import("dma.zig");
|
||||||
|
|
||||||
/// Re-exported so a user binary can `pub const panic = runtime.panic;`.
|
/// Re-exported so a user binary can `pub const panic = runtime.panic;`.
|
||||||
pub const panic = start.panic;
|
pub const panic = start.panic;
|
||||||
|
|||||||
@@ -37,9 +37,18 @@ pub const SystemCall = enum(u64) {
|
|||||||
irq_ack = 15, // irq_ack(id, resource_index): re-arm a bound IRQ after servicing it
|
irq_ack = 15, // irq_ack(id, resource_index): re-arm a bound IRQ after servicing it
|
||||||
device_register = 16, // device_register(parent_id, descriptor) -> id: publish a child of a device you claimed
|
device_register = 16, // device_register(parent_id, descriptor) -> id: publish a child of a device you claimed
|
||||||
system_spawn = 17, // system_spawn(name_ptr, name_len) -> 0: start a named initial-ramdisk binary as a new ring-3 process
|
system_spawn = 17, // system_spawn(name_ptr, name_len) -> 0: start a named initial-ramdisk binary as a new ring-3 process
|
||||||
|
dma_alloc = 18, // dma_alloc(len, flags) -> vaddr (rax), paddr (rdx): contiguous, pinned, uncacheable DMA memory
|
||||||
|
dma_free = 19, // dma_free(vaddr, len) -> 0: release a prior dma_alloc
|
||||||
_,
|
_,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// `dma_alloc` flags. `coherent` (uncacheable) is the portable default; the others are
|
||||||
|
/// opt-in for specific hardware. `write_combining` needs PAT programming (not yet — it
|
||||||
|
/// currently falls back to coherent); see docs/driver-model.md (M14).
|
||||||
|
pub const dma_coherent: u64 = 1; // strong-uncacheable — the default, the only portable one
|
||||||
|
pub const dma_write_combining: u64 = 2; // write-combining (framebuffers); needs PAT
|
||||||
|
pub const dma_below_4g: u64 = 4; // physical address must fit 32 bits (legacy DMA engines)
|
||||||
|
|
||||||
/// Set in the badge returned by `ipc_reply_wait` when what arrived is an
|
/// Set in the badge returned by `ipc_reply_wait` when what arrived is an
|
||||||
/// **asynchronous notification** (today: a device interrupt bound with `irq_bind`)
|
/// **asynchronous notification** (today: a device interrupt bound with `irq_bind`)
|
||||||
/// rather than a message from a client. There is no payload and no reply owed; the
|
/// rather than a message from a client. There is no payload and no reply owed; the
|
||||||
|
|||||||
@@ -168,6 +168,12 @@ pub fn mapUserDeviceInto(root: u64, virtual: u64, physical: u64, len: u64) void
|
|||||||
paging.mapUserDeviceInto(root, virtual, physical, len);
|
paging.mapUserDeviceInto(root, virtual, physical, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Map coherent DMA RAM into address space `root`: strong-uncacheable, RW+NX, but
|
||||||
|
/// reclaimed on teardown (real RAM, not MMIO). For dma_alloc.
|
||||||
|
pub fn mapUserDmaInto(root: u64, virtual: u64, physical: u64, len: u64) void {
|
||||||
|
paging.mapUserDmaInto(root, virtual, physical, len);
|
||||||
|
}
|
||||||
|
|
||||||
/// Map a page into the kernel address space (non-executable). For the heap, etc.
|
/// Map a page into the kernel address space (non-executable). For the heap, etc.
|
||||||
pub fn mapPage(virtual: u64, physical: u64, writable: bool) void {
|
pub fn mapPage(virtual: u64, physical: u64, writable: bool) void {
|
||||||
paging.map(virtual, physical, writable);
|
paging.map(virtual, physical, writable);
|
||||||
|
|||||||
@@ -273,6 +273,30 @@ pub fn mapUserDeviceInto(pml4: u64, virtual: u64, physical: u64, len: u64) void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Map `[physical, physical+len)` into the user half rooted at `pml4` as **coherent
|
||||||
|
/// DMA memory**: strong-uncacheable (PCD|PWT — a device reads/writes this RAM without
|
||||||
|
/// snooping the CPU caches) but, unlike `mapUserDeviceInto`, **without** `device_grant`
|
||||||
|
/// — because these frames are real RAM from `pmm.allocContiguous`, so teardown
|
||||||
|
/// (`freeSubtree`) must return them to the allocator like any other user page. RW + NX.
|
||||||
|
/// The caller aligns `virtual`/`physical` and places `virtual` in the DMA arena.
|
||||||
|
pub fn mapUserDmaInto(pml4: u64, virtual: u64, physical: u64, len: u64) void {
|
||||||
|
const flags: u64 = present | user | writable | no_execute | pcd | pwt;
|
||||||
|
const first = physical & ~@as(u64, page_size - 1);
|
||||||
|
const last = (physical + (if (len == 0) 1 else len) - 1) & ~@as(u64, page_size - 1);
|
||||||
|
var off: u64 = 0;
|
||||||
|
while (first + off <= last) : (off += page_size) {
|
||||||
|
const v = virtual + off;
|
||||||
|
const pml4e = &tableAt(pml4)[(v >> 39) & 0x1FF];
|
||||||
|
const pdpt = descendUser(pml4e);
|
||||||
|
const pdpte = &tableAt(pdpt)[(v >> 30) & 0x1FF];
|
||||||
|
const pd = descendUser(pdpte);
|
||||||
|
const pde = &tableAt(pd)[(v >> 21) & 0x1FF];
|
||||||
|
const pt = descendUser(pde);
|
||||||
|
tableAt(pt)[(v >> 12) & 0x1FF] = ((first + off) & address_mask) | flags;
|
||||||
|
invalidate(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a new address space: a fresh PML4 with an empty user half and the
|
/// Create a new address space: a fresh PML4 with an empty user half and the
|
||||||
/// kernel's higher half shared in (copying PML4[256..512), whose entries point
|
/// kernel's higher half shared in (copying PML4[256..512), whose entries point
|
||||||
/// at the kernel's PDPTs — pre-created at init and never restaled, so growth in
|
/// at the kernel's PDPTs — pre-created at init and never restaled, so growth in
|
||||||
|
|||||||
@@ -173,3 +173,31 @@ pub fn free(address: u64) void {
|
|||||||
used_frames -= 1;
|
used_frames -= 1;
|
||||||
if (f < next_hint) next_hint = f;
|
if (f < next_hint) next_hint = f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Allocate `count` physically **contiguous** frames whose highest byte is below
|
||||||
|
/// `max_phys` (pass `~0` for no limit; use a real limit for DMA engines with 32-bit
|
||||||
|
/// addressing). Returns the physical base, or null if no free run of that size fits.
|
||||||
|
/// A DMA descriptor ring needs contiguity, a known physical address, and pinning —
|
||||||
|
/// none of which one-frame `alloc` gives. Linear scan for a run of clear bits: fine
|
||||||
|
/// for the small rings DMA needs; a buddy allocator is a later optimisation. The
|
||||||
|
/// frames are freed individually with `free`, so there is no bespoke free path.
|
||||||
|
pub fn allocContiguous(count: usize, max_phys: u64) ?u64 {
|
||||||
|
if (count == 0) return null;
|
||||||
|
const limit: usize = @intCast(@min(@as(u64, total_frames), max_phys / page_size));
|
||||||
|
var start: usize = 1; // frame 0 stays reserved as the "none" address
|
||||||
|
while (start + count <= limit) {
|
||||||
|
if (isUsed(start)) {
|
||||||
|
start += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var run: usize = 0;
|
||||||
|
while (run < count and !isUsed(start + run)) : (run += 1) {}
|
||||||
|
if (run == count) {
|
||||||
|
for (0..count) |i| setUsed(start + i);
|
||||||
|
used_frames += count;
|
||||||
|
return @as(u64, start) * page_size;
|
||||||
|
}
|
||||||
|
start += run + 1; // the frame at start+run is used; skip past it
|
||||||
|
}
|
||||||
|
return null; // no contiguous run of `count` frames below max_phys
|
||||||
|
}
|
||||||
|
|||||||
@@ -62,6 +62,13 @@ pub const user_half_end: u64 = 0x0000_8000_0000_0000;
|
|||||||
pub const device_arena_base: u64 = 0x0000_7100_0000_0000;
|
pub const device_arena_base: u64 = 0x0000_7100_0000_0000;
|
||||||
pub const device_arena_end: u64 = device_arena_base + (4 << 30);
|
pub const device_arena_end: u64 = device_arena_base + (4 << 30);
|
||||||
|
|
||||||
|
/// The DMA arena: where `dma_alloc` places coherent DMA buffers, in PML4[228] — a
|
||||||
|
/// user-exclusive region distinct from the MMIO arena. Unlike MMIO grants these back
|
||||||
|
/// real RAM (contiguous frames), so they are reclaimed on teardown. Per-process cursor
|
||||||
|
/// in `Task.dma_map_next`.
|
||||||
|
pub const dma_arena_base: u64 = 0x0000_7200_0000_0000;
|
||||||
|
pub const dma_arena_end: u64 = dma_arena_base + (256 << 20); // 256 MiB per process
|
||||||
|
|
||||||
/// Largest single `mmap` grant, in pages (1 MiB). The user heap grows in small
|
/// Largest single `mmap` grant, in pages (1 MiB). The user heap grows in small
|
||||||
/// chunks, so this bound is generous; it also caps the frame scratch array below.
|
/// chunks, so this bound is generous; it also caps the frame scratch array below.
|
||||||
const maximum_mmap_pages = 256;
|
const maximum_mmap_pages = 256;
|
||||||
@@ -150,6 +157,8 @@ fn system_call(state: *architecture.CpuState) void {
|
|||||||
.irq_ack => systemIrqAck(state),
|
.irq_ack => systemIrqAck(state),
|
||||||
.device_register => systemDeviceRegister(state),
|
.device_register => systemDeviceRegister(state),
|
||||||
.system_spawn => systemSpawn(state),
|
.system_spawn => systemSpawn(state),
|
||||||
|
.dma_alloc => systemDmaAlloc(state),
|
||||||
|
.dma_free => systemDmaFree(state),
|
||||||
_ => fail(state),
|
_ => fail(state),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -261,6 +270,64 @@ fn systemMmioMap(state: *architecture.CpuState) void {
|
|||||||
architecture.setSystemCallResult(state, base_v + (r.start & (page_size - 1))); // register base
|
architecture.setSystemCallResult(state, base_v + (r.start & (page_size - 1))); // register base
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// dma_alloc(len, flags) -> vaddr (rax), paddr (rdx): grant `len` bytes (rounded up to
|
||||||
|
/// whole pages) of DMA-capable memory — physically contiguous, zeroed, pinned, and
|
||||||
|
/// strong-uncacheable (coherent) — mapping it into the caller's DMA arena and handing
|
||||||
|
/// back both the virtual address to touch and the physical address to program into the
|
||||||
|
/// device. This is what `sysMmap` can't do: mmap frames are scattered, cacheable, and
|
||||||
|
/// their physical address is never disclosed. `dma_below_4g` caps the physical address
|
||||||
|
/// for legacy engines; `dma_write_combining` is accepted but falls back to coherent
|
||||||
|
/// until PAT is programmed. See docs/driver-model.md (M14).
|
||||||
|
fn systemDmaAlloc(state: *architecture.CpuState) void {
|
||||||
|
const len = architecture.systemCallArg(state, 0);
|
||||||
|
const flags = architecture.systemCallArg(state, 1);
|
||||||
|
const t = scheduler.current();
|
||||||
|
if (t.aspace == 0 or len == 0) return fail(state);
|
||||||
|
|
||||||
|
const pages: usize = @intCast((len + page_size - 1) / page_size);
|
||||||
|
const max_phys: u64 = if (flags & abi.dma_below_4g != 0) (@as(u64, 4) << 30) else ~@as(u64, 0);
|
||||||
|
const phys = pmm.allocContiguous(pages, max_phys) orelse return fail(state);
|
||||||
|
|
||||||
|
if (t.dma_map_next == 0) t.dma_map_next = dma_arena_base;
|
||||||
|
const base_v = t.dma_map_next;
|
||||||
|
if (base_v + pages * page_size > dma_arena_end) {
|
||||||
|
for (0..pages) |i| pmm.free(phys + i * page_size); // arena exhausted; give the frames back
|
||||||
|
return fail(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zero through the physmap (the frames aren't mapped in the caller yet), then map.
|
||||||
|
const kernel_view: [*]u8 = @ptrFromInt(boot_handoff.physicalToVirtual(phys));
|
||||||
|
@memset(kernel_view[0 .. pages * page_size], 0);
|
||||||
|
architecture.mapUserDmaInto(t.aspace, base_v, phys, pages * page_size);
|
||||||
|
|
||||||
|
t.dma_map_next = base_v + pages * page_size;
|
||||||
|
architecture.setSystemCallResult(state, base_v); // virtual address for the CPU
|
||||||
|
architecture.setSystemCallResult2(state, phys); // physical address for the device
|
||||||
|
}
|
||||||
|
|
||||||
|
/// dma_free(vaddr, len) -> 0: release a prior `dma_alloc`. Bounded to the DMA arena so
|
||||||
|
/// it can never unmap-and-free the caller's stack, heap, or an MMIO grant; only pages
|
||||||
|
/// actually mapped are freed (an unmapped hole is skipped). Teardown also reclaims any
|
||||||
|
/// DMA pages left mapped at exit (they carry no `device_grant`, so `freeSubtree` frees
|
||||||
|
/// them as ordinary RAM), so a driver that just dies leaks nothing.
|
||||||
|
fn systemDmaFree(state: *architecture.CpuState) void {
|
||||||
|
const base_v = architecture.systemCallArg(state, 0);
|
||||||
|
const len = architecture.systemCallArg(state, 1);
|
||||||
|
const t = scheduler.current();
|
||||||
|
if (t.aspace == 0) return fail(state);
|
||||||
|
const pages: usize = @intCast((len + page_size - 1) / page_size);
|
||||||
|
if (base_v < dma_arena_base or base_v + pages * page_size > dma_arena_end) return fail(state);
|
||||||
|
|
||||||
|
for (0..pages) |i| {
|
||||||
|
const va = base_v + i * page_size;
|
||||||
|
if (architecture.translate(t.aspace, va)) |phys| {
|
||||||
|
architecture.unmapUserPageInto(t.aspace, va);
|
||||||
|
pmm.free(phys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
architecture.setSystemCallResult(state, 0);
|
||||||
|
}
|
||||||
|
|
||||||
/// device_register(parent_id, descriptor_ptr) -> id: publish a child device below a device
|
/// device_register(parent_id, descriptor_ptr) -> id: publish a child device below a device
|
||||||
/// this process has claimed. The bus-driver primitive: a process that owns a bus
|
/// this process has claimed. The bus-driver primitive: a process that owns a bus
|
||||||
/// enumerates it and hands each device it finds to the table, where a class driver
|
/// enumerates it and hands each device it finds to the table, where a class driver
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ pub const Task = struct {
|
|||||||
ipc_reply_ptr: u64 = 0, // client: reply buffer (vaddr)
|
ipc_reply_ptr: u64 = 0, // client: reply buffer (vaddr)
|
||||||
ipc_reply_cap: u64 = 0,
|
ipc_reply_cap: u64 = 0,
|
||||||
ipc_status: i64 = 0, // client: reply length / -errno, written by the replier
|
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)
|
||||||
ipc_send_cap: u64 = ~@as(u64, 0), // handle to transfer with this message (abi.no_cap = none)
|
ipc_send_cap: u64 = ~@as(u64, 0), // handle to transfer with this message (abi.no_cap = none)
|
||||||
ipc_received_cap: u64 = ~@as(u64, 0), // client: handle the reply's transferred cap landed at (abi.no_cap = none)
|
ipc_received_cap: u64 = ~@as(u64, 0), // client: handle the reply's transferred cap landed at (abi.no_cap = none)
|
||||||
next: ?*Task = null, // ready-queue link (also the endpoint sender-FIFO link)
|
next: ?*Task = null, // ready-queue link (also the endpoint sender-FIFO link)
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
|
|||||||
ipcCallTest();
|
ipcCallTest();
|
||||||
} else if (eql(case, "ipc-cap")) {
|
} else if (eql(case, "ipc-cap")) {
|
||||||
capabilityTest();
|
capabilityTest();
|
||||||
|
} else if (eql(case, "dma")) {
|
||||||
|
dmaTest();
|
||||||
} else if (eql(case, "smp")) {
|
} else if (eql(case, "smp")) {
|
||||||
smpTest();
|
smpTest();
|
||||||
} else if (eql(case, "affinity")) {
|
} else if (eql(case, "affinity")) {
|
||||||
@@ -935,6 +937,53 @@ fn capabilityTest() void {
|
|||||||
result();
|
result();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// DMA memory (M14): the properties a bus-mastering driver needs — physically
|
||||||
|
/// contiguous, a known physical address, correct cacheability, pinned, and reclaimed
|
||||||
|
/// on teardown. Exercises the kernel mechanism directly (`pmm.allocContiguous` +
|
||||||
|
/// `mapUserDmaInto`); the `dma_alloc`/`dma_free` syscalls are thin wrappers over it,
|
||||||
|
/// following the tested `mmap`/`mmio_map` shape, and land their first real use with the
|
||||||
|
/// first DMA driver.
|
||||||
|
fn dmaTest() void {
|
||||||
|
log("DANOS-TEST-BEGIN: dma\n", .{});
|
||||||
|
const base_free = pmm.stats().free_frames;
|
||||||
|
|
||||||
|
// A contiguous run: aligned, and it consumed exactly that many frames.
|
||||||
|
const frames = 4;
|
||||||
|
const phys = pmm.allocContiguous(frames, ~@as(u64, 0)) orelse {
|
||||||
|
check("allocContiguous(4) succeeded", false);
|
||||||
|
result();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
check("contiguous run is page-aligned", phys % abi.page_size == 0);
|
||||||
|
check("contiguous run consumed 4 frames", pmm.stats().free_frames == base_free - frames);
|
||||||
|
|
||||||
|
// The below-4G cap is honoured (legacy 32-bit DMA engines).
|
||||||
|
const low = pmm.allocContiguous(2, @as(u64, 4) << 30) orelse 0;
|
||||||
|
check("below-4G run stays under 4 GiB", low != 0 and low + 2 * abi.page_size <= (@as(u64, 4) << 30));
|
||||||
|
|
||||||
|
// Map the run into a fresh address space as coherent DMA and translate each page
|
||||||
|
// back: the same physical run, in order — proving contiguity and the mapping.
|
||||||
|
const aspace = architecture.createAddressSpace().?;
|
||||||
|
architecture.mapUserDmaInto(aspace, process.dma_arena_base, phys, frames * abi.page_size);
|
||||||
|
var mapped_ok = true;
|
||||||
|
for (0..frames) |i| {
|
||||||
|
const va = process.dma_arena_base + i * abi.page_size;
|
||||||
|
const got = architecture.translate(aspace, va) orelse {
|
||||||
|
mapped_ok = false;
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
if (got != phys + i * abi.page_size) mapped_ok = false;
|
||||||
|
}
|
||||||
|
check("DMA pages translate to the contiguous physical run", mapped_ok);
|
||||||
|
|
||||||
|
// Teardown must reclaim the DMA RAM (the leaves carry no device_grant, so
|
||||||
|
// freeSubtree frees them as ordinary frames) — a driver that just dies leaks none.
|
||||||
|
architecture.destroyAddressSpace(aspace);
|
||||||
|
for (0..2) |i| pmm.free(low + i * abi.page_size);
|
||||||
|
check("no frames leaked after DMA teardown", pmm.stats().free_frames == base_free);
|
||||||
|
result();
|
||||||
|
}
|
||||||
|
|
||||||
var proc_worker_run: bool = true;
|
var proc_worker_run: bool = true;
|
||||||
var proc_worker_ran: bool = false;
|
var proc_worker_ran: bool = false;
|
||||||
|
|
||||||
|
|||||||
@@ -130,6 +130,11 @@ CASES = [
|
|||||||
{"name": "ipc-cap",
|
{"name": "ipc-cap",
|
||||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
|
# DMA memory (M14): contiguous frame allocation, below-4G cap, coherent mapping,
|
||||||
|
# and reclaim on teardown.
|
||||||
|
{"name": "dma",
|
||||||
|
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||||
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
# Parallelism: needs more than one core, so this case boots with -smp 4.
|
# Parallelism: needs more than one core, so this case boots with -smp 4.
|
||||||
{"name": "smp",
|
{"name": "smp",
|
||||||
"smp": 4,
|
"smp": 4,
|
||||||
|
|||||||
Reference in New Issue
Block a user