LAB记录
Unix Utilities
- 花费时间:6小时
- 得分:100/100
- 难度:较难
- 结果:
== Test sleep, no arguments ==
$ make qemu-gdb
sleep, no arguments: OK (3.4s)
== Test sleep, returns ==
$ make qemu-gdb
sleep, returns: OK (0.9s)
== Test sleep, makes syscall ==
$ make qemu-gdb
sleep, makes syscall: OK (0.9s)
== Test pingpong ==
$ make qemu-gdb
pingpong: OK (1.1s)
== Test primes ==
$ make qemu-gdb
primes: OK (1.3s)
== Test find, in current directory ==
$ make qemu-gdb
find, in current directory: OK (1.2s)
== Test find, recursive ==
$ make qemu-gdb
find, recursive: OK (1.3s)
== Test xargs ==
$ make qemu-gdb
xargs: OK (1.8s)
== Test time ==
time: OK
Score: 100/100此LAB难度主要在于primes的实现
primes需要注意作为子进程和父进程时分别需要做的事情,同时需要注意fork之后会复制文件描述符
需要恰当设置好父子进程的分工,同时注意管道的流式传输
xargs有一定难度,主要在于字符串的处理
System Calls
- 花费时间:4小时
- 得分:35/35
- 难度:中等
- 结果:
== Test trace 32 grep ==
$ make qemu-gdb
trace 32 grep: OK (4.5s)
== Test trace all grep ==
$ make qemu-gdb
trace all grep: OK (0.8s)
== Test trace nothing ==
$ make qemu-gdb
trace nothing: OK (0.8s)
== Test trace children ==
$ make qemu-gdb
trace children: OK (30.5s)
== Test sysinfotest ==
$ make qemu-gdb
sysinfotest: OK (3.6s)
== Test time ==
time: OK
Score: 35/35此LAB理解系统调用的原理之后难度不大
用户空间中通过usys.pl自动生成函数调用的封装函数,内容就是设置寄存器之后执行ecall
执行ecall之后陷入内核,通过一系列处理抵达kernal/syscall.c中的syscall()函数,之后根据系统调用号调用对应的内核函数sys_XXX()
sys_XXX()函数中通过argint()等函数获取参数,之后跳转到真正的内核函数中执行
用户态和内核态之间的参数传递是通过trapframe实现的
Page Tables
- 花费时间:3小时
- 难度:中等
- 通过:3/3
- 结果:
== Test pgtbltest: ugetpid ==
pgtbltest: ugetpid: OK
== Test pgtbltest: pgaccess ==
pgtbltest: pgaccess: OK
== Test pte printout ==
$ make qemu-gdb
pte printout: OK (0.7s)理解页表结构之后这个LAB便不难
Traps
- 花费时间:2.5小时
- 难度:中等
- 通过:4/4
- 结果:
== Test backtrace test ==
$ make qemu-gdb
backtrace test: OK (2.9s)
== Test running alarmtest ==
$ make qemu-gdb
(4.6s)
== Test alarmtest: test0 ==
alarmtest: test0: OK
== Test alarmtest: test1 ==
alarmtest: test1: OK
== Test alarmtest: test2 ==
alarmtest: test2: OK此LAB理解trap的处理流程之后难度不大
需要注意在于alarm部分的a0寄存器的恢复,注意不要被对应syscall的处理函数的返回值覆盖掉了
Copy-on-write fork
- 花费时间:3小时
- 难度:较难
- 通过:3/3
- 结果:
== Test simple ==
simple: OK
== Test three ==
three: OK
== Test file ==
file: OK此LAB需要注意几点
- 全局页引用数组需要上锁,注意该锁与空闲链表锁的顺序,避免死锁
- 触发COW页错误时,创建新的页之后记得复制父进程的内容到新的页中
其实LAB题干已经写的很清楚要怎么做了,照着干就行
P.S.感觉自己这里代码写的好丑陋
Thread
- 花费时间:2小时
- 难度:中等
- 分数:60/60
- 结果:
== Test uthread ==
$ make qemu-gdb
uthread: OK (4.9s)
== Test answers-thread.txt == answers-thread.txt: OK
== Test ph_safe == make[1]: Entering directory '/home/fancy/MIT6S081/xv6-labs-2021'
gcc -o ph -g -O2 -DSOL_THREAD -DLAB_THREAD notxv6/ph.c -pthread
make[1]: Leaving directory '/home/fancy/MIT6S081/xv6-labs-2021'
ph_safe: OK (16.8s)
== Test ph_fast == make[1]: Entering directory '/home/fancy/MIT6S081/xv6-labs-2021'
make[1]: 'ph' is up to date.
make[1]: Leaving directory '/home/fancy/MIT6S081/xv6-labs-2021'
ph_fast: OK (36.8s)
== Test barrier == make[1]: Entering directory '/home/fancy/MIT6S081/xv6-labs-2021'
gcc -o barrier -g -O2 -DSOL_THREAD -DLAB_THREAD notxv6/barrier.c -pthread
make[1]: Leaving directory '/home/fancy/MIT6S081/xv6-labs-2021'
barrier: OK (3.1s)
== Test time ==
time: OK
Score: 60/60LAB有三个部分,第一个部分是XV6中实现用户态的单线程模拟多线程,这个挺简单的,和内核中的线程调度事实差不多,很多可以照抄,而且因为是单线程模拟的,也不需要管锁之类的问题。最需要注意的是RISC-V中栈向下增长,所以sp应该传stack + STACKSIZE
二三部分是和LINUX编程有关,主要涉及pthread库。第二个实现是为并发访问的哈希表加上锁,我的实现使用了读写锁pthread_rwlock_t而不是实验要求的互斥锁pthread_mutex_t,性能快了一些。下面是三种实施办法的速率对比。可以看到在多线程读的情况读写锁显著提高了吞吐量(我的云服务器CPU只有1核2线程,太慢了)
| 场景 | 无锁 | 读写锁 | 互斥锁 |
|---|---|---|---|
| 1线程 put | 8963/s | 9893/s | 9544/s |
| 1线程 get | 9009/s | 10025/s | 8680/s |
| 2线程 put | 18409/s | 15775/s | 15962/s |
| 2线程 get | 16899/s | 20095/s | 14866/s |
| 2线程 keys missing | 15594 | 0 | 0 |
第三个部分是实现屏障,这个想初次写对还是有点困难的,实验提示给了很多。我总结主要是两点,首先pthread_cond_wait可能会意外唤醒,所以需要包在循环里面,另外不能使用bstate.nthread == 0作为退出循环条件,因为有线程会抢先修改,导致别的线程看不到0又睡下去了,这就是实验教材里面说的
Net
- 花费时间:4小时
- 难度:难
- 分数:100/100
- 结果:
== Test nettest: ping ==
nettest: ping: OK
== Test nettest: single process ==
nettest: single process: OK
== Test nettest: multi-process ==
nettest: multi-process: OK
== Test nettest: DNS ==
nettest: DNS: OK
== Test time ==
time: OK
Score: 100/100这道题要求实现一个网卡的驱动,主要是两个函数,发送的e1000_transmit()和中断时会调用的e1000_recv()
主要难点在于理解E1000网卡的原理
其中最重要的是内存中的两个环形描述符缓存区,分别是tx_ring用于发送,rx_ring用于接收。每个环都有16个槽位,分别为struct tx_desc类型和struct rx_desc类型。这两种结构体分别表示发送的数据包和接受的数据包,其中有addr字段用于指向实际的数据。描述符的意味就是如此,不直接存储数据,而是储存数据的指针。另外还有length字段表示长度,status字段表示状态(这个非常重要)
E1000有4个重要的寄存器,分为两类,每一类对应操作一个环。注意,这些寄存器都是表示环槽位的索引,必须在0~15之间。当递增时,应该要取余16
- 第一类:
TDT和TDH,对应发送环tx_ring
发送环被划分为两个部分,硬件拥有的部分,表示软件已经写好的对应的描述符(填入了要发送数据的地址,设置状态和指令等),已经软件拥有的部分,表示正在写或者每写的部分。具体而言,硬件拥有[TDH, TDT)左闭右开的部分。硬件从TDH开始尝试发包,每次发完一个包之后,如果对应描述符的cmd的RS位被置位,发完之后硬件会在status中置位DD。随后TDH会向前移动,硬件继续处理。软件需要添加发送包时,从TDT开始写入描述符,写完之后将TDT向前移动
容易发现,一般情况下TDT与TDH并不相等,对应TDT为为软件拥有的部分,DD应该为1。如果TDT对应DD为0,只能表示为TDT写的太快,回环追上了TDH,因此此时软件不应该写入
网卡的数据传输是通过DMA实现的。当TDT被CPU直接写入更新,网卡命令DMA从TDH处读取描述符结构体,之后得到具体数据地址之后,再次通过DMA取得具体数据,才将数据发送出去
- 第二类:
RDT和RDH,对应接收环rx_ring
接受环的两部分也包含硬件和软件。硬件包含的部分为[RDH, RDT],注意左闭右闭,软件拥有其余部分。对于硬件而言,其拥有的部分表示可以用于接收的描述符;对于软件而言,其拥有的部分表示可以写入要接受数据缓冲区的描述符空位。硬件从RDH开始尝试接受数据包,每次接受完之后都会置位DD,之后RDH向前移动。软件在处理收到的数据包时,从RDT + 1(需要取余)开始写入描述符,写完之后将RDT向前移动
如果软件读取RDT + 1对应的DD为0,说明回环RDT追上了RDH(位于前一位),此时软件应该停止写入
接受数据也是通过DMA实现的,分两次,一次取描述符,一次写内存
有了上述前置知识足以完成本实验。下面是列举出的一些注意事项
e1000_transmit需要用e1000_lock锁住,防止多线程同时写入tx_ring,导致TDT和TDH混乱。但是e1000_recv不能用锁,原因在于其将网络包交给net_rx,而net_rx最后会发回复调用e1000_transmit导致死锁。其实加锁也不必要,因为e1000_recv是由中断触发的,按中断路径设计,不需要加同一把锁tx_mbufs和rx_mbufs数组是软件中追踪mbuf结构体的数组,mbuf结构体是直接的包数据来源,tx_desc和rx_desc结构体中的数据直接来自mbuf结构体。必须保持同一个索引,对应的mbufs数组和ring数组中的mbuf和desc是对应的desc中的addr应当填入mbuf的head字段,而不是data字段。因为mbuf结构体中有一个head字段,表示整个包的起始地址,而data字段表示包的有效数据的起始地址。网卡发送时需要整个包的内容,因此应该使用head- 发送的
desc结构体中的cmd字段需要设置为RS,这样硬件才会自动在发送完成之后置为DD e1000_recv需要处理所有DD为1的desc,因为可能一次中断会收到多个包。软件应该处理所有已经收到的包
lock
- 花费时间:6小时
- 难度:难
- 通过:5/5
- 结果:
== Test running kalloctest ==
$ make qemu-gdb
(155.4s)
== Test kalloctest: test1 ==
kalloctest: test1: OK
== Test kalloctest: test2 ==
kalloctest: test2: OK
== Test kalloctest: sbrkmuch ==
$ make qemu-gdb
kalloctest: sbrkmuch: OK (32.8s)
== Test running bcachetest ==
$ make qemu-gdb
(34.1s)
== Test bcachetest: test0 ==
bcachetest: test0: OK
== Test bcachetest: test1 ==
bcachetest: test1: OK第一个为每个CPU维持一个空闲链表很简单,分了两个阶段
- 原始情况
- 为每个CPU维持一个空闲链表,但是初始全部挂载到CPU0上
- 初始时就全部平均分配
| 场景 | kmem 锁 | #test-and-set | #acquire() |
|---|---|---|---|
| 1 | kmem | 4,971,856 | 433,018 |
| 2 | kmem_0 | 0 | 169,029 |
kmem_1 | 0 | 125,372 | |
kmem_2 | 0 | 138,662 | |
| 3 | kmem_0 | 0 | 137,754 |
kmem_1 | 0 | 137,778 | |
kmem_2 | 0 | 124,762 |
可以看到为每个CPU维持链表之后,test-and-set即锁冲突的次数降为0,而且每个CPU获取锁的次数平均了
b_cache的实现比较难,这里我总结了几点,主要在于bget()函数
- 维持多个桶,每个桶内的空闲块数量不固定。空闲块总量固定,且可以从一个桶移动到另一个桶
bget()函数中,首先查找桶中是否有直接命中的块,这一步加桶锁而不加全局锁。注意此时会命中brelse之后refcnt == 0但是dev和blockno都是旧值的块,会与之后的占用新分配块可能冲突- 没有命中则尝试分配新的。注意获取全局锁之后还要在桶内再查找一次,因为可能有双并发请求,导致在获取全局锁之后,其他线程已经分配了该块,之后不检查就多次分配
- 锁的约定:必须获取全局锁再获取桶锁(可以只获取桶锁)。获取桶锁最多只能同时获取一把
fs
- 花费时间:5小时
- 难度:较难
- 通过:3/3
- 结果:
$ bigfile
..................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................
wrote 65803 blocks
bigfile done; ok
== Test running symlinktest ==
$ make qemu-gdb
(1.2s)
== Test symlinktest: symlinks ==
symlinktest: symlinks: OK
== Test symlinktest: concurrent symlinks ==
symlinktest: concurrent symlinks: OK该实验分两部分
第一个部分要拓宽文件最大大小,就是减少一个直接块,然后新增一个二级间接块,二级间接块指向256个一级间接块,每个一级间接块指向256个直接块。注意事项实验写的很清楚
第二个部分实施符号链接,这种新文件类型可以当作一个普通文件,文件内容是目标文件的路径名。symlink系统调用不检查目标路径名。只有在open系统调用时才真正检查。需要注意的是namei函数会加锁,在此之前需要放掉文件对象的inode锁
mmap
- 难度:难
- 花费时间:7小时
- 通过:8/8
- 结果:
== Test mmaptest: mmap f ==
mmaptest: mmap f: OK
== Test mmaptest: mmap private ==
mmaptest: mmap private: OK
== Test mmaptest: mmap read-only ==
mmaptest: mmap read-only: OK
== Test mmaptest: mmap read/write ==
mmaptest: mmap read/write: OK
== Test mmaptest: mmap dirty ==
mmaptest: mmap dirty: OK
== Test mmaptest: not-mapped unmap ==
mmaptest: not-mapped unmap: OK
== Test mmaptest: two files ==
mmaptest: two files: OK
== Test mmaptest: fork_test ==
mmaptest: fork_test: OK 这个实验手册步骤写的很清楚,大致可以分为4部分
- 实现
mmap系统调用和vma结构体,在proc结构体中维护vma数组,当mmap系统调用时取数组中的空位填入数据。注意mmap必须检查参数合法性,填入的数据都要页对齐,同时注意当flags为MAP_SHARED时,prot必须包含PROT_WRITE - 处理页错误,具体在
usertrap函数中判断scause是否为13或者15,如果是则处理页错误,包括查找vma结构体,分配物理页,读入文件,映射等等 - 处理
munmap系统调用,主要是查找vma结构体,根据flags决定是否写回文件,根据pte的dirty位决定该页是否被修改过,然后写回,解除映射释放物理页。最后检查vma整段是否都被解除,如果是则释放vma结构体 - 处理
exit和fork系统调用,主要是file,exit时候减,fork加。然后uvmcopy,uvmunmap等函数处理没有映射的页时候要跳过,和前面的COW LAB类似
感觉后面写的有点仓促了,心里想的就是赶紧通过测试,没有细细地思考
日程
- 2026-08-31
- LEC: 01 Introduction and Examples
- BOOK: Chapter 1 Operating Systems Interfaces
- LAB: Unix Utilities: sleep - find
- 2026-09-01
- LAB: Unix Utilities: xargs
- 2026-09-04
- LEC: 03 OS Organization and System Calls
- BOOK: Chapter 2 Operating Systems Organization
- LAB: System Calls
- 2026-09-05
- BLOG: LEC02
- BOOK: Chapter 3 Page Tables: 3.1-3.3
- 2026-09-06
- BOOK: Chapter 3 Page Tables: 3.4-3.10
- BLOG: LEC03
- 2026-09-07
- LEC: 04 Page Tables
- LEC: 05 Calling Conventions and Stack Frames RISC-V
- 2026-09-08
- LAB: Page tables
- 2026-09-14
- BOOK: Chapter 4 Traps and system calls
- BLOG: LEC04
- 2026-09-15
- LEC: 06 Isolation & system call entry/exit
- 2026-09-16
- LEC: 08 Page faults
- LAB: Traps
- 2026-09-17
- BLOG: LEC08
- 2026-09-20
- BOOK: Chapter 5 Interrupts and device drivers
- 2026-09-21
- LEC: 09 Interrupts
- BLOG: LEC09
- 2026-09-23
- LAB: Copy-on-write fork
- 2026-09-25
- BOOK: Chapter 6 Locking
- LEC: 10 Multiprocessors and locking
- 2026-10-02
- BLOG: LEC10
- 2026-10-08
- BOOK: Chapter 7 Scheduling: 7.1-7.4
- BLOG: LEC11
- LEC: 11 Thread switching
- 2026-10-09
- BOOK: Chapter 7 Scheduling: 7.5-7.10
- BLOG: LEC13
- LEC: 13 Sleep & Wake up
- 2026-10-10
- LAB: Thread
- 2026-10-11 ~ 2026-10-15
- BOOK: Chapter 8 File System: 8.1-8.3;8.7-8.15
- BLOG: LEC14
- 2026-10-16
- LAB: Network driver
- 2026-10-21
- BOOK: Chapter 8 File System: 8.4-8.6
- LEC: 14 File systems
- LEC: 15 Crash recovery
- 2026-10-22
- PAPER: Journaling the Linux ext2fs Filesystem (1998)
- LEC: 16 File system performance and fast crash recovery
- 2026-10-23
- PAPER: Virtual Memory Primitives for User Programs (1991)
- LEC: 17 Virtual memory for applications
- 2026-10-28
- LAB: Parallelism/locking: PART 1
- 2026-10-29
- LAB: Parallelism/locking: PART 2
- 2026-10-30
- PAPER: he Performance of micro-Kernel-Based Systems (1997)
- LEC: 18 OS Organization
- LEC: 19 Virtual Machines
- 2026-11-01
- LAB: File system
- 2026-11-02
- LAB: Mmap
后记
操作系统课我觉得是一门意义非常大的课,他不仅贯通了硬件和软件,同时教会了一件事:计算机没有魔法。以前觉得不可思议的事情,仔细学习之后能够明白原理,这是非常享受的事情。例如,计算机怎么开机,虚拟内存如何建立与运行,系统调用如何处理,进程如何执行切换,如何实现文件的事务等等,看上去非常神秘莫测的话题在学习代码之后都变得清晰明了,操作系统的神秘的面纱被一层一层揭开来了。从未知到已知,甚至发现原理是如此浅显(或者说抽象被拆开之后变得清晰),是非常让人开心,喜悦的事情
有些可惜的是,我学习这门课程的后半段时间不太够了,决定做完LAB匆匆结束,那些论文都没有详细阅读,其实很可惜,主要归咎于我的懒惰,从上面日程可以看到我休息了挺多天,这样偷懒的态度需要克服
总而言之,MIT 6.S081课程是非常值得学习的课程,但是其也有不足之处,主要是对于考试而言,课程的覆盖面不太广,例如其余的调度算法换页算法等没有涉及。不过我觉得这个是很合适的,与其看几种算法的描述,不如真正看一眼XV6中进程是如何切换的,我觉得后者比看教科书有意义的多。同时XV6也是一个十分简化的操作系统,不能认为OS局限于此了,真实的OS系统仍然是非常深入复杂的,学无止境