跳到主要内容

启动栈、内核任务栈与用户栈

TGOSKits 没有把物理 RAM 静态切成一个“栈区”和一个“堆区”。CPU0 最早期栈来自内核镜像 .bss,每 CPU 启动栈由 someboot 早期线性分配器预分配,普通内核任务栈从运行时分配器获取,Starry 用户栈则是用户地址空间中的 Virtual Memory Area(虚拟内存区域,VMA)。

1. 栈类型与生命周期

不同栈存在于不同启动阶段和地址空间。区分这些栈是分析内存占用、guard page 和释放行为的前提。

1.1 栈来源总览

当前主要栈类型如下。默认大小来自当前 linker/build 配置,平台配置可以覆盖任务栈大小。

栈类型默认大小来源生命周期
CPU0 最早期 linker 栈STACK_SIZE = 0x40000,256 KiBkernel .bss / KImage启动早期,镜像范围始终保留
每 CPU boot/main 栈someboot::mem::stack_size(),默认 256 KiBearly bump 的 per-CPU 区系统生命周期,bootstrap resource 不持有可释放 stack handle
普通内核任务栈默认 0x40000,可由构建配置覆盖axruntime 的 heap 或 KernelVirtualAllocationthread resource reaper 释放
idle 栈与运行时任务栈配置一致axruntime stack allocatoridle thread 生命周期
Starry 用户栈loader/应用程序二进制接口选择的虚拟内存区域大小用户地址空间 backend,按需填页exec/exit/unmap 时回收

栈大小不是物理连续 RAM 的全局配额。只有具体 stack allocation 会消耗页;用户栈预留的虚拟内存大小也不等于所有页面已经 resident。

1.2 栈与 heap 的关系

“栈和堆如何划分”在运行期表现为不同 owner 使用同一 allocator,而不是两个永久物理分区。下图展示来源关系。

内核与用户栈资源来源

普通任务栈默认 256 KiB。未启用保护页和 vmap-task-stack 时使用 heap;启用任一配置后,由 KernelVirtualAllocation 预留连续虚拟区并逐页分配 backing。保护页仅占虚拟地址,物理页统计使用 UsageKind::TaskStack

1.3 架构差异

栈的 owner 与分配来源跨架构一致,架构入口仅负责把栈顶写入本架构栈寄存器并跳转:x86_64 使用 rsp,AArch64 和 RISC-V 使用 sp,LoongArch64 使用 $sp。启动 entry 必须在进入 Rust 前满足相应调用约定的栈对齐,栈 owner 不保存架构私有寄存器状态。

Guard page 的架构差异只在本地地址转换缓存指令;四架构的跨 CPU 覆盖都由上层软件 mask、远程失效和确认协议拥有。地址窗口和指令细节统一见多架构内存实现,本章后续只说明栈特有的 owner 和 guard 时序。

2. CPU0 启动栈

CPU0 在 allocator、完整页表和 per-CPU 映射可用之前就需要栈。这个阶段使用 linker 明确预留的静态范围,避免任何动态依赖。

2.1 链接布局

platforms/someboot/src/ld/bss.ld.bss 末尾定义 __cpu0_stack__cpu0_stack_top,并移动 location counter STACK_SIZEdefaults.ld 为该符号提供 256 KiB 默认值。

.bss
├── ordinary BSS and COMMON
├── __cpu0_stack
├── STACK_SIZE bytes
└── __cpu0_stack_top

该范围包含在 kernel image 的结束边界中,someboot::mem::early_init() 将整个镜像记为 KImage。它不会进入 Free,也不会由运行时 allocator 单独释放。

2.2 切换到每 CPU 栈

建立目标页表和 per-CPU 映射后,someboot::prime_entry() 读取当前 CPU 的 PerCpuMeta::stack_top,转换到 per-CPU 虚拟地址,并通过架构 jump_to() 切换 SP 后进入 __someboot_main

阶段SP 来源可用能力
最早架构入口linker CPU0 stack最小启动代码、扁平设备树/页表准备
MMU/per-CPU 初始化后PerCpuMeta::stack_top_virtdynamic platform main、ax-runtime
scheduler 初始化后同一 boot stack 被 main task 借用正常内核任务调度

切换后 linker stack 仍属于 KImage,只是不再作为 main task 的运行栈。代码不能假定该旧范围会被回收到 Buddy。

3. 每 CPU 启动栈

每个可启动 CPU 都在引导处理器 early boot 阶段获得自己的 boot stack。应用处理器启动不依赖通用 heap,避免并发 bring-up 时 allocator 和 per-CPU storage 尚未就绪的问题。

3.1 预分配布局

platforms/someboot/src/smp/layout.rs 只保留一种每 CPU 连续布局。layout_info() 从 linker template 大小、PerCpuMeta 大小、stack 大小、页大小和区域对齐计算偏移;所有 CPU 共享同一个 area_stride

