Lab4实验分享

一.预备知识

在前面三个Lab的实验中,我们成功的搭建起了操作系统的内核,建立了内存管理机制和进程调度机制。一般来说,进程是给用户使用的,而用户无法直接对系统内核进行存取。另一方面,进程与进程之间的虚拟地址互相独立,这使得两个进程之间的互相通信变得困难。但是,用户会在有些情况下需要使用只有内核才能进行的操作。为了解决这个问题,操作系统设计了系统调用。

img

指导书上已有的知识,我在此不再赘述。在进行实验之前,我们需要稍微补习一点知识,主要是关于汇编函数方面的东西。这些知识,指导书或者其他地方都有,只不过比较零碎。我稍微聚集了一下这些知识,如果想要了解的更详细,可以深入了解。

1. 汇编函数构造宏(include/asm/asm.h)

为了方便的像C语言一样构造函数,我们的操作系统事先为我们提供了函数的宏,我们可以直接使用。这个宏的代码并非由本校人员开发,应当是较为通用的定义方式。文件中为我们提供了两种函数的宏,即叶函数(LEAF)和嵌套函数(NESTED)。

我们把函数体中没有函数调用语句的函数称为叶函数,自然如果有函数调用语句的函数称为非叶函数。在MIPS 的调用规范中,进入函数体时会通过对栈指针做减法的方式为自身的局部变量、返回地址、调用函数的参数分配存储空间(叶函数没有后两者),在函数调用结束之后会对栈指针做加法来释放这部分空间,我们把这部分空间称为栈帧(Stack Frame)。

——OS指导书

下面是宏的具体实现定义。可以看到,函数定义无非是声明一个全局符号,给定一个标签用于跳转和返回。

下面是文件中部分代码的引用。有些代码后面我没有写注释,是因为我自己也弄不太清楚,不敢乱讲,怕引起误会。如果有同学明白,希望可以给我讲讲。

#define LEAF(symbol)                            \
        .globl  symbol;                         \声明"symbol"为全局变量
        .align  2;                              \下一个数据的地址空间按字对齐
        .type   symbol,@function;               \
        .ent    symbol,0;                       \告诉汇编器"symbol"函数的起始点,用于调试
        symbol:         .frame  sp,0,ra          提供一个名为"symbol"的标签,将跳转到此处

#define NESTED(symbol, framesize, rpc)          \
        .globl  symbol;                         \
        .align  2;                              \
        .type   symbol,@function;               \
        .ent    symbol,0;                       \
        symbol:         .frame  sp, framesize, rpc   确定栈帧大小以及结束时的返回地址

#define END(function)                           \
        .end    function;                       \指出函数结尾,用于调试
        .size   function,.-function              在符号表中列出函数名和函数指令字节数
2.C函数和汇编函数的参数、返回值传递

有时候,我们会不可避免的在C语言中调用汇编函数,也会在汇编语言中调用C函数。根据MIPS软件标准(ABI)的定义,函数的参数传递按照如下原则:

  • 如果函数参数个数≤4,则将参数依次存入a0-a3寄存器中,并在栈帧底部保留16字节的空间(即sp的值减去16),但并不一定使用这些空间。
  • 如果函数参数个数>4,则前4个参数依次存入a0-a3寄存器中,从第5个参数开始,依次在前4个参数预留空间之外的空间内存储,即没有寄存器去保存这些值。
  • 举例,如果一个C函数有6个参数,在汇编语言中需要调用的时候,应当将前4个参数存在a0-a3寄存器中,第5个参数存在16(sp)的位置,第6个参数存在20(sp)的位置。区间0-15的空间保留但不使用。

img

而关于函数的返回值,MIPS ABI规定,返回值存在v0寄存器中。某些特殊的情况下也会用到v0寄存器中。某些特殊的情况下也会用到v1寄存器,但不常见。想了解更多关于返回值的知识,请查阅书籍See MIPS Run Linux

3.栈帧方法宏(include/stackframe.h)

我们在进行用户态和内核态之间的切换,或者进程之间的切换时,需要保存现场。所谓现场,就是include/trap.h中所定义的trap结构体,其中包含的信息有:

  • 32个寄存器的值
  • CP0部分寄存器的值
  • HI、LO两个乘除法寄存器的值
  • 程序的指令计数器PC

