主线二:治法 1——KmBuf RAII
主线二:治法 1——KmBuf RAII,为什么堆不栈
per-call buffer 的容器选什么?第一反应是「栈上 uint8_t buf[4096] 不就完了」。这里不行,原因是栈深度约束。
demand page 的 #PF 跑在受限栈上(它可能嵌在中断上下文、甚至嵌在别的 ISR 里被再入,可用栈深本来就宝贵;而且文件读写调用链会深递归进 demand-page,16KB 任务栈也吃紧)。源码注释把这条约束写在 KmBuf 的类文档里:
/// RAII kmalloc'd scratch buffer for ext2 SMP read/write paths (block_buf_ is
/// shared/non-thread-safe). Heap not stack: demand-page #PF runs on IST2 which
/// is only 4 KB (IRQ_STACK_PAGES=1). ~KmBuf kfrees; operator bool checks OOM.
class KmBuf {
void* p_;
public:
explicit KmBuf(uint64_t n) : p_(cinux::mm::kmalloc(n, 16)) {}
~KmBuf() {
if (p_ != nullptr) {
cinux::mm::kfree(p_);
}
}
KmBuf(const KmBuf&) = delete;
KmBuf& operator=(const KmBuf&) = delete;
explicit operator bool() const { return p_ != nullptr; }
void* get() const { return p_; }
uint8_t* data() const { return static_cast<uint8_t*>(p_); }
};在 #PF 里再撑一个 4KB 的栈数组就贴着栈底跑了;就算不在 #PF 里,文件读写的调用链也会深递归进 demand-page 路径,把 8KB 任务栈(TaskBuilder::STACK_PAGES = 2、AP 内核栈 kStackPages = 4,都见 task_builder.hpp / ap_main.cpp)也吃紧。所以选堆。
关于那条源码注释的常量名:注释里写的是「
#PF runs on IST2 which is only 4 KB (IRQ_STACK_PAGES=1)」。这个写法在「#PF走不走 IST」上要分清——kernel/arch/x86_64/idt.cpp的 IDT 路由表里#PF(ExceptionVector::PF)的ist = 0,即#PF不走 IST、跑在主任务栈上;而 IST2 那 4KB(IRQ_STACK_PAGES = 1,见gdt.hpp:115-119)是给硬件 IRQ用的,不是给#PF。所以注释把#PF跟 IST2 混着说,措辞不准。但结论本身成立——#PF跑在主任务栈上(ist = 0),栈深受限、调用链还深,4KB buffer 不该再压栈。咱们这里把注释当动机引用,选堆的真正理由是上面那段「调用链深 + 栈余量宝贵」,而不是字面上那 4KB 的 IST。
KmBuf 刻意做小,就这四个方法:
- 构造
kmalloc(n, 16)——按 16 对齐(ext2 块结构体里很多字段需要uint32_t对齐,块大小 4096 也是 16 的倍数)。 - 析构
kfree——这是 RAII 的核心:调用方在自己栈帧上KmBuf scratch(4096);,出作用域编译器自动kfree,既消除共享又不用记着 free。 operator bool查 OOM——kmalloc在内存紧时可能失败,调用方必须检查,不能假设成功就往里写(否则就是空指针写)。整个库里每个KmBuf的调用点后面都跟着if (!scratch) return ...;。get()/data()取指针。
它不是 smart pointer,不引入所有权复杂度,不 share、不 move、不 count ref。它就是一个一次性 scratch 载体,构造、用、析构,完事。范围小是刻意的——这个场景不需要更多。