计算量公式保证条件
metadata offsetalign_up(data_size, meta_alignment)metadata 至少按 max(align_of::<PerCpuMeta>(), 64) 对齐
stack offsetalign_up(metadata_end, page_size)stack 起点按页对齐
area stridealign_up(stack_end, region_alignment)每个 CPU slot 可独立寻址
allocation sizearea_stride * cpu_countchecked multiplication,不能回绕

alloc_percpu() 按固件 CPU 数一次申请完整区域。最终高地址初始化阶段复制 linker per-CPU template,并为每个 CPU 写入 hardware ID、logical index、stack top 和 secondary entry;完成 cache maintenance 后才发布运行期 CPU 数。

3.2 调度器借用

动态平台的 boot_stack_bounds(cpu_idx)somehal::smp::cpu_meta() 返回 stack bottom 和 size。调度器安装 bootstrap thread 时只接管当前架构 context 与 TLS;create_bootstrap_resources() 把 stack handle 设为 StackHandle::NONE,明确表示该 boot stack 仍由启动层拥有。

Owner 状态运行时表示回收行为
boot/main/secondary stackbootstrap ThreadResources 中为 StackHandle::NONE不释放,仅由启动层持有物理范围
plain task allocationopaque StackHandle 指向 StackBacking::Heap用原 Layout 归还 runtime allocator
guard-page task allocationopaque StackHandle 指向 StackBacking::VirtualPages清除 usable PTE 并完成 TLB shootdown 后释放 backing 和 VA

StackHandle::NONE 在 bootstrap resource bundle 中表达“任务正在使用,但 scheduler 没有获得该 stack 的回收所有权”。这防止 bootstrap thread 退休时把 early bump 的系统级 stack 错误释放给 Buddy。

4. 普通内核任务栈

components/ax-task 只持有运行时提供的 opaque StackHandle;分配策略和地址空间操作属于 os/arceos/modules/axruntime/src/task/resources.rs::RuntimeStack。普通任务创建先分配 stack/TLS/context,再把三个 handle 作为一个 ThreadResources bundle 转交给 scheduler;线程退出后由 resource reaper 按相反顺序销毁。

4.1 普通分配

未启用 stack-guard-pagevmap-task-stack 时,allocate_heap_stack() 使用请求的 usable size 与 alignment 构造 Layout,再经 ax_alloc::global_allocator() 分配。ArceOS 传入 16 字节 alignment;默认 256 KiB 请求走 Buddy 大对象路径。

操作实现失败语义
allocationglobal_allocator().alloc(layout)返回 typed RuntimeStatus
publicationBox<RuntimeStack> 转为唯一 non-zero StackHandlehandle 必须只转交和销毁一次
releaseglobal_allocator().dealloc(ptr, layout)必须使用原 size/align

plain stack 没有页级溢出隔离。需要越界后立即 fault 的配置应启用 guard page。

4.2 保护页分配

启用 stack-guard-pagevmap-task-stack 后,allocate_virtual_stack() 构造 KernelVirtualAllocationLayout。usable 和 guard 大小按请求对齐向上取整,with_alignment() 校验对齐约束。KernelVirtualAllocation::allocate() 从 kernel address space 的空洞预留整个区间,guard 范围始终没有 PTE,也没有对应物理页。

backing 的 VecArc 和 data frame 在 kernel address-space 锁外准备;每个 PTE 通过 plan_map_page() 获取计划,在锁外准备 page-table deposit,再加锁重验并安装。并发改变目录会返回 stale deposit,失败的未发布页表页在锁外释放。任何部分安装失败均由已经创建的 token 把整段 reservation 标成 Retiring

5. 栈保护一致性

KernelVirtualAllocationKernelVirtualAllocationBackend 共同持有唯一虚拟区和 backing 生命周期。栈 token 的 Drop 只发布退休状态;同步释放与有界重试负责撤销页表和确认 TLB,resource reaper 才能最终释放内存。

5.1 发布和退休

新 reservation 从未发布过有效 guard PTE,因此创建栈不需要撤销 direct-map 页,也不等待尚未上线的 CPU。usable 页全部建立后才把栈 handle 交给 scheduler。

阶段页表与所有权
prepare锁外申请 backing,元数据预留 VA,再逐页安装 usable PTE
liveruntime 唯一 stack handle 持有 token,MM 元数据持有 backing
retiringtoken 已放弃使用;元数据仍保留 VA 和全部 frame
quarantinedusable PTE 已分离,等待全 CPU TLB 确认
reclaimed移除元数据,锁外 Drop backing;VA 可再次使用

prepare_kernel_virtual_release() 保留共享 kernel 页表目录,仅清除属于本 allocation 的 leaf。局部失败或 TLB 超时不会释放物理页,也不会复用 reservation;retry_kernel_virtual_quarantines() 按有界扫描继续处理,每轮失败的区间不会阻止后续独立区间。

