BUAA_OS lab4 难点梳理

lab4体会到了OS难度的飞升。实验需要掌握的重点有以下:

  1. 系统调用流程

  2. 进程通信机制

  3. 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 }
  1. 设置缺页异常处理函数pgfault。

  2. 使用syscall_env_alloc()创建新进程

  3. 如果是子进程,将env设为该进程,直接返回

  4. 如果是父进程,将地址空间使用duppage复制一份给子进程

  5. 为子进程alloc出一块异常处理栈,位置为UXSTACKTOP-BY2PG

  6. 为子进程设置异常处理函数

  7. 设置子进程状态为可执行

以上fork流程在流程图中已有展现,需要特别强调的是duppage函数。

duppage函数对于操作的具体要求如下:

对于可写页面,给父进程和子进程都加PTE_COW的时候要注意顺序。必须要先给子进程加,再给父进程加。至于原因,下图展现了如果先给父进程加可能会造成的问题。

如果先给父进程加PTE_COW,然后修改了该页,该页将进行写时复制,父进程指向新的页,而新页没有被加上PTE_COW。此时再map子进程,子进程该页加上PTE_COW位而父进程没有。在随后程序运行中,若父进程进行修改,由于缺失PTE_COW,导致无法进行写时复制,因此子进程的运行出现错误(子进程该页本来不该被改,但却由于父进程被改而一起改了)。

(代码仓库位于右上角Github)

BUAA_OS lab4 难点梳理的更多相关文章

  1. BUAA_OS lab3 难点梳理

    BUAA_OS lab3 难点梳理 实验难点 进程创建 对于初始化部分,首先需要在pmap.c中修改mips_vm_init()函数,为envs开空间,并map到UENVS空间. 其次,模仿page_ ...

  2. BUAA_OS lab2 难点梳理

    BUAA_OS lab2 难点梳理 实验重点 所列出的实验重点为笔者在进行lab2过程中认为需要深刻理解的部分. 进行内存访问的流程 熟悉mips内存映射布局,即理解mmu.h内图 二级页表的理解和实 ...

  3. Collection集合重难点梳理,增强for注意事项和三种遍历的应用场景,栈和队列特点,数组和链表特点,ArrayList源码解析, LinkedList-源码解析

    重难点梳理 使用到的新单词: 1.collection[kəˈlekʃn] 聚集 2.empty[ˈempti] 空的 3.clear[klɪə(r)] 清除 4.iterator 迭代器 学习目标: ...

  4. HRMS(人力资源管理系统)-从单机应用到SaaS应用-系统介绍

    上周发布的<2018,全新出发(全力推动实现住有所居)>文章,其中记录了个人在这5年过程中的成长和收获,有幸认识了不少博客园的朋友,大家一起学习交流,在这个过程当中好多朋友提出SaaS系统 ...

  5. 《BAT前端进阶[师徒班]》学习总结

    这是一个培训课 是的,这是一个面向中级前端的培训班,但明显跟传统的填鸭式培训班不太一样.这边的老师都是大牛这是毫无疑问的,而且都是一线开发人员.而且课程一开始就说明了面向了是有1-3年有工作经验的前端 ...

  6. 什么是泛型?,Set集合,TreeSet集合自然排序和比较器排序,数据结构-二叉树,数据结构-平衡二叉树

    ==知识点== 1.泛型 2.Set集合 3.TreeSet 4.数据结构-二叉树 5.数据结构-平衡二叉树 ==用到的单词== 1.element[ˈelɪmənt] 要素 元素(软) 2.key[ ...

  7. 什么是可变参数?如何创建不可变集合?Steam三类方法是什么?获取流方法特点?流中间方法特点?终结流方法特点?

    ==知识梳理== ==重难点梳理== ==今日目标== 1.能够了解什么是可变参数 2.能够了解如何去创建不可变集合 3.能够掌握Stream流的使用 ==知识点== 1.可变参数 2.Stream流 ...

  8. 多线程,线程类三种方式,线程调度,线程同步,死锁,线程间的通信,阻塞队列,wait和sleep区别?

    重难点梳理 知识点梳理 学习目标 1.能够知道什么是进程什么是线程(进程和线程的概述,多进程和多线程的意义) 2.能够掌握线程常见API的使用 3.能够理解什么是线程安全问题 4.能够知道什么是锁 5 ...

  9. JDBC基础:JDBC快速入门,JDBC工具类,SQL注入攻击,JDBC管理事务

    JDBC基础 重难点梳理 一.JDBC快速入门 1.jdbc的概念 JDBC(Java DataBase Connectivity:java数据库连接)是一种用于执行SQL语句的Java API,可以 ...

随机推荐

  1. 生态建设者为何青睐低风险、低成本的NGK算力?

    自从BGV推向市场以来,生态建设者的目光都聚集于BGV这个去中心化金融的新星,其实NGK的其他项目也都在稳健进行当中. NGK在未来将推出"算力市场奖励计划",NGK将会对算力市场 ...

  2. node_puppeteer无界爬虫

    环境:node----v14.5.0 vscode----2019 依赖库 (需要自行设置好目录结构,否则会报目录错误) const puppeteer = require("puppete ...

  3. vue:表格中多选框的处理

    效果如下: template中代码如下: <el-table v-loading="listLoading" :data="list" element-l ...

  4. SpringBoot整合开发

    1.SpringBoot分模块 分模块就是将一个项目分成多个模块,即maven项目. 1)首先创建一个springboot的项目: 第一步:选择springboot的项目 第二步:填写项目的相关信息, ...

  5. SpringCloud Stream

    1.介绍 官网:https://www.springcloud.cc/spring-cloud-dalston.html#_spring_cloud_stream 1.1定义 是一个构建消息驱动微服务 ...

  6. Spring IoC - 循环依赖

    Spring 复习 3.循环依赖 3.1 定义 循环依赖指多个对象的创建过程中均需要注入对方对象,如下所示 class A{ B b; public A(){ } public A(B b){ thi ...

  7. CSS的定位布局(position)

    定位 static(默认值) 没有开启定位 relative 相对定位的性质 包含块(containing block)概念 没有开启定位时包含块就是当前元素最近的祖先块元素 开启绝对定位后的元素包含 ...

  8. 肝了很久,冰河整理出这份4万字的SpringCloud与SpringCloudAlibaba学习笔记!!

    写在前面 不少小伙伴让我整理下有关SpringCloud和SpringCloudAlibaba的知识点,经过3天的收集和整理,冰河整理出这份4万字的SpringCloud与SpringCloudAli ...

  9. Python中OS对目录的操作以及引用

    路径的获取 对当前目录的获取 1 path = os.getcwd() 2 print("获取到的当前目录是:({})".format(path)) 获取当前文件所在的绝对路径 i ...

  10. [UWP] 模仿哔哩哔哩的一键三连

    1. 一键三连 什么是一键三连? 哔哩哔哩弹幕网中用户可以通过长按点赞键同时完成点赞.投币.收藏对UP主表示支持,后UP主多用"一键三连"向视频浏览者请求对其作品同时进行点赞.投币 ...