进程调度的实现

818 阅读7分钟

进程调度在时钟中断中完成,最核心的语句是mov esp, [proc_ready],意思是把从时钟中断中回到用户进程时选择下一个要执行的进程的堆栈。

怎么选择下一个要执行的进程的堆栈呢?这就是进程调度所做的事情。所谓调度,就是安排。

本文只实现根据进程优先级调度的算法。

8253

时钟中断频率

时钟中断每隔X个时间单位发生一次。这个X可以通过8253设置。8253如同8259A一样,是一个可编程的硬件。

8253提供一个16位计数器,最大值是65535。8253的计数器的默认初始值是65535。每隔1秒,计数器会减1,一直减到值为0为止,然后重新恢复初始值,继续上一轮的递减。在从初始值到0的这X秒内,会发生1193180次时钟中断。

时钟中断的发生频率是frequency = 1193180/计数器的初始值,因此,计数器的初始值 = 1193180/frequency

如果需要设置时钟中断的发生频率为100HZ,即每10ms发生一次时钟中断,计数器的初始值应该是:1193180/100

BCD

一个十进制数,345,用BCD码表示成,0003 0004 00005。BCD码有多种,这是最常用的一种。

8253的使用

8253的作用就是设置计数器的初始值,达到设置时钟中断频率的目的。

四个端口

8253有四个端口:

  1. 0x40。8253 Counter0。
  2. 0x41。8253 Counter1。
  3. 0x42。8253 Counter2。
  4. 0x43。8253 模式控制寄存器。

我们这次使用的是0x400x43端口。

先设置0x43端口的值,再设置0x40的值。

0x43端口的值由8个bit组成,分成几部分,每部分有特定含义。书上有详细记载。我不打算记住,这不是常用知识点。

总之,按照要求填充那8个bit,直接把这个bit组成的数字写成二进制或十六进制都行,然后写入0x43端口。

注意,要分两次向0x40端口写入一个字(这个字中的数据就是我们在上文计算出来的计数器的初始值),每次写入一个字节,先写入低字节,再写入高字节。

调度算法

delay

伪代码如下:

void 		delay(int milli_second)
{
  			// 通过系统调用获取当前时钟中断次数
  			int ticks = get_ticks();
  			while((get_ticks()-ticks)*10 < milli_second)
        {
          		// do nothing
        }
  
}

进程调度

伪代码如下:

typedef 	struct {
  		// 进程剩余能够执行的时钟中断次数,初始值是 priority
  		int	 ticks;
  		// 进程优先级
  		int	 priority;	
  		// 进程名称
  		char	name[16];
}Process;

Process  A = {150, 150, "TestA"};
Process  B = {50, 50, "TestB"};
Process  C = {30, 30, "TestC"};

Process	 procs = {A,B,C};
int	N = 3;

void 	TestA()
{
  		while(1){
        		// get_ticks()获取时钟中断已经发生的次数
        		// 打印时钟中断次数
        		disp_str(get_ticks());
        		// 延时200ms
        		delay(200);
      }
}

void 	TestB()
{
  		while(1){
        		disp_str(get_ticks());
        		delay(200);
      }
}

void 	TestC()
{
  		while(1){
        		disp_str(get_ticks());
        		delay(200);
      }
}
// 时钟中断处理
void	clockHandler()
{
  	// 时钟中断发生时,当前进程剩余可用时钟中断次数减去1
  	current_process->ticks--;	
  	schedule()
}
// 进程调度算法,根据进程ticks分配调度次数。所有进程的ticks用完后再重新初始化
void	schedule()
{
  		int greastTicks = 0;
  		
  		while(!greastTicks){
        		for(Process p = procs; p < procs + N; p++){
              	if(p->ticks > greastTicks){
                  	current_process = p;
                  	greastTicks = p->ticks;
                }
            }
        
        		if(!greastTicks){
              		for(Process p = procs; p < procs + N; p++){
              					p->ticks = p->priority;
            			}
            }
      }
}

运行结果解读

下面是三个进程运行结果图。所运行的代码是这样的:

  1. A、B、C三个进程的ticks分别是30、30、30
  2. 在每个进程中,会打印当前ticks,形式是ticks;然后打印各自的进程名称:A、B、C
  3. 每个进程都会延迟20个时钟中断。
  4. 每10毫秒发生一次时钟中断。
  5. 在调度程序中,会打印每个进入备选调度进程的ticks,形式是<ticks>

