简介:这份HNU_ostep操作系统实验资料包面向高校操作系统课程学习者与实验指导教师,围绕进程管理、内存管理、文件系统及I/O系统等核心模块提供可动手实践的实验素材,帮助读者把调度、虚拟内存、页面置换、IPC与设备管理等抽象原理落到代码层面。压缩包共2000个文件,约42.51MB,以910个C源文件和760个头文件为主体,辅以Python脚本、Shell命令、CMake与Make构建文件,以及Markdown、TXT、PDF、DOCX等说明与讲义,兼顾源码研读、编译运行和实验指导。已有166人学习。包内源码目录可作为实验实现蓝本,配合实验说明中的总览、环境搭建与任务要求,读者能够对照修改调度算法、观察不同策略对进程执行效率的影响,并借助附书资料理解操作系统内部机制。整体目录结构清晰,适合按进程、内存、文件系统和I/O模块逐项练习,也便于教师组织课程实验与学生查漏补缺。
1. 从一道 fork 判断题说起:这份 OSTEP 实验包到底在解决什么
第一次打开这份操作系统实验包时,我以为它只是把教材《Operating Systems: Three Easy Pieces》的课后题整理了一下。翻完目录才发现,真正占篇幅的是内核侧的动手部分:进程切换、页表遍历、系统调用注入、文件系统扩容,每一项都要你改真实内核代码,再用脚本判分。它面向的读者很明确——上完理论课能背出 PCB 字段,但没亲手让一个进程从 RUNNABLE 走到 RUNNING 的人。
这份实验包的定位,是把 OSTEP 三大主题(虚拟化、并发、持久化)从纸面拉到可运行状态:你在教材里看到的调度算法、页表项标志位、日志式文件系统,在这里都有对应的代码位置和一个能跑起来的 QEMU 实例。新手可以照着走通一条链路,熟手则能拿它当二次开发的脚手架。
2. 实验体系与技术栈:从 QEMU 到内核的完整运行模型
先说明这套实验的技术底座。它沿用的是教学内核的经典形态:一个精简的类 Unix 内核跑在 RISC-V 上,通过 QEMU 模拟整机,用交叉编译工具链产出内核镜像。这和你在真实服务器上写内核模块完全不同——没有 insmod,没有 dmesg 可看,所有调试都靠串口输出和 GDB 远程连接。
这种选择的原因很直接:从零实现一个能跑用户程序的系统,比自己造一套模拟器或依赖某个大型内核要可控得多。整个内核大概一万行 C 加少量汇编,一天能通读完主路径,一学期能改透。代价是抽象层次低,写错一个release()就死锁,没有任何运行时保护替你兜底。
2.1 目录结构:内核源码、用户程序、判分脚本三件套
拿到包先看目录,它决定了后续所有操作的位置。
. ├── Makefile # 顶层构建入口,qemu / gdb / grade 都挂在这里 ├── kernel/ # 内核本体,所有内核实验改这里 │ ├── main.c # 第一个 CPU 的启动流程 │ ├── proc.c # 进程表、调度器、fork/exec/exit │ ├── vm.c # 页表建立、walk、copyin/copyout │ ├── trap.c # 陷阱入口,系统调用和中断在这里分岔 │ ├── syscall.c # 系统调用分发表 │ ├── sysproc.c # 进程相关系统调用的具体实现 │ ├── fs.c # 文件系统调用层 │ ├── log.c # 日志事务,崩溃一致性靠它 │ └── spinlock.c # 自旋锁与 sleep/wakeup ├── user/ # 运行在用户态的程序 │ ├── usys.pl # 生成系统调用桩代码的 Perl 脚本 │ ├── user.h # 用户态可见的系统调用声明 │ └── sh.c # shell,跑通后你看到的第一个界面 ├── mkfs/ # 把用户程序打包进文件系统镜像 └── grade-lab-*/ # 每个实验对应的自动判分目录kernel/是按功能切的,不是按对象切的,所以读代码时经常要横跳:追一次read()要从sysfile.c跳到file.c再到log.c。这是教学内核的常态,换来的是每个文件都很短,vm.c不到八百行。
判分脚本目录grade-lab-*每套实验一份,里面通常有个gradelib.py负责跑 QEMU、抓串口输出、正则匹配期望结果。很多人跑不通判分不是内核写错了,是 Python 依赖或 QEMU 参数变了,这个后面专门讲。
2.2 技术栈:RISC-V 交叉工具链加 QEMU 模拟器
工具链是三件套,缺一不可。下面的安装方式我在 Debian/Ubuntu 系上验证过多次:
# 交叉编译器:在 x86 主机上产出 riscv64 目标代码 sudo apt install gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu # 模拟器:带 5.1 版本以上的 QEMU 才支持 rv64 的 virt 机器 sudo apt install qemu-system-misc # 调试器,必须是支持 riscv 的多架构版本 sudo apt install gdb-multiarch三个包的版本要能对上。血泪经验:QEMU 太老会在加载内核时直接停在booting...不动,且没有任何报错,看起来像内核挂死,实际是模拟器不认镜像格式。判断方法很简单,手动跑一次 QEMU 看它能不能识别-machine virt:
qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -nographic能打出 OpenSBI 之类的启动信息,说明模拟器没问题,问题在内核。什么都不打或者报 unknown machine,回去升 QEMU。这条命令也是后面裸调试的底子,make qemu本质上就是给它加了一堆参数。
关掉 QEMU 的方式是Ctrl-A松开再按X,不是Ctrl-C。第一次用的人往往在终端里按半天Ctrl-C出不来,以为卡死了。
2.3 每个 lab 的输入输出契约:make grade 在测什么
理解了判分机制,写代码才有方向。判分脚本的工作流是固定的三步:
- 用实验专用的
make qemu启动内核,把内建的用户程序列表塞进文件系统镜像; - 等 shell 起来后,通过标准输入逐条灌入测试命令;
- 抓串口全部输出,按预设的正则匹配,匹配上给分。
这意味着两件事。第一,你的输出格式必须严格对齐实验文档,多一个空格、少一个换行都可能判不过。第二,判分脚本超时时间通常只有几十秒,如果你的实现里有死循环或死锁,表现是整轮判分挂起而不是某一条失败。
先跑一次空白判分,看基线长什么样,比上来就改代码高效得多:
make grade # 或指定某个实验的判分入口 python3 grade-lab-util/maketest.py输出里每条测试会有 ok / FAIL / Timeout 三种状态。看到全部 FAIL 先别改内核,先确认make qemu单独能跑通、shell 能交互。这一步能挡掉一半以上的环境问题。
3. 环境搭建与第一次跑通:从编译到在 gdb 里打断点
环境这一步没什么技巧,就是把工具链凑齐然后跑make。但第一次编译大概率不会一次过,所以这一章按「编译 → 启动 → 调试」三段走,每段都给出失败时的判断依据。
3.1 依赖安装与 make 目标速查
顶层 Makefile 是唯一入口,常用目标就四个,先记住:
| 目标 | 作用 | 什么时候用 |
|---|---|---|
make | 编译内核和全部用户程序,产出kernel/kernel和fs.img | 改完代码第一步 |
make qemu | 编译并启动 QEMU,进入 shell | 手动验证功能 |
make qemu-gdb | 启动 QEMU 但暂停,等 GDB 连上来 | 需要单步调试 |
make clean | 清掉所有产物 | 怀疑残留文件干扰时 |
编译过程本身分两段:先编译user/下的程序,再用mkfs把它们打包成fs.img;然后编译kernel/下的目标文件,按kernel/kernel.ld链接成镜像。链接脚本决定了内核被加载到0x80000000,这是 RISC-V virt 机器的固定入口地址,改错这个地址的表现是 QEMU 启动后立刻复位。
第一次编译报错,八成是这两类:
# 报错:riscv64-unknown-elf-gcc: command not found # 原因:Makefile 默认用 unknown-elf 前缀,而 apt 装的是 linux-gnu 前缀 # 解决:要么装对应前缀的工具链,要么改 Makefile 顶部这一行 TOOLPREFIX = riscv64-linux-gnu-第二类是undefined reference to 'memcpy'之类的链接错误,通常是编译器内置函数和内核自己实现的string.c打架,加-fno-builtin就能压住,Makefile 里一般已经带了,被自己改坏才会出现。
3.2 make qemu 的启动链路:从 kernel.ld 到第一个 shell
跑通之后看到的东西很朴素——几行启动日志,然后一个$提示符。但中间发生的事值得捋一遍,因为后面所有实验都挂在这条链路上。
make qemu # qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel \ # -m 128M -smp 3 -nographic -drive file=fs.img,if=none,format=raw,id=x0 \ # -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0参数逐个看:-m 128M给了 128MB 物理内存,这个值在kernel/memlayout.h里有对应常量,改内存大小要两边一起改;-smp 3是三个 CPU 核心,多核实验(比如 per-CPU 锁)必须保证这个数大于 1,否则测不出竞态;-nographic把串口直接接到终端,这是你能看到输出的唯一通道,内核里的printf最终都走 UART。
启动时第一个跑起来的是kernel/start.c里的机器态代码,做完基本配置后mret掉到main()。main()在kernel/main.c里,流程是固定的:
// kernel/main.c,每个 CPU 都会进来一次 void main() { if(cpuid() == 0){ consoleinit(); // 串口,之后 printf 才有输出 printfinit(); printf("\n"); printf("xv6 kernel is booting\n"); kinit(); // 物理页分配器 kvminit(); // 内核页表 kvminithart(); // 打开分页 procinit(); // 进程表,NPROC 个槽位 trapinit(); // 陷阱向量 trapinithart(); plicinit(); // 中断控制器 binit(); // 块缓存 iinit(); // inode 表 fileinit(); virtio_disk_init(); userinit(); // 造出第一个进程,执行 init } else { // 其他 CPU 只做最小初始化,然后进 scheduler } scheduler(); }顺序不能乱:kinit必须在kvminit之前,因为建页表要分配物理页;userinit必须在procinit之后;plicinit要在开中断之前。改启动流程是最容易翻车的地方,加一行printf到consoleinit之前,输出直接消失,看起来像什么都没执行。
userinit()造出的第一个进程执行/init,init再 fork 出sh,你看到的$就是它。这条链跑通,说明页表、进程、文件系统、驱动四条线都活着,可以开始做实验了。
3.3 用 gdb 单步:让黑匣子开口
内核调试没有 print 之外的捷径时,GDB 是唯一能看真相的工具。开两个终端:
# 终端 A:启动 QEMU 并停在第一条指令 make qemu-gdb # 输出里会打印 + target remote localhost:26000 # 终端 B:连上去 gdb-multiarch -ex "set architecture riscv:rv64" \ -ex "target remote localhost:26000" \ kernel/kernel连上之后layout src打开源码视图,b scheduler打断点,c继续执行。切进程的时候会自动停在调度器里,这时候p *p能看进程结构体的全部字段,比在内核里 print 强太多——print 改代码要重新编译重启,GDB 不用。
几个高频命令记一下:info registers看当前寄存器,x/10i $pc反汇编当前位置,p/x $satp看当前页表基址,bt看调用栈。排查 page fault 时最有用的是bt配合p/x $sepc,sepc是出错的那条指令地址,用addr2line反查就能定位到源码行:
riscv64-linux-gnu-addr2line -e kernel/kernel 0x80001234这条命令能省掉大量"到底哪行崩了"的猜测。注意make qemu-gdb会额外生成.gdbinit,里面已经配好了架构和断点,直接用gdb-multiarch不加参数也能连,但仓库里如果有多份.gdbinit会互相覆盖,看到断点行为诡异先检查这个文件。
4. 核心模块拆解:系统调用、调度器、页表三条主线
实验改来改去,落点永远是这三处。看懂了它们的代码形状,加功能就是填空。
4.1 系统调用链路:从 ecall 到 sys_xxx 的完整路径
用户程序调一个函数,最终怎么进内核,这条链路每个实验都要用。以加一个sys_trace为例,六处改动,缺一不可:
// 1) kernel/syscall.h —— 分配一个系统调用号 #define SYS_trace 22 // 2) kernel/syscall.c —— 声明实现并挂进分发表 extern uint64 sys_trace(void); static uint64 (*syscalls[])(void) = { [SYS_trace] sys_trace, }; // 3) kernel/syscall.c 的分发函数里,把返回值存进 a0 void syscall(void) { int num; struct proc *p = myproc(); num = p->trapframe->a7; // 调用号放在 a7 寄存器 if(num > 0 && num < NELEM(syscalls) && syscalls[num]) { p->trapframe->a0 = syscalls[num](); // 返回值写回 a0 } else { printf("%d %s: unknown sys call %d\n", p->pid, p->name, num); p->trapframe->a0 = -1; } }剩下三处是kernel/sysproc.c里写实现体、user/user.h加用户态声明、user/usys.pl加一行entry("trace")。usys.pl在构建时生成usys.S,内容就是每个系统调用的汇编桩:把调用号放进a7,然后ecall。
// kernel/sysproc.c —— 实现体,参数用 argint/argaddr/argstr 取 uint64 sys_trace(void) { int mask; if(argint(0, &mask) < 0) // 从用户态栈上取第 0 个参数 return -1; myproc()->tracemask = mask; // 存进 PCB,后面 fork 时记得继承 return 0; }参数为什么不能直接解引用?因为argint取到的只是用户态的虚拟地址空间里的数值,而寄存器里的指针值在你切到内核页表后仍然有效(内核做了直接映射),但用户传的字符串、结构体指针不能直接读——那是页表切换后的另一个地址空间。所以必须走copyin/copyout,这条规则后面第 4.3 节细讲。
这里最常犯的错是漏掉fork里的继承。新加的 PCB 字段在fork()里不复制,测试单进程能过,一到 fork 场景就丢。检查方法很粗暴:全局搜一遍p->tracemask,所有出现的地方都对一遍。
4.2 调度器:一个进程如何从 RUNNABLE 走到 RUNNING
调度是 OSTEP 前半本书的主线,代码在这:
// kernel/proc.c void scheduler(void) { struct proc *p; struct cpu *c = mycpu(); c->proc = 0; for(;;){ intr_on(); // 开中断,否则时钟进不来,时间片轮不起来 int found = 0; for(p = proc; p < &proc[NPROC]; p++){ acquire(&p->lock); // 每个进程一把锁,防并发抢 if(p->state == RUNNABLE){ p->state = RUNNING; c->proc = p; swtch(&c->context, &p->context); // 上下文切换,返回时已换过 CPU c->proc = 0; found = 1; } release(&p->lock); } if(found == 0){ intr_on(); asm volatile("wfi"); // 没活干就睡,等中断唤醒 } } }swtch是汇编写的,只保存被调用者保存寄存器(ra、sp、s0-s11),把当前 CPU 的上下文指针和进程的上下文指针换一下。函数返回时,CPU 已经在跑另一个进程了——这是协程式的切换,不是中断现场。真正的中断上下文在trapframe里,两者别搞混:trapframe是用户态寄存器快照,context是内核态调用栈快照。
改调度策略(比如加优先级)的位置就在这个for循环里。把线性扫描换成按优先级挑最小,改起来只要十几行,但要注意两点:一是acquire的顺序不能乱,遍历时逐把拿逐把放,不要同时持有两把进程锁;二是挑选过程中不能睡眠,scheduler跑在中断关闭临界区内,任何可能导致睡眠的调用都是死锁起点。
多核场景还有一个坑:mycpu()返回的是当前 CPU 的结构体,靠tp寄存器里的 hartid 索引。如果你在scheduler里做全局统计,必须加锁,否则三个 CPU 同时加减计数会丢更新。测试用例里一般会开-smp 3专门打这个。
4.3 页表与 copyout:用户指针为什么不能直接解引用
页表这条线是实验里改动最深的。先看核心的两个函数:
// kernel/vm.c —— 沿三级页表走到最末级,返回 PTE 指针 pte_t * walk(pagetable_t pagetable, uint64 va, int alloc) { if(va >= MAXVA) panic("walk"); for(int level = 2; level > 0; level--){ pte_t *pte = &pagetable[PX(level, va)]; if(*pte & PTE_V){ pagetable = (pagetable_t)PTE2PA(*pte); } else { if(!alloc || (pagetable = (pde_t*)kalloc()) == 0) return 0; // 没分配就返回空,调用方决定怎么办 memset(pagetable, 0, PGSIZE); *pte = PA2PTE(pagetable) | PTE_V; } } return &pagetable[PX(0, va)]; } // kernel/vm.c —— 内核往用户页写数据,唯一合法通道 int copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len) { uint64 n, va0, pa0; while(len > 0){ va0 = PGROUNDDOWN(dstva); // 按页对齐 pa0 = walkaddr(pagetable, va0); // 查物理地址 if(pa0 == 0) return -1; // 用户页不存在,返回错误而不是 panic n = PGSIZE - (dstva - va0); // 本页还能拷多少 if(n > len) n = len; memmove((void *)(pa0 + (dstva - va0)), src, n); len -= n; src += n; dstva = va0 + PGSIZE; } return 0; }逐段看。walk里PX(level, va)是取虚拟地址对应层级的 9 位索引,三级页表就是 9/9/9 加 12 位页内偏移。alloc参数决定遇到缺页时是建页还是返回空——这个参数是做 lazy allocation 实验的着力点,把alloc传 0 然后在上层捕获0返回值去补页,就是懒分配的骨架。
copyout的循环处理跨页拷贝,每轮算本页剩余空间n,拷完再前进。if(pa0 == 0) return -1这行是错误处理的样板,用户传个野指针进来,内核返回 -1 让用户程序自己崩,绝不能跟着崩。对照着看,很多人在实现自己的系统调用时图省事直接*(char*)addr,一旦用户传非法地址,整个内核 panic,这就是"翻车"的典型现场。
walkaddr比walk多一层判断:查到 PTE 后要检查PTE_V和PTE_U,用户态不能访问没打PTE_U的页。这也是为什么内核往用户态写数据必须走copyout——不是因为地址空间不同(内核做了直接映射),而是因为只有走这条路径才会做权限和存在性校验。
做 COW(写时复制)实验时,改动集中在这两个函数和usertrap的 page fault 分支:copyout里遇到PTE_COW标志要先复制物理页再写,walkaddr不能直接把只读页返回给写路径。判断依据写错的表现是判分脚本里 fork 相关测试全挂,但简单的 echo 能过——遇到这种"部分通过",先看PTE_U、PTE_W、PTE_COW三个标志位的组合。
5. 避坑与排查:编译、panic、判分三类高频现场
这一章全是踩过的坑,按「现象 → 原因 → 解决」写,遇到相似的直接对照。
5.1 编译期:工具链与构建残留
现象:make报recipe for target 'user/_cat' failed,错误信息里混着一堆No such file or directory,但user/cat.c明明存在。
原因:user/_cat这类带下划线的目标是mkfs打包时生成的中间文件,make clean没清干净或者上次编译中断,留下半截产物。也可能是UPROGS里新加的程序名拼错,mkfs找不到源文件。
解决:make clean后重编。如果还报,进user/目录单独跑make cat看真实错误。新加用户程序要同时改三处:user/下建.c文件、Makefile的UPROGS里加$U/_xxx、user/user.h里加声明(如果它要调系统调用)。
现象:改了kernel/proc.h的结构体,编译只重编了一部分文件,跑起来行为诡异,字段值明显错位。
原因:Makefile 的依赖规则不完整,头文件改动没触发所有包含它的.o重编。教学内核的 Makefile 一般用-MMD -MP生成依赖文件,但如果你手动改过规则或者用了并行编译make -j,可能踩到。
解决:改结构体、改宏定义这类影响面大的操作,一律make clean && make。别省这几十秒。
5.2 运行期:panic 与页错误定位
现象:启动直接panic: kvmmake,或者卡在xv6 kernel is booting之后再无输出。
原因:main()里的初始化顺序被打乱,最常见的是在kvminit()之前调用了需要访问内核页表的函数,或者kinit()没跑就kalloc()。另一个可能是物理内存大小和PHYSTOP常量对不上。
解决:panic会打印文件名和行号,直接跳过去看。如果是自己新加的初始化代码,往前挪到procinit()之后,因为进程表初始化依赖kalloc。
现象:跑某个测试时panic: uvmunmap: not mapped或panic: uvmunmap: not a leaf。
原因:这两个 panic 都出自uvmunmap,是进程退出时释放页表的过程。前者是 PTE 标记为有效但walk查不到物理页,典型是 lazy allocation 实验没改完;后者是最后一级 PTE 缺少PTE_V,通常出现在你手动改过walk的返回值却没同步改uvmunmap的容错分支。
解决:定位到kernel/vm.c的uvmunmap,看它遍历时对空 PTE 的处理。做懒分配实验时必须在这里加if(!(*pte & PTE_V)) continue;,否则进程一退出就崩。这个坑几乎每个做懒分配的人都会踩一次。
现象:多核测试偶发panic: acquire或直接死锁,单核跑一切正常。
原因:自旋锁重复获取,或者持锁期间调用了可能睡眠的函数(比如sleep、copyout触发磁盘 I/O)。-smp 1时锁竞争几乎不发生,问题被掩盖。
解决:把CPUS加到 3 以上复现,然后在acquire里加一段打印lk->name和cpuid()的调试代码,看是谁在持锁。原则是持锁区间尽量短,锁内不调用任何可能让出 CPU 的函数。
5.3 判分期:脚本不过但手动能跑
现象:手动敲命令输出完全正确,make grade却报 Timeout 或正则不匹配。
原因:三个高频来源。一是输出里混了额外的调试printf,正则匹配的是整行,多几个字符就废;二是测试脚本等的是特定提示符,你改了 shell 的提示符格式;三是 Python 版本差异,老脚本用了 Python 2 的print语法。
解决:先看判分脚本里期望的正则是什么,拿grep搜expected关键字。然后对照自己的输出逐字节比,特别注意行尾空格和\r\n。调试输出在提交前一定清掉——这是最容易忽略的失分点。
现象:判分卡住不动,也不报错,等几分钟才退出。
原因:内核里死循环或者等一个永远不会来的中断。常见于改了调度器之后某个进程永远 RUNNABLE 却抢不到 CPU,或者sleep之后没有对应的wakeup。
解决:手动跑同样的命令序列,看卡在哪一步。如果手动也卡,用make qemu-gdb加Ctrl-C(GDB 里的)中断,bt看当前栈。如果是判分才卡,把gradelib.py里的超时时间临时调大,看它到底在等什么输出。
6. 二次开发:加一个系统调用并替换调度策略的完整路径
如果要在这套实验上做点自己的东西,我建议从一个最小闭环开始:加一个能读进程运行时的系统调用,再用它改调度。这条路径把三条主线全串了一遍,改完基本就摸清了整个内核的交互方式。
具体做法分五步。第一步在kernel/proc.h的struct proc里加uint64 runtime;,记录累计 CPU 时间。第二步在kernel/trap.c的usertrap里,时钟中断分支(which_dev == 2那条路)给当前进程加计数:
// kernel/trap.c,时钟中断里累加运行时间 if(which_dev == 2){ struct proc *p = myproc(); p->runtime += 1; // 每个 tick 加一,粒度取决于时钟频率 if(++p->timeslice >= TIMESLICE_TICKS){ p->timeslice = 0; yield(); // 时间片用完,主动让出 } }第三步,把scheduler()里的线性扫描换成按runtime挑最小值,实现最简单的短作业优先。第四步,加两个系统调用getruntime和setpriority,走第 4.1 节那六处改动。第五步,写个用户程序循环打印自己的运行时,验证计数和调度都生效。
改造过程有三条验证习惯值得固定下来。其一是每改一处就make一次,不要一口气改五个文件再编,报错根本分不清是谁的锅。其二是改动前后各跑一次make grade存基线,用diff比对,新引入的失败一目了然。其三是判分脚本和手动验证必须都过,只过判分说明你猜对了正则,只过手动说明你还没对齐输出格式,两个都过才算真通过。
| 改造点 | 涉及文件 | 验证方式 |
|---|---|---|
| 加 PCB 字段 | kernel/proc.h、kernel/proc.c的allocproc/fork | fork 后打印字段确认继承 |
| 系统调用注册 | syscall.h、syscall.c、sysproc.c、user.h、usys.pl | 用户程序调用后看返回值 |
| 改调度策略 | kernel/proc.c的scheduler() | -smp 1先跑通,再上多核 |
| 时钟计数 | kernel/trap.c的usertrap | 打印计数,与 wall time 粗略比对 |
有一点必须提醒:改调度器时yield()的调用位置很关键。放在时钟中断里是抢占式的,放在系统调用返回路径上是协作式的,两者语义完全不同。我见过有人把yield放进usertrap的最外层,结果每次陷阱返回都切一次进程,fork都跑不完就超时。判断方式很简单,跑一个纯计算程序,如果它被频繁打断且总时间暴涨,说明切得太碎了。
从那以后我每次动内核结构体或者调度路径,都强制走一遍「make clean全量编译 → 单核手动跑三条命令 → 三核跑判分」这套流程,一次都不省。教学内核没有运行时保护,任何侥幸都会被多核场景放大成随机 panic。上面这些链路和坑点对着源码过一遍,再动手改会顺很多,希望帮到你。
本文还有配套的精品资源,点击获取