简介:面向计算机专业学生与操作系统内核入门者的西北工业大学计算机学院《计算机操作系统》实验一资料,围绕创建GeekOS内核线程展开,适用于课程实验复现、内核线程机制理解与多任务调度观察。资源包共1个docx文档,约699KB,以实验报告形式按目的、要求、过程结果与分析组织,完整记录引导扇区编写、键盘按键线程、双线程字符串输出及自定义大小爱心交替打印等过程,并附有关键代码、运行截图与问题分析,便于对照GeekOS源码梳理线程结构体、键盘中断、线程创建和同优先级调度等知识点。该资料已有1519人学习,适合需要完成同类实验、复盘操作流程或查漏补缺的读者参考,也可作为理解操作系统引导流程与内核线程实现的基础案例。
1. 从一行 Hello, OS world! 说起
第一次跟着操作系统实验跑 GeekOS 的引导扇区,屏幕上只蹦出一行Hello, OS world!,接着光标就彻底不动了。很多人会以为卡死了,其实这正是成功——那段汇编最后就是一句jmp $,把自己钉在原地。这份实验报告来自某高校计算机学院 2022 年 10 月那一版操作系统实验,把引导扇区、内核线程创建、键盘中断、多任务调度四件事串成了一条能跑通的链路:boot.asm汇编进软盘镜像、bochs 启动、Start_Kernel_Thread建线程、keycode 位标志解析按键、两个同优先级线程交替打印爱心。刚拿到 GeekOS 源码不知道从哪下手的新手,能照着把每一步复现;想回头把内核线程调度链路捋一遍的老手,也能从线程结构体和Setup_Kernel_Thread里看到设计取舍。
2. 项目定位与技术栈:GeekOS 一棵教学内核撑起的四个实验
GeekOS 不是一个能装到真机上的发行版,它是为操作系统课程准备的教学内核:代码量小、模块边界清楚、每个实验都能精确落在某个源文件上。这份报告里的 project0 版本,内核启动后先跑一串初始化,再拉起用户指定的内核线程,调度器按优先级和时间片轮转。理解这个前提,后面所有代码才有落点。
2.1 四个任务分别在练什么
第一个任务是写引导扇区。目标不是做一个能用的 bootloader,而是让你亲眼看到 BIOS 把 512 字节搬到0x7c00、检查末尾0xaa55、跳过去执行这一整套动作。涉及的文件只有一个boot.asm,但它是后面所有实验的地基——镜像怎么生成、bochs 怎么挂载,全在这里定型。
第二个任务是创建一个能接收键盘输入并回显的内核线程。它把两个子系统接上了:线程调度(kthread.c)和键盘中断(keyboard.c)。你需要写一个void print_key(ulong_t arg)形式的线程函数,在Main里用Start_Kernel_Thread拉起来,然后处理 keycode 的高位标志。这一步开始,你就不是在写应用代码,而是在和中断、等待队列打交道。
第三个任务是两个线程分别打印 A 和 B。代码短到只有几行,但它把调度器的行为直接暴露在屏幕上:如果调度正常,你会看到 A、B 交替出现;如果哪个线程霸占 CPU,屏幕上就只剩一个字母。这是用肉眼验证调度器最快的方式。
第四个任务是自定义线程,报告里选了大小两个爱心交替输出。本质上和第三题同构,但多了Clear_Screen、Put_Cursor和按行计算的星号打印,输出节奏更明显,也更容易看出两个同优先级线程是如何被公平切片的。四个任务从硬件引导一路走到调度公平性,难度是阶梯式的,前一题的产物基本就是后一题的环境。
2.2 目录结构与构建链路
project0 的源码布局是典型的教学内核组织方式,几个关键位置记住就够用了:
| 路径 | 作用 |
|---|---|
src/project0/src/geekos/main.c | 内核入口Main,所有初始化和你加的线程都在这里启动 |
src/project0/src/geekos/kthread.c | 内核线程创建、调度、切换的核心实现 |
src/project0/src/geekos/keyboard.c | 键盘中断处理与Wait_For_Key/Read_Key接口 |
src/project0/include/geekos/kassert.h | TODO宏定义所在,这个宏会直接把内核钉死 |
src/project0/include/geekos/keyboard.h | keycode 标志位与扫描码表 |
src/project0/build/ | make的输出目录,软盘镜像和bochsrc都放这里 |
构建链路是一条直线:make调 Makefile 编译内核 ELF,然后把内核按约定写入软盘镜像的指定扇区,最后 bochs 加载这个镜像启动。第一次跑实验最容易翻车的地方不是 C 代码,而是这条链路里的某个环节没接上——镜像没更新、bochsrc放错目录、bochs 读的还是旧文件。所以每次改完main.c,我习惯先看build目录里镜像的时间戳变没变,再启动虚拟机。
引导扇区实验是独立的:nasm编译出 512 字节的boot.bin,直接写进a.img的第一个扇区,bochs 从软盘 A 启动。两套流程不要混着理解,一个验证 BIOS 引导,一个验证内核线程。
2.3 工具链与运行环境准备
这套实验对工具版本不挑,但缺一个都跑不起来。常见做法是准备nasm(汇编引导扇区)、gcc(编译内核 C 代码,Makefile 里通常会加-m32之类的 32 位选项)、bochs(x86 模拟器,带调试版更好)以及dd或 Makefile 里自带的镜像写入工具。在 Linux 下一条命令就能装齐,Windows 下建议直接上 WSL 或者虚拟机里的 Linux 环境,省得为路径和权限折腾。
提示:bochs 装的时候尽量选带调试器支持的那一版,后面线程切换卡住时可以直接
info threads看寄存器,比盲猜快得多。
环境验证有个很土但有效的办法:先不加任何自己的代码,把原始 project0 编译一遍跑起来,看到Welcome to GeekOS!就说明工具链通了。这一步过了再动手改main.c,出问题时能立刻判断是自己代码的问题还是环境的问题。
3. 引导扇区实战:512 字节 boot.asm 从汇编到软盘镜像
引导扇区这题看起来最吓人,其实代码量最少。报告里给出的boot.asm和示例完全一致,但它值得逐行读一遍——后面内核镜像怎么写进磁盘,思路是同源的。
3.1 boot.asm 逐行拆解
org 07c00h ; 告诉编译器这段程序会被加载到 0x7c00 mov ax, cs mov ds, ax mov es, ax call DispStr ; 调用显示字符串例程 jmp $ ; 原地死循环,防止执行到后面的填充数据 DispStr: mov ax, BootMessage mov bp, ax ; ES:BP 指向字符串地址 mov cx, 16 ; CX = 字符串长度 mov ax, 01301h ; AH=13 写字符串,AL=01 光标跟随 mov bx, 000ch ; BH=0 页号,BL=0x0C 黑底红字 mov dl, 0 ; 从第 0 列开始显示 int 10h ; 调用 BIOS 显示中断 ret BootMessage: db "Hello, OS world!" times 510-($-$$) db 0 ; 用 0 填满到第 510 字节 dw 0xaa55 ; 第 511-512 字节:引导扇区结束标志org 07c00h是这段代码的灵魂。BIOS 自检结束后,会把可引导设备的第一个扇区原样搬到内存0x0000:0x7c00,然后跳过去执行,所以代码里所有标号地址都必须以0x7c00为基准计算。少了这一句,BootMessage的地址就会算错,int 10h打出来一堆乱码。
DispStr这一段用的是 BIOS 的 13h 号功能:ES:BP给字符串地址,CX给长度,AH=13h表示写字符串,AL=01h表示写完以后光标跟着走。BX的高字节是页号,低字节是显示属性,000ch就是黑底红字高亮。DL指出起始列,这里给 0,从最左边开始打。
最后两行是引导扇区能不能被认出来的关键。times 510-($-$$) db 0表示从当前位置一直填零到第 510 字节,保证整个文件刚好 512 字节;dw 0xaa55在末尾写入魔数。BIOS 只认最后两个字节是不是55 AA,不认代码内容,所以哪怕你写的是空操作,只要魔数对了,它也会跳过来执行。
3.2 nasm 编译与 dd 写入第一个扇区
进入boot.asm所在目录,先编译:
nasm boot.asm -o boot.bin ls -l boot.bin # -rw-r--r-- 1 user user 512 ... boot.binnasm默认输出纯二进制(flat binary),没有 ELF 头也没有段表,正好符合 BIOS 对引导扇区的预期。编译完第一时间用ls -l确认大小是 512 字节,多一字节少一字节都不行——少了魔数不在末尾,多了 BIOS 根本不会加载。
接着把boot.bin写进软盘镜像的第一个扇区。报告里描述为“把这个文件写入到软盘镜像文件的第一个扇区”,常见做法是先用dd造一个 1.44M 的空镜像,再把boot.bin覆盖到头部:
# 造一个 1.44M 的空软盘镜像,内容全零 dd if=/dev/zero of=a.img bs=1024 count=1440 # 把 boot.bin 写到镜像的第一个扇区,conv=notrunc 保证不截断镜像 dd if=boot.bin of=a.img bs=512 count=1 conv=notruncbs=512 count=1表示只写一个块、正好是一个扇区;conv=notrunc是关键参数,不加的话dd写完 512 字节会把后面 1.44M 的内容直接截掉,镜像就废了。这一步做完可以用xxd a.img | head抽查,前两字节应该是EB之类的指令码,最后两字节是55 aa。
3.3 bochsrc 配置与启动
bochs 需要有配置文件才知道从哪启动。报告里直接复用了之前实验的bochsrc,放在a.img同目录即可。教学环境里最常见的配置长这样:
# 内存大小,16M 够跑这个小镜像 megs: 16 # BIOS 和 VGA BIOS 镜像,路径按自己机器上的 bochs 安装位置改 romimage: file=$BXSHARE/BIOS-bochs-latest vgaromimage: file=$BXSHARE/VGABIOS-lgpl-latest # 挂一个 1.44M 软盘,就是我们刚写好的 a.img floppya: 1_44=a.img, status=inserted # 从软盘 A 启动 boot: a # 用终端字符界面显示,避免图形界面在服务器上起不来 display_library: termboot: a告诉 bochs 从软盘 A 引导,floppya里1_44指定容量是 1.44M,和前面dd造出来的镜像大小对应。display_library按环境选,图形桌面下用默认的x或sdl,纯终端用term或nogui。
配置就绪后启动:
bochs -f bochsrc启动界面会列出一串启动方式,输入6回车就是开始模拟(不同版本菜单编号可能不同,看清提示里的 “Begin simulation” 即可)。如果屏幕出现Hello, OS world!且光标停止,引导扇区这题就完成了。这里有个血泪经验:改完boot.asm一定要重新nasm再dd,只改源码不重建镜像,bochs 里看到的永远是旧结果。
4. 内核线程链路拆解:Start_Kernel_Thread 到键盘线程的完整走法
引导扇区解决的是“机器怎么把第一条指令跑起来”,内核线程解决的是“跑起来之后怎么让多个执行流轮流用 CPU”。GeekOS 的线程模型很朴素:一个Kernel_Thread结构体、一个由esp保存的上下文、一个按优先级组织的就绪队列,切换时靠汇编级的栈操作完成。
4.1 Main 启动序列与 TODO 宏这个黑匣子
Main是内核 C 世界的入口,顺序不能乱。屏幕、内存、TSS、中断、调度器、陷阱、定时器、键盘,每一步都在为后面的线程铺路,尤其是Init_Scheduler和Init_Interrupts,少了任何一个Start_Kernel_Thread都跑不起来。
void Main(struct Boot_Info* bootInfo) { Init_BSS(); Init_Screen(); Init_Mem(bootInfo); Init_CRC32(); Init_TSS(); Init_Interrupts(); Init_Scheduler(); Init_Traps(); Init_Timer(); Init_Keyboard(); Set_Current_Attr(ATTRIB(BLACK, GREEN | BRIGHT)); Print("Welcome to GeekOS!\n"); Set_Current_Attr(ATTRIB(BLACK, GRAY)); /* TODO 必须注释掉,它会让内核停在这里不再往下走 */ // TODO("Start a kernel thread to echo pressed keys and print counts"); struct Kernel_Thread *thread = Start_Kernel_Thread(&print_key, 0, PRIORITY_NORMAL, false); /* Main 本身也是一个线程,拉起子线程后自己退出 */ Exit(0); }TODO宏定义在kassert.h里,做的事是设置终端颜色、打印错误提示,然后进入一个死循环。换句话说,只要那一行还在,Main执行到那里就再也回不来,后面你加多少Start_Kernel_Thread都不会有反应。第一次做实验时屏幕上只有Welcome to GeekOS!然后卡死,八成就是这个宏没注释。它不是编译期错误,运行期也不报错,所以隐蔽性很强,属于典型的黑匣子。
4.2 Start_Kernel_Thread 参数与 Setup_Kernel_Thread 的内核栈操作
Start_Kernel_Thread在kthread.c里,干的事情可以概括成三步:分配线程结构体、伪造一份“刚从函数返回”的栈上下文、把线程挂进就绪队列。
struct Kernel_Thread* Start_Kernel_Thread( Thread_Start_Func startFunc, /* 线程入口函数指针 */ ulong_t arg, /* 传给入口函数的参数 */ int priority, /* 优先级,实验里用 PRIORITY_NORMAL */ bool detached) /* 是否分离,内核线程一般传 false */ { struct Kernel_Thread* kthread = Create_Thread(priority, detached); if (kthread != 0) { /* 核心:把 startFunc 和 arg 压进新线程的栈里 */ Setup_Kernel_Thread(kthread, startFunc, arg, priority, detached); Make_Runnable_Atomic(kthread); } return kthread; }Thread_Start_Func的类型定义大致是void (*)(ulong_t),这解释了为什么线程函数必须写成void print_key(ulong_t arg)而不是void print_key(void)。报告里贴的代码写成无参形式,编译时只会给个指针类型不匹配的警告,运行时能不能跑通看编译器脸色——我一般直接按签名写全,省得后面查半天。
Setup_Kernel_Thread是整个链路里最值得读的一段。它不直接调用startFunc,而是在线程自己的栈顶手工构造一个假的调用帧:把入口地址放进去,把arg放进去,再对齐好esp。等到调度器第一次切到这个线程时,Switch_To_Thread只是执行一条ret或者类似指令,CPU 就从那个假帧“返回”到startFunc,线程就这么跑起来了。报告里的思考题专门问了这个:Setup_Kernel_Thread里的 Push 函数和汇编指令push的区别,答案就在这儿——前者保存的是可还原的上下文,后者只是往栈上丢数据。
4.3 线程结构体的三组指针
Kernel_Thread里塞了不少字段,报告把指针分了三组,这个分法对读懂调度逻辑很有帮助:
struct Kernel_Thread { ulong_t esp; /* 保存的栈指针,切换时用 */ volatile ulong_t numTicks; /* 已运行的时间片计数 */ int priority; /* 优先级 */ struct Kernel_Thread* next; /* 就绪队列里的下一个 */ struct Kernel_Thread* prev; /* 就绪队列里的上一个 */ struct Thread_Queue joinQueue; /* 等待该线程结束的其他线程队列 */ DEFINE_LINK(Thread_Queue, Kernel_Thread); /* 双向链表节点 */ struct Kernel_Thread* parent; /* 父线程指针 */ /* ... 省略其他字段 */ };第一组joinQueue管的是“谁在等我结束”。一个线程调用Join等另一个线程退出时,就挂到对方的joinQueue上,等目标线程Exit时统一唤醒。第二组DEFINE_LINK展开后是一对prev/next指针,把线程串进就绪队列或等待队列,调度器挑下一个可运行线程时就靠它遍历。第三组parent指向创建者,Exit时用来通知父线程。三组指针分别对应同步、调度、生命周期,相互独立又互相配合。
4.4 keycode 位布局与 print_key 实现
键盘这题的关键是 keycode 的位布局。GeekOS 把按键信息压成一个 16 位整数:低 8 位是字符 ASCII 码,高 8 位是各种标志位。
| 标志 | 值 | 含义 |
|---|---|---|
KEY_SPECIAL_FLAG | 0x0100 | 特殊键(F1、方向键等) |
KEY_KEYPAD_FLAG | 0x0200 | 小键盘键 |
KEY_SHIFT_FLAG | 0x1000 | 左/右 Shift |
KEY_ALT_FLAG | 0x2000 | 左/右 Alt |
KEY_CTRL_FLAG | 0x4000 | 左/右 Ctrl |
KEY_RELEASE_FLAG | 0x8000 | 键弹起事件 |
keyboard.c提供两个接口:Wait_For_Key()在等待队列上阻塞,直到有按键事件才返回 keycode;Read_Key(Keycode*)从已有队列里尝试取一个,取不到立刻返回失败。做回显线程用前者更自然——没按键时线程自动挂起,不占 CPU,有按键时由中断唤醒。
void print_key(ulong_t arg) { Keycode keycode; Print("Put in ctrl + d to exit hit.\n"); while (1) { keycode = Wait_For_Key(); /* 特殊键和松键事件直接丢弃,否则一个按键会打印两遍 */ if ((keycode & KEY_SPECIAL_FLAG) || (keycode & KEY_RELEASE_FLAG)) continue; int ascii = keycode & 0xff; /* Ctrl + d 作为退出组合键 */ if ((keycode & KEY_CTRL_FLAG) == KEY_CTRL_FLAG && ascii == 'd') { Print("\nOver!Bye!\n"); Exit(1); } /* 回车键的键码是 \r,屏幕上要换成 \n 才能换行 */ Print("%c", (ascii == '\r') ? '\n' : ascii); } }这段代码有两个坑。第一,按下和弹起都会产生扫描码,如果不过滤KEY_RELEASE_FLAG,按一个键屏幕上就出现两个相同字符。第二,Print是 GeekOS 自己实现的输出函数,不是标准库的printf,格式串支持很有限,直接把printf抄过来会编译不过。
4.5 双线程打印与爱心交替输出
两个线程分别打印 A 和 B,代码短到没什么好讲的,但它是观察调度公平性最直观的实验:
static void print_A(ulong_t arg) { while (1) { Print("A"); } } static void print_B(ulong_t arg) { while (1) { Print("B"); } } /* Main 里先后拉起两个同优先级线程 */ struct Kernel_Thread *thread1 = Start_Kernel_Thread(&print_A, 0, PRIORITY_NORMAL, false); struct Kernel_Thread *thread2 = Start_Kernel_Thread(&print_B, 0, PRIORITY_NORMAL, false); Exit(0);跑起来如果看到 A、B 交替出现,说明时间片轮转在工作;如果屏幕上只有一种字母,那就要回头查调度器的初始化或者线程优先级有没有传错。这里不要给两个线程不同的优先级,否则高优先级线程会把 CPU 占满,低优先级线程可能永远等不到执行。
爱心那题是同一套机制的视觉化版本。output1和output2各自在一个无限循环里Clear_Screen、Put_Cursor(0, 0),然后按行打印星号拼出大小两个爱心:
void output1(ulong_t arg) { while (1) { int i, j; Clear_Screen(); Put_Cursor(0, 0); /* 爱心上沿的两段圆弧 */ Print(" ****** ******\n"); Print(" ********** **********\n"); Print(" ************* *************\n"); /* 中段:三行等宽实心区域 */ for (i = 0; i < 3; i++) { for (j = 0; j < 29; j++) Print("*"); Print("\n"); } /* 下沿:逐行收缩,形成尖底 */ for (i = 0; i < 7; i++) { for (j = 0; j < 2 * (i + 1) - 1; j++) Print(" "); for (j = 0; j < 27 - i * 4; j++) Print("*"); Print("\n"); } for (i = 0; i < 14; i++) Print(" "); Print("*\n"); } }output2结构一样,只是尺寸小一圈、行数少几行。两个线程都用PRIORITY_NORMAL,所以调度器会在两个Clear_Screen之间来回切,屏幕上就呈现出大爱心和小爱心快速交替的效果。报告里也提到,如果能加个时延,交替会更稳定——不加的话刷新太快,肉眼看到的是闪烁而不是两个完整图案。
5. 避坑与排查:GeekOS 实验里最容易翻车的五处
这套实验的代码量不大,但环境、宏、类型、构建链路每一环都能让你卡上半天。下面五条是我和周围人反复踩过的,按“现象 → 原因 → 解决”写,照着对号入座基本能省掉大半调试时间。
5.1 按一个键打印两个字符
现象:键盘回显线程跑起来后,按一次a,屏幕上出现aa;按一次回车,出现两个换行。
原因:键盘硬件在按下和松开时各发一次扫描码,中断处理程序把两次事件都压进了队列,Wait_For_Key会原样返回。如果不区分按下和弹起,每个按键都会被处理两遍。
解决:在回显逻辑里判断KEY_RELEASE_FLAG,命中的直接continue丢弃。同理,KEY_SPECIAL_FLAG的特殊键也不该当普通字符打印,一并过滤掉。改动就两行,但这是这份报告里最有价值的一个细节。
5.2 Print 写成了 printf,编译直接报错
现象:make报implicit declaration of function 'printf',或者链接阶段提示找不到符号。
原因:GeekOS 内核没有链接标准 C 库,Print是内核自己实现的输出函数,功能和printf类似但签名和格式支持都不一样。从应用层代码习惯性敲出printf就会翻车。
解决:全文搜一遍把printf换成Print,同时注意格式串只支持%c、%s、%d这类最基本的形式。报告里提到把所有函数都当成“自己创建的或汇编构建的”来用,这个心态对了就不会再犯。
5.3 bochs 起不来,bochsrc 不在 build 目录
现象:make成功,进入build目录执行bochs -f bochsrc,提示找不到配置文件;或者 bochs 启动了但从硬盘引导,直接黑屏。
原因:bochsrc必须和镜像放在同一目录,或者用绝对路径指定。build目录每次make可能被部分清理,配置文件如果只放在源码根目录,make之后就“消失”了。
解决:把bochsrc直接拷进build目录,并在里面用相对路径引用镜像文件。我一般会在make之后顺手ls build/确认bochsrc和镜像都在,养成习惯就不用每次排查。
5.4 改了 main.c 但虚拟机行为没变
现象:明明注释掉了TODO、加了Start_Kernel_Thread,bochs 里还是老样子,屏幕上只有Welcome to GeekOS!。
原因:make没有真正重建镜像,或者 bochs 加载的是另一份旧镜像。教学 Makefile 有时候把镜像生成和内核编译拆成两个目标,只敲make不一定会重新写盘。
解决:改完代码先make clean再make,然后看build目录里镜像文件的修改时间是不是刚刚。启动前用md5sum对一下镜像和上一次的哈希,变了才说明新代码进去了。这一步多花十秒,能省掉半小时的自我怀疑。
5.5 TODO 没注释,程序卡死不动
现象:内核启动、打印完欢迎信息后彻底停住,键盘没反应,也没有任何报错。
原因:Main里那行TODO(...)展开后是“设颜色 → 打印错误 → 死循环”,执行到那里就再也出不来,后面所有线程创建代码都成了摆设。
解决:把TODO这一行整行注释掉,或者直接用Start_Kernel_Thread调用替换掉。注意是注释整行,不是注释参数——宏展开后是完整语句,只注释一半反而会编译报错。这个问题在报告里被单独拎出来讲,说明踩的人不少。
6. 二次开发:把爱心线程改成调度观察器
爱心实验跑通之后,如果想再往前走一步,最值得改的不是图案,而是让调度行为变得可测量。我一般会做三件事:给每个线程加一个自己的 tick 计数、用不同优先级跑一组对照、把输出从“刷屏”改成“定期打印统计”。
先改线程函数,让它每循环一次就累加自己的计数,并在固定次数后打印一次:
static void counter_A(ulong_t arg) { ulong_t n = 0; while (1) { n++; /* 每 10000 次循环打印一次,避免刷屏淹没统计信息 */ if (n % 10000 == 0) { Print("[A] ticks=%lu\n", n); } } } static void counter_B(ulong_t arg) { ulong_t n = 0; while (1) { n++; if (n % 10000 == 0) { Print("[B] ticks=%lu\n", n); } } }然后在Main里用两组参数各跑一次:两个线程都传PRIORITY_NORMAL,观察 A、B 的计数值是否大致同步增长;再把 B 改成更高优先级,观察 B 的计数是不是明显跑在前面、A 是不是被压得很慢。这个对比比看字母交替更有说服力,因为它把“调度公平”从视觉感受变成了数字。
第二个改造方向是给线程加显式延时。GeekOS 里没有现成的sleep,但可以查一下kthread.c里有没有按 tick 等待的接口,或者在循环里插一段空转来拉长单次执行时间。加延时之后,爱心图案会更完整,A/B 交替的节奏也从“闪烁”变成“可读的切换”。
第三个方向是把输出目标从屏幕换成串口或者调试端口,这样 bochs 的图形窗口只负责显示,统计数据从另一个通道出来,不会互相覆盖。教学内核一般都有Print的底层实现,顺着它找到写显存的函数,改成写 I/O 端口就行,代码量不大,但对后续做实验很省事。
从那以后我每次做这类内核实验,都强制自己先跑一遍“最小可观测版本”——哪怕只是两个计数器,也要先确认调度器在按预期切片,再往上堆图案和业务逻辑。不然图案再好看,底层是乱的,出了问题根本分不清是显示代码写错还是调度没跑起来。希望这套改造思路能帮到你。
本文还有配套的精品资源,点击获取