其他分享
首页 > 其他分享> > BUAA OS Lab4 系统调用与fork

BUAA OS Lab4 系统调用与fork

作者:互联网

我的lab4总结

我打算从当前lab开始进行OS学习总结的分享。
(前几个Lab因为缺乏形式化表述,所以总是有些bug,怕发出来误人子弟。lab4对之前的lab都做了一次检查,目前代码总体上相对较为规范)
其中配图有些是cscore上扒的,不过觉得表述模糊的或者缺乏配图的地方都是自己画的

本单元主要实现:

  1. 系统调用的概念和流程
  2. 进程间的通讯(IPC)
  3. fork函数实现
  4. 缺页中断的处理流程

系统调用

什么是系统调用

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

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

进入系统调用

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

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

你将见到这几种函数:

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

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

  • sys_……:内核函数

    有趣的是,在这里我们会发现msyscall需要6个参数,这引起了我们的一个新知识点:大量的参数是如何进行传递的
    对于$n$个参数的传递,栈帧sp会保留$n * 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函数通过分析传入的参数来找到具体的系统调用目标函数,并将传入的参数放到寄存器中,然后进入目标函数。

具体系统调用函数

所有的系统调用目标函数都在lib/syscall_all.c中定义,他们执行相应的功能,包括对页表的操作、进程的状态转换等等,在此按下不表。

进程通信 IPC

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

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

image

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

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

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

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

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

Fork函数

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

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; // 注意这个返回值是返回到父进程的
}

在分道扬镳后,父子各自的工作

子进程

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

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

设置进程控制块

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

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

父进程

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

进程映射

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

这个功能由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寄存器,令进程恢复执行。

image

标签:fork,调用,函数,BUAA,sys,内核,env,Lab4,进程
来源: https://www.cnblogs.com/Nortonary/p/14752529.html