启动栈、内核任务栈与用户栈
TGOSKits 没有把物理 RAM 静态切成一个“栈区”和一个“堆区”。CPU0 最早期栈来自内核镜像 .bss,每 CPU 启动栈由 someboot 早期线性分配器预分配,普通内核任务栈从运行时分配器获取,Starry 用户栈则是用户地址空间中的 Virtual Memory Area(虚拟内存区域,VMA)。
1. 栈类型与生命周期
不同栈存在于不同启动阶段和地址空间。区分这些栈是分析内存占用、guard page 和释放行为的前提。
1.1 栈来源总览
当前主要栈类型如下。默认大小来自当前 linker/build 配置,平台配置可以覆盖任务栈大小。
| 栈类型 | 默认 大小 | 来源 | 生命周期 |
|---|---|---|---|
| CPU0 最早期 linker 栈 | STACK_SIZE = 0x40000,256 KiB | kernel .bss / KImage | 启动早期,镜像范围始终保留 |
| 每 CPU boot/main 栈 | someboot::mem::stack_size(),默认 256 KiB | early bump 的 per-CPU 区 | 系统生命周期,bootstrap resource 不持有可释放 stack handle |
| 普通内核任务栈 | 默认 0x40000,可由构建配置覆盖 | axruntime 的 heap 或 KernelVirtualAllocation | thread resource reaper 释放 |
| idle 栈 | 与运行时任务栈配置一致 | axruntime stack allocator | idle 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_SIZE。defaults.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_virt | dynamic 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 offset | align_up(data_size, meta_alignment) | metadata 至少按 max(align_of::<PerCpuMeta>(), 64) 对齐 |
| stack offset | align_up(metadata_end, page_size) | stack 起点按页对齐 |
| area stride | align_up(stack_end, region_alignment) | 每个 CPU slot 可独立寻址 |
| allocation size | area_stride * cpu_count | checked 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 stack | bootstrap ThreadResources 中为 StackHandle::NONE | 不释放,仅由启动层持有物理范围 |
| plain task allocation | opaque StackHandle 指向 StackBacking::Heap | 用原 Layout 归还 runtime allocator |
| guard-page task allocation | opaque StackHandle 指向 StackBacking::VirtualPages | 清除 usable PTE 并完成 TLB shootdown 后释放 backing 和 VA |
StackHandle::NONE 在 bootstrap resource bundle 中表达“任务正在使用,但 scheduler 没有获得该 stack 的回收所有权”。这防止 bootstrap thread 退休时把 early bump 的系统级 stack 错误释放给 Buddy。