BUAA-OSLab4学习笔记
前言
本系列正式改名为学习笔记!考虑到过程性博客的内容与标题中“总结”字眼实属不符,记录学习过程的所思所想才是真正的核心内容~✍
本周是劳动节放假噢!祝所有家人朋友节日快乐!🎉感觉宅属性大爆发了,没有人约出去玩的话感觉不如呆在学校里(瘫瘫),虽然没出去玩,但是在北京周围和朋友吃了很多好吃的、玩了好多好玩的,玩其实也是蛮累的!
坐在图书馆里静静回想平日的繁忙,再体会体会假期的闲暇,时光如风中落叶簌簌地落入心潭,每次回忆都是我们从潭中拾起那片片落叶,摩挲回味一番,便归还心底。
每当脑子漫无目的地联想的时候,我感觉自己像得了阿尔兹海默症,我好像在哪里见过这一幕?心中萌生出一种垂垂老矣的忧郁感——
罢啦,不记得了。
I think I've seem this film before.
其实这一幕我已然熟稔。
思考题与我的迷思
Thinking4.1
思考并回答下面的问题:
• 内核在保存现场的时候是如何避免破坏通用寄存器的?
• 系统陷入内核调用后可以直接从当时的 \(a0-\)a3 参数寄存器中得到用户调用 msyscall 留下的信息吗?
• 我们是怎么做到让 sys 开头的函数“认为”我们提供了和用户调用 msyscall 时同样 的参数的?
• 内核处理系统调用的过程对 Trapframe 做了哪些更改?这种修改对应的用户态的变 化是什么?
- 当发生异常(比如中断、系统调用或错误)时,CPU 会跳转到内核的异常处理程序。这个处理程序需要完整地保存当前被打断的程序(无论是用户程序还是内核自身)的状态,以便后续能够恢复执行。这个状态包括了所有的通用寄存器($0 到 $31)、特殊寄存器(如 HI/LO)以及状态寄存器(如 CP0_STATUS, CP0_CAUSE, CP0_EPC 等)。但是实际上,执行保存操作本身也需要使用寄存器。这就带来了一个矛盾:如何使用通用寄存器来保存它们自己原来的值,而不在保存完成前就覆盖掉这些值?
我们在./include/stackframe.h
中可以读到SAVE_ALL
的宏函数源码,其处理关键如下:
使用内核保留的“暂存”寄存器 (\(k0,\)k1):
他们本就是内核临时寄存器,对于
mfc0 k0, CP0_STATUS
这种覆盖操作而言是无感的。此后,宏会通过sw $26, TF_REG26(sp)
指令,将 $k0 在异常发生时的原始值也保存到栈帧里。特殊处理栈指针($sp, $29):
1
2
3
4
5
6
7
8
9mfc0 k0, CP0_STATUS
andi k0, STATUS_UM
beqz k0, 1f # 1是label f是后缀,意为forward,即search forward for "1"
move k0, sp
/*
* If STATUS_UM is not set, the exception was triggered in kernel mode.
* $sp is already a kernel stack pointer, we don't need to set it again.
*/
li sp, KSTACKTOP如代码所示,根据异常发生在不同的模式下,判断是否要提前保留栈指针,因为需要用它来定位保存所有其他寄存器的内存区域,也就是我们在MIPS中看到的
Trap Frame
陷阱帧。
- 可以。因为内核在陷入内核、保存现场的过程中,寄存器
a0-a3
中的值都没有被破坏。 - 用户在调用 msyscall 时,传入的参数会被保存在 a0-a3
寄存器和堆栈中。当陷入内核时,a0-a3
寄存器不会被破坏,并且会将用户栈中的相应参数复制取出到内核现场寄存器与内核栈中。因此,
sys_*
函数可以从寄存器和用户栈处获得用户调用 msyscall 时传入的参数值。 - 首先,将栈中存储的EPC寄存器值增加4,这是因为系统调用后,将会返回下一条指令;然后,将返回值存入$v0,返回用户态后可以获取调用后的返回值。
Thinking4.2
思考 envid2env 函数: 为什么 envid2env 中需要判断 e->env_id != envid 的情况?如果没有这步判断会发生什么情况?
在我们使用envid
获取对应的进程控制块时,只取出了后10位作为数组下标,具体实现在./include/env.h
中,相关代码如下:
1 | #define LOG2NENV 10 |
因此我们要再一次使用语句e->env_id != envid
判断二者是否完全相等,只有完全相等才能确定找到正确的控制块,否则有可能取到错误的、甚至本该销毁的控制块。
Wondering1
在第二个测试点中,syscall_mem_map2
这个点一直无法通过,经过笔者的反复printk调试发现,在进入sys_mem_map
前,dstva
就发生了一些奇妙的变化。后来想明白了,是do_syscall
函数传参出现了问题。
我之所以没发现,是因为我以为这个点是直接调用的sys_mem_map
函数,但是仔细看了好久才发现原来不是,syscall_mem_map
先调用msyscall
之后才进入sys_mem_map
函数。根据参数位置不难定位的可疑代码段:
1 | u_int arg4, arg5; |
显然这种写法把参数的地址取了出来...那进行类型转换后解引用便是正确的写法了。
1 | u_int arg4, arg5; |
Thinking4.3
思考下面的问题,并对这个问题谈谈你的理解:请回顾 kern/env.c 文件 中 mkenvid() 函数的实现,该函数不会返回 0,请结合系统调用和 IPC 部分的实现与 envid2env() 函数的行为进行解释。
感觉像是伏笔。
我们回过头来看,envid2env
如果接收到mkenvid
返回的0值会如何。先看envid2env
相关的代码段:
1 | /* Exercise 4.3: Your code here. (1/2) */ |
大意就是,如果envid
的值确实为0,那就将*penv设置成当前的curenv,这是一个特殊情况处理。而我们在sys_ipc_try_send
中调用了envid2env
,那意味着如果的确存在envid == 0
的进程,那他将永远无法正常通信。
Thinking4.4
Thinking 4.4 关于 fork 函数的两个返回值,下面说法正确的是:
A、fork 在父进程中被调用两次,产生两个返回值
B、fork 在两个进程中分别被调用一次,产生两个不同的返回值
C、fork 只在父进程中被调用了一次,在两个进程中各产生一个返回值
D、fork 只在子进程中被调用了一次,在两个进程中各产生一个返回值
答案是C;根据我们做的实验得到的信息,也是指导书的提示,我们可知:
• fork 之前只有父进程存在。
• fork 之后,父子进程同时开始执行 fork 之后的代码段。
• fork 在不同的进程中返回值不一样,在子进程中返回值为 0,在父进程中返回值不为 0, 而为子进程的 pid(Linux 中进程专属的 id,类似于 MOS 中的 envid)。
也就是说子进程是由fork产生的,自然不可能被子进程调用,所以fork只在父进程被调用了一次,并且在两个进程中各自返回了一个值。
Thinking4.5
我们并不应该对所有的用户空间页都使用 duppage 进行映射。那么究竟哪 些用户空间页应该映射,哪些不应该呢?请结合 kern/env.c 中 env_init 函数进行的页 面映射、include/mmu.h 里的内存布局图以及本章的后续描述进行思考。
在 0 ~ UTOP
范围的内存需要使用 duppage 进行映射;
先回顾一下mmu.h
中layout:
1 | o ULIM -----> +----------------------------+------------0x8000 0000------- |
其中我们知道,UTOP
也就是0x7f40 0000
以上的地址中存放的内存与页表是全体进程共享的,且用户进程无法访问,自然不需要duppage
做映射了。
而USTACKTOP
到UTOP
之间的地址中,layout
把它描述为User Exception Stack
,也就是用户异常栈,用于处理页写入异常,包含了一段缓冲区地址空间。在往年的学姐博客BUAA-OS-lab4 |
YannaのBlog中提到以下观点:
USTACKTOP 到 UTOP 之间的 user exception stack 是用来进行页写入异常的,不会在处理COW异常时调用 fork() ,所以 user exception stack 这一页不需要共享;
USTACKTOP 到 UTOP 之间的 invalid memory 是为处理页写入异常时做缓冲区用的,所以同理也不需要共享;
感觉这样的做法像是一种特定的性能优化,但这样对整个系统的可扩展性和一致性造成了一定的破坏,考虑到用户异常栈其实总计不会有很多页,所以笔者更倾向于也将用户异常栈进行映射。
Thinking4.6
在遍历地址空间存取页表项时你需要使用到 vpd 和 vpt 这两个指针,请参考 user/include/lib.h 中的相关定义,思考并回答这几个问题:
• vpt 和 vpd 的作用是什么?怎样使用它们?
• 从实现的角度谈一下为什么进程能够通过这种方式来存取自身的页表?
• 它们是如何体现自映射设计的?
• 进程能够通过这种方式来修改自己的页表项吗?
定义如下:
1 | #define vpt ((const volatile Pte *)UVPT) |
作用:
- vpd:vpd 是一个指向当前进程页目录 (Page Directory) 的虚拟地址指针。它允许用户程序以只读方式访问其自身页目录中的每一个页目录项 (PDE - Page Directory Entry)。
- vpt:vpt 是一个指向一个巨大的虚拟内存区域的指针,这个区域映射了当前进程所有(或大部分)页表 (Page Tables)。它允许用户程序以只读方式访问其自身页表中的每一个页表项 (PTE - Page Table Entry)。
使用 vpd:
- vpd 可以被当作一个 Pde 类型的数组(或指针)来使用。
- 要访问页目录中的第 i 个页目录项,可以使用 vpd[i]。
- 通常,i 是通过从虚拟地址中提取页目录索引得到的,即 PDX(va)。
使用 vpt:
vpt 可以被当作一个巨大的 Pte 类型的数组(或指针)来使用。
要访问某个虚拟地址对应的页表项,需要进行稍微复杂一点的计算,因为 vpt 线性地映射了所有的页表。你需要知道这个虚拟地址属于哪个页表(由其页目录索引确定),以及它在该页表中的索引。
遍历地址空间:
- 通过虚拟页号 vpn 从 0 到 VPN(UTOP - 1) 循环,直接使用 vpt[vpn]来访问对应的页表项,并通过 vpd[PDX(vpn << PGSHIFT)] 来检查相关的页目录项。
关于自映射:
- 自映射的核心:自映射指的是页表结构中的一部分(一个页目录项)被用来映射页表结构自身(通常是页目录)。
- 自映射设计使得页表结构本身也成为了可以通过虚拟地址访问的对象,而 vpd 和 vpt 就是访问这些对象的便捷指针。
进程无权修改页表项。
// 摘自page_insert函数 *pte = page2pa(pp) | perm | PTE_C_CACHEABLE | PTE_V;
- 根据
page_insert
函数中的代码以及其参数perm = PTE_G
,我们可以确定,每个页表项的权限位应该是:PTE_G | PTE_C_CACHEABLE | PTE_V
。故无权修改。
Thinking4.7
在 do_tlb_mod 函数中,你可能注意到了一个向异常处理栈复制 Trapframe 运行现场的过程,请思考并回答这几个问题:
• 这里实现了一个支持类似于“异常重入”的机制,而在什么时候会出现这种“异常重入”?
• 内核为什么需要将异常的现场 Trapframe 复制到用户空间?
“异常重入”的时机:
当一个用户态的异常处理程序(比如由 env_user_tlb_mod_entry 指向的函数)在执行过程中,它自己又触发了一个需要内核介入的异常,这时就可以看作是一种“异常重入”的场景。但是要明确这里的“异常重入”与内核态自身的异常重入(即内核代码执行时又发生异常)可能有所不同。我们这里讨论的应该是用户态异常处理过程中的进一步异常,或者说嵌套的用户态异常处理。
为什么需要复制
Trapframe
?- 向用户态处理程序提供异常上下文信息。
- 隔离内核数据与用户逻辑,防止用户态代码直接访问内核栈。
- 支持用户态处理程序时修改并恢复上下文信息。
Wondering2
cow_entry
到底做了什么?它在整个fork中又是什么角色?
cow_entry 是一个用户态函数,作为 fork 后写时复制机制的一部分。当进程尝试写入一个被 fork 标记为COW(只读)的页面时,它被内核通过异常处理机制调用。它的核心任务是响应这个写保护错误,通过系统调用请求内核分配新页面、复制数据、并以可写权限重新映射出错的虚拟地址,从而完成页面的“写时复制”,最后恢复到原先失败的写操作处继续执行。 它正是保证COW语义正确实现的关键执行单元。
Thinking4.8
在用户态处理页写入异常,相比于在内核态处理有什么优势?
- 首先,正如指导书所说,它减少了内核的复杂性,符合微内核的设计理念,将功能实现留在用户空间中。
- 其次,处理方法具有更高的灵活性与可定制性,能够快速的进行实验。
Thinking4.9
请思考并回答以下几个问题:
• 为什么需要将 syscall_set_tlb_mod_entry 的调用放置在 syscall_exofork 之前?
• 如果放置在写时复制保护机制完成之后会有怎样的效果?
因为在syscall_exofork
中,我们有可能就会读写父进程的相关页,要在此之前处理好父进程的页写入异常。
父进程在调用写时复制保护机制可能会引发缺页异常,而异常处理未设置好,则不能正常处理。
实验体会
感觉Lab4的内容相对之前来说比较规整,更加清晰地展现在了我们面前,可能也是因为逐渐接近了操作系统比较上层的内容了(?),总之做起来有一种很神清气爽的感觉,但是有些内容还是基于前面的Lab,在做课上的时候,我感觉对前面的内容以及C语言基础不熟悉成为了我最大败笔。希望以后有所精进,有所收获。😊
后记
五一过后的第一周嘛,刚准备放松放松就要搞冯如杯入围答辩了?疑似有点太紧凑了。
话说怎么莫名其妙就入围了,“答辩”也能入围答辩吗?🤔既然如此,好好准备答辩,编一编内容,在大佬云集的入围答辩中跳好最后一舞吧💃
着急赶PPT,我们下期见!👋
If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. If the images used in the blog infringe your copyright, please contact the author to delete them. Thank you !