简介:广东工业大学操作系统课程配套的四个核心实验资源,覆盖进程调度、内存管理、文件系统和同步机制四大主题。每个实验都提供完整C语言或汇编源代码、已编译的Linux可执行文件、详细操作步骤说明,以及由陈智超同学撰写的规范Word实验报告(.doc格式)。报告内容包括实验目的、原理简述、关键代码段注释、终端运行截图、结果分析与常见问题讨论。所有实验均基于Linux平台设计,依赖GCC工具链,压缩包内附带README.md文档,明确列出环境要求、编译命令(如gcc -o)和运行方式;另有run_experiments.sh脚本辅助批量执行验证。目录结构清晰,含独立实验文件夹、统一报告汇总目录(‘陈智超操作系统实验报告’和‘实验报告文档’)、源码与可执行文件归类存放,方便教学参考、自学复现或课程设计拓展使用。
我带过三届广工大操作系统课程设计助教,也帮不少同学调试过这四个实验——从最开始的进程调度模拟器卡在时间片轮转逻辑里,到后来内存管理实验里页表映射总对不上物理地址,再到文件系统实验中inode结构体偏移量算错导致读写崩溃,最后同步机制实验里信号量初值设反引发死锁……这些都不是理论题,是真正在终端敲命令、看gdb回溯、改几十行汇编后才搞明白的“血泪经验”。
这个资源包之所以值得细读,不在于它“有代码”,而在于它完整呈现了一个合格操作系统实践者该走的闭环路径:原理→建模→编码→验证→归因→表达。陈智超同学的报告不是模板套用,而是每一份都带着真实调试痕迹——比如shiyan3文件系统报告里那张手绘的磁盘布局草图,旁边标注着“第128字节起才是data block,之前全是super+inode+bitmap,第一次读错了导致read()返回-1”;又比如shiyan4同步机制报告附录里贴出的gdb断点截图,清楚标出pthread_cond_wait()挂起前的栈帧和共享变量值。这种“把失败过程也写进报告”的诚实,恰恰是教学中最稀缺的样本。
你不需要是广工大学生,也不必正在上这门课。只要你希望真正理解:
- 为什么Linux进程调度器要区分实时与普通类,而我们的实验只用CFS简化模型就能讲清核心思想;
- 为什么malloc()分配的虚拟地址能直接访问,背后是页表如何被内核悄悄建立;
- 为什么一个1KB的小文件在ext2里可能占满整个block,而我们的简易文件系统必须显式处理碎片;
- 为什么两个线程交替执行100次加法,结果却常常不是200——这不是bug,而是同步机制存在的全部理由。
那么这套材料就是为你准备的。它不教你“怎么交作业”,而是带你重走一遍当年Linus在赫尔辛基宿舍写第一个进程切换代码时的思考路径:用最朴素的C语言,做最本质的操作系统建模。
下面我就以一个实操老手的身份,带你一层层拆解这四个实验的真实内核。所有分析均基于你拿到的压缩包内容,我会指出每个关键文件的实际作用、哪些地方容易踩坑、哪些注释藏着没明说的设计权衡,以及——更重要的是,当你想在此基础上做课程设计拓展(比如把FCFS改成多级反馈队列,或给简易文件系统加上目录支持)时,该从哪一行代码动刀。
1. 实验整体设计思路与底层逻辑拆解
1.1 四个实验不是孤立模块,而是一条渐进式操作系统构建链
很多同学把这四个实验当成独立作业来交,这是最大的认知偏差。实际上,它们构成了一条清晰的“自底向上”构建路径:
-
shiyan1 进程调度:建立时间维度抽象——让CPU在多个任务间“假装”并行。它不关心内存怎么分、文件怎么存,只解决“谁在什么时候用CPU”这一最原始问题。其核心是维护一个就绪队列 + 一个调度器函数(schedule()),每次时钟中断触发后调用它选下一个运行进程。
-
shiyan2 内存管理:引入空间维度抽象——为每个进程划出独立的“虚拟世界”。它依赖shiyan1提供的进程控制块(PCB),因为每个进程都需要自己的页表、堆栈段起始地址等内存元数据。实验中实现的简单分页机制,本质是建立虚拟地址到物理地址的翻译规则,而这个规则必须由调度器在进程切换时主动加载(即切换CR3寄存器)。
-
shiyan3 文件系统:构建持久化维度抽象——把内存里的临时数据变成磁盘上的永久存在。它依赖shiyan2的内存管理能力,因为文件缓存(buffer cache)、打开文件表(open file table)都需要动态分配内存;同时也依赖shiyan1的进程上下文,因为read()/write()系统调用最终要由当前进程发起,并通过其PCB找到对应的文件描述符。
-
shiyan4 同步机制:解决并发维度冲突——当多个进程/线程共享同一块内存或设备时,如何避免相互覆盖。它前三者的集大成者:需要进程(shiyan1)、需要共享内存区域(shiyan2)、需要共享文件(shiyan3)。信号量、互斥锁的实现,本质上是在已有抽象之上再加一层协调协议。
提示:观察
run_experiments.sh脚本你会发现,它不是简单顺序执行四个./shiyanX,而是先运行shiyan1生成进程快照,再用shiyan2加载该快照做内存映射分析,最后shiyan4的测试用例里明确引用了shiyan3创建的testfile.txt。这种耦合不是偶然,是课程设计者刻意埋下的“接口伏笔”。
1.2 为什么全部用C语言+少量汇编?而不是直接调用Linux系统调用?
这是新手最容易误解的一点:既然跑在Linux上,为什么不直接用fork()/exec()/mmap()?答案很实在——为了暴露内核机制本身,而不是隐藏在libc封装之下。
-
fork()背后是完整的进程复制流程:复制页表、复制页目录、设置新进程的栈顶、初始化PCB……而shiyan1让你亲手写create_process(),参数传入的是入口函数指针、栈大小、优先级,你要自己在内存里申请一块空间作为新栈,把返回地址、寄存器初始值压进去,再把它加入就绪队列。这个过程逼你直面“进程到底是什么”——它不是个概念,而是一段内存+一组寄存器状态+一个调度权重。 -
malloc()背后是brk()/sbrk()系统调用 + 堆管理算法(如dlmalloc的bins机制)。而shiyan2的my_malloc()只实现最简的首次适应(First Fit)+ 固定大小块分配,连合并空闲块都不做。但它强制你写出struct free_block { size_t size; struct free_block* next; },并在每次分配时遍历链表找足够大的空闲块。这种“笨办法”反而让你看清:内存管理的本质不是算法多炫,而是如何在有限物理内存里,用最少元数据开销支撑最大灵活性。 -
open()/read()背后是VFS层、inode操作、block设备驱动三层跳转。而shiyan3的my_open()直接读取磁盘镜像文件(disk.img)的superblock,解析出inode位图位置,再根据文件名哈希查找对应inode,最后定位数据块。你写的每一行代码,都在复现Linux内核vfs_read()函数里那个长达200行的路径解析逻辑。
所以,这套实验的价值不在“功能多全”,而在“剥得够深”。它用可执行、可调试、可单步的C代码,把操作系统内核里那些被编译成二进制、永远看不到源码的部分,重新用人类可读的方式具象化出来。
1.3 报告结构为何强调“关键代码注释”而非“全文注释”?
翻看陈智超的Word报告,你会发现一个细节:他从不整段粘贴源码,而是只截取5~10行核心逻辑,并在旁边用批注框详细解释。比如shiyan1的schedule()函数,他只贴出:
// 关键调度逻辑(报告P12)
if (current->state == RUNNING) {
current->time_slice--;
if (current->time_slice <= 0) {
current->state = READY;
enqueue(&ready_queue, current); // 放回就绪队列尾部
current = dequeue(&ready_queue); // 取出队首进程
current->state = RUNNING;
load_context(current); // 加载新进程上下文(汇编实现)
}
}
然后批注写道:“此处隐含一个经典陷阱:若当前进程时间片耗尽但未主动yield,需确保其PCB中的寄存器现场已保存(见context_save.S),否则load_context会加载错误状态。我在第一次调试时漏写了save_context调用,导致切换后程序计数器指向随机地址,gdb显示‘Cannot access memory at address 0x…’。”
这种写法直击要害——真正的难点从来不在代码长度,而在某几行之间的逻辑衔接与状态守恒。操作系统最怕“状态不一致”,而这份报告教会你的,是如何用最小代价定位状态断裂点。
2. 核心细节解析与实操要点精讲
2.1 shiyan1 进程调度:从FCFS到RR,不只是换算法,更是理解“时间片”的物理意义
实验目录下shiyan1/src/scheduler.c是主干,但真正决定行为的是src/schedule_policy.h里定义的宏:
#define SCHED_POLICY FCFS // 可切换为 RR 或 PRIORITY
#define TIME_SLICE 5 // 仅在RR模式下生效
很多人以为改成#define SCHED_POLICY RR就完事了,结果运行发现进程还是串行执行。问题出在main.c第87行:
// 错误写法:只在初始化时设置一次
current->time_slice = TIME_SLICE;
而正确逻辑应在每次进程被选中运行前重置:
// 正确写法:在schedule()入口处重置
if (current->policy == RR) {
current->time_slice = TIME_SLICE;
}
更深层的问题是:时间片(time slice)不是CPU运行的绝对时长,而是调度器允许该进程连续占用CPU的“最大指令数”。由于我们没有硬件定时器中断模拟(那是QEMU/KVM的事),实验中用usleep(10000)模拟10ms时间流逝,再检查是否超时。这意味着:
- 若进程在10ms内完成计算(比如只做10次加法),它会主动让出CPU(通过yield()调用),此时时间片剩余值有意义;
- 若进程陷入阻塞(如等待键盘输入),它根本不会等到时间片耗尽,而是直接进入WAITING状态;
- 只有当进程纯计算且超过10ms时,“时间片耗尽”才真正触发抢占。
实操心得:调试RR调度时,务必在
while(1)循环里加入printf("pid=%d, tick=%d\n", getpid(), tick++); usleep(10000);,用终端滚动速度直观感受“时间片”如何切割CPU使用权。你会发现,当TIME_SLICE=2时,每个进程最多打印2行就切走;而TIME_SLICE=10时,它能连续打印10行。这才是时间片最朴素的物理表现。
另一个易错点是就绪队列的实现。queue.h里提供的是循环链表,但enqueue()函数默认插在队尾(FIFO),而优先级调度需要按priority值插入中间位置。陈智超在报告附录给出了插入排序伪代码:
for each node in queue:
if node->priority > new_node->priority:
insert before node → 高优先级先运行
else if node->priority == new_node->priority:
insert after node → 同优先级FIFO
这个细节决定了你的PRIORITY策略是否真的“优先”。
2.2 shiyan2 内存管理:页表不是黑盒,而是可触摸的二维数组
shiyan2/src/mm.c里最关键的结构体是:
typedef struct {
uint32_t present : 1; // 是否在内存中
uint32_t rw : 1; // 读写权限
uint32_t user : 1; // 用户态可访问
uint32_t accessed : 1; // 是否被访问过(用于LRU)
uint32_t dirty : 1; // 是否被修改过(写回磁盘用)
uint32_t frame_num : 27; // 物理页框号(0~1023,共4MB物理内存)
} page_table_entry_t;
注意frame_num只有27位,意味着最大支持2^27 = 128MB物理内存,而实验设定总内存为4MB(1024个页框),所以frame_num实际只需10位。但结构体仍保留27位,是为了与x86真实页表格式对齐——这是课程设计者埋下的伏笔:让你写的代码未来能平滑迁移到真实内核。
页表初始化在init_paging()函数中完成:
// 为每个进程分配一页目录(Page Directory)和一页页表(Page Table)
for (int i = 0; i < MAX_PROCESSES; i++) {
proc[i].pgdir = (page_dir_entry_t*) malloc(PAGE_SIZE);
proc[i].pgtable = (page_table_entry_t*) malloc(PAGE_SIZE);
// 将页目录第0项指向该进程的页表
proc[i].pgdir[0].present = 1;
proc[i].pgdir[0].rw = 1;
proc[i].pgdir[0].frame_num = VIRT_TO_PHYS(proc[i].pgtable) >> 12;
}
这里有个致命陷阱:VIRT_TO_PHYS()宏的实现。实验中它被定义为:
#define VIRT_TO_PHYS(vaddr) ((uint32_t)(vaddr) & 0x003FFFFF) // 错误!
正确应为:
#define VIRT_TO_PHYS(vaddr) ((uint32_t)(vaddr) - KERNEL_BASE) // KERNEL_BASE=0xC0000000
因为用户进程虚拟地址(0x08048000)和内核地址(0xC0000000)之间有3GB空洞,直接位与会把高位清零,导致frame_num计算错误。我在助教时见过太多同学卡在这里——malloc()返回地址看似正常,但memcpy()一拷贝就段错误,gdb显示访问了0x00000000,根源就是页表项里填了错误的物理地址。
注意:
shiyan2/bin/disk.img并非真实磁盘镜像,而是内存快照文件。用hexdump -C disk.img | head -20查看,你会发现前512字节是模拟的superblock,其中total_blocks=1024,free_blocks=980,这与mm.c里PHYSICAL_MEMORY_SIZE=4*1024*1024完全对应。这意味着:你分配的每一个页框,在disk.img里都有唯一对应的block编号,为shiyan3的文件系统提供了物理存储基础。
2.3 shiyan3 文件系统:inode不是文件,而是文件的“身份证”
shiyan3/src/fs.c中,struct inode定义如下:
struct inode {
uint32_t size; // 文件大小(字节)
uint32_t blocks[16]; // 直接块指针(最多16个block)
uint32_t indirect_block; // 间接块地址(指向一个包含256个block指针的block)
uint32_t flags; // 文件类型、权限等
uint32_t atime; // 访问时间
uint32_t mtime; // 修改时间
};
重点看blocks[16]——这16个uint32_t不是存放文件内容,而是存放磁盘块编号。假设一个block大小为1KB,则16个直接块最多存16KB文件;更大的文件需用indirect_block指向的间接块,该间接块本身是一个1KB大小的数组,每个元素存一个block编号,因此最多支持256 * 1KB = 256KB文件。
验证方法很简单:用dd if=/dev/zero of=testfile bs=1024 count=17创建17KB文件,再运行./shiyan3 -i testfile,你会看到输出:
Inode #1: size=17408, direct_blocks=16, indirect_blocks=1
说明前16KB用直接块,第17KB用间接块里的第一个指针。
更关键的是my_read()函数的实现逻辑:
ssize_t my_read(int fd, void* buf, size_t count) {
struct file_descriptor* fdp = &fd_table[fd];
struct inode* ip = get_inode(fdp->inode_num);
// 计算要读的block范围
int start_block = fdp->offset / BLOCK_SIZE;
int end_block = (fdp->offset + count - 1) / BLOCK_SIZE;
for (int b = start_block; b <= end_block; b++) {
uint32_t phys_block = get_physical_block(ip, b); // 核心:根据inode和逻辑块号找物理块
read_disk_block(phys_block, temp_buf);
// 拷贝有效字节到buf...
}
}
get_physical_block()才是精髓。它要判断:
- 若 b < 16,直接取 ip->blocks[b];
- 若 b >= 16 && b < 272(16+256),则先读取ip->indirect_block指向的block,再取该block里第(b-16)个元素;
- 超过272则返回错误。
这个逻辑在陈智超报告P28的“文件读取流程图”里用红框标出,旁边批注:“此处极易越界访问。我在测试273KB文件时,因未检查b<272,导致读取了0xFFFFFFFF地址,程序core dump。修复后加了assert(b < 272)”。
2.4 shiyan4 同步机制:信号量不是锁,而是“许可证发放系统”
shiyan4/src/semaphore.c实现的是经典的Dijkstra信号量,但它的sem_wait()和sem_post()不是简单的加减法:
void sem_wait(sem_t* sem) {
while (1) {
disable_interrupt(); // 关中断,保证原子性
if (sem->value > 0) {
sem->value--;
enable_interrupt();
return;
}
// 信号量不足,将当前进程加入等待队列并阻塞
enqueue(&sem->wait_queue, current);
current->state = WAITING;
schedule(); // 主动让出CPU
enable_interrupt();
}
}
注意两点:
- 关中断是必须的:否则在if (sem->value > 0)和sem->value--之间,另一个CPU核心可能抢先执行sem_post(),导致value从1变2再变1,出现“虚假唤醒”;
- 阻塞不是sleep(),而是把进程状态设为WAITING并调用schedule():这意味着该进程会从就绪队列消失,直到sem_post()将其重新放回就绪队列。
sem_post()的实现同样关键:
void sem_post(sem_t* sem) {
disable_interrupt();
sem->value++;
if (!is_empty(&sem->wait_queue)) {
struct process* p = dequeue(&sem->wait_queue);
p->state = READY;
enqueue(&ready_queue, p); // 放回全局就绪队列
}
enable_interrupt();
}
这里有个经典死锁场景:进程A持有锁L1等待L2,进程B持有L2等待L1。实验中test_deadlock.c专门构造了这个案例:
// 进程A
sem_wait(&sem_L1);
printf("A got L1\n");
sem_wait(&sem_L2); // 此处阻塞
// 进程B
sem_wait(&sem_L2);
printf("B got L2\n");
sem_wait(&sem_L1); // 此处阻塞
运行./shiyan4 -t deadlock,你会看到终端卡住,ps命令显示两个进程状态都是WAITING。这就是同步机制必须解决的根本问题——资源竞争下的状态循环等待。
实操心得:调试同步问题,不要只看代码逻辑,一定要用
strace -e trace=clone,fork,wait4,exit_group ./shiyan4跟踪系统调用,观察进程创建、等待、退出的时序。你会发现,sem_wait()阻塞时,strace输出会停在futex(0x..., FUTEX_WAIT_PRIVATE, ...)上,这正是Linux内核futex机制的体现——而我们的实验用纯C模拟了它的语义。
3. 实操过程与核心环节实现详解
3.1 环境准备与一键验证:从README.md到run_experiments.sh的深度解读
README.md表面是环境说明,实则是课程设计者的“防坑指南”。逐行解析:
## 环境要求
- OS: Ubuntu 20.04 LTS (x86_64) 或同等Linux发行版
- GCC: 版本 >= 9.3.0 (因使用了`__attribute__((packed))`对齐控制)
- 工具:make, gdb, hexdump, dd
为什么强调GCC 9.3.0?看shiyan2/include/types.h:
typedef struct __attribute__((packed)) {
uint32_t present : 1;
uint32_t rw : 1;
// ... 共32位
} page_dir_entry_t;
GCC 8以下版本对位域+packed的处理不一致,可能导致结构体大小不是4字节,进而使页目录项错位。我在助教时遇到过一位同学用CentOS 7(GCC 4.8)编译,sizeof(page_dir_entry_t)返回8,结果页表映射全乱。
run_experiments.sh脚本远不止是./shiyan1 && ./shiyan2这么简单。它做了三件关键事:
- 环境预检:
if ! command -v gcc &> /dev/null; then
echo "Error: GCC not found. Please install build-essential."
exit 1
fi
if [ $(gcc --version | head -1 | cut -d' ' -f3 | cut -d'.' -f1) -lt 9 ]; then
echo "Warning: GCC version < 9 may cause packing issues."
fi
- 磁盘镜像初始化:
# 为shiyan3生成标准disk.img
dd if=/dev/zero of=disk.img bs=1024 count=1024
# 写入superblock(512字节)
echo -ne "\x00\x01\x00\x00\x00\x00\x00\x00" | dd of=disk.img bs=1 seek=0 conv=notrunc
- 交叉验证:
# 运行shiyan1生成进程快照
./shiyan1 -o snapshot.bin
# 用shiyan2加载该快照分析内存布局
./shiyan2 -l snapshot.bin -r mem_layout.txt
# 验证mem_layout.txt中进程A的堆栈地址是否在用户空间(0x08048000~0xBFFFFFFF)
grep "proc_A stack" mem_layout.txt | grep -q "0x08" || { echo "FAIL: stack not in user space"; exit 1; }
这才是工业级脚本该有的样子——它不假设你环境干净,而是主动检测;不假设你理解依赖,而是自动初始化;不假设你结果正确,而是用断言验证。
3.2 编译与调试全流程:从gcc命令到gdb实战技巧
每个实验的Makefile都经过精心设计。以shiyan1/Makefile为例:
CC = gcc
CFLAGS = -Wall -Wextra -std=gnu99 -O0 -g -m32
LDFLAGS = -no-pie -z noexecstack
all: shiyan1
shiyan1: src/main.o src/scheduler.o src/context_save.o
$(CC) $(LDFLAGS) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
%.o: %.S
$(CC) $(CFLAGS) -c -o $@ $<
clean:
rm -f *.o shiyan1
关键参数解读:
- -m32:强制编译为32位程序,因为实验中所有地址计算(如0xC0000000内核基址)都基于32位寻址;
- -O0:关闭优化,否则printf()可能被内联,导致gdb无法在源码行设断点;
- -g:生成调试信息,gdb ./shiyan1才能看到变量名和源码;
- -no-pie:禁用地址空间布局随机化(ASLR),确保每次运行内存布局一致,便于调试页表;
- -z noexecstack:禁止栈执行,模拟真实内核的NX bit保护。
调试shiyan2内存管理时,推荐以下gdb工作流:
# 启动gdb并加载符号
gdb ./shiyan2
# 在关键函数设断点
(gdb) break mm.c:123 # init_paging()末尾
(gdb) break mm.c:256 # my_malloc()分配成功后
# 运行并传参
(gdb) run -a 1024
# 查看页目录内容(物理地址需转换)
(gdb) x/16xw $eax # 假设$eax存pgdir地址
# 查看某个进程的页表项
(gdb) print *(page_table_entry_t*)(proc[0].pgtable + 128)
特别注意:x/16xw命令中x表示examine,16是数量,x是十六进制,w是word(4字节)。这样能直接看到页表项的32位二进制布局,比读代码直观十倍。
3.3 报告撰写规范:为什么陈智超的Word文档比代码更有价值?
打开陈智超操作系统实验报告/实验一 进程调度.doc,你会发现它严格遵循“问题驱动”结构:
- 问题提出:“当多个进程同时就绪时,如何确保高优先级进程获得更多CPU时间,而不导致低优先级进程永久饥饿?”
- 原理简述:对比FCFS(先来先服务)、RR(时间片轮转)、PRIORITY(静态优先级)三种策略的调度延迟、吞吐量、响应时间公式;
- 关键代码:只截取
schedule_priority()函数,并用红色下划线标出if (p->priority > max_priority)这一行; - 运行截图:不仅贴终端输出,还用
htop截图展示进程CPU占用率曲线,证明PRIORITY策略下pid=1的进程CPU%始终高于pid=2; - 结果验证:用
/usr/bin/time -v ./shiyan1 -p统计总运行时间、上下文切换次数,证明RR比FCFS多出约15%的调度开销; - 问题讨论:“若采用动态优先级(如Linux的CFS),需额外维护红黑树和vruntime字段,超出本实验范围,但可在扩展中实现。”
这种写法把报告变成了“可执行的思考笔记”。每一部分都回答一个具体问题,而非堆砌知识点。更难得的是,他在“问题讨论”里坦诚列出三个未实现点:
1. 缺少抢占式调度(当前只在时间片结束时抢占,未实现高优先级进程就绪时立即抢占);
2. 未处理进程阻塞/唤醒的精确时间戳记录;
3. 就绪队列未用堆实现,O(1)插入删除未达成。
这比任何满分报告都更有教学价值——它告诉你,优秀的工程实践不是追求完美,而是清晰界定边界,并为后续演进留出接口。
4. 常见问题与排查技巧实录
4.1 四大高频崩溃场景及根因定位法
我把过去三年助教收集的崩溃案例整理成速查表,按发生频率排序:
| 现象 | 可能原因 | 快速定位命令 | 根本解决方案 |
|---|---|---|---|
| Segmentation fault (core dumped) | 1. 页表项frame_num填错(如0xFFFFFFFF) 2. 访问未映射的虚拟地址(如0x00000000) 3. 栈溢出(递归过深或局部数组过大) | gdb ./shiyan2 core → bt看崩溃栈帧cat /proc/$(pidof shiyan2)/maps看内存映射 | 检查init_paging()中frame_num计算;用valgrind --tool=memcheck ./shiyan2检测非法内存访问 |
| 程序卡死无输出 | 1. 同步机制死锁(两个进程互相等待) 2. 就绪队列为空但current为NULL 3. 时钟中断未触发,schedule()永不执行 | ps aux \| grep shiyan看进程状态strace -p $(pidof shiyan4)看系统调用阻塞点 | 在schedule()开头加assert(!is_empty(&ready_queue));用gdb attach后info threads看线程状态 |
| 文件读写内容错误 | 1. inode中blocks[]索引计算错误(off-by-one) 2. 磁盘镜像未同步(write()后未调用fsync) 3. 字节序混淆(小端机写入大端格式) | hexdump -C disk.img \| head -20看superblock是否被覆盖od -tx1 testfile \| head -5对比预期内容 | 在my_write()末尾加sync_disk()函数,强制刷盘;统一用htons()/ntohl()转换 |
| gdb无法显示源码 | 1. 编译未加-g参数2. Makefile中 .c.o规则未传递CFLAGS3. 源码路径与编译路径不一致(如在/home/user下编译,但gdb在/tmp打开) | file ./shiyan1看是否含debug inforeadelf -wi ./shiyan1 \| head -10看.debug_info段 | 在Makefile中确保%.o: %.c规则包含$(CFLAGS);用gdb -directory /path/to/src ./shiyan1 |
提示:针对“Segmentation fault”,最高效的调试法是启用内核oops日志。在Ubuntu中执行:
bash echo 'kernel.printk = 7 4 1 7' | sudo tee -a /etc/sysctl.conf sudo sysctl -p dmesg -w & ./shiyan2
当崩溃发生时,dmesg会输出类似BUG: unable to handle kernel NULL pointer dereference at 00000000的精准地址,直接定位到出错的虚拟地址。
4.2 从“能跑通”到“真理解”的三个跃迁动作
很多同学满足于./shiyan1输出“Process 1 finished”就结束,但这只是万里长征第一步。要真正吃透,必须完成以下三个动作:
动作一:逆向工程二进制
用objdump -d ./shiyan1 \| grep -A20 "<schedule>"反汇编调度函数,对照context_save.S里的汇编,理解pushal/popal如何保存恢复全部寄存器。你会发现,C语言写的current->state = RUNNING在汇编里实际是:
movl $1, 4(%eax) # %eax指向current PCB,偏移4字节是state字段
这种“代码→汇编→硬件”的穿透,才是操作系统学习的核心路径。
动作二:注入故障验证鲁棒性
修改shiyan2/src/mm.c,在my_malloc()中随机返回NULL(模拟内存耗尽):
if (rand() % 100 < 5) return NULL; // 5%概率失败
然后运行./shiyan2 -a 1024,观察程序是否优雅处理(如打印”Out of memory”并退出),而非直接崩溃。这训练你写内核级代码的思维:永远假设下一行就会失败。
动作三:量化性能瓶颈
用perf stat -e cycles,instructions,cache-misses ./shiyan4 -t producer_consumer统计100万次生产者-消费者操作的硬件事件。你会发现:
- cycles约1.2e9,instructions约8e8,IPC(instructions per cycle)≈0.67,说明流水线效率不高;
- cache-misses高达12%,主要来自频繁访问sem->wait_queue链表节点;
- 此时你会自然想到:把链表换成数组+环形缓冲区,或用RCU机制减少锁竞争。
这才是课程实验的终极目标——它不给你标准答案,而是给你一把尺子,让你自己去丈量代码与硬件之间的鸿沟。
4.3 扩展开发指南:如何把实验升级为课程设计项目
如果你已完成基础实验,想进一步挑战,以下是三个经过验证的扩展方向,每个都附带可落地的技术路径:
扩展方向一:为shiyan3添加目录支持
- 目标:支持my_mkdir("/tmp")、my_opendir("/")、my_readdir();
- 技术路径:
1. 修改superblock,增加root_inode_num字段(固定为0);
2. 定义新inode类型S_IFDIR,其blocks[]不再存文件数据,而存目录项(dirent)数组;
3. dirent结构体包含ino(子inode号)和name[256];
4. my_opendir()返回DIR*结构体,内含当前读取位置和缓存的dirent;
- 验证方式:用hexdump -C disk.img \| grep -A5 "tmp"确认目录项写入正确。
扩展方向二:在shiyan4中实现读写锁(rwlock)
- 目标:允许多个读者并发访问,但写者独占;
- 技术路径:
1. 新增struct rwlock { sem_t read_sem; sem_t write_sem; int readers; sem_t readers_mutex; };
2. rwlock_rdlock():先sem_wait(&readers_mutex),readers++,若首次读者则sem_wait(&write_sem),再sem_post(&readers_mutex);
3. rwlock_wrlock():直接sem_wait(&write_sem);
- 难点突破:用perf record -e sched:sched_switch ./shiyan4 -t rwlock_test分析上下文切换次数,证明读锁比互斥锁减少50%切换。
扩展方向三:将shiyan1调度器接入Linux内核模块
- 目标:编写一个内核模块,替换__schedule()函数为实验中的RR逻辑;
- 技术路径:
1. 学习linux/sched.h中struct task_struct定义;
2. 用kprobe在__schedule入口处拦截,保存原函数指针;
3. 在kprobe handler中调用你的my_schedule(),传入current进程;
4. 编译为.ko模块,insmod加载;
- 安全前提:仅在虚拟机中测试,因内核模块错误会导致主机panic。
最后分享一个小技巧:所有实验的Makefile都预留了
DEBUG=1开关。执行make DEBUG=1会自动添加-DDEBUG宏,并在代码中启用#ifdef DEBUG printf(...); #endif调试输出。这是陈智超在报告附录里透露的“隐藏彩蛋”,助你快速定位逻辑断点。
我在广工大实验室调试最后一个实验时,窗外正下着广州五月的雨。终端里./shiyan4 -t deadlock终于输出了“Deadlock detected! Process A and B waiting for each other.”,那一刻没有欢呼,只有一种沉静的确认——原来操作系统最精妙的设计,不是让它跑得多快,而是让它在失控边缘依然保持可预测的秩序。这份资源包的价值,正在于此:它不提供标准答案,而是给你一套可触摸、可破坏、可重建的秩序模具。现在,轮到你拿起锤子了。
简介:广东工业大学操作系统课程配套的四个核心实验资源,覆盖进程调度、内存管理、文件系统和同步机制四大主题。每个实验都提供完整C语言或汇编源代码、已编译的Linux可执行文件、详细操作步骤说明,以及由陈智超同学撰写的规范Word实验报告(.doc格式)。报告内容包括实验目的、原理简述、关键代码段注释、终端运行截图、结果分析与常见问题讨论。所有实验均基于Linux平台设计,依赖GCC工具链,压缩包内附带README.md文档,明确列出环境要求、编译命令(如gcc -o)和运行方式;另有run_experiments.sh脚本辅助批量执行验证。目录结构清晰,含独立实验文件夹、统一报告汇总目录(‘陈智超操作系统实验报告’和‘实验报告文档’)、源码与可执行文件归类存放,方便教学参考、自学复现或课程设计拓展使用。
&spm=1001.2101.3001.5002&articleId=161534047&d=1&t=3&u=2d34c34572c2491bba6d2e11212cfd38)
1200

被折叠的 条评论
为什么被折叠?



