zoukankan      html  css  js  c++  java
  • 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会在父子进程中拥有不同的返回值

    • 在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寄存器,令进程恢复执行。

    image

  • 相关阅读:
    算法(Algorithms)第4版 练习 1.5.22
    基于RPC原理的dubbo
    java的动态代理机制详解
    xxxx interview
    mysql创建账号
    elasticsearch搜索集群基础架构
    zookeeper可视化管理工具node-zk-browser安装
    kafka监控搭建
    kafka集群安装
    zookeeper集群安装
  • 原文地址:https://www.cnblogs.com/Nortonary/p/14752529.html
Copyright © 2011-2022 走看看