skip to content

Search

记录

21 min read Updated:

学习MIT 6.S081课程的记录

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/60

LAB有三个部分,第一个部分是XV6中实现用户态的单线程模拟多线程,这个挺简单的,和内核中的线程调度事实差不多,很多可以照抄,而且因为是单线程模拟的,也不需要管锁之类的问题。最需要注意的是RISC-V中栈向下增长,所以sp应该传stack + STACKSIZE

二三部分是和LINUX编程有关,主要涉及pthread库。第二个实现是为并发访问的哈希表加上锁,我的实现使用了读写锁pthread_rwlock_t而不是实验要求的互斥锁pthread_mutex_t,性能快了一些。下面是三种实施办法的速率对比。可以看到在多线程读的情况读写锁显著提高了吞吐量(我的云服务器CPU只有1核2线程,太慢了)

场景无锁读写锁互斥锁
1线程 put8963/s9893/s9544/s
1线程 get9009/s10025/s8680/s
2线程 put18409/s15775/s15962/s
2线程 get16899/s20095/s14866/s
2线程 keys missing1559400

第三个部分是实现屏障,这个想初次写对还是有点困难的,实验提示给了很多。我总结主要是两点,首先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实现的,分两次,一次取描述符,一次写内存

有了上述前置知识足以完成本实验。下面是列举出的一些注意事项

  1. e1000_transmit需要用e1000_lock锁住,防止多线程同时写入tx_ring,导致TDT和TDH混乱。但是e1000_recv不能用锁,原因在于其将网络包交给net_rx,而net_rx最后会发回复调用e1000_transmit导致死锁。其实加锁也不必要,因为e1000_recv是由中断触发的,按中断路径设计,不需要加同一把锁
  2. tx_mbufs和rx_mbufs数组是软件中追踪mbuf结构体的数组,mbuf结构体是直接的包数据来源,tx_desc和rx_desc结构体中的数据直接来自mbuf结构体。必须保持同一个索引,对应的mbufs数组和ring数组中的mbuf和desc是对应的
  3. desc中的addr应当填入mbuf的head字段,而不是data字段。因为mbuf结构体中有一个head字段,表示整个包的起始地址,而data字段表示包的有效数据的起始地址。网卡发送时需要整个包的内容,因此应该使用head
  4. 发送的desc结构体中的cmd字段需要设置为RS,这样硬件才会自动在发送完成之后置为DD
  5. 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维持一个空闲链表很简单,分了两个阶段

  1. 原始情况
  2. 为每个CPU维持一个空闲链表,但是初始全部挂载到CPU0上
  3. 初始时就全部平均分配
场景kmem 锁#test-and-set#acquire()
1kmem4,971,856433,018
2kmem_00169,029
kmem_10125,372
kmem_20138,662
3kmem_00137,754
kmem_10137,778
kmem_20124,762

可以看到为每个CPU维持链表之后,test-and-set即锁冲突的次数降为0,而且每个CPU获取锁的次数平均了

b_cache的实现比较难,这里我总结了几点,主要在于bget()函数

  1. 维持多个桶,每个桶内的空闲块数量不固定。空闲块总量固定,且可以从一个桶移动到另一个桶
  2. bget()函数中,首先查找桶中是否有直接命中的块,这一步加桶锁而不加全局锁。注意此时会命中brelse之后refcnt == 0但是dev和blockno都是旧值的块,会与之后的占用新分配块可能冲突
  3. 没有命中则尝试分配新的。注意获取全局锁之后还要在桶内再查找一次,因为可能有双并发请求,导致在获取全局锁之后,其他线程已经分配了该块,之后不检查就多次分配
  4. 锁的约定:必须获取全局锁再获取桶锁(可以只获取桶锁)。获取桶锁最多只能同时获取一把

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部分

  1. 实现mmap系统调用和vma结构体,在proc结构体中维护vma数组,当mmap系统调用时取数组中的空位填入数据。注意mmap必须检查参数合法性,填入的数据都要页对齐,同时注意当flags为MAP_SHARED时,prot必须包含PROT_WRITE
  2. 处理页错误,具体在usertrap函数中判断scause是否为13或者15,如果是则处理页错误,包括查找vma结构体,分配物理页,读入文件,映射等等
  3. 处理munmap系统调用,主要是查找vma结构体,根据flags决定是否写回文件,根据pte的dirty位决定该页是否被修改过,然后写回,解除映射释放物理页。最后检查vma整段是否都被解除,如果是则释放vma结构体
  4. 处理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系统仍然是非常深入复杂的,学无止境