但是这个文件中只有结构体的定义,没有将数据存入结构体的操作。将寄存器中的值存入内存,显然要用汇编语言去完成。stackframe.h中定义了一些汇编函数的宏,方便我们对现场进行存取操作。下面摘录了其中的宏,并作出相应的解释。

//TF_SIZE是Trapframe寄存器的字节大小
.macro STI					//Set Interrupt,打开全局中断使能(允许中断)
.macro CLI					//Close Interrupt,关闭全局中断使能(屏蔽中断)
.macro SAVE_ALL             //保存所有现场,将数据以Trapframe结构体形式存在sp为开头的空间中
.macro RESTORE_SOME         //恢复部分现场,此处的“部分”仅不包括sp的值
.macro RESTORE_ALL          //恢复所有现场,包括栈顶的位置
.macro RESTORE_ALL_AND_RET  //恢复现场并从内核态中返回
.macro get_sp               //获取栈顶位置,此函数会判断当前的状态是异常还是中断,
                            //从而决定栈顶是TIMESTACK还是KERNEL_SP。
                            //系统调用是编号为8的异常,进程切换是时钟中断信号。

二.系统调用

1.什么是系统调用

在硬件实现上,用户态的进程无法访问内核的地址空间,这意味着:

  • 无法存取内核内存数据
  • 无法调用内核函数

而所有对硬件的操作都是内核函数,因此用户需要使用系统调用来调用内核的函数。

2.进入系统调用

一件事情在脑海中浮现,在MIPS编程中我们是这样进行输入输出——向特定寄存器存放特殊值并调用syscall。而MOS中我们也是这样做的,系统调用的关键就在于用户态和内核态的切换,而这个切换就是在我们调用syscall指令时产生的。

而就在syscall指令调用后,CPU在硬件层面陷入内核态,其将触发异常分发机制,并最终调用到handle_sys()函数。该函数相当于系统调用的分发,其根据某特定寄存器的值从而找到需要调用的内核函数。

你将见到这几种函数:

  • syscall_……:用户空间内的函数,与sys_……成对存在

  • msyscall:设置系统调用号并让系统陷入内核态的函数

  • sys_……:内核函数

    有趣的是,在这里我们会发现msyscall需要6个参数,这引起了我们的一个新知识点:大量的参数是如何进行传递的?
    对于nn个参数的传递,栈帧sp会保留n∗4n∗4个字节的空间,而前4个参数会被放在a0到a3这四个寄存器中,但是栈帧中对应空间还是会被预留,其余参数存储在前四个参数的预留空间之上的区域。

注意到一个问题,多于四个的参数会被放到内存中,而这个空间是存在于用户态的,因此我们需要在内核中将这些参数转移到内核空间内,这步工作需要在handle_sys()函数的汇编代码实现了。

我们先来整理一下在MOS中进行系统调用的流程:

  1. 调用一个封装好的用户空间的库函数(如writef)
  2. 调用用户空间的syscall_* 函数
  3. 调用msyscall,用于陷入内核态
  4. 陷入内核,内核取得信息,执行对应的内核空间的系统调用函数(sys_*)
  5. 执行系统调用,并返回用户态,同时将返回值“传递”回用户态
  6. 从库函数返回,回到用户程序调用处

msyscall

msyscall执行的职能只是陷入内核态,并不涉及系统调用的分发。

syscall
jr ra
nop

handle_sys

syscall发生后,OS根据中断向量发现是调用了系统调用,从而通过中断分发到handle_sys函数。

handle_sys函数通过分析传入的参数来找到具体的系统调用目标函数,并将传入的参数放到寄存器中,然后进入目标函数。

三.进程通信 IPC

进程间通信机制是基于系统调用来实现的。通信的本质就是交换数据,而交换数据的最大问题在于:在进程间,用户地址空间相互独立。

因此,我们需要通过以内核的2g空间来作为传递信息的媒介,同时我们可以发现,进程控制块是存储在内核空间内的,因此我们完全可以将需要传递的数据放在目标的进程控制块内,然后目标进程在从中读取。

image

值得一提的是,由于在我们的用户程序中,会大量使用srcva 为0 的调用来表示不需要传递物理页面,因此在编写相关函数时也需要注意此种情况。

这两个过程是通过系统调用中的sys_ipc_recvsys_ipc_can_send来实现。

