Bootstrap

第三章 xv6操作系统中级实验1——xv6操作系统进程调度


前言

  Hello,小伙伴们,经过本专栏的第一章和第二章实验,相信大家对xv6操作系统实验有了一个很好的入门,第一章讲述了对如何根据自己的需求添加一些简单的自定义操作系统功能(其实就是把我们写的C程序代码加入到xv6源码中,并且关联到Makefile文件中),第二章讲述了如何在进程之间共享内核全局变量(这是进程间通信的基础,完成了这个实验,对进程间通信会有一个简单的认识)。
  本篇文章延续了第一章和第二章,来给大家演示一下xv6操作系统中级实验,中级实验和初级实验有一个很大的区别是:需要在更多的xv6源代码文件中添加我们自己的代码块,可以使得我们对xv6操作系统源代码有更加深入的认识,并且巩固对应的操作系统理论知识。本篇文章中级实验为进程的调度实验,主要包括两个部分:
  第一个部分为调整进程占用CPU的时间片大小(联想时间片轮转调度算法,时间片大小决定了CPU在何时进行进程调度,只有一个进程占用CPU的时间片用完了或者进程在时间片内提前运行结束,CPU才会进行下一个进程的调度)
  第二个部分为设置进程优先级调度策略(这个部分是对第一部分的进一步拓展,目的是当存在不同优先级进程的时候,优先调度优先级高的进程,当所有进程优先级一样的时候,按照进程的执行顺序进行时间片轮转)


提示:以下是本篇文章正文内容,下面案例可供参考