5.2 调度与分配边界

components/ax-task 负责线程资源生命周期,ax-runtime 负责消费 StackHandleax-mm 负责 VMA/PTE/backing。调度切换不运行虚拟栈最终释放;普通资源准备会先重试一批退休区间,再申请新栈。

这与 Linux vunmap_pte_range() 保留页表目录、finish_task_switch() 在 runqueue 解锁后安排 MM 释放的责任划分一致。具体目标 CPU 的选择、上线同步和确认仍由 ax_hal::cache::flush_tlb_range_all_cpus() 负责。

6. Starry 用户栈

Starry 用户栈属于用户虚拟地址空间,不是 runtime StackHandle。loader 和进程内存策略建立 stack 虚拟内存区域,物理页由缺页或 populate 路径按需分配。

6.1 虚拟区与驻留页

用户栈的虚拟范围计入虚拟内存大小,只有已映射的匿名页计入常驻内存集大小。os/StarryOS/kernel/src/mm/stats.rs::ProcessMemStats 通过 [stack] 名称或进程 stack range 将虚拟内存区域分类到 stack_pages

指标用户栈含义物理占用关系
VmStk被识别为 stack 的虚拟内存区域页数可能包含未驻留页
VmSize全部虚拟内存区域的虚拟内存大小不等于 Buddy 已分配页
RssAnon已驻留匿名页包含实际 fault/populate 的 stack page
kernel task stack内核态执行栈不计入用户进程虚拟内存区域统计

用户栈释放通过 address space unmap/clear 和 backend page owner 完成,不调用 runtime kernel-stack deallocator。

6.2 保护边界

用户访问权限由 Stage-1 页表项和 Starry 虚拟内存区域 flags 共同决定。kernel stack guard feature 只保护 axruntime 分配的内核栈,不会自动给所有 Starry 用户 stack 增加 guard 虚拟内存区域。

边界负责组件故障处理
用户 stack 虚拟内存区域权限Starry AddrSpace / backendhandle_page_fault() 返回是否成功,trap 层再处理 signal
kernel task guard pageaxruntime + ax-mm诊断 diagnose_current_stack_guard_page_fault()
CPU boot stack 范围someboot / ax-hal启动配置,当前无动态 guard

分析 stack overflow 时必须先确认 fault address 属于哪种 stack。把用户虚拟内存区域 fault 误判成 kernel guard,或把 boot stack 当作 allocator 泄漏,都会得出错误结论。

7. 配置与审计入口

栈行为跨链接布局、启动平台、调度器和用户虚拟内存,修改默认大小或保护页 feature 时需要同时检查这些边界。

7.1 配置来源

默认值存在于不同构建阶段,最终以平台和 axbuild 生成配置为准。重复默认值必须保持语义一致或由构建脚本明确覆盖。

配置当前默认源码入口
someboot STACK_SIZE0x40000platforms/someboot/src/ld/defaults.ld
ax-runtime task stack0x40000os/arceos/modules/axruntime/build.rs;作为 StackRequest 传给 runtime allocator
API exposed task stack0x40000arceos_api / arceos_posix_api config
user pthread compatibility default2 MiBos/arceos/ulib/axstd/src/os/libc_compat.rs

修改一个默认值时必须同步生成的 build info、公开 API 和实际 task creation 参数,避免文档或 resource limit 仍报告旧值。

7.2 源码检查点

下面的文件覆盖 stack 从静态布局到释放的完整生命周期。resource handle owner 与 guard shootdown 的用例见内存管理测试

源码审计重点
platforms/someboot/src/ld/bss.ldCPU0 linker stack 是否位于 KImage
platforms/someboot/src/smp/layout.rs每 CPU offset、stride、总大小和 checked arithmetic
platforms/someboot/src/smp/mod.rstyped layout 初始化、CPU metadata 发布与 cache maintenance
platforms/axplat-dyn/src/boot.rsboot_stack_bounds() 元数据来源
os/arceos/modules/axruntime/src/task/bootstrap.rsbootstrap thread 如何保留外部 boot stack owner
components/ax-task/src/thread/spec.rsscheduler 如何持有并一次性释放 opaque resource handles
os/arceos/modules/axruntime/src/task/resources.rsheap/virtual stack 分配与回收
os/arceos/modules/axmm/src/kernel_alloc.rsreservation、逐页安装、退休和全 CPU TLB shootdown
os/StarryOS/kernel/src/mm/stats.rs用户 stack 虚拟内存区域统计分类

容量计算应包含每 CPU 固定 stack 总开销、最大 task 数乘以配置栈大小、guard page 的虚拟地址开销以及 Starry 用户 stack 的虚拟内存大小/常驻内存集大小差异。

