需求分析

需求分析:根据每个运行进程的彩票数量比例分配处理器资源,一个进程的彩票越多,运行的就越多。每次切片时,随机抽签确定彩票中奖者。获胜的进程将获得下一个时间片。

思路

设计这个彩票调度的核心算法并不复杂。我采取了一个比较暴力的思路,每次需要调度的时候,进行两轮对进程列表的遍历。第一轮遍历计算出总的彩票数量,第二轮遍历之前,我们已经生成了随机数并且根据这个随机数确定了中奖者,因此在第二轮遍历中,我们直接维护一个和计数器来记录当前已经遍历过的进程的彩票数量是多少,当第一次出现大于等于关系的时候,我们就认定这个进程是中奖者。

当然在这里遍历的过程中还涉及到很多细节问题,比如对进程的运行状态的判定等等。最初我对进程的判定状态表达式设置错误导致了一些诡异的崩溃问题。

从实现上,主要可以将要做的事情分成几个部分。

  • 添加并实现系统调用
  • 修改相关的数据结构
  • 修改调度器的代码
  • 编写 ps
  • 简单测试调度器
  • 编写调度程序以进一步测试调度器

实现与问题解决

所有对程序的调试都是通过 printfcprintf 实现的,代码中还有相当大量的残留痕迹。

添加系统调用的部分和必做实验非常类似。但这里设计到用 argint 做指针传递的一些事情,如

struct pstat* x;
if(argint(0, (int*)(&x)) < 0)
return -1;

其它部分就是搬运一下数据,不是彩票调度的核心,在此略去。

修改数据结构主要包括我们自己加入的 pstat 和我们修改 proc 结构

// Per-process state
struct proc {
  uint sz;                     // Size of process memory (bytes)
...
  char name[16];               // Process name (debugging)
  // FOR LOTTERY SCHEDULER
  int inuse;
	int ticks;
	int tickets;
};

调度器代码的修改主要是按照上述算法思路进行实现,其中一些数据的来源和去向基本可以在原有的代码基础上修改

for (;;)
  {
    ...
    for (p = ptable.proc; p < &ptable.proc[NPROC]; p++)
    {
      total += p->tickets;
      ...
    }
    int winner = rand() % total;
    ...
    acquire(&ptable.lock);
    for (p = ptable.proc; p < &ptable.proc[NPROC]; p++)
    {
      presum += p->tickets;
      if (p->state != RUNNABLE || presum < winner)
        continue;
      c->proc = p;
      ...
      break;
    }
    release(&ptable.lock);
    ...
  }

ps 的实现我们有非常多可以借鉴的来源,在这里我基本上用了一个现成的 ps。

#include "types.h"
...
int main(int argc, char *argv[])
{
    struct pstat info;
    getpinfo(&info);
    printf(1, "PID\tTICKETS\tTICKS\n");
    for (int i = 0; i < NPROC; ++i)
    {
        if (info.pid[i] == 0)
            continue;
        printf(1, "%d\t%d\t%d\n", info.pid[i], info.tickets[i], info.ticks[i]);
    }
    exit();
}

它基本就是调用了一下系统调用,没啥别的东西。

最后是去编写一个测试程序。我撰写了一个特别的 mytest 程序,用于测试这个彩票调度器。这个程序 fork 出三个子进程,将它自身的彩票数量设为 999,将三个子进程的彩票数量分别设为 300,200,100,符合 3:2:1 的要求。父进程会不断睡觉并计数,每次醒来时借用 ps 的代码来输出当前系统中运行的所有进程的情况。我稍微调整了一下这种输出的格式,使得它更加紧凑。每次输出只占用一行,每个进程用一个三元组表示,其中三个数字分别代表它的 PID,它的彩票数量 ticket 和它被调用的次数 tick

这个程序的主体部分如下所示:

settickets(999);
    int pid1 = fork();
    if (pid1 == 0)
    {
        // sub1 - 4
        settickets(300);
        while (1)
            ;
    }
    else
    {
        int pid2 = fork();
        if (pid2 == 0)
        ...
        else
        {
            int pid3 = fork();
            ...
            else
            {
                // main
                for (int i = 1; i <= 100; i++)
                {
                    ps();
                    sleep(1);
                }
                ...
            }
        }
    }

实验的结果可以在后面的部分中看到。

我在实验过程中遇到了一些非常诡异的情况。其中一些情况我至今没有找到缘由。另一些问题由于使用了不当的随机数生成器导致。我从一个以前写过的 C++ 程序中搬运了一个 xorshift128 程序并将它简单修改类型后应用在 32 位(实际上是 16 位)随机整数的生成中。当然我观察到了一些和溢出相关的问题后做了简单的修改,但是还是存在因为生成了负数而导致的一些诡异问题。最后我通过将整个结果与 0xFFFF 按位与草率地解决了这个问题。另一个问题是在初始化 xorshift 时随手打上去一个 0 最后导致了百思不得其解的诡异结果。很悲惨的是,在进行一个进程调度实验时,在随机数上出现了各种奇怪的问题。我想这种问题侧面说明了不太习惯的调试环境带来了一些麻烦,以及在一个现有的工程上进行修改时,总会面临着各种方面的不确定与手足无措。

测试结果

这里我主要对两部分进行了测试。一个是 ps 命令的功能,另一个是彩票调度过程是否是符合我们预期的。事实上,对于父进程创建子进程时的彩票数量的管理等其它问题,在我实验的过程中已经顺便进行了确认,因其较为简易,在此处就不再赘述。

ps 程序

ps 命令可以轻松地打印出当前系统中运行的所有进程的情况,如下图所示。

xv6 Lottery Scheduler

测试彩票调度

如何测试这个彩票调度程序起初令我非常头疼。后来我想了这样一个或许不太严谨的办法。

前面已经提到,我撰写了一个特别的 mytest 程序,用于测试这个彩票调度器。这个程序 fork 出三个子进程,将它自身的彩票数量设为 999,将三个子进程的彩票数量分别设为 300,200,100,符合 3:2:1 的要求。

实验测试结果如下图所示。其中进程 3 是父进程,进程 4-6 是创建出来的用于测试的子进程。可以很明显地看到,随着时间的推移,4-6 号进程被调度的次数 tick 逐渐收敛到一种非常接近于 3:2:1 的状态,这与我们的实验预期是完全相符的。

xv6 Lottery Scheduler

(我是不是可以用这种图代替一下要求的 graph 呢?我觉得它们在精神内涵上是差不多的~)

由于我才用了一种并不优雅的方式来结束所有的父进程和子进程,这会导致 zombie 的呼唤。不过在这里作为一个测试程序也无伤大雅吧。

相关文章: