mit6.S081小结
这个课程的大部分视频看完了,lab也全部做完或者抄完了。
这个课程的好处就是基于一个开发出来的简易操作系统xv6来进行讲课还有实验,让自己对于OS的理解不再基于书本视频的一些很理论的东西,而是实际看了一些代码还有添加了一些代码的。课程很棒!
lab1 : utilities
利用xv6的syscall来实现一些功能。
理解fork exec pipe的基本工作原理,还是很有用的,至少对于后面写csapp的shell lab。
1: 很简单 调用系统的sleep即可
2: 利用递归的一个函数来fork子进程,同时进行pipe的数据传输模拟实验要求。
3: 这个我感觉有点难,因为代码有点不好读,find一个文件,认真读懂ls是怎么做的就可以了
lab2: system call
添加sys-call
按照要求添加相应的文件。
1: trace进程,按照实验要求就明白了。
2: sysinfo 我忘了是干嘛了 不太难。
lab3: virtual memory page table
虚拟内存
这个的的却却是最难的lab, lab的实验要求还有提示看了很久都不懂。弄清sys-call的过程,包括了几个过程 调用ecall 转换kernel MODE, 保存寄存器,跳转到对应的system的处理过程,对于具体的调用命令具体处理。返回同理。对应了两个不同的状态,内核态 用户态,他们使用的不同的虚拟内存,再系统初始化时候初始化内核的虚拟内存,在进程创建的时候初始化进程的基本虚拟内存。
对于某些系统调用,比如read write 需要内存的char*,但是进入内核态它的地址(虚拟地址) 并不对应他实际数据的位置,而是当前内核这个虚拟内存对应的位置,因此需要模拟这个进程的虚拟内存来找到这个char*对应的物理内存。这也再实现拷贝,拷贝到内核分配的一块内存上再进行进一步处理,比如写入磁盘。 因此实验的目的是不要做这个操作,希望直接memmove。实现的方式就是通过对内核必须的页面,在每个进程也进行内核的映射。如果一个系统调用进入内核态,直接进入对应进程创建的内核,内核的虚拟内存和进程的虚拟内存想对应,可以直接进行拷贝。
这个lab属实受折磨的灵魂,20行不到的代码,一开始没有思路,后面又是各种page trap或者panic等问题,最后抄了老师的作业才完成。
lab 4:
trap, 在CSAPP里面讲异常部分说到了trap是OS的一种trick,一直不太理解为什么是一种trick,感觉上很直观啊。 后面的几个lab可以让你理解虚拟内存缺页或者页面访问权限的问题的妙用。
1是几个问答题,不难
2 制作一个类似于gdb的函数调用的一个跟踪工具,但是没有gdb那么高级可以显示函数名,只是显示函数调用的地址。搞清楚xv6的stack的构造,stack中有一个指向上一个stack frame地址的变量很重要。
3: alarm system call,这个还是很有难度的,用处就是大概操作系统的进程调度调度每一个进程的运行实际,如果一个进程运行时间太多了操作系统需要把CPU这种资源分配给其他进程(不然就饿死掉了~)。这里并不是实现这个功能,类似于实现进程调用计时器,到达时间运行某个函数,这个的难点在于,我被中断后会进入内核态,如何在内核态运行一个用户态的函数然后在返回内核态,再yield或者返回进程态。
user mode => tick trap=> kernel mode => excute function(handle) => user mode
过程大概就是这样。
lab5: lazy allocation.
这个就是利用虚拟内存缺页的问题来,对于sbrk系统调用,我们只是简单地增加size,而不是去分配相应的物理内存(1是不需要分配物理内存的时间 2是节约了内存)。 但是问题就是 如果我增长的内存是这个进程需要使用的,我这个进程在使用这个lazy allocation分配的内存的时候,硬件(qemu模拟的硬件)会发生异常,因为没有实际的物理内存和这个虚拟内存对应,因此需要在trap处理中处理这种情况,同时为了内核安全性的考虑,只有真正是我lazy allocation分配的内存才会进行这种处理。
处理的方式就是如果发生相应的缺页,我再去分配就OK了,然后重新执行这条指令,相应的内存就会被分配,也就不会异常了。
lab6 : COW
COW(奶牛lab), copy-on-write: 原理就是对于某个已经分配了物理内存的页我进行拷贝,我不进行实际的物理页面创建,甚至相应的虚拟页面创建我也不进行创建,也就是一种lazy的方法。
这么做的好处,比如shell调用fork 然后fork的子程序再去执行一个什么程序。我fork的内存完全没有用到就"抛弃"了,非常浪费内存以及内存拷贝 虚拟内存创建的时间,因此采用一种lazy的方法来做。
如果这个拷贝的内存是要使用的,和上面一样 会触发一个 virtual memory trap, 一样在trap中处理进行虚拟地址实际物理地址的创建。
这里的理论用一个图来说明,如何保证这样的正确性。
在STL中也有用到这个技巧的,就是string, 比如你拷贝一个string, 不做实际的char*的拷贝,而是仅仅建立一个reference, 但是标记为只读。如果这两个某一个需要拷贝,这个时候再进行拷贝,标记为读和写(write&read),这样就可以做modify了。
lab7 : multi-threading.
1: 模拟内核切换进程时候需要记录的寄存器,直接CV 不多BB
2 :用一下linux下的pthread的lock unlock的东西,实现hash的线程安全版本,对于hash的每个slot创建一个lock就可以了。
3: barrier 一种同步的机制,多个执行的线程(进程) 执行到同一个点, 才会向下进行,我觉得用C++的方法类似于 一个
condition_variable _cond;
size_t _cur_size;
size_t _max_size;
因为我需要知道多少个到达这个点,因此需要size来初始化。比如对于某个进行,进入到这个条件,当前的size如果没有到达最大的size,那么就sleep, 如果到达了相当于用一个条件变量通知所有对此等待的线程进行唤醒,继续执行。 比较简单
但是这个相关内容的视频讲解有点难度,我没想到sleep 等和thread相关的函数有这么多细节,包括锁的使用。真的是细节满满。
同时降到了lock是如何实现的,只是讲了最简单的spin-lock,以前不直觉得就算是lock也不能保证数据正确性。听了知道是通过硬件的机制来保证lock的原子性的。
lab7: lock
这个lab难度我觉得仅次于lab3, 因为第二个问题如何避免死锁的同时增大并发度对我有点难度。
1: 对于OS,我们管理真是的物理内存是用一个free_list, 记住xv6模拟的是多个cores,也就是是多进程可以同时运行的。而物理内存是只有一块内存,也就是说多个cores可以同时去索取物理内存的,因此需要lock。这里用了一个spin-lock来锁住对于free_list的改变,不管你是alloc还是free 直接锁住,是为了保证数据的正确性。
对于多线程程序,1:首先保证正确性 2: 尽可能增加并行度。
这里要求我们增大free_list的并发度,并且提示我们了怎么做。
明天画图说明这里,
对于每个core创建一个free_list来分割所有内存,每个free_list加一个lock,同时加一个全局的lock。
如何保证没有死锁?
首先对于某个core进入相应的free_list, 如果没有内存可以分配了,他需要其其他core的free_list来借内存。 你和其他内存借内存,你会改变其他core的free_list 这里就是一个竞争的点。因此我可以通过直接或者你的锁。(就算是ordering也不行 画图说明) 这样会有一个死锁,死锁的原因是两个core同时需要找其他的core借物理内存。
于是加一个全局锁,这个全局的锁就是保证我要去获取这个全局锁。这样别的core没有内存需要等我这个core来借内存,向下的时候首先check是否holding-lock 如果hold我们可以假设你也是没有内存再等这个全局锁(问题可能是他有内存,只是刚好再分配或者释放的时候加了一个锁), 但是我们不去等它,而是不断循环每一个core寻找能够进入的,能够进入如果可以分配(借给我)内存,那么就OK。饭hi这个内存就可以了。
2: 这个问题和文件系统相关,文件系统的访问也是有一层缓存,这个缓存是通过LRU进行缓存和分配的。这里是用一个循环链表加一个lock来实现,但是很多进程都需要访问文件系统,只有一个锁,数据保证是对的了,但是并发度非常非常低,而且一个文件读取的实际非常久,这样这个等待可能非常非常久。
于是要求就是提高文件系统缓存的并发度。提示我们用一个static-hash配合LRU, 其实实现一个没有dead-lock的方案我感觉挺难的 。
中途写了3个方案,一个是局部的LRU(可以没有死锁) 第二个是一个有点复杂的(很多锁,我中途卡死了) 最后实现了一个方法, 用goto的办法实现了一个没有锁的办法。
hash提高并发度非常显而易见, 多个slots 分别加锁,假如对于都是访问的操作,那么肯定是提高并发度的,而且你的hash约均匀 这个并发度越高。问题在于会有写操作,缓存的大小是固定的,也就是说如果发生了cache miss,你必须要选择一个victim 来进行替换。同时利用time-stamp(利用系统的tick)组织一个LRU的算法进行victim的选择。
问题1: 如何组织全局的LRU
问题2: 如何处理假如选择了一个victim, 如何进行替换,因为涉及两个hash-slot的访问,很可能死锁。
问题3:选择了一个victim,假如这个时候又来访问了,这个victim如何处理?
对于问题3是一个trade-off问题,我认为这个时候访问,这个victim应该还是做为缓存, 因此这个victim应该从新进行选择。但这样死锁的风险似乎变大了。
系统图(还妹画呢 憋急) :
对于所有可能死锁的问题进行考虑,
最终通过了测试。奈斯。
课堂笔记: 避免死锁的一个方法是order 这里就现学现用了一下。锁还不能多次释放或者多次lock 是错误的 注意。
小结: 一开始学习死锁觉得这不是有手就行? 还能死锁? 现在觉得我真是NT, 这样一个简单的问题都要考虑很久。
lab8: file system
file system 远不是我想想的那么简单,很多layer,强烈建议看视频,讲的非常清楚。
结构如下(图1 2 3):
理解了结构 lab还是比较容易的
1: 扩容xv6的文件系统,支持更大的file。搞清楚图就行了。比较简单
图:
2: 软链接,这个有点难度。因为需要查资料弄清楚软链接,硬链接,看了一个别人的解决方案才搞懂的。
软链接查找文件的过程是一个递归的,因为链接的可能也是一个软链接,递归yyds。
图 :
lab 9: mmap
这个lab其实有点像 lazy location。 lazy location的目标是物理内存,它的目标是文件。
目标: 我们操作文件的方式通过 file descriptor(fd) 通过open来完成的,offset是记录在fd中,在我们不知道的地方默默记录着(以前还一直搞不明白这,知道这个了豁然开朗)。
现在对于文件我们认为是一段虚拟内存, 认为可以像指针一样 随意地取地址,实验的内容就是建立这样的映射。当然有标志位的设置,意义也很清晰,read 就是单纯的只读,不能更改。 write允许修改,但是这个修改是否对真正的文件产生影响取决于另一个flag: private || public 如果是private 那么这个write只是对我这个进程可见,不会持久化到文件系统。但是如果是public 那么这个修改会持久化到文件系统(这种情况需要lock一下这个文件,因为允许我修改,其他人也修改,那么这个文件最终的一致性没法保证,因此使用一个文件lock(我忘了是不是用lock还是什么,大概意思是这样), 从而保证原子性)。 相应的mmap的虚拟内存如果被释放,是可以写的文件那么会写回磁盘。
课程笔记: 这节课讲了 logging system, log也并不简单,之前听分布式系统觉得log 系统可以保证recovery 但是没想到一个log system很难构建,有非常多的细节。!!! xv6实现了一个简易的但是性能很差的log 系统。后面降到了linux实现的log系统。 通过对多个事务进行糅合增加性能。
讲了一个非常重要的assumption: 对于disk的单个block的写入是原子的,这个应该是用硬件来完成的,否则之上的log系统也不能保证 crash recovery.
lab 10: networking.
最后一个lab了。 芜湖,这个lab要看一些文档,我感觉迷迷糊糊的但是大概明白的是 通过buf来分配,设备可以直接写入内存中,通过一些标志位(设置在寄存器中) 告诉网络设备 下一个来的包写入哪里(哪个buf)
network stack: 比如一个包从设备接受了,一步一步向上到应用层的过程。
小结
配置环境可以先配置riscv-gcc qemu 再从清华的镜像下载最近的riscv-gdb编译,简单无痛,无需github龟速下载工具。
lab过程,老师也提到了很重要的一点:
你的程序的不变量(invariant)到底是什么,你通过什么样的才做来保证(maintain),同时通过一些运行时检查措施来保证你不变量。这是一种很好的写程序方式,对应于lab就是多写panic再你觉得可能有问题的时候。其他程序比如用_assert() 。比如一些异常可以通过函数调用(trace)的方法来进行调试,多线程的调试printf() yyds.
视频也值得看,有的老师关于一个点进行了讲解,直接分析所有。比如fork, 进程切换, system-call, sleep, uart(键盘输入输出的设备) 等等。 非常棒!
有的图还没配,后面上传。