BUAA_OS lab4 难点梳理
BUAA_OS lab4 难点梳理
lab4体会到了OS难度的飞升。实验需要掌握的重点有以下:
系统调用流程
进程通信机制
fork
本lab理解难度较高,接下来将以以上三部分分别梳理。
系统调用
概念
一般情况下,进程不能存取内核数据。但有的场景必须通过内核执行,因此操作系统设计了陷入异常后调用特定内核函数的过程。
系统调用流程
系统调用的具体层次结构为:
按照这个流程,首先来看syscall_lib.c中的函数们。
1 void syscall_putchar(char ch)
2 {
3 msyscall(SYS_putchar, (int)ch, 0, 0, 0, 0);
4 }
以此函数为例,在调用用户空间的syscall_*()函数后,该函数会将传入的参数,连带系统调用号SYS_*()一起,传入msyscall函数。msyscall函数比较简单,将调用syscall。
1 LEAF(msyscall)
2 syscall
3 jr ra
4 nop
5 END(msyscall)
调用syscall指令后,陷入内核态,pc将被指向一个内核异常入口,即handle_sys()函数。该函数作用为将传入的参数安置到合适的位置,然后调用对应的内核态系统调用函数。这就出现了系统调用部分最难理解的区块:传入参数的位置。
1 #include <asm/regdef.h>
2 #include <asm/cp0regdef.h>
3 #include <asm/asm.h>
4 #include <stackframe.h>
5 #include <unistd.h>
6
7 /*** exercise 4.2 ***/
8 NESTED(handle_sys,TF_SIZE, sp)
9 SAVE_ALL // Macro used to save trapframe
10 CLI // Clean Interrupt Mask
11 nop
12 .set at // Resume use of $at
13
14 // TODO: Fetch EPC from Trapframe, calculate a proper value and store it back to trapframe.
15 lw t0, TF_EPC(sp)
16 addiu t0, t0, 4
17 sw t0, TF_EPC(sp)
18 // TODO: Copy the syscall number into $a0.
19 lw a0, TF_REG4(sp)
20 addiu a0, a0, -__SYSCALL_BASE // a0 <- relative syscall number
21 sll t0, a0, 2 // t0 <- relative syscall number times 4
22 la t1, sys_call_table // t1 <- syscall table base
23 addu t1, t1, t0 // t1 <- table entry of specific syscall
24 lw t2, 0(t1) // t2 <- function entry of specific syscall
25
26 lw t0, TF_REG29(sp) // t0 <- user's stack pointer
27 lw t3, 16(t0) // t3 <- the 5th argument of msyscall
28 lw t4, 20(t0) // t4 <- the 6th argument of msyscall
29
30 // TODO: Allocate a space of six arguments on current kernel stack and copy the six arguments to proper location
31 lw a0, TF_REG4(sp)
32 lw a1, TF_REG5(sp)
33 lw a2, TF_REG6(sp)
34 lw a3, TF_REG7(sp)
35 addiu sp, sp, -24
36 sw t3, 16(sp)
37 sw t4, 20(sp)
38
39
40 jalr t2 // Invoke sys_* function
41 nop
42
43 // TODO: Resume current kernel stack
44 addiu sp, sp, 24
45 sw v0, TF_REG2(sp) // Store return value of function sys_* (in $v0) into trapframe
46
47 j ret_from_exception // Return from exeception
48 nop
49 END(handle_sys)
50
51 sys_call_table: // Syscall Table
52 .align 2
53 .word sys_putchar
54 .word sys_getenvid
55 .word sys_yield
56 .word sys_env_destroy
57 .word sys_set_pgfault_handler
58 .word sys_mem_alloc
59 .word sys_mem_map
60 .word sys_mem_unmap
61 .word sys_env_alloc
62 .word sys_set_env_status
63 .word sys_set_trapframe
64 .word sys_panic
65 .word sys_ipc_can_send
66 .word sys_ipc_recv
67 .word sys_cgetc
handle_sys
在进入handle_sys函数时,原先的寄存器都是被以trapframe的形式传入的,因此参数也都保存在trapframe中。msyscall函数的前四个参数(即系统调用号+前三个参数)分别被存储在trapframe的a0-a3寄存器,即需要用TF_REG4-7(sp)进行获取。而我们的目标为,将这四个参数装入a0-a3寄存器。第5、6个参数,分别被安置在16(TF_REG29(sp))和20(TF_REG29(sp)),我们的目标为,sp自减24,后将他们转移到16(sp)和20(sp)。可以理解为,为了装入这六个参数,栈指针下降了24字节来保存他们,而他们根据顺序由地址小到地址大存放。但由于前四个函数在a0-a3中已经存储,所以0(TF_REG29(sp))到12(TF_REG29(sp))空余即可,而5、6个参数依旧需要被存放在16(TF_REG29(sp))和20(TF_REG29(sp))。
理解了这些,handle_sys函数的操作就比较明显了。首先,需要将TF_EPC(sp)+4,让系统调用后进程能返回下一条指令继续执行;从TF_REG4(sp)取出a0,用以跳转到对应的sys_*函数;在按照以上分析的,将参数从tf中取出,安置到对应的位置。
系统调用函数
系统调用函数的实现,即不全syscall_all.c中的各函数,没有什么理解难度,在此就不赘述了。
进程通信
进程间通信机制IPC,需要通过系统调用来实现进程之间的数据交流。由于进程的地址空间都是独立的,要想把数据从一个地址空间转移到另一个空间,需要利用各个进程都共享的空间——内核的2G空间(具体原因lab3中已阐述)。
因此,选择使用内核中的进程控制块来实现进程通信,即修改PCB的某些属性。至此,也没有什么理解难度了。
fork
首先需要直到,从顶层来看,fork函数执行后的效果,就是产生了一个和原本进程几乎一模一样的子进程,但他们相互独立。
fork 在不同的进程中返回值不一样,在父进程中返回值不为0(返回子进程的id),在子进程中返回值为0。
调用fork之后的具体流程如下图,也是一个理解fork的保命图:
父进程正常执行之上的部分主要展示了fork()函数的流程,而之下有关缺页中断的部分主要涉及写时复制机制,也是这部分的理解难点。
写时复制
在fork时,父进程会为子进程分配新的虚拟地址空间,但是父子进程实际上共用物理空间。在父进程或子进程需要修改内存时,需要调用写时复制机制,为发生修改的页单独分配新的物理空间,父进程指向新的空间,而子进程依旧指向原来的空间。
对于每一页,都会用PTE_COW标志位保护起来,即表示当它被修改时,需要进行写时复制。
与写时复制相关的函数主要有以下。
1 void
2 page_fault_handler(struct Trapframe *tf)
3 {
4 struct Trapframe PgTrapFrame;
5 extern struct Env *curenv;
6 // printf("start page fault handler\n");
7
8 bcopy(tf, &PgTrapFrame, sizeof(struct Trapframe));
9
10 if (tf->regs[29] >= (curenv->env_xstacktop - BY2PG) &&
11 tf->regs[29] <= (curenv->env_xstacktop - 1)) {
12 tf->regs[29] = tf->regs[29] - sizeof(struct Trapframe);
13 bcopy(&PgTrapFrame, (void *)tf->regs[29], sizeof(struct Trapframe));
14 } else {
15 tf->regs[29] = curenv->env_xstacktop - sizeof(struct Trapframe);
16 bcopy(&PgTrapFrame,(void *)curenv->env_xstacktop - sizeof(struct Trapframe),sizeof(struct Trapframe));
17 }
18 // TODO: Set EPC to a proper value in the trapframe
19 tf->cp0_epc=curenv->env_pgfault_handler;
20 // printf("end page fault handler\n");
21 return;
22 }
该函数主要进行写时复制前的一些处理,返回前需要将cp0_epv指向env_pgfault_handler函数入口。而env_pgfault_handler指向的函数,就是pgfault(),即真正处理缺页异常的函数。(写时复制依赖于缺页异常实现)。
1 static void
2 pgfault(u_int va)
3 {
4 u_int *tmp;
5 u_int ret;
6 u_int perm=(*vpt)[VPN(va)]&0xfff;
7 if((perm&PTE_COW)==0){
8 user_panic("not a copy-on-write page\n");
9 return;
10 }
11 tmp=USTACKTOP;
12 u_int round_va=ROUNDDOWN(va,BY2PG);
13 ret=syscall_mem_alloc(0,tmp,PTE_V|PTE_R);
14 if(ret<0){
15 user_panic("alloc error\n");
16 }
17 //map the new page at a temporary place
18 user_bcopy((void*)round_va,(void*)tmp, BY2PG);
19 //map the page on the appropriate place
20 ret=syscall_mem_map(0,tmp,0,round_va,PTE_V|PTE_R);
21 if(ret<0){
22 user_panic("map error\n");
23 }
24 //unmap the temporary place
25 ret=syscall_mem_unmap(0,tmp);
26 if(ret<0){
27 user_panic("unmap error\n");
28 }
29 }
该函数首先判断是否为写时复制页,如果是,则先分配新的内存页到临时位置,将要复制的内容拷贝到刚刚分配的页中,再将临时位置上的内容映射到发生缺页中断的虚拟地址上,注意设定好对应的页面权限,然后解除临时位置对内存的映射。至此,完成缺页异常的处理。
fork函数
解决完缺页异常和写时复制问题,我们再来看一下fork函数的具体流程。
1 extern void __asm_pgfault_handler(void);
2 int
3 fork(void)
4 {
5 // Your code here.
6 u_int newenvid;
7 extern struct Env *envs;
8 extern struct Env *env;
9 u_int i,j;
10 u_int ret;
11
12 //The parent installs pgfault using set_pgfault_handler
13 //alloc a new alloc
14 set_pgfault_handler(pgfault);
15
16 newenvid=syscall_env_alloc();
17 if(newenvid == 0){
18 // writef("start son\n");
19 env = &envs[ENVX(syscall_getenvid())];
20 // writef("son fork end\n");
21 return 0;
22 }
23
24 for(i=0;i<USTACKTOP;i+=BY2PG){
25 // writef("0x%x\n",i);
26 if((Pde*)(*vpd)[i>>PDSHIFT]){
27 // writef("start duppage\n");
28 if((Pte*)(*vpt)[i>>PGSHIFT]){
29 duppage(newenvid,VPN(i));
30 }
31 }
32 }
33
34 // writef("duppage end\n");
35 // writef("start alloc in fork\n");
36 ret=syscall_mem_alloc(newenvid,UXSTACKTOP-BY2PG,PTE_V|PTE_R);
37 // writef("end alloc in fork\n");
38 if(ret<0) {
39 return ret;
40 }
41 ret=syscall_set_pgfault_handler(newenvid,__asm_pgfault_handler,UXSTACKTOP);
42 // writef("end pgdault\n");
43 if(ret<0) {
44 return ret;
45 }
46 ret=syscall_set_env_status(newenvid,ENV_RUNNABLE);
47 // writef("end status\n");
48 if(ret<0) {
49 return ret;
50 }
51
52 return newenvid;
53 }
设置缺页异常处理函数pgfault。
使用syscall_env_alloc()创建新进程
如果是子进程,将env设为该进程,直接返回
如果是父进程,将地址空间使用duppage复制一份给子进程
为子进程alloc出一块异常处理栈,位置为UXSTACKTOP-BY2PG
为子进程设置异常处理函数
设置子进程状态为可执行
以上fork流程在流程图中已有展现,需要特别强调的是duppage函数。
duppage函数对于操作的具体要求如下:
对于可写页面,给父进程和子进程都加PTE_COW的时候要注意顺序。必须要先给子进程加,再给父进程加。至于原因,下图展现了如果先给父进程加可能会造成的问题。
如果先给父进程加PTE_COW,然后修改了该页,该页将进行写时复制,父进程指向新的页,而新页没有被加上PTE_COW。此时再map子进程,子进程该页加上PTE_COW位而父进程没有。在随后程序运行中,若父进程进行修改,由于缺失PTE_COW,导致无法进行写时复制,因此子进程的运行出现错误(子进程该页本来不该被改,但却由于父进程被改而一起改了)。
(代码仓库位于右上角Github)
BUAA_OS lab4 难点梳理的更多相关文章
- BUAA_OS lab3 难点梳理
BUAA_OS lab3 难点梳理 实验难点 进程创建 对于初始化部分,首先需要在pmap.c中修改mips_vm_init()函数,为envs开空间,并map到UENVS空间. 其次,模仿page_ ...
- BUAA_OS lab2 难点梳理
BUAA_OS lab2 难点梳理 实验重点 所列出的实验重点为笔者在进行lab2过程中认为需要深刻理解的部分. 进行内存访问的流程 熟悉mips内存映射布局,即理解mmu.h内图 二级页表的理解和实 ...
- Collection集合重难点梳理,增强for注意事项和三种遍历的应用场景,栈和队列特点,数组和链表特点,ArrayList源码解析, LinkedList-源码解析
重难点梳理 使用到的新单词: 1.collection[kəˈlekʃn] 聚集 2.empty[ˈempti] 空的 3.clear[klɪə(r)] 清除 4.iterator 迭代器 学习目标: ...
- HRMS(人力资源管理系统)-从单机应用到SaaS应用-系统介绍
上周发布的<2018,全新出发(全力推动实现住有所居)>文章,其中记录了个人在这5年过程中的成长和收获,有幸认识了不少博客园的朋友,大家一起学习交流,在这个过程当中好多朋友提出SaaS系统 ...
- 《BAT前端进阶[师徒班]》学习总结
这是一个培训课 是的,这是一个面向中级前端的培训班,但明显跟传统的填鸭式培训班不太一样.这边的老师都是大牛这是毫无疑问的,而且都是一线开发人员.而且课程一开始就说明了面向了是有1-3年有工作经验的前端 ...
- 什么是泛型?,Set集合,TreeSet集合自然排序和比较器排序,数据结构-二叉树,数据结构-平衡二叉树
==知识点== 1.泛型 2.Set集合 3.TreeSet 4.数据结构-二叉树 5.数据结构-平衡二叉树 ==用到的单词== 1.element[ˈelɪmənt] 要素 元素(软) 2.key[ ...
- 什么是可变参数?如何创建不可变集合?Steam三类方法是什么?获取流方法特点?流中间方法特点?终结流方法特点?
==知识梳理== ==重难点梳理== ==今日目标== 1.能够了解什么是可变参数 2.能够了解如何去创建不可变集合 3.能够掌握Stream流的使用 ==知识点== 1.可变参数 2.Stream流 ...
- 多线程,线程类三种方式,线程调度,线程同步,死锁,线程间的通信,阻塞队列,wait和sleep区别?
重难点梳理 知识点梳理 学习目标 1.能够知道什么是进程什么是线程(进程和线程的概述,多进程和多线程的意义) 2.能够掌握线程常见API的使用 3.能够理解什么是线程安全问题 4.能够知道什么是锁 5 ...
- JDBC基础:JDBC快速入门,JDBC工具类,SQL注入攻击,JDBC管理事务
JDBC基础 重难点梳理 一.JDBC快速入门 1.jdbc的概念 JDBC(Java DataBase Connectivity:java数据库连接)是一种用于执行SQL语句的Java API,可以 ...
随机推荐
- 开始 nx
官网 video 详解Nx, 必读 配置代理 每次创建lib都要重启编辑器 创建项目 选择empty,然后选择Angular CLI 因为可以使用Angular Console λ npm init ...
- DBA 的效率加速器——CloudQuery v1.3.0 上线!
好久不见! 自 CloudQuery v1.2.1 发布至今,已有月余,在此期间我们收到了很多朋友对 CloudQuery 的反馈和建议,很多朋友表达了对 v1.3.0 的期待,非常感谢. Cloud ...
- 为什么我们在定义HashMap的时候,就指定它的初始化大小呢
在当我们对HashMap初始化时没有设置初始化容量,系统会默认创建一个容量为16的大小的集合.当HashMap的容量值超过了临界值(默认16*0.75=12)时,HashMap将会重新扩容到下一个2的 ...
- Dyno-queues 分布式延迟队列 之 辅助功能
Dyno-queues 分布式延迟队列 之 辅助功能 目录 Dyno-queues 分布式延迟队列 之 辅助功能 0x00 摘要 0x01 前文回顾 0x2 Ack机制 2.1 加入Un-ack集合 ...
- Java基本概念:内部类
一.简介 描述: 很多时候我们创建类的对象的时候并不需要使用很多次,每次只使用一次,这个时候我们就可以使用内部类了. 内部类不是在一个java源文件中编写两个平行的类,而是在一个类的内部再定义另外一个 ...
- Spring IoC - 循环依赖
Spring 复习 3.循环依赖 3.1 定义 循环依赖指多个对象的创建过程中均需要注入对方对象,如下所示 class A{ B b; public A(){ } public A(B b){ thi ...
- DS线段树优化最短路&&01bfs浅谈
1简介 为什么需要?原因很简单,当需要有大量的边去连时,用线段树优化可以直接用点连向区间,或从区间连向点,或从区间连向区间,如果普通连边,复杂度是不可比拟的.下面简单讲解一下线段树(ST)优化建图. ...
- 剑指 Offer 32 - II. 从上到下打印二叉树 II + 层次遍历二叉树 + 按层存储
剑指 Offer 32 - II. 从上到下打印二叉树 II Offer_32 题目描述: 题解分析: 这道题我一开始想到的解决方法较粗暴,就是使用两个变量来记录当前层的节点数和下一层的结点数. 以上 ...
- js 浮点运算bug
js几个浮点运算的bug,比如6.9-1.1,7*0.8,2.1/0.3,2.2+2.1 实现思路 通过将浮点数放大倍数到整型(最后再除以相应倍数),再进行运算操作,这样就能得到正确的结果了 比如:1 ...
- Mybatis系列全解(六):Mybatis最硬核的API你知道几个?
封面:洛小汐 作者:潘潘 2020 年的大疫情,把世界撕成几片. 时至今日,依旧人心惶惶. 很庆幸,身处这安稳国, 兼得一份安稳工. · 东家常讲的一个词:深秋心态 . 大势时,不跟风.起哄, 萧条时 ...