8. 栈布局实例

栈内存的地址、物理占用和释放规则取决于栈类型。下面分别计算 per-CPU 区、guarded task stack 和 Starry 用户栈,三个例子不能互换释放逻辑。

8.1 四 CPU 启动区

以 data=128 B、metadata=64 B、stack=4096 B、page/region alignment=4096 B 为例,对四个 CPU,计算结果是 metadata offset 128、stack offset 4096、stride 8192、总 allocation 32768 B。

let metadata_end = meta_offset.checked_add(metadata_size)?;
let stack_offset = checked_align_up_pow2(metadata_end, page_alignment)?;
let stack_end = stack_offset.checked_add(stack_size)?;
let area_stride = checked_align_up_pow2(stack_end, region_alignment)?;
let total_size = area_stride.checked_mul(cpu_count)?;

假设 early bump 返回区域起点 0x8100_0000,四个 CPU slot 的物理布局如下。

CPU0 0x8100_0000..0x8100_2000
data [0x8100_0000,0x8100_0080)
meta [0x8100_0080,0x8100_00c0)
pad [0x8100_00c0,0x8100_1000)
stack[0x8100_1000,0x8100_2000), top=0x8100_2000

CPU1 0x8100_2000..0x8100_4000, stack top=0x8100_4000
CPU2 0x8100_4000..0x8100_6000, stack top=0x8100_6000
CPU3 0x8100_6000..0x8100_8000, stack top=0x8100_8000

实际生产 stack 默认 256 KiB,data 和 PerCpuMeta 大小由 linker/template 与目标应用程序二进制接口决定,计算公式相同。所有乘加和 alignment 都返回 PerCpuLayoutError,极端固件 CPU 数不会 wrapping 到小 allocation。

8.2 保护页任务栈

启用 stack-guard-page 后,256 KiB usable stack 加一个 4 KiB guard 预留 260 KiB 连续虚拟地址。backing 只分配 64 个独立物理页,不再将 65 页连续申请向上提升为 128 页 Buddy block。

创建时每个 frame 和 page-table deposit 均在锁外准备。销毁时先清除 leaf,再完成 TLB 确认,最后在锁外释放 backing;guard hole 本身没有物理页需要归还。

8.3 普通任务栈

未启用上述两个 feature 时,allocate_heap_stack()Layout(usable_size, alignment) 进入 runtime allocator。默认 256 KiB 超过 Slab 上限,使用 Buddy large allocation,但所有权仍表现为 byte allocation。

属性Heap stackVirtual stack
入口global_allocator().alloc(Layout)KernelVirtualAllocation::allocate()
物理连续要求由 byte allocator 决定每页独立分配
overflow 检测无页级隔离配置 guard 时为 unmapped hole
回收Layout deallocationleaf detach → TLB 确认 → 锁外释放

bootstrap resource bundle 的 stack handle 仍为 NONE;someboot 的 Reserved 区不进入 runtime allocator 的退休流程。

8.4 Starry 用户栈

用户栈顶部来自当前 AddrSpace 捕获的不可变 UserVirtualAddressLayout。x86_64 的策略上限为 0x0400_0000_0000;LoongArch64 还会把该上限裁剪到 CPUCFG VALEN 给出的 lower canonical half,和 Linux 的 STACK_TOP_MAX = TASK_SIZE64 原理一致。例如实际 VALEN=40 时,TASK_SIZE 和栈顶都是 0x80_0000_0000。虚拟内存区域大小为 8 MiB;loader 先建立完整 [stack] 虚拟内存区域,再只 populate 初始 argv/envp/auxv 实际覆盖的尾部页。

let ustack_top = uspace.stack_top();
let ustack_size = crate::config::USER_STACK_SIZE;
let ustack_start = ustack_top - ustack_size;
uspace.map(
ustack_start,
ustack_size,
MappingFlags::READ | MappingFlags::WRITE | MappingFlags::USER,
false,
MappingOperation::new_alloc(ustack_start, PAGE_SIZE_4K, "[stack]"),
)?;

假设初始 stack image 为 13 KiB,user_sp 向下移动 13 KiB,populate range 再向下按页对齐,最多使 16 KiB resident。此时虚拟内存大小增加 8 MiB,常驻内存集大小 Anon只增加实际填充的四页;其余页面在后续用户访问时 fault-in。

用户栈量示例结果
虚拟内存区域8 MiB
初始 stack data13 KiB
初始 resident upper bound16 KiB / 4 页
初始 SP当前 MM 的 stack_top - 13 KiB,再满足应用程序二进制接口 alignment

Starry 当前使用固定大小 stack 虚拟内存区域,不实现 Linux VM_GROWSDOWN。非 FIXED mmap 的上界还会避开 STACK_GUARD_GAP,但这不是一个已映射的物理 guard page;两种 guard 语义不能混用。