前言: 欢迎来到 XV6 系列的第三篇!如果说前两个实验是在和硬件打交道,那么本期实验则是纯粹的“逻辑博弈”。 在《操作系统》408 考研大纲中,进程/线程的管理与同步互斥 是每年必考的重头戏。本次 Lab3 要求我们在用户态实现线程切换(Uthread),并在多线程环境下解决哈希表的竞态条件(Using threads)和屏障同步(Barrier)。 今天,我将继续以第一人称视角,带大家还原答辩现场,看看如何将干瘪的并发理论化为生动的代码!

🚀 一、 实验成果与运行展示(答辩现场实况)

在答辩现场,这个实验需要分别在 QEMU 模拟器和真实的 Linux 宿主机上进行演示。

1. 演示 uthread(用户态线程切换)

首先,在终端输入 make qemu 进入系统,执行:

$ uthread
thread_a started
thread_b started
thread_c started
thread_c 0
thread_a 0
thread_b 0
thread_c 1
...
thread_c: exit
thread_a: exit
thread_b: exit
thread_schedule: no runnable threads

🎙️ 现场话术: “老师您好,首先为您演示任务一 uthread。您可以看到屏幕上 A、B、C 三个线程在交替打印数字,最后依次平滑退出。这证明我手写的汇编上下文切换代码已经生效,成功在 XV6 的用户空间实现了一个协作式的多线程调度器。”

2. 演示 phbarrier(并发锁与同步)

退出 QEMU,在宿主机的真实 Linux 终端下编译并执行:

$ make ph
$ ./ph 2
100000 puts, 4.382 seconds
100000 gets, 4.215 seconds

然后演示屏障同步:

$ make barrier
$ ./barrier 2
OK; passed

🎙️ 现场话术: “接下来是在宿主机下利用真实 pthread 库完成的任务二和任务三。在 ph(哈希表)测试中,我使用了细粒度的互斥锁,多线程并发不仅没有丢失数据,时间也比单线程大幅缩短;而在 barrier(同步屏障)测试中,多个线程能够完美在屏障点互相等待,齐步进入下一轮,测试结果均为 OK。”

🧠 二、 核心代码背后的 408 考点剖析(高分关键)

并发编程极容易出现 Bug,而排错的唯一指导思想就是扎实的理论基础。以下是我结合 408 理论对核心考点的深度剖析:

📌 考点 1:用户级线程与上下文切换(结合 uthread 实验)

408 中常考“用户级线程(ULT)”与“内核级线程(KLT)”的区别。

  • 用户态的轻量级切换: 我在实验中实现的正是纯粹的用户级线程。这种线程的创建、调度和切换完全在用户空间完成,不需要触发系统调用(ecall)陷入内核态,因此开销极小。

  • 到底要保存哪些寄存器? 在手写 uthread_switch.S 汇编时,我深刻理解了什么是“上下文(Context)”。根据 RISC-V 函数调用约定,我只需要保存 Callee-saved(被调用者保存) 的 14 个寄存器(如 ra, sp, s0-s11)。为什么不保存 a0-a7?因为这部分属于 Caller-saved,C 语言编译器在发生函数调用时已经自动帮我们压栈保存了。这种软硬件的默契配合,极大减少了切换时的内存读写量。

📌 考点 2:临界区保护与并发粒度(结合 ph 实验)

在多线程哈希表实验中,我遇到了经典的“丢失更新(Lost Update)”问题。

  • 竞态条件(Race Condition): 当两个线程同时向同一个哈希桶插入数据时,如果读取了相同的旧头节点,后写入的线程会直接覆盖掉前一个,导致前一个数据凭空消失。在 408 理论中,修改链表指针的代码段就是临界区(Critical Section)

  • 从“全局大锁”到“细粒度锁”: 为了解决冲突,我引入了互斥锁(Mutex)。但我没有简单粗暴地加一把“全局大锁”(那样会让多线程退化成串行排队,失去并发意义),而是为 5 个哈希桶(Bucket)分别分配了一把独立的锁

  • 性能优化: 这样一来,只要两个线程操作的不是同一个桶,它们就可以真正实现并行写入。这种降低锁粒度的思想,完美诠释了操作系统中“并发度与互斥开销的权衡”。

📌 考点 3:条件变量与避免“忙等待”(结合 barrier 实验)

同步屏障要求所有线程必须在某个集合点到齐后,才能一起放行。

  • 为什么不用 while 死循环? 理论课上讲过“让权等待”原则。如果先到的线程用 while(未到齐) 一直循环检查,会疯狂占用 CPU 资源(也就是忙等待 Busy Waiting),这在现代操作系统中是极其低效的。

  • 条件变量的精妙: 我在代码中使用了 pthread_cond_wait。它的底层逻辑是:当线程发现人没到齐时,主动交出 CPU 并在条件变量上沉睡(Sleep),同时自动释放它占用的互斥锁;而当最后一个线程到达时,它会调用 pthread_cond_broadcast,像发令枪一样瞬间唤醒所有沉睡的线程。这一套连招,完美验证了 408 中“通过阻塞唤醒机制实现同步互斥”的底层原理。

🎯 三、 总结

通过 Lab3 的历练,我终于将“死锁”、“临界区”、“条件变量”这些印在教科书上的冰冷名词,变成了能在 CPU 上真实跑起来的多线程程序。

在汇编层面拨弄寄存器,让我看清了线程切换的真面目;在并发海洋里捕捉“丢失的数据”,让我体会到了加锁的艺术。并发编程虽难,但理论之光足以照亮迷途!

以上就是我 XV6 Lab3 的完整总结与答辩思路。如果你也在被多线程折磨,希望这篇剖析能让你豁然开朗。欢迎点赞收藏!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