一、调整进程占用CPU的时间片大小

  本文第一部分的实验较为简单,既然我们要调整进程占用CPU时间片大小,学过进程控制块(PCB)可知,PCB中可以记录进程的运行时间,所以不难想到,我们可以为每个进程的PCB中添加一个时钟计数器(记录该进程从占用CPU这一刻开始,运行了多少个时钟周期,时钟周期 = 1/CPU频率,然后,再定义一个全局变量,规定进程运行的时间片大小(也是以时钟周期为单位),最后,当我们创建新进程时,将记录时间片大小的全局变量赋值给PCB的时钟计数器,在新进程运行过程中,时钟计数器的值不断减小,直到减小到0,说明新进程的时间片用完了,这个时候CPU就得进行进程调度了。
  根据上述思路,我们接下来对其进行具体的实现。

1、为进程控制块增加时钟计数器&增加全局变量设置时间片大小

  我们打开xv6操作系统源码的proc.h文件,修改proc结构体(这个结构体就是xv6创建进程时,为进程分配PCB的数据结构),在结构体内增加slot变量,用来作为PCB的时钟计数器,然后,在结构体外侧定义一个SLOT全局变量,并且赋初始值为8**(即时间片大小为8个CPU时钟周期)**,如下图。
在这里插入图片描述

2、修改分配进程控制块函数和进程状态打印函数

  在proc.h文件中的proc结构体中添加时钟计数器slot变量和全局变量SLOT后,我们还需修改xv6操作系统源码proc.c文件中的allocproc()函数和procdump()函数,其中,前者负责将全局变量SLOT的值赋值给新进程PCB的slot变量,后者负责打印该进程剩余的时间片大小,如下两幅图所示。
在这里插入图片描述
在这里插入图片描述

3、修改xv6操作系统的进程调度函数

  完成了上述两步之后,我们设置的结构体slot变量就绑定在了进程的PCB中,成为了描述进程运行状态的一部分,但是,还远远不够,我们要让xv6操作系统的进程调度实现逻辑受限于结构体slot变量,让xv6操作系统在slot == 0; //时间计数器为0,表面时间片用完了的时候进行进程调度。
  在我们没有修改xv6操作系统进程调度函数的时候,xv6在每一个CPU时钟周期结束后就进行一次进程调度(也就是默认时间片大小为1),要想让我们时间片大小设置为8生效,所以就得修改进程调度函数,xv6操作系统进程调度函数在trap.c文件中,如下图所示。
在这里插入图片描述

4、编写C程序验证调整进程占用CPU时间片大小是否成功

  在xv6中编写程序并且运行,如果从本专栏最开始部分看,相信大家都会吧,不会的请移步第一章 xv6操作系统初级实验1——编写一个C语言程序。完成了以上三步之后,调整时间片大小的功能就实现了,我们还需要编写一个程序名为loop的C程序来验证它,如下图所示。
在这里插入图片描述
  程序源代码如下:

#include "types.h"
#include "stat.h"
#include "user.h"

int
main(int argc, char *argv[])
{       int pid;
        int data[8];
        int i,j,k;
        pid=fork();
        for(i=0;i<2;i++)
        {
            for(j=0;j<1024*100;j++)
                for(k=0;k<1024*1024;k++)
                    data[k%8]=pid*k;
         }
         printf(1,"%d ",data[0]);
         exit();
}

  程序编写完成后,不要忘记修改Makefile文件,然后在xv6操作系统源码目录下打开终端,执行make qemu命令,用QEMU虚拟环境启动我们xv6操作系统,ls命令查看我们的loop程序,接着输入loop命令,运行我们的C程序,在运行我们C程序过程中,连续按两次CTRL+P 命令,可以查看当前系统进程的运行状态,如下图所示。
在这里插入图片描述
  从该程序的执行结果来看,发现程序并没有执行完毕,这是正常的(我们正是需要这个情况,不然,当程序运行时间太短,我们来不及按两次CTRL+P 来查看进程的运行状态),从我们设置的三层循环来看,进行时间复杂度分析,发现这三层循环时间复杂度是O(n^11),所以程序没有执行完毕是正常的,从CTRL+P的结果来看,我们的程序和其创建的子进程都是一直处于运行状态,而xv6操作系统启动的初始进程号为1和2的进程处于睡眠状态,而且运行状态中的进程,两次剩余的时间片大小都不一样。
  如果学过进程调度理论知识,且细心的读者可能会问,为什么只能有两个进程同时运行呢?其实,这恰恰就说明了xv6操作系统是运行在一个物理核心数为2的CPU上,大家可以尝试退出QEMU虚拟环境,然后再次输入make qemu命令,会发现如下界面。
在这里插入图片描述
  这就不难发现了,我们的xv6操作系统启动的时候,只对cpu1和cpu0模拟了硬件自检和初始化,两个物理核心数自然只能同时两个进程同时运行
  至此,本文第一部分实验就成功完成啦!!

二、设置进程优先级调度策略

  本文前言部分介绍到,第二部分实验是对第一部分实验的拓展,优先级调度策略是在第一部分RR(Round-Robin,时间片轮转)调度算法的基础上增加优先级,优先调度高优先级的进程,同优先级的进程按照RR调度算法调度,本部分实验便是实现了xv6操作系统上的静态(一旦进程优先级确定,在运行过程中始终不变)优先级调度,具体实现思路和第一部分实现类似,首先为PCB添加优先级属性,然后创建进程时为进程设置优先级,最后修改xv6操作系统的进程调度器,根据优先级调度进程的执行,实现步骤如下。

1、为PCB添加优先级属性

  这一步和第一部分实验的第一步是一样的,在xv6操作系统源码proc.h文件中的proc结构体中添加priority变量,如下图。
在这里插入图片描述

2、创建进程时为进程指定默认优先级&修改优先级

   PCB中有了优先级属性后,需要在创建进程的时候就需要指定一个优先级或设置一个默认优先级。本部分实验选择创建时使用默认优先级,在进程运行时视情况修改优先级,因此创建进程时proc结构体的priority变量需要赋初始值,我们打开xv6操作系统源码中proc.c文件,找到allocproc()函数,添加设置新进程PCBpriority = 10的代码段,最后,为了能查看进程优先级,我们还要修改procdump()函数打印的内容,如下两幅图所示。
在这里插入图片描述
在这里插入图片描述
   对于进程运行时视情况设置优先级,我们通过系统调用来实现,系统调用的实现如果不会,可以移步:第二章 xv6操作系统初级实验2——为xv6操作系统定义一个内核全局变量,用于进程间共享。我们把修改进程优先级的系统调用命名为chpri(change priority),具体步骤如下多幅图。

  • 首先,在xv6操作系统源码syscall.h中为新的系统调用定义其编号:
    在这里插入图片描述

  • 然后,在xv6操作系统源码user.h中增加用户态函数原型int chpri (int pid, int priority),第一个参数用于指出进程号,第二个参数指出新的优先级:
    在这里插入图片描述

  • 接着,在xv6操作系统源码usys.S中添加chpri()函数的汇编实现代码(宏展开后对应于sys_chpri函数):
    在这里插入图片描述

  • 再接下来修改系统调用的跳转表,在xv6操作系统源码syscall.c中的syscalls[]数组中添加一项:
    在这里插入图片描述

  • 由于syscall.c中未定义sys_chpri()函数,因此需要在syscalls[]数组前面增加一个外部函数声明:
    在这里插入图片描述

  • sysproc.c中实现sys_chpri()函数,简单地检查进程号和优先级不能为负数,然后调用chpri(pid,pr)函数为编号为pid的进程优先级设置为pr
    在这里插入图片描述

  • 最后,在proc.c中实现chpri()函数,并且在defs.hproc.c部分添加函数原型int chpri(int, int),以便内核代码访问该函数:
    在这里插入图片描述
    在这里插入图片描述

3、修改xv6操作系统的进程调度器

  这一小步的较难,作者本人其实对代码也没有彻底的完全理解,在参考了深圳大学罗秋明老师著的《操作系统原型-xv6分析与实验》 本部分实验后。大致意思是,为进程添加优先级的信息后,还需要在调度器中修改调度行为。增加一个变量来记录上一次调度的进程号last_proc_num来辅助同一优先级进程之间的RR轮转调度;每次调度时,用prio从0到20的优先级逐渐检查,最先找到的就绪进程就是优先级最高的进程;为了应对一个优先级有多个进程的情况,本实验记录了上一次调度时进程在proc[]数组中的编号last_proc_num,扫描时从last_proc_num+1开始而不是从0下标开始,这样就实现了同级别进程的RR调度;我们对proc.c中的void scheduler(void)函数进行修改,源代码如下:

void
scheduler(void)
{
	struct proc *p,*temp;
	int priority;


	for(;;){
		// Enable interrupts on this processor.
		sti();
		
		// Loop over process table looking for process to run.
		acquire(&ptable.lock);
	
		priority = 19;
	
		for(temp  = ptable.proc; temp < &ptable.proc[NPROC]; temp++) //获取当前可运行的最高当前优先级
		{
			if(temp->state == RUNNABLE&&temp->priority < priority)
			  priority = temp->priority;
		}
	
	
		for(p = ptable.proc; p < &ptable.proc[NPROC]; p++)
		{
			if(p->state != RUNNABLE)
			  continue;      
			if(p->priority > priority)
			  continue;
			else
			{
			   priority = p->priority ;
			}
	
	
			 // Switch to chosen process.  It is the process's job
			 // to release ptable.lock and then reacquire it
			 // before jumping back to us.
			proc = p;
			switchuvm(p);
			p->state = RUNNING;
			swtch(&cpu->scheduler, p->context);
			switchkvm();
	
			// Process is done running for now.
			// It should have changed its p->state before coming back.
			proc = 0;
		}//endif for proc_num
		release(&ptable.lock);
	}
}
4、编写C程序验证优先级调度是否成功

  完成了上述三步之后,xv6操作系统的优先级RR进程调度算法就实现了,我们通过编写名为prio_sched的C程序来进行验证,该程序主要创建4个进程,其中优先级为5和15的各2个,然后查看它们被调度的情况;代码如下:

#include "types.h"
#include "stat.h"
#include "user.h"

int
main(int argc, char *argv[])
{	
	int pid;
	int data[8];
	printf(1,"This is a demo for prio-schedule!\n");
	pid=getpid();
	chpri(pid,19); //如果系统默认优先级不是19,则需先设置

	pid=fork();
	if(pid!=0){
		chpri(pid,15);                           //set 1st child’s prio=15
		printf(1,"pid = %d prio = %d\n",pid,15);
		pid=fork();
		if(pid!=0){
			chpri(pid,15);                       //set 2nd child’s prio=15
			printf(1,"pid = %d prio = %d\n",pid,15);
			pid=fork();
			if(pid!=0){
				chpri(pid,5);                     //set 3rd child’s prio=5
				printf(1,"pid = %d prio = %d\n",pid,5);
				pid=fork();
				if(pid!=0){
					chpri(pid,5);                  //set 4th child’s prio=5
					printf(1,"pid = %d prio = %d\n",pid,5);
			    }
		    }
		 }
	 }
	sleep(20);				//该睡眠是为了保证子进程创建完成,不是必须的 
	pid=getpid();
	printf(1,"pid = %d started\n",pid);
	
	int i,j,k;
	for( i=0;i<2;i++)
	{       
		printf(1,"pid = %d runing\n",pid);
		for( j=0;j<1024*100;j++)
			for( k=0;k<1024;k++)	
				data[k%8]=pid*k;
	}
	printf(1,"pid = %d finished %d\n",pid,data[pid]);
	exit();
}

  程序编写完成后,不要忘记修改Makefile文件,然后在xv6操作系统源码目录下打开终端,执行make qemu命令,用QEMU虚拟环境启动我们xv6操作系统,ls命令查看我们的prio_sched程序,接着输入prio_sched命令,运行我们的C程序,如下图所示。
  至此,xv6操作系统进程调度第二部分实验也完成了,这部分实验难度有点大,作者也尝试将源码分享了出来,就是为了方便读者反复阅读。从最后的执行结果看,在进程执行时间几乎一样的情况下,优先级越高的进程越先执行完毕,说明我们的优先级进程调度策略是成功实现了的。


总结

  本篇博文是xv6操作系统中级实验,和该专栏前面两章实验相比,不管是代码量还是难度,都提升了一个档次,如果读者可以认真的跟着每一步做下来,对xv6操作系统的源码一定会有更加深入的认识,并且能够极大的巩固操作系统进程调度理论部分(本章实验主要是基于时间片轮转调度算法和优先级调度的结合),体现了操作系统理论与实践的结合,使得读者能够有自己修改xv6操作系统源码的成就感

注:本专栏所有内容都是参考深圳大学罗秋明老师著的《操作系统原型-xv6分析与实验》,并且提取了较为简单和重要的部分,想要进一步深入xv6操作系统,可以学习本书更多内容。

;