前者需要将当前接收者的进程控制块的相应域设置好,并使用sys_yield使得当前进程放弃CPU。

后者需要检查目标是否准备好接受,并修改目标进程的进程控制块,将需要的信息放到他们的进程控制块内。

需要注意,如果需要传递物理页面信息,需要调用sys_mem_map函数将当前进程srcva对应位置的页面映射到目标进程的dstva处

四.Fork函数

fork函数能够从一个进程生成另一个进程,使得子进程拥有和旧进程绝大部分相同的信息。同时,fork会在父子进程中拥有不同的返回值。

  • 在fork 之前的代码段只有父进程会执行。
  • 在fork 之后的代码段父子进程都会执行。
  • fork 在不同的进程中返回值不一样,在父进程中返回值不为0,在子进程中返回值为0。
  • 父进程和子进程虽然很多信息相同,但他们的env_id 是不同的。

image

写时复制机制

父进程会为子进程设置虚拟空间,但是我们通过上图能够发现,实际的分配过程其实是通过duppage复制页表,并设置PTE_COW。COW就是写时复制的意思(Copy On Write)。

只有当父子进程中有修改内存的举动时,内核会根据PTE_COW捕获中断(一般指缺页中断,Page Fault),并单独为修改内存的进程分配物理页面,然后将该页面复制过去后再实行修改。

区分父子进程的理论基础

fork()能够通过返回值来区别当前进程是否是子进程,若返回值为0则为子进程,否则为父进程。

而实现返回值差异性的函数是syscall_env_alloc函数,其属于用户函数,其触发系统调用后进行sys_env_alloc来创建和初始化一个新进程块。

sys_env_alloc

这个函数需要利用当前进程为模板来填写一个新的子进程块。其工作包括复制一份当前的运行现场**、复制一下当前的PC值、**修改子进程状态为阻塞、以及初始化其他进程控制块信息。

int sys_env_alloc(void)
{
	int r;
	struct Env *e;
	r = env_alloc(&e, curenv->env_id);
	if (r < 0) return r;
	e->env_status = ENV_NOT_RUNNABLE;
	e->env_pri = curenv->env_pri;
	bcopy((void *)KERNEL_SP - sizeof(struct Trapframe), (void *)&(e->env_tf), sizeof(struct Trapframe));
	e->env_tf.pc = e->env_tf.cp0_epc;
	e->env_tf.regs[2] = 0;

	return e->env_id; // 注意这个返回值是返回到父进程的
}
在分道扬镳后,父子各自的工作

子进程

子进程当前虽然拥有了一个进程控制块,但是仍然存在着几个问题:

  • 子进程被第一次调度时,其处在fork函数中(准确来说,是syscall_env_alloc返回后),此时函数中的各个变量仍然指向父进程中对应数据结构,子进程如何替换掉这些变量?
  • 子进程的用户空间没有初始化,如何实现COW的设想?

我们将在子进程中解决第一个问题,而第二个问题交由父进程解决

设置进程控制块

当从syscall_env_alloc返回后,子进程需要将当前函数内的进程控制块指针改为自己的。这一步通过调用syscall_getenvid这一系统调用实现。这一步后,子进程就能够从fork函数退出了(虽然当前处于阻塞状态)。

newenvid = syscall_env_alloc();
if(newenvid==0) {env = envs + ENVX(syscall_getenvid());return 0;}

父进程

父进程需要为子进程进行很多初始化工作,包括遍历进程空间并合理设置空间权限,实现空间共享**、**实现写时复制的缺页中断机制

进程映射

通过遍历当前页目录,将页面按以下规则进行设置:

  • 只读页面 按照相同权限(只读)映射给子进程即可
  • 共享页面 即具有PTE_LIBRARY 标记的页面,这类页面需要保持共享的可写的状态
  • 写时复制页面 即具有PTE_COW 标记的页面,这类页面是上一次的fork 的duppage的结果
  • 可写页面 需要给父进程和子进程的页表项都加上PTE_COW 标记

这个功能由duppage函数实现。

缺页中断

MIPS下存在两种缺页中断。一种是TLB缺失导致的缺页中断,其会触发trap并分发到handle_tlb下,然后按照正常逻辑进行查表、重填等,此处按下不表。

