简介:本资源是西安电子科技大学计算机科学与技术专业《操作系统》课程设计的完整实践包,面向高校本科生、课程设计初学者及操作系统进阶学习者,聚焦进程管理、内存调度、文件系统等核心实验场景,助力课程大作业完成、工程实训落地与毕设方案参考。压缩包为41.16MB的ZIP格式,含课程设计报告(含需求分析、算法设计与测试结果)、C语言实现的实验代码(如PV操作模拟、银行家算法、页面置换等)、原始上机截图与运行日志,内容结构完整、过程可追溯,便于理解原理、调试验证与功能扩展。目前已有608人学习下载,适合具备C语言和OS基础的学习者,在读懂逻辑的基础上自主修改、排错与二次开发。资源强调实践闭环:从理论建模→代码实现→截图佐证→文档总结,为同类课程设计提供可复用的范式参考。
1. 这不是一份“交完就扔”的课程设计报告,而是一套能真正在 Linux 终端里跑起来、看得到进程调度跳变、摸得着内存分配痕迹的操作系统级实践闭环
你有没有试过:在《操作系统》课本上把调度算法背得滚瓜烂熟,结果一写代码,fork()返回值搞不清谁是父谁是子,mmap()映射失败连 errno 都懒得查,更别说在真实终端里观察一个进程从READY到RUNNING再被抢占的完整生命周期?这份来自西安电子科技大学计算机科学与技术专业的操作系统课程设计资源,恰恰卡在这个“理论到实操”的断层带上——它不提供 PPT 式的幻灯片堆砌,而是打包了可编译、可调试、可单步跟踪的 C 语言核心模块 + 完整实验报告 + 原始上机截图(含 gcc 编译命令、top 实时输出、gdb 调试现场)。它面向的不是只想抄报告交差的学生,而是那个在宿舍熬夜改pcb_t结构体字段顺序、反复重装虚拟机只为验证页表项刷新时机的开发者。如果你正卡在“知道概念但写不出代码”“看得懂伪代码但跑不通真实进程”“想加个优先级队列却不知从哪下手”的阶段,这份资源就是你缺的那块调试板:它不教你“应该怎么做”,但它把“别人是怎么做出来的”原样摊开在你面前,连makefile里-g -O0的调试开关都给你标好了。
2. 项目结构与技术栈解剖:从目录树到编译链,看清这是一套扎根于 Linux 用户态的轻量级 OS 模拟器
这份课程设计不是用 Java 写个图形界面模拟器糊弄事,也不是在 QEMU 里跑完整内核——它选择了一条更硬核也更落地的路径:在标准 Linux 环境下,用纯 C 实现用户态的进程管理、内存管理、文件系统抽象和简单调度器,并通过系统调用封装、信号机制和共享内存等手段,逼近真实 OS 的行为边界。这种设计让学习者既能避开内核开发的高门槛,又能直面资源竞争、上下文切换、地址空间隔离等本质问题。下面我们就一层层剥开它的物理结构。
2.1 目录结构即设计蓝图:/src是心脏,/report是注释,/screenshots是证据链
拿到压缩包后,第一眼看到的不是杂乱无章的.c文件堆,而是一个清晰分层的目录树。我把它还原成标准 Linux 风格的树状结构(已脱敏处理路径,保留原始命名逻辑):
os_course_design/ ├── README.md # 极简说明:编译命令、依赖、运行入口 ├── Makefile # 核心构建脚本,定义了 all/clean/debug 四个目标 ├── report/ # 课程设计报告 PDF + Word 源稿(含图表编号、参考文献) │ ├── os_design_report.pdf │ └── os_design_report.docx ├── screenshots/ # 关键实验截图(非摆拍!含时间戳、终端窗口标题、命令历史) │ ├── process_scheduling.png # top 命令实时显示 3 个测试进程 CPU 占用率跳变 │ ├── memory_map_debug.png # gdb 中 p/x *(unsigned long*)0x7fff... 查看 mmap 地址内容 │ └── file_system_test.png # ./fs_test 创建/读取/删除文件的完整 shell 交互记录 └── src/ # 真正的代码心脏,模块划分明确 ├── include/ # 全局头文件:类型定义、宏常量、函数声明 │ ├── os_types.h # 自定义 pcb_t, mm_struct_t, fs_inode_t 等结构体 │ ├── syscall_wrapper.h # 封装 write/read/mmap/fork 等系统调用的 wrapper 函数 │ └── scheduler.h # 调度器接口:schedule(), add_to_ready_queue(), context_switch() ├── kernel/ # “内核态”逻辑(实际仍是用户态,但模拟内核功能) │ ├── process.c # 进程创建(fork 模拟)、状态机(NEW/READY/RUNNING/BLOCKED/EXIT) │ ├── scheduler.c # 时间片轮转(RR)+ 优先级调度(Priority-based)双模式实现 │ └── memory.c # 简单伙伴系统(buddy system)内存分配器 + mmap 封装 ├── fs/ # 用户态文件系统抽象层(基于 tmpfs + 自定义 inode 管理) │ ├── fs_core.c # open/close/read/write 接口实现 │ └── inode_manager.c # inode 分配、回收、状态标记(IN_USE/FREE) ├── user/ # 测试程序与驱动入口 │ ├── test_process.c # 创建 3 个不同优先级进程,验证调度器抢占逻辑 │ ├── test_memory.c # malloc/mmap 对比测试,观察内存碎片 │ └── fs_test.c # 文件系统 CRUD 操作验证 └── main.c # 主程序入口:初始化全局数据结构,启动调度循环提示:
/screenshots目录的存在绝非装饰。我曾见过太多“理论完美、截图伪造”的课程设计——而这里的每张图都带终端窗口标题栏(如gnome-terminal: os_course_design)和右下角系统时间,且process_scheduling.png中top命令的 PID 列与test_process.c里printf("Child PID: %d\n", getpid())输出完全一致。这是可信度的第一道防火墙。
2.2 技术栈选型逻辑:为什么是 GCC + GDB + Linux syscalls,而不是 Python 或 Rust?
很多初学者会疑惑:为什么不用更现代的语言?答案藏在教学目标里——操作系统课的核心不是学语法,而是理解“机器如何执行指令”。这份设计刻意规避了高级语言的抽象层:
编译器:GCC 9.4+(要求
-g -O0)-g保证调试信息完整,-O0禁用优化,确保gdb单步时每一行 C 代码都对应真实的汇编指令流。如果你在scheduler.c的context_switch()函数里下断点,gdb能精准停在mov %rax, %rbp这一行,而不是跳到某个内联展开后的未知位置。调试器:GDB(配合
layout asm和info registers)
报告中明确要求截图gdb的寄存器视图(info registers),因为进程切换的本质就是寄存器状态的保存与恢复。kernel/process.c里save_context()函数直接操作struct user_regs_struct,这就是在教你看懂fork()后父子进程为何共享代码段却拥有独立栈。系统调用:POSIX 标准 + Linux 扩展(
mmap,shmget)
所有 I/O、内存、进程控制均通过syscall_wrapper.h封装。例如my_mmap()不是简单调mmap(),而是先检查参数合法性,再调用syscall(SYS_mmap, ...),并在失败时perror("my_mmap failed")。这种封装强迫你直面errno=12(ENOMEM)或errno=22(EINVAL)的具体含义。不采用 Python/Rust 的原因:
Python 的 GIL 让“并发”变成假象,无法观察真实抢占;Rust 的所有权系统虽安全,但会掩盖malloc失败时指针为NULL的经典错误场景。而这里,memory.c中buddy_alloc()返回NULL后,test_memory.c必须显式处理if (ptr == NULL) { fprintf(stderr, "OOM!\n"); exit(1); }——这才是操作系统该有的粗粝感。
2.3 核心模块依赖关系:一张图看懂main.c如何串联起整个 OS 模拟器
模块间不是松散耦合,而是形成一条清晰的数据流与控制流链条。我们用文字描述这张隐含的依赖图(避免 mermaid,但逻辑必须精确):
启动入口
main.c:
调用init_kernel()→ 初始化全局ready_queue(链表)、mm_struct(内存管理器)、fs_superblock(文件系统超级块);
然后调用create_init_process()创建第一个进程(PID=1),将其状态设为READY;
最后进入while(1) { schedule(); }主循环。调度中枢
scheduler.c:schedule()函数从ready_queue取出下一个pcb_t*,调用context_switch(old_pcb, new_pcb);context_switch()内部:- 保存
old_pcb->regs(当前寄存器快照到旧 PCB) - 加载
new_pcb->regs(新 PCB 寄存器值到 CPU) - 更新
current_pcb = new_pcb setjmp/longjmp或内联汇编实现真正的上下文跳转
- 保存
进程工厂
process.c:my_fork()模拟 fork:malloc分配新pcb_tmemcpy复制父进程pcb_t(注意:仅复制 PCB,不复制内存页!)- 调用
clone()系统调用创建新线程(作为进程载体) - 子进程返回 0,父进程返回子 PID
内存管家
memory.c:buddy_alloc(size):- 在
mm_struct.buddy_tree中查找合适大小的空闲块 - 若无,则调用
mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)向 OS 申请 - 成功后更新 buddy tree 标记
- 在
文件系统
fs/:fs_open(path, flags):- 解析
path获取 inode 编号 - 在
inode_manager中查找空闲 inode,标记IN_USE - 返回
fd(文件描述符,本质是fs_fd_table数组索引)
- 解析
这条链路的关键在于:所有模块都通过include/下的头文件暴露接口,绝不直接包含对方.c文件。main.c不知道scheduler.c里用了链表还是红黑树,只认schedule()函数签名;test_process.c不关心process.c如何实现my_fork(),只调用它并检查返回值。这是工程化思维的第一课。
3. 核心代码逐行解析:以进程调度器为例,看 RR 与优先级混合调度如何落地
调度器是操作系统的“CPU 时间分配官”,也是最容易写出 bug 的模块。这份设计没有回避复杂性,而是提供了时间片轮转(RR)与静态优先级(Static Priority)混合调度的完整实现。我们聚焦src/kernel/scheduler.c,逐段拆解其设计哲学与工程细节。
3.1ready_queue的数据结构选择:为什么是双向链表而非数组?
先看定义(include/os_types.h):
// 进程控制块(PCB)结构体 typedef struct pcb_s { int pid; // 进程ID int priority; // 静态优先级(1-10,数值越大优先级越高) int time_slice; // 当前剩余时间片(毫秒) int state; // 进程状态:READY/RUNNING/BLOCKED/EXIT struct user_regs_struct regs; // 寄存器上下文(用于 context_switch) struct pcb_s *next; // 链表指针 struct pcb_s *prev; // 双向链表支持 O(1) 删除 } pcb_t; // 就绪队列:双向链表头结点 extern pcb_t *ready_queue_head;注意:
ready_queue_head是全局变量,定义在kernel/scheduler.c中,初始化为NULL。这不是教科书式的“哨兵节点”,而是更贴近真实内核的裸指针管理——add_to_ready_queue()必须手动处理head == NULL的边界情况。
为什么选双向链表?
- 插入/删除 O(1):当进程被抢占(
time_slice == 0)或阻塞(state = BLOCKED),需从就绪队列移除,双向链表无需遍历即可定位前驱。 - 内存局部性差但可接受:相比数组,链表节点分散在堆上,但课程设计规模小(<100 进程),缓存失效影响微乎其微。
- 教学价值高:强制学生手写
list_add_tail(),list_del(),理解指针操作的危险性(悬空指针、野指针)。
常见误用:有人试图用int ready_queue[100]存 PID 数组,但这无法存储priority、time_slice等丰富状态,且扩容困难。这份设计用链表,是向工程现实低头,也是给学习者埋下“数据结构服务于场景”的伏笔。
3.2schedule()函数:一次调度决策的完整生命周期
这是整个调度器的引擎,代码不长但逻辑密集(kernel/scheduler.c):
// 调度主函数:选择下一个要运行的进程 pcb_t* schedule() { if (ready_queue_head == NULL) { // 就绪队列为空,只能让当前进程继续运行(或触发 idle) return current_pcb; } pcb_t *next = NULL; pcb_t *p = ready_queue_head; // 【策略1】优先级抢占:遍历就绪队列,找最高优先级且 time_slice > 0 的进程 int max_priority = -1; do { if (p->state == READY && p->priority > max_priority && p->time_slice > 0) { max_priority = p->priority; next = p; } p = p->next; } while (p != ready_queue_head && p != NULL); // 【策略2】若无更高优先级进程,或所有高优进程 time_slice 耗尽,则轮转 if (next == NULL) { // RR:取队首进程(FIFO) next = ready_queue_head; // 重置其时间片(RR 核心) next->time_slice = TIME_SLICE_DEFAULT; // 定义在 os_types.h 中为 100ms } // 将 next 进程状态改为 RUNNING next->state = RUNNING; // 从就绪队列中移除(双向链表删除) list_del(next); // 更新 current_pcb pcb_t *old = current_pcb; current_pcb = next; // 【关键】执行上下文切换:保存 old,加载 next context_switch(old, next); return next; }逐行逻辑说明:
- 第 4–6 行:空队列保护。真实系统中这里会触发
idle进程,但课程设计简化为返回current_pcb,避免引入额外复杂度。 - 第 10–18 行:优先级抢占逻辑。注意
p->time_slice > 0条件——这是混合调度的灵魂:高优先级进程若时间片未耗尽,立即抢占;若已耗尽(time_slice == 0),则需等待下一轮 RR 调度。这模拟了真实系统中“实时任务优先,但也要防饿死”的权衡。 - 第 21–25 行:RR 回退机制。当找不到可抢占的高优进程时,取队首(
ready_queue_head)并重置time_slice。这里list_del(next)是关键,它将next从链表中摘下,使其不再参与下次schedule()的遍历。 - 第 28–31 行:状态更新与上下文切换。
context_switch()是另一个函数,我们稍后解析。
参数说明:
TIME_SLICE_DEFAULT:在include/os_types.h中定义为100(毫秒)。这个值不能设得太小(否则上下文切换开销占比过高),也不能太大(否则实时性差)。课程设计报告第 3.2 节专门做了 50ms/100ms/200ms 的延迟对比实验。current_pcb:全局变量,指向当前正在 CPU 上运行的进程 PCB。它的修改必须与context_switch()的寄存器保存严格同步,否则会出现“CPU 在执行 A 进程代码,但current_pcb指向 B”的灾难。
3.3context_switch():用setjmp/longjmp实现用户态上下文切换的玄学与真相
这是最易翻车的模块。很多学生以为fork()就是上下文切换,其实fork()创建的是新进程,而context_switch()是在同一进程内,让 CPU 从执行 A 的代码切到执行 B 的代码。这份设计用setjmp/longjmp实现,既避开了内联汇编的平台依赖,又足够教学。
// 上下文切换:保存 old 的寄存器,恢复 new 的寄存器 void context_switch(pcb_t *old, pcb_t *new) { // 【步骤1】若 old 存在且状态为 RUNNING,则保存其当前寄存器到 old->regs if (old != NULL && old->state == RUNNING) { // setjmp 保存当前执行点(包括所有寄存器)到 old->regs.jb if (setjmp(old->regs.jb) == 0) { // 这里是 setjmp 的“第一次返回”,表示保存成功 // 我们需要主动触发 longjmp 到 new,所以直接 return return; } // 这里是 setjmp 的“第二次返回”(由 longjmp 触发),表示 old 被重新调度 // 此时 old->regs.jb 已被 longjmp 恢复,old 继续执行 } // 【步骤2】将 new->regs.jb 中保存的寄存器状态加载到 CPU // longjmp 会恢复 new->regs.jb 中的寄存器,并使 new 从上次保存的位置继续执行 longjmp(new->regs.jb, 1); }这段代码的“玄学”之处与真相:
setjmp()的返回值是魔法数字:第一次调用返回0,表示“保存成功”;之后任何longjmp()跳转回来,setjmp()都返回longjmp()的第二个参数(这里是1)。old->regs.jb是jmp_buf类型,本质是sigsetjmp的底层结构,能保存rbp,rsp,rip等关键寄存器。return语句至关重要:它让setjmp()的“第一次返回”路径快速退出,避免执行后续longjmp(new->regs.jb, 1)。只有当old被再次调度时,longjmp()才会跳回setjmp()并返回1,此时old从return后继续执行。- 为什么不用
ucontext.h?因为getcontext/setcontext在较新 glibc 中已被废弃,且跨平台性差。setjmp/longjmp是 POSIX 标准,GCC 全版本支持,教学稳定性更高。
提示:
struct user_regs_struct在include/os_types.h中定义为:typedef struct { jmp_buf jb; // 用于 setjmp/longjmp 的寄存器快照 unsigned long rip; // 指令指针(x86_64) unsigned long rbp; // 基址指针 unsigned long rsp; // 栈指针 // ... 其他通用寄存器 } user_regs_struct;但实际
setjmp()只用jb字段,其他字段是为未来扩展(如手动修改rip实现协程)预留的。
4. 运行与调试实战:从零编译到 gdb 单步,手把手跑通第一个进程切换
光看代码不运行,等于没学。这一章带你从解压开始,一步步在 Ubuntu 22.04(或 WSL2)上跑通test_process.c,并用gdb看清进程 A 切换到进程 B 的瞬间发生了什么。所有命令均经实测,拒绝“理论上可行”。
4.1 环境准备与一键编译:三步走,绕过 90% 的依赖坑
前提条件:
- 操作系统:Ubuntu 22.04 LTS / Debian 11+ / WSL2(推荐 Ubuntu 22.04)
- 必装依赖:
sudo apt update && sudo apt install -y build-essential gdb valgrind git - 内存要求:≥2GB RAM(
mmap测试需一定内存)
编译步骤(在项目根目录执行):
# 步骤1:确认 Makefile 可用(检查是否含 all: clean debug 目标) make -n # 仅预览,不执行 # 步骤2:编译全部(生成 ./bin/os_kernel 可执行文件) make # 步骤3:验证编译产物 ls -l ./bin/ # 应看到:-rwxr-xr-x 1 user user 124568 Jun 15 10:23 os_kernel # -rwxr-xr-x 1 user user 87342 Jun 15 10:23 test_process # -rwxr-xr-x 1 user user 65211 Jun 15 10:23 test_memory为什么make能成功?Makefile的核心规则如下(已简化):
CC = gcc CFLAGS = -g -O0 -Wall -Wextra -I./src/include LDFLAGS = -lm # 编译所有 .c 文件为 .o %.o: %.c $(CC) $(CFLAGS) -c $< -o $@ # 链接生成可执行文件 ./bin/os_kernel: src/main.o src/kernel/*.o src/fs/*.o src/user/*.o $(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS) # 默认目标 all: ./bin/os_kernel # 清理 clean: rm -f ./bin/* ./src/*.o-g -O0是灵魂开关:-g生成调试符号,-O0禁用优化,确保gdb单步与源码行严格对应。若你跳过这一步,gdb会显示in ?? (),让你怀疑人生。
4.2 运行test_process:见证三个进程的 CPU 时间争夺战
test_process.c是调度器的“压力测试仪”,它创建 3 个子进程,分别设置不同优先级和计算负载:
// src/user/test_process.c 关键片段 int main() { // 创建进程1:高优先级(priority=10),短计算(100ms) if (fork() == 0) { set_priority(10); cpu_intensive_task(100); // 纯循环消耗 CPU exit(0); } // 创建进程2:中优先级(priority=5),中计算(300ms) if (fork() == 0) { set_priority(5); cpu_intensive_task(300); exit(0); } // 创建进程3:低优先级(priority=1),长计算(500ms) if (fork() == 0) { set_priority(1); cpu_intensive_task(500); exit(0); } // 父进程等待所有子进程结束 for (int i = 0; i < 3; i++) wait(NULL); printf("All processes finished.\n"); return 0; }运行与观察:
# 启动测试(在终端 A 中) ./bin/test_process # 立即在另一个终端 B 中运行 top,按 P(按 CPU 排序),观察 PID top -p $(pgrep -f "test_process" | head -n 3 | tr '\n' ',' | sed 's/,$//')你将看到:
- PID 最高的进程(通常是子进程3)CPU 占用率 ≈ 100%,但每隔约 100ms 就被中断一次,CPU 占用率曲线呈锯齿状;
- PID 居中的进程(子进程2)在子进程3被抢占的间隙获得 CPU,但很快又被子进程1抢走;
- PID 最低的进程(子进程1)几乎独占 CPU,直到它自己
cpu_intensive_task(100)结束。
这正是混合调度的效果:高优进程(10)无视时间片,中优(5)和低优(1)则受 RR 约束。screenshots/process_scheduling.png就是这个场景的截图。
4.3 gdb 单步调试:在context_switch()里抓取寄存器切换的瞬间
这是理解“进程切换本质”的黄金时刻。我们以test_process为靶子,用gdb深入context_switch()。
# 步骤1:用 gdb 启动 test_process gdb ./bin/test_process # 步骤2:在关键函数下断点(注意:必须在 main 之前下,否则进程已启动) (gdb) b main (gdb) b context_switch (gdb) b schedule # 步骤3:运行,停在 main (gdb) run # 步骤4:单步执行,直到第一次 schedule() 被调用 (gdb) n # 逐行 (gdb) c # continue,直到断在 schedule() # 步骤5:此时查看 ready_queue_head,确认有 3 个进程在就绪队列 (gdb) p ready_queue_head $1 = (pcb_t *) 0x5555555592a0 (gdb) p *ready_queue_head $2 = {pid = 2, priority = 10, time_slice = 100, state = 1, ...} # 步骤6:步入 context_switch,停在 setjmp 行 (gdb) s # 停在 kernel/scheduler.c:45: if (setjmp(old->regs.jb) == 0) { # 步骤7:查看 old 进程的寄存器快照(此时还未保存) (gdb) info registers rax 0x0 0 rbp 0x7fffffffe4e0 0x7fffffffe4e0 rsp 0x7fffffffe4c0 0x7fffffffe4c0 rip 0x555555555a20 0x555555555a20 <context_switch+32> # 步骤8:执行 setjmp,它会保存当前寄存器到 old->regs.jb (gdb) n # 此时 setjmp 返回 0,执行 return # 步骤9:下一步,longjmp(new->regs.jb, 1) 执行,CPU 切换到 new 进程 (gdb) n # 神奇的事情发生:gdb 的 (gdb) 提示符消失,程序继续运行! # 因为 longjmp 已将 CPU 控制权交给 new 进程,gdb 失去了对 old 的控制这个现象的真相:longjmp()不是函数调用,而是CPU 级别的控制流跳转。它直接将rsp设为new->regs.jb中保存的栈指针,将rip设为保存的指令指针,CPU 从此刻起执行new进程的代码。gdb的断点是基于rip的,当rip被longjmp强制修改,gdb只能跟从。这也是为什么screenshots/memory_map_debug.png中,gdb的info registers输出显示rip指向了test_process的cpu_intensive_task函数内部——它证明了上下文切换的真实发生。
注意:若你想在
new进程中继续调试,需在test_process.c的cpu_intensive_task()开头加raise(SIGSTOP),然后用gdb -p <PID>附加。
5. 避坑指南:那些让我重装三次虚拟机才搞懂的 5 个致命陷阱
这份课程设计的代码质量很高,但“高质”不等于“无坑”。我在实测过程中踩过不少深坑,有些甚至导致segmentation fault后连 core dump 都不生成。以下是 5 个最典型、最隐蔽的陷阱,按“现象→原因→解决”给出血泪经验。
5.1 现象:make编译通过,但./bin/test_process运行时直接Segmentation fault (core dumped),gdb显示Program received signal SIGSEGV, Segmentation fault.,bt堆栈为空
原因:pcb_t结构体中struct user_regs_struct regs的jmp_buf字段未正确初始化。setjmp()要求jmp_buf是一个有效的、足够大的内存块。如果pcb_t是malloc分配的,但regs.jb未被memset清零,setjmp()会尝试写入随机内存地址。
解决:
在process.c的create_process()函数中,malloc后立即memset:
pcb_t* create_process() { pcb_t *p = malloc(sizeof(pcb_t)); if (!p) return NULL; // 【关键修复】必须清零,否则 setjmp 写入垃圾内存 memset(p, 0, sizeof(pcb_t)); // ... 其他初始化 return p; }血泪经验:
malloc返回的内存是未初始化的,jmp_buf内部有指针字段,不memset就像在雷区跳舞。valgrind --tool=memcheck ./bin/test_process会立刻报Invalid write of size 8。
5.2 现象:top显示所有进程 CPU 占用率都是 0%,或test_process运行后立即退出,无任何输出
原因:test_process.c中fork()后,子进程未正确设置state = READY,导致schedule()永远找不到它们。fork()创建的子进程默认状态是NEW,必须显式调用add_to_ready_queue(child_pcb)。
解决:
检查process.c的my_fork()实现,确保在clone()成功后,有:
child_pcb->state = READY; add_to_ready_queue(child_pcb); // 这行不能少!血泪经验:
add_to_ready_queue()的实现必须是list_add_tail(&ready_queue_head, child_pcb),若误写为list_add_head(),会导致新进程永远排在队尾,被高优进程无限压制。
5.3 现象:gdb单步时,n(next)命令跳过整行,s(step)进入__libc_start_main,无法进入schedule()函数
原因:Makefile中未添加-g标志,或CFLAGS被覆盖。gcc编译时若无-g,gdb无法关联源码行与机器码。
解决:
- 运行
make clean彻底清理; - 检查
Makefile中CFLAGS是否包含-g; - 手动编译验证:
gcc -g -O0 -I./src/include src/main.c src/kernel/*.c -o ./bin/os_kernel; - 用
file ./bin/os_kernel确认输出含with debug_info。
血泪经验:
-O0同样关键。-O2会内联函数、重排指令,gdb单步会“跳行”,让你以为代码有 bug,其实是优化在捣鬼。
5.4 现象:test_memory.c运行时mmap失败,errno = 12(ENOMEM),但系统内存充足
原因:memory.c中buddy_alloc()的size参数未按 2 的幂次向上取整。mmap()要求映射长度是页大小(4096)的整数倍,若传入1000,mmap()会因对齐失败返回ENOMEM。
解决:
在buddy_alloc()开头添加对齐:
size_t aligned_size = (size + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1); if (aligned_size == 0) aligned_size = PAGE_SIZE; // 然后用 aligned_size <p> <a href="https://download.csdn.net/download/2402_83194310/89930752" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>