图中红色字母,例如B是进程中打印的进程名称。

image-20210310181656217.png

执行流程大概如下:

  1. 当前进程是A,执行完restart,即将运行进程A,突然发生时钟中断。
  2. 执行save保存进程A的快照。
  3. 进入调度函数。
    1. 进程ticks
      1. A->ticks = 30,B->ticks = 30,C->ticks = 30。
      2. 修正A->ticks。进入调度函数前、在进入时钟中断处理函数时,A->ticks--。
      3. 因此,第1步的数据修正为:A->ticks = 29,B->ticks = 30,C->ticks = 30。
        1. 根据调度算法,greatesTicks和B比较后值为30,因此C不会进入备选调度进程,这与图中<0x1D><0x1E>是吻合的。
    2. 显而易见,调度算法的结果是进程B。
  4. 时钟中断发生
    1. ticks的值:A->ticks = 29,B->ticks = 29,C->ticks = 30。
    2. 调度算法的结果是进程C。
  5. 时钟中断发生
    1. ticks的值:A->ticks = 29,B->ticks = 29,C->ticks = 29。
    2. 调度算法的结果是进程A。
  6. 若干次时钟中断发生,都是系统调用中的中断重入,不调度进程。
    1. 具体情形如何,实在难以模拟结果。最好不要对这个执行过程做任何推测,非常徒劳!
  7. 进入进程中的delay后,后续执行情况,已经难以模拟。不再分析。

重点中的重点

进程执行次数计算方法

用伪代码设置进程可以执行的时钟中断次数

Process  A = {150, 150, "TestA"};
Process  B = {50, 50, "TestB"};
Process  C = {30, 30, "TestC"};

进程A执行次数是:(100 + 20 * 2 + 30 * 3)/20

进程B执行次数是:(20 * 2 + 30 * 3)/20

进程C执行次数是:(30 * 3)/20

为什么

执行时间

假如只有两个进程,并且条件如下:

Process  A = {20, 20, "TestA"};
Process  B = {20, 20, "TestB"};

一共需要多少次时钟中断才能执行完两个进程?答案是20*2 = 40个。

我们的调度算法中,需要两个进程的ticks都变成0,才结束调度。每个时钟中断只会让一个进程的ticks减少1,要让两个进程的ticks都变成0,需要的时钟中断次数是两个进程的ticks之和。

为什么除以20

当每个进程的优先级相等时,时钟中断每发生20次,获得CPU的进程的延迟函数都会结束执行,这意味着这个进程完成了一次执行。由于我们采用调度算法,每个进程或早或晚都会获得CPU,因此,在时钟中断每发生20次时,每个进程都会或早或晚完成一次执行。

获取当前已经发生的时钟中断次数的系统调用get_ticks在不同进程执行时获得的数值是大致相同的(只存在一次或两次差值),类似于全局变量。

这一点,不容易说清楚,我在此耗费了非常多时间,尽量多说几句。

模拟三个进程执行流程,如下:

  1. 进程A进入循环,获得当前ticks=3。
  2. 进程B进入循环,获得当前ticks=4。
  3. 进程C进入循环,获得当前ticks=5。
  4. 经过若干个时钟中断后。
    1. 进程A运行,循环中,获得当前ticks=23,循环结束,进程完成一次执行。
    2. 进程B运行,循环中,获得当前ticks=24,循环结束,进程完成一次执行。
    3. 进程C运行,循环中,获得当前ticks=25,循环结束,进程完成一次执行。
    4. 就是这样。

20个时钟中断,能让三个进程全部执行完成一次。

之前的误区是,每个进程都需要消耗20个时钟中断,因此每20个时钟中断只能供一个进程执行一次。

实际上,已经发生了20次时钟中断的时候,总有一个进程的延迟函数会结束执行,当调度到其他进程时,其他进程的延迟函数也会结束执行。也就是说,三个进程的延迟函数处于”同一时间下“。当A已经延迟了20个时钟中断时,B、C也同样即将延迟了20个时钟中断。所有进程生活在同一个钟表之下。