三、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中多次修改。它的行为如下。
- 通过allocproc()创建一个np进程即子进程。
- 使用uvmcopy()将p的user memory拷贝到np。
- 保存trapframe中的用户寄存器,设置子进程中fork的返回值为0(a0处)。
- 文件部分,将父进程的打开文件表复制给子进程。
- 拷贝名称,设置pid,释放在allocproc()时获取的np进程的锁。
- 在两把锁的保护下,设置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中手动实现。