XV6 RISC-V 源码阅读报告之进程模型

三、xv6进程模型

进程是对运行程序的抽象,通过CPU调度和虚拟地址等机制,为每个程序提供了独占处理器和内存空间的错觉。

3.1代码阅读

        代码主要在proc.h和proc.c

        3.1.1proc.h

        proc.h定义了切换上下文时保存的上下文结构(包含寄存器)、CPU结构(包含指向正在运行的进程的指针和上下文结构等)、陷阱帧trapframe和进程结构体(包含锁、状态、进程号、页表、陷阱帧、上下文、文件指针、iNode、页表等)。procstate枚举说明了进程的6个状态。

        3.1.2proc.c

        proc.c包含一系列操作进程需要的函数。

        首先定义的是一些小型的函数。procinit()初始化进程的锁和kstack起始地址。cpuid()返回当前的CPU号,mycpu()则返回指向当前CPU对应的结构体指针。myproc()返回指向当前进程结构的指针,通过查询CPUid再访问CPU结构中的进程指针来实现。allocpid()为进程分配递增的pid。

        allocproc()是对进程分配PCB的过程。在procinit中,进程表proc[NPROC]的每个块初始化时初值被赋为0,状态为UNUSED,在创建进程时,遍历这个数组,找到第一个状态为UNUSED的可用进程,然后对其进行操作。首先分配PID并改变状态为USED,随后用kalloc()分配内核栈空间,创建陷阱帧和页表的空间,,并且检查这些空间是否分配成功。接着,函数初始化上下文,设置ra寄存器让程序从forkret地址返回。最后,函数将返回一个指向新进程的指针。

        在freeproc函数中,调用kfree和proc_freepagetable分别释放trapframe和pagetable,并把p结构中所有成员变量赋值为0。

        proc_pagetable和proc_freepagetable分别为进程创建和释放页表,不再赘述。

        userinit()是在main.c中使用的创建第一个用户进程的函数。首先用allocproc给进程分配PCB,然后分配用户页,设置PC和栈指针。接着将名字设置为initcode,状态设置为RUNNABLE,应该是要运行user/initcode.S。

        growproc()可以调整进程的内存空间,通过输入参数的正负来控制增长或减小,是uvmalloc()和uvmdealloc()的包装,在后面的章节中再讲述。最终正确时返回0,错误时返回-1.

        fork()是一个比较复杂的函数,在lab中多次修改。它的行为如下。

  1. 通过allocproc()创建一个np进程即子进程。
  2. 使用uvmcopy()将p的user memory拷贝到np。
  3. 保存trapframe中的用户寄存器,设置子进程中fork的返回值为0(a0处)。
  4. 文件部分,将父进程的打开文件表复制给子进程。
  5. 拷贝名称,设置pid,释放在allocproc()时获取的np进程的锁。
  6. 在两把锁的保护下,设置np的parent和运行状态。

        fork的调用并不总是成功的,返回-1即是失败,在无空闲进程PCB或复制内存失败时都会返回-1并释放内存。

        exit()函数退出当前进程。函数先判断是否想要退出了初始进程,如果退出就panic。在wait_lock的保护下把子进程托管给初始进程,唤醒父进程,将本进程的状态设置为ZOMBIE,然后通过sched(),从此再也不返回这个函数,如果继续运行的话又会触发一个panic。

        scheduler()函数为每个CPU进行调度,它从不返回,不断循环规划。在循环中,先开中断避免死锁,然后遍历进程表,获取每个进程的锁,寻找到第一个状态为RUNNABLE的进程,并更改状态为RUNNING,切换内存、CPU上的进程和上下文,让被调度的进程开始运行。

        sched()在一番检查后通过swtch调用scheduler函数。在调用前,必须保证已获得进程表锁,进程状态已被修改,已关中断等。swtch.S是切换上下文的汇编代码。

        yield()让当前进程获取进程表锁,更改状态为RUNNABLE,通过sched来重新调度,让出CPU。

        forkret()在allocproc()中被调用,进程在第一次调度时会进入这个函数。进入时会先释放在scheduler函数中获取的进程锁,在forkret中调用usertrapret,从内核空间返回用户空间。

        sleep()使当前进程被阻塞,等待唤醒:将当前进程设置为阻塞态并添加到阻塞进程队列chan,切换进程继续运行;被唤醒时,从sched()处开始执行,将chan设置为0后继续运行之前剩余的内容。

        wakeup把所有在chan上睡眠的进程唤醒,遍历进行。

        procdump()函数调试时会打印状态不为UNUSED的进程信息。

3.2 机制

        进程是正在运行程序的实例,是操作系统调度的最小单位。xv6中只实现了进程,没有对线程的支持。PCB是管理进程的数据结构,包括管理进程需要的信息和应用程序运行的环境(虚拟内存,文件和IO信息等)。操作系统中进程PCB都储存在proc[NPROC]数组中。最大进程数为NPROC即64。

        在操作系统初始化时,通过userinit()和allocproc()创建第一个进程init,同时完成proc数组的初始化。xv6只允许中断从用户进入内核态,因此allocproc()创建trap调用的栈结构,而userinit()会设置其中的值。最后系统调用schedule(),开始循环进行user进程的调度。在init被启动后,会创建shell进程用于接受用户的命令行,接下来的进程就可以通过fork()来创建。

        在fork()新进程时,首先调用allocproc()来创建并初始化进程的PCB,并为其内核栈分配内存。然后复制父进程的信息,不同的是在父进程中返回子进程的pid,而在子进程中返回1。通过设置状态为RUNNABLE,可以准备开始调度。

        xv6中进程的状态转移如下。

        

 

        与课程中的五状态七状态模型不同的是,xv6的进程有6个状态,可能是因为PC初始化的过程相对简单。空闲的用UNUSED标记,而分配完尚未运行的则是USED,随后在schedule中会有RUNNABLE、SLEEPING和RUNNING的状态切换,最后进程销毁成为ZOMBIE。

3.3小结

        在这一部分中我们基本介绍了xv6的进程模型,包含进程PCB的结构,进程创建和fork的流程和进程状态转换的一些函数。xv6的进程模型相对简单,通常使用遍历进程表的方式进行调度和其他操作。在fork()时也没有COW,需要在LAB中手动实现。

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值