MINIX的进程调度
MINIX3中使用的是一种多级调度算法,进程被赋予一个相关的初始优先级,进程的优先级在执行的过程中可以动态改变。不同的系统进程(时钟、系统任务、驱动)具有不同的优先级,但是用户进程比所有的系统进程优先级都低,用户进程的初值相等。
进程维护16个可运行进程队列,每个队列的索引代表优先级(数字越小,优先级越高)。队列的内部采用时间片轮转调度算法。如果一个运行的进程用完了时间片,移到队列尾部分配新时间片。然而如果是一个被阻塞的进程唤醒,他会被放到队首,继续上次时间片运行。当进程要执行的时候,加入队列。一个运行进程被阻塞,会被移出调度队列。
队列中仅有可运行进程!
为什么采用这种16个优先队列的方式
在Linux中,他有些版本的调度算法也是这种实现(算法复杂度近O(1))。
如果普通的寻找优先级最高的进程。能直接想到的办法就是在每个进程对应的结构中添加一项表示优先级的成员。然后每次需要选取一个任务运行的时候就遍历整个进程链表。问题就在于遍历时很浪费时间的,尤其对于实时任务,不可预测的搜索延时会为估算时间带来极大的困难。
因此,规定好固定的优先级(最多16)。每种优先级设定一个进程链表,同一个链表上的进程具有相同的优先级。每当有任务使用完他的时间片,就立即重新计算新的时间片和优先级(动态优先级)。,并根据优先级将他插入到对应的优先级进程链表尾部。
好处很明显,系统进程和普通进程可以用不同的优先级链表进行区分。在查找下一个最高优先级的进程时,直接线性搜索这个优先级数组,找到第一个非空的进程队列,这个队列的头,就是具有最高优先级的进程。
16个进程如图2.43:
静态优先级与动态优先级
静态优先级是固定的优先级,内核不会主动修改(通过系统调用进行修改)。我们都知道时间片耗光要重新计算任务的时间片,这个计算过程主要与静态优先级有关。静态优先级越高,时间片就越长,相应的吞吐量增长,但响应能力和交互性变的很差。
静态优先级已经可以决定进程的优先级,并为其分配时间片,那为什么还需要动态优先级。原因是需要区分CPU-bound任务(计算密集型任务)和I/O-bound(I/O密集型任务)。
IO密集型任务应该具有很高的优先级,尽管总是睡眠等待外界输入,但是一旦输入完成,内核唤醒之后应该被很快执行(例如用户点击一个桌面程序,如果很久没有得到反应就很不合适)。而计算密集型任务主要是做持续的运算,他们会长时间处于可执行状态,一般不需要较高优先级。(比如编译程序,即使多运行几秒钟,用户也不会有什么感觉)
因此,动态优先级就是为了综合处理各种进程而存在的。内核可以根据进程在一段时间内的表现(检查休眠时间和运行时间)判断进程是哪一种任务,对其优先级进行动态调整,基本上就是对现有的优先级进行一些调整。
if ( ! time_left) { /* 判断时间片是否有剩余 ? */
rp->ticks = rp->p_quantum_size; /* 赋予新的时间片 */
if (prev_ptr == rp) penalty ++; /* 计算惩罚值 */
else penalty --; /* 计算惩罚值 */
...
}
...
if (penalty != 0) {
rp->p_priority += penalty; /* 动态调整优先级 */
...
}
用这段代码解释动态优先级的实现,其实就是计算这样一个惩罚值penalty加到原来的静态优先级上,这里惩罚值的得出仅仅是判断上次执行的进程是否还是次进程,如果是就降低他的优先级,如果不是就提高他的优先级。
这样可以保证高优先级的任务不会一直执行,低优先级的任务也不会一直得不到运行的机会,避免了饥饿任务的出现。
静态优先级:用户程序设置的优先级(如果未设置,则使用默认值),称为静态优先级。这是进程优先级的基准,在进程执行的过程中往往是不改变的;
动态优先级:优先级动态调整后,实际生效的优先级。这个值是可能时时刻刻都在变化的;
调度触发的时机
- 当前进程由运行态变为非运行状态
进程执行过程中由于系统调用变为非运行状态,比如执行exit退出。或是进程请求的资源未得到满足被挂起,例如read等待输入。 - 进程被抢占
一般是时间片用完或是出现了更高优先级的进程。内核在相应时钟中断的过程中发现当前进程的时间片用完。或是内核发现某个高优先级的进程所等待的外部资源变为可用,将其唤醒。
调度算法代码
调度算法主要是enqueue、dequeue操作对16个不同优先级的队列进行出入队操作,留在队列中的进程都是可执行进程,从中选出最高优先级的即可。
enqueue()
/*===========================================================================*
* enqueue *
*===========================================================================*/
PUBLIC void enqueue(struct proc *rp)
{
int q; /* 选择使用的优先级队列 */
int front; /* 插入队列头部还是尾部 */
/* 决定插入哪个队列的头部还是尾部 */
sched(rp, &q, &front);
/* 将进程插入队列 */
if (rdy_head[q] == NIL_PROC) { /* 要插入到一个空队列中 */
rdy_head[q] = rdy_tail[q] = rp; /* 建立一项新项 */
rp->p_nextready = NIL_PROC; /* 将rp的下一个指向空(队列中只有一项,下一项自然为空) */
}
else if (front) { /* 插入队列头部 */
rp->p_nextready = rdy_head[q]; /* 修改队列头 */
rdy_head[q] = rp;
}
else { /* 插入队列尾 */
rdy_tail[q]->p_nextready = rp; /* 修改队列尾 */
rdy_tail[q] = rp;
rp->p_nextready = NIL_PROC; /* 将rp的下一个指向空(队列中只有一项,下一项自然为空) */
}
/* 选择下一个执行的进程 */
pick_proc();
}
enqueue使用一个指向进程表项的指针rp作为参数。调用sched()函数决定了这个进程应该插入哪个队列的头部还是尾部,参数q说明进程位于哪个队列,front说明位于头部还是尾部。
rdy_head是一个指针数组,分别指向16个优先级队列的头部。rdy_tail类似于rdy_head,分别指向16个优先级队列的尾部。
p_nextready是进程结构的一个成员,指向这个队列中下一个进程,因此通过一个rdy_head和进程的p_nextready就可以遍历这个优先级队列。
最后调用一下pick_proc()拿出最高优先级的进程作为下一个执行的进程即可。
sched()
/*===========================================================================*
* sched *
*===========================================================================*/
PRIVATE void sched(struct proc *rp, int *queue, int *front)
{
static struct proc *prev_ptr = NIL_PROC; /* 上次运行的进程 */
int time_left = (rp->ticks > 0); /* 时间片是否耗完 */
int penalty = 0; /* 动态优先级的惩罚值 */
/* 检查进程是否有时间片剩余,如果用完就分一片新的,并根据惩罚值动态调整优先级 */
if ( ! time_left) { /* 没有时间片剩余 */
rp->ticks = rp->p_quantum_size; /* 给一片新的时间片 */
if (prev_ptr == rp) penalty ++; /* 如果上一次还是运行的这个进程,降低他的优先级 */
else penalty --; /* 否则增高优先级 */
prev_ptr = rp; /* 保留上一个运行的进程 */
}
/* 动态优先级的确定 */
if ((penalty != 0) && (!iskernel(rp) && (isIDLE(rp))){
rp->p_priority += penalty; /* 根据惩罚值动态更新优先级 */
if (rp->p_priority < rp->p_max_priority) /* 检查是否超过上界 */
rp->p_priority=rp->p_max_priority;
else if (rp->p_priority > IDLE_Q-1) /* 检查是否低于下界 */
rp->p_priority = IDLE_Q-1;
}
/* 如果有时间片剩余,就把进程加到队头,保证可以立即运行 */
*queue = rp->p_priority;
*front = time_left;
}
sched被enqueue调用,决定新就绪的进程应该放到哪个队列中的头部还是尾部。进程表项中记录有时间片、剩余时间片、优先级、允许的最大优先级。
重点在于动态优先级,也就是惩罚值的确定。检查这个进程是否可以在其它进程没运行的情况下连续运行两次(也就是会一直运行),惩罚值+1,否则,惩罚值-1。这样做的原因就是为了让高优先级的进程不会一直占用CPU,低优先级的进程可以获得运行的机会(避免饥饿)
pick_proc()
/*===========================================================================*
* pick_proc *
*===========================================================================*/
PRIVATE void pick_proc()
{
struct proc *rp; /* 现在运行的进程 */
int q; /* 遍历队列用的变量 */
/* 遍历优先级队列,选择优先级最高的进程运行即可 */
for (q=0; q < NR_SCHED_QUEUES; q++) {
if ( (rp = rdy_head[q]) != NIL_PROC) {
p_proc_ready = rp; /* 发现不空的队列把队头设置成下一个要运行的进程即可 */
return;
}
}
}
线性搜索优先级最高的进程(第一个非空优先级队列的头部)
dequeue()
/*===========================================================================*
* dequeue *
*===========================================================================*/
PUBLIC void dequeue(struct proc *rp)
/* this process is no longer runnable */
{
/* 当进程被阻塞的时候,必须把它从优先级队列中移除(因为优先级队列和调度算法一定是针对运行进程而言 */
int q = rp->p_priority; /* 选择使用的优先级队列 */
struct proc **xpp; /* 遍历用变量 */
struct proc *prev_xp; /* 上一次运行的队列 */
/* 如果进程存在队列中,要把他从该优先级队列中删除,因为他的状态已经不是可运行了,或是不应该运行*/
prev_xp = NIL_PROC;
for (xpp = &rdy_head[q]; *xpp != NIL_PROC; xpp = &(*xpp)->p_nextready) {
if (*xpp == rp) { /* 找到了要出队的进程 */
*xpp = (*xpp)->p_nextready; /* 下一项指向下下项 */
if (rp == rdy_tail[q]) /* 删除队尾 */
rdy_tail[q] = prev_xp;
if (rp->p_flags != 0) /* 如果他是一个正在执行的进程 */
pick_proc(); /* 立即选择一个新的进程执行 */
break;
}
prev_xp = *xpp; /* 保留上一次运行的进程 */
}
}
当需要把一个进程转换为非就绪态时(一般是阻塞),调用dequeue函数。一个要阻塞的进程必定处于运行态,所以将要被移出的进程一定在队列的头。当然也存在一种情况,存在向一个正在休眠的进程发送的信号,需要遍历整个队列找到他,然后调整p_nextready来把他移出队列。如果进程正在运行,要调用pick_proc()调度一个新进程来运行。