广工大操作系统四实验包:进程调度/内存管理/文件系统/同步机制全实现(含源码、可执行文件与规范报告)

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:广东工业大学操作系统课程配套的四个核心实验资源,覆盖进程调度、内存管理、文件系统和同步机制四大主题。每个实验都提供完整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=1024free_blocks=980,这与mm.cPHYSICAL_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这么简单。它做了三件关键事:

  1. 环境预检
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
  1. 磁盘镜像初始化
# 为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
  1. 交叉验证
# 运行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 corebt看崩溃栈帧
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规则未传递CFLAGS
3. 源码路径与编译路径不一致(如在/home/user下编译,但gdb在/tmp打开)
file ./shiyan1看是否含debug info
readelf -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.hstruct 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.”,那一刻没有欢呼,只有一种沉静的确认——原来操作系统最精妙的设计,不是让它跑得多快,而是让它在失控边缘依然保持可预测的秩序。这份资源包的价值,正在于此:它不提供标准答案,而是给你一套可触摸、可破坏、可重建的秩序模具。现在,轮到你拿起锤子了。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:广东工业大学操作系统课程配套的四个核心实验资源,覆盖进程调度、内存管理、文件系统和同步机制四大主题。每个实验都提供完整C语言或汇编源代码、已编译的Linux可执行文件、详细操作步骤说明,以及由陈智超同学撰写的规范Word实验报告(.doc格式)。报告内容包括实验目的、原理简述、关键代码段注释、终端运行截图、结果分析与常见问题讨论。所有实验均基于Linux平台设计,依赖GCC工具链,压缩包内附带README.md文档,明确列出环境要求、编译命令(如gcc -o)和运行方式;另有run_experiments.sh脚本辅助批量执行验证。目录结构清晰,含独立实验文件夹、统一报告汇总目录(‘陈智超操作系统实验报告’和‘实验报告文档’)、源码与可执行文件归类存放,方便教学参考、自学复现或课程设计拓展使用。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值