另一种是写时复制触发的缺页中断,其会触发trap分发到另一个处理函数handle_mod下。这个函数会跳转到page_fault_handler下,处理当前写时复制异常。

注意!MOS系统在此处应用了微内核的思想,将处理异常的方式交由用户进程自身,即在进程控制块内定义了一个域env_pgfault_handler用于指定异常处理的函数,使得用户能够自定义处理过程。

处理写时复制异常的流程为:

  1. page_fault_handler将当前现场保存在异常处理栈中,设置epc的值,以使得中断退出后跳转到指定用户进程指定的异常处理函数中。
  2. 退出中断,此时根据epc地址跳转到指定函数(注意这个函数是fork.c中的pgfault函数,这意味着它是用户态下执行的)中,处理缺页,然后恢复现场和sp寄存器,令进程恢复执行。

五.实验难点图解

1.MIPS调用规范(ABI)

MIPS ABI规定寄存器传参不需要复制到堆栈内
在这里插入图片描述

2.进程间通信机制

进程间通信机制是基于系统调用来实现的。通信的本质就是交换数据。

这是通过系统调用中的sys_ipc_recvsys_ipc_can_send来实现的。

前者需要将当前接收者的进程控制块的相应域设置好,并使用sys_yield使得当前进程放弃CPU。

后者需要检查目标是否准备好接受,并修改目标进程的进程控制块,将需要的信息放到他们的进程控制块内。
在这里插入图片描述

3.缺页中断的处理流程

在这里插入图片描述

Y86 Tools (Student Distribution) Copyright (c) 2002, R. Bryant and D. O Hallaron, All rights reserved. May not be used, modified, or copied without permission. This directory contains the student distribution of the Y86 tools. It is a proper subset of the master distribution, minus the solution files found in the master distribution. yas Y86 assembler yis Y86 instruction (ISA) simulator hcl2c HCL to C translator ssim SEQ simulator ssim+ SEQ+ simulator psim PIPE simulator y86-code/ Examples programs and and benchmark testing scripts ptest/ Scripts for detailed automated regression testing 1. Building the Y86 tools The Y86 simulators can be configured to support TTY and GUI interfaces. A simulator running in TTY mode prints all information about its run-time behavior on the terminal. Hard to understand what s going on, but useful for automated testing, and doesn t require any special installation features. A simulator running in GUI mode uses a fancy graphical user interface. Nice for visualizing and debugging, but requires installation of Tcl/Tk on your system. To build the Y86 tools, perform the following steps: NOTE: If your instructor prepared this distribution for you, then you can skip Step 1 and proceed directly to Step 2. The Makefile will already have the proper values for GUIMODE, TKLIBS, and TKINC for your system. Step 1. Decide whether you want the TTY or GUI form of the simulators, and then modify ./Makefile in this directory accordingly. (The changes you make to the variables in this Makefile will override the values already assigned in the Makefiles in the seq and pipe directories.) Building the GUI simulators: If you have Tcl/Tk installed on your system, then you can build the GUI form by initializing the GUIMODE, TKLIBS, and TKINC variables, as appropriate for your system. (The default values work for Linux systems.) Assigning GUIMODE=-DHAS_GUI causes the necessary GUI support code in the simulator sources to be included. The TKLIBS variable tells gcc where to look for the libtcl.so and libtk.so libraries. And the TKINC variable tells gcc where to find the tcl.h and tk.h header files. Building the TTY simulators: If you don t have Tcl/Tk installed on your system, then build the TTY form by commenting out all three of these variables (GUIMODE, TKLIBS, and TKINC) in the Makefile. Step 2: Once you ve modified the Makefile to build either the GUI or TTY form, then you can construct the entire set of Y86 tools by typing unix> make clean; make 2. Files Makefile Builds the Y86 tools README This file misc/ Source files for the Y86 assembler yas, the Y86 instruction simulator yis, and the isa.c file that is used by the -t option of the processor simulators to check the results against the ISA simulation. seq/ Code for the SEQ and SEQ+ simulators. Contains HCL files for labs and homework problems that involve modifying SEQ. pipe/ Code for the PIPE simulator. Contains HCL files for labs and homework problems that involve modifying PIPE. y86-code/ Example .ys files from CS:APP and scripts for conducting automated benchmark teseting of the new processor designs. ptest/ Automated regression testing scripts for testing processor designs.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值