MINIX3 进程调度分析

5.1MINIX3 进程调度概要

MINIX3 的进程调度还是非常简单的,调度算法是非常短小的,其目的就是体现 了一个简单和高效的设计原则,当然简单和高效其实很难并存,整体而言,就是 一个多队列调度算法,根据优先级来放到相应的位置。

我们现在来看下整个图示:(下面这个图其实就是 MINIX3 初始化时用户还没有 创建进程时所显示的图),在层次一,被认为是系统层,级别最高,是包括 system 和 clock 任务,之后第 2 层就是 tty(终端进程,在 I/O 中用到的一个进程,本分 析文档暂且忽略不考虑)等等,以此往上跑,等到第 16 层,也是整个调度优先 级最低的一个进程,被称为 IDLE_Q,这个进程其实就是 nop 操作,就是空操作, 不做任何事情。当进程内部所有的进程都不能运行时,才会运行这个进程,一般 这种可能性非常小,除非系统出现了故障。

现在我们往深入分析,上面分析了整个调度的一个架构图,现在我们想想看,什
么时候会启动这个调度程序 ?首先  MINIX3  是一个基于分时的操作系统 ,
time-sharing’os,也就是每次当发生时钟中断时,判断用户进程是否耗尽了它的时

75

间片,如果耗尽了它的时间片,我们将启动调度算法,来完成调度。可以看出, 这个调度程序其实就是依附在时钟任务的一个小程序,调度算法不会单独单独成 为一个进程而存在。这点非常的重要。

其次,我们想想,如果当前的进程处于阻塞或者挂起状态,我们该怎么处理呢? 我们也会进入内核来完成相关的调度工作。

5.2 MINIX3 进程调度源码分析

基本上就是以上 2 种情况。我们现在往源码内部分析,!

在分析之前我们应该先看下调度涉及到得一个数据结构,数据结构示意图就是上 面那副,现在看看数据结构的具体内容:

EXTERN struct proc  *rdy_head[NR_SCHED_QUEUES];  /* ptrs to ready list headers  */

EXTERN struct proc  *rdy_tail[NR_SCHED_QUEUES];  /* ptrs to ready list tails  */、

这是2个指针数组,也就是图的两边,rdy_head[]数组是调度队列的对头,

rdy_tail[]是调度队列的队尾。这里的队列不是普通意义上的队列,是队列的一 个变形,队头和队尾都是指向进程的,这样的处理就是为了插入的高效率。 这里就不分析其中的效率。

继续往下面看,现在看看是哪些域把这些进程连接起来的,

struct proc  *p_nextready;  /* pointer to next ready process  */

这个域就是指向下一个进程。

继续分析与调度算法相关的进程表项域

char p_priority;

//当前进程优先级

char p_max_priority;
//调度队列最大优先级
char p_ticks_left;
//进程还有多少时钟剩下
char p_quantum_size;
//本进程有多少个时钟

clock_t p_user_time;

//用户时间

clock_t p_sys_time;

//系统时间

/* current scheduling priority  */

/* maximum scheduling priority  */

/* number of scheduling ticks left  */

/* quantum size in ticks  */

/* user time in ticks  */

/* sys time in ticks  */

现在我们分析源码,由于涉及到调度的源码很少,主要是在 proc.c 和 restart.c 中 涉及到。先分析 proc.c

看下  2  个函数与进程外界调度的接口函数  :enqueue()和  dequeue()函数以及 lock_enqueue()和 lock_enqueue()调度函数。

可以看出 sched()和 pick_proc()函数是一个功能函数,就是提供给 enqueue()和

76

dequeue()函数使用。

/*=================================================================== ========*

* enqueue *

*====================================================================

=======*/

PRIVATE void enqueue(rp)

register struct proc  *rp; /* this process is now runnable  */

{

/* Add ‘rp‘ to one of the queues of runnable processes.    This function
is responsible for inserting a process into one of the scheduling queues.
* The mechanism is implemented here.      The actual scheduling policy is

* defined in sched() and pick_proc().

将‘rp’加到其中一个运行进程队列中。这个函数主要负责将一个进程插入到

77

调度队列中。这种机制就在这里实现。事实上真正意义上的调度策略是被定义在 sched()和pick_proc()函数

*/

int q; /* scheduling queue to use  */

int front; /* add to front or back  */

#if DEBUG_SCHED_CHECK

check_runqueues("enqueue");

if  (rp->p_ready) kprintf("enqueue() already ready process\n"); #endif

/* Determine where to insert to process.  */

这个函数在下面会有介绍,主要是用于决定插入哪个进程

sched(rp, &q, &front);

/* Now add the process to the queue.  */

//现在就将进程加入到队列中 这里的q代表的就是哪个队列号,由前面的sched() 函数加入。

if  (rdy_head[q]  == NIL_PROC)  { /* add to empty queue  */

//由前面的函数可以知道,q就是想要的运行队列,如果这个队列上暂且没有进

//程,则必须要创造一个一个新的队列,并且将本进程发到进程里准备投入使用。

rdy_head[q]  = rdy_tail[q]  = rp; /* create a new queue  */

rp->p_nextready  = NIL_PROC; /* mark new end  */

}

//如果是队列已经有进程,我们需要仔细往下处理

//如果进程的时间有剩余,则将本进程加入队列表的表头

else if  (front)  { /* add to head of queue  */

rp->p_nextready  = rdy_head[q]; /* chain head of queue  */

rdy_head[q]  = rp; /* set new queue head  */

}

//如果时间用完了,就应应该加入到队列表的末尾。

else  { /* add to tail of queue  */

rdy_tail[q]->p_nextready  = rp; /* chain tail of queue  */

rdy_tail[q]  = rp; /* set new queue tail  */

rp->p_nextready  = NIL_PROC; /* mark new end  */

}

/* Now select the next process to run.  */ 现在选择下一个进程进程运行

pick_proc();

#if DEBUG_SCHED_CHECK
rp->p_ready  =  1;

78

check_runqueues("enqueue");
#endif

}

/*=================================================================== ========*

* sched 这个函数和下个函数是MINIX调度器的主要

*内容,通过这个函数计算出谁应该被调度,之后将相应的值给设定好之后,在 内核中

另外一个函数restart就从硬件上进行真正意义上的调动

*==================================================================== =======*/

PRIVATE void sched(rp, queue, front)

register struct proc  *rp;

//计划被调度的进程

int  *queue;

//返回值1:被使用的队列号 int  *front;

//前者或者是后者

{

/* process to be scheduled  */

/* return: queue to use  */

/* return: front or back  */

/* This function determines the scheduling policy.    It is called whenever
a process must be added to one of the scheduling queues to decide where
to insert it.    As a side-effect the process‘ priority may be updated.
这个函数决定调度策略。这个函数被调用出来决定一个进程必须加到调度队列中
并且决定将这个进程插入哪里。一个负面效应就是这个进程的优先级必须被改
变。

*/

static struct proc  *prev_ptr  = NIL_PROC;/* previous without time  */
int time_left  =  (rp->p_ticks_left  >  0);  /* quantum fully consumed */
//这个变量决定是否被消耗完

int penalty  =  0; /* change in priority  */

//这个变量是用于改变进程的优先级

/* Check whether the process has time left. Otherwise give a new quantum
* and possibly raise the priority.    Processes using multiple quantums
* in a row get a lower priority to catch infinite loops in high priority

* processes  (system servers and drivers).

检查这个进程是否有时间剩余,如果没有时间剩余,就给出一个新的量值并且可 能会提示这个进程的优先级。正在使用多值的量值(在数组中)的进程得到一个 较低的优先级来捕获这个高优先级进程的无限循环。

*/

//如果time_left==0,也就是时间消耗完了,就进入if内部。

if  (  ! time_left)  { /* quantum consumed  ?  */

//重新给这个进程分配一个量值

79

rp->p_ticks_left  = rp->p_quantum_size;  /* give new quantum  */

//这里是如果前一个进程也是rp,则准备捕获这个无限循环,但是必须把本进程 //的优先级降低一位,也就是把处罚值加1

if  (prev_ptr  == rp) penalty  ++; /* catch infinite loops  */

else penalty  --; /* give slow way back  */

//这里将prev_ptr执行rp,主要是为了下面的调度做好准备

prev_ptr  = rp; /* store ptr for next  */

}

/* Determine the new priority of this process. The bounds are determined
* by IDLE‘s queue and the maximum priority of this process. Kernel task
* and the idle process are never changed in priority.
决定这个进程的新优先级。界限是由IDLE和这个进程最大的优先级所决定。内核 任务和IDLE进程是从来不会来改变优先级的

*/

//如果惩罚值不是0并且rp不是内核进程,则可以改变其优先级。

//改变的方法很简单:就是将本进程的优先级加上处罚值。并且检查是否超出优 先级的上界和优先级的下界。

if  (penalty  !=  0 &&  ! iskernelp(rp))  {

rp->p_priority  += penalty; /* update with 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;

}

/* check upper bound */

/* check lower bound  */

/* If there is time left, the process is added to the front of its queue, * so that it can immediately run. The queue to use simply is always the    process‘ current priority.

如果这里有时间剩余,进程被加到它的队列的前端。所以它能够马上投入运 行。这个队列总是使用简单的进程当前优先级

*/

*queue  = rp->p_priority;
*front  = time_left;
}

/*=================================================================== ========*

* pick_proc 这个函数从字面上就能够看出:这是一个选择进

程的函数。 *

*==================================================================== =======*/

PRIVATE void pick_proc()

80

{

/* Decide who to run now.    A new process is selected by setting ‘next_ptr‘. * When a billable process is selected, record it in ‘bill_ptr‘, so that the clock task can tell who to bill for system time.
决定现在谁来运行。一个新的进程被选择处理 通过将其设定为next_ptr指向的 地方。当一个付账进程被选择,将它记录在bill_ptr里。所以系统任务能够告诉 是谁为这个系统时间付账

*/

register struct proc  *rp; /* process to run  */

int q; /* iterate over queues  */

/* Check each of the scheduling queues for ready processes. The number of queues is defined in proc.h, and priorities are set in the task table.
* The lowest queue contains IDLE, which is always ready.
为每个正在准备的进程检查每一个调度队列。队列的数量被定义在proc.h文件
中,并且优先级被设定在任务进程表中。最低进队列包括IDLE进程,这个进程总 是在就绪队列中。

*/

//这个函数主要是找从低优先级往高优先级扫描,只有有一个队列的队列头不是 //NIL_PROC,就将next_ptr设置成相应指向的进程-----rp。Next_ptr在后面会 //restart函数会有用。

for  (q=0; q  < NR_SCHED_QUEUES; q++)  {

if  (  (rp  = rdy_head[q])  != NIL_PROC)  {

next_ptr  = rp; /* run process ‘rp‘ next  */

if  (priv(rp)->s_flags & BILLABLE)

bill_ptr  = rp;

return;

}

}

}

/* bill for system time  */

/*=================================================================== ========*

* dequeue 将正在运行的进程从调度队列中移除

*

*==================================================================== =======*/

PRIVATE void dequeue(rp)

register struct proc  *rp; /* this process is no longer runnable  */

{

/* A process must be removed from the scheduling queues, for example, because it has blocked.    If the currently active process is removed, a new process is picked to run by calling pick_proc().

81

一个进程必须从调度队列中去除掉。举一个例子,因为他已经阻塞了。如果当前
活动进程被移除,一个新的进程需要被选择来运行,通过调用pick_proc()函数
*/

register int q  = rp->p_priority; /* queue to use  */

register struct proc  **xpp; /* iterate over queue  */

register struct proc  *prev_xp;

/* Side-effect for kernel: check if the task‘s s tack still is ok? */ //检查这个进程是不是内核进程,如果是内核进程,就应该做相应的处理
if  (iskernelp(rp))  {

if  (*priv(rp)->s_stack_guard  != STACK_GUARD)

panic("stack overrun by task", proc_nr(rp));
}

#if DEBUG_SCHED_CHECK

check_runqueues("dequeue");

if  (! rp->p_ready) kprintf("dequeue() already unready process\n"); #endif

/* Now make sure that the process is not in its ready queue. Remove the
* process if it is found. A process can be made unready even if it is
not    running by being sent a signal that kills it.

现在我们确保这个进程不在他的准备队列中,如果发现这个进程就移除这个进 程。如果一个进程不在运行队列中,并且发送了一个信号杀死这个进程 则这个 进程能够移除不在等待状态。

*/

//上面的注释看起来比较费劲,我们现在来往下面好好看下代码。
//

prev_xp  = NIL_PROC;

//扫描这个q队列,找到要找到的进程,将他从就绪队列中移除。

for (xpp = &rdy_head[q]; *xpp != NIL_PROC; xpp = &(*xpp)->p_nextready)

{

//找到要移除的进程,我们就进入if内部

if  (*xpp  == rp)  { /* found process to remove  */

//找到了,就替换将这个进程替换掉,这个语句是结合本循环最后一条语句实现

//的,注意看最本质的内容,这里的移除仅仅就是将调度队列绕过本进程。而需

要移除的进程不会做任何的处理。

*xpp  =  (*xpp)->p_nextready; /* replace with next chain */

//如果rp是对尾队列,则将队尾队列队尾指向遍历搜索链表的已经储存的前一个

表项。

if  (rp  == rdy_tail[q]) /* queue tail removed  */

rdy_tail[q]  = prev_xp; /* set new tail  */

if  (rp  == proc_ptr  || rp  == next_ptr) /* active process

82

removed  */

//如果rp是当前活动的进程,应该调用pick_proc()函数,重新选择一个进程投 //入使用

pick_proc(); /* pick new process to run  */

break;

}

prev_xp  =  *xpp;  /* save previous in chain  */

//储存遍历搜索的链表的前一个表项,和前面的语句结合使用来将本进程移除队

}

#if DEBUG_SCHED_CHECK
rp->p_ready  =  0;

check_runqueues("dequeue");
#endif

}

/*=================================================================== ========*

* lock_enqueue *

*====================================================================

=======*/

PUBLIC void lock_enqueue(rp)

struct proc  *rp; /* this process is now runnable  */

{

/* Safe gateway to enqueue() for tasks.  */

lock(3, "enqueue");

enqueue(rp);

unlock(3);

}

/*=================================================================== ========*

* lock_dequeue *

*====================================================================

=======*/

PUBLIC void lock_dequeue(rp)

struct proc  *rp; /* this process is no longer runnable  */

{

/* Safe gateway to dequeue() for tasks.  */
lock(4, "dequeue");

dequeue(rp);

83

unlock(4);

}

5.3 MINIX3进程调度与中断返回处理的结合

我们现在来回顾前面中断系统讲到的restart函数和结合内核调度实例来进一步 分析整个调度机制

先看restart函数,同样,先把restart函数源码附上:

!*=========================================================== ================*

!* restart    我们从 hwint_inhandler(irq)进入到这个函数,我们现在

来着重分析这个函数 !在分析之前,我们同样要搞清楚栈和相关寄存器的内

容,首先栈还是内核栈

之后 CS 和 IP 同样是指向这个函数的首地址,现在我们进入函数内部分析

*

!*=========================================================== ================*

_restart:

! Restart the current process or the next process if it is set.

!!这里非常的重要,因为这里主要是用于进程的调度。

cmp (_next_ptr), 0 ! see if another process is scheduled,这里的_next_ptr

表示在运行 restart 之后指向的进程地址。其实也就是

jz 0f

mov     eax, (_next_ptr)  !如果不是为 NULL,则执行流就从这里开始

!将_next_ptr 的值存入 eax,之后将 eax 的值设置存入_proc_ptr 内存地址中,pro_ptr 是指向当前进程指针,这些指针都是已经设定好的,这个指针就就是用于下次返 回所运行的指针

mov (_proc_ptr), eax ! schedule new process

mov (_next_ptr), 0! 这里为啥把_next_ptr 设置成为 NULL,主要原因是标

志下次在执行

!这个 restart 例程时是否有重新设置上面那 2 个指针。如果没有就直接进入 0 标志,这个就是从最高代码效率来考虑

这里就是通过设置前面的next_ptr指针,前面pick_proc()函数设置了next_ptr,
就是供这里使用。这个的_next_ptr和NULL比较是有原因的,你们可以想象一下,
如果发生非时钟中断时可能还是本进程作为下一次调度的对象,不是时钟中断
时,我们是可能不会来调用调度算法,那么直接就将本进程继续投入使用。并且
我们这里_next_ptr是上面调度算法函数和本restart的一个枢纽。上面的调度算
法函数就是选出了_next_ptr这个函数,现在_restart函数就将这个_next_ptr
函数投入使用。

84

5.4MINIX3时间调度与内核其他模块的关系

哪里会出现调度进程调度函数呢?时钟中断有可能就会调用这个调度队列。之后 相关的消息机制也是会调用这个调度队列:

用2个图形给予说明:

第一个就是时钟机制可能导致触发这个调度算法函数:

第2种可能就是消息机制触发这个调度过程:

85

MINIX3的调度分析基本上分析完,对于MINIX3而言,进程调度算法简单,实效便 于理解!

时间: 2024-11-07 14:21:45

MINIX3 进程调度分析的相关文章

MINIX3 导读分析

一个操作系统的分析是属于一个非常庞大的工程,操作系统就像是一个人造的 人,每一个模块想完全发挥功效,很有可能需要很多模块的支持才能够实现.所 以在分析 MINIX3 时,我认为同时看多个模块对于理解 MINIX3 是有好处的,特 别是因为 MINIX3 是采用微内核结构,也就造成阅读源码的一个比较大的障碍. 在此我统领的描述下 MINIX3 和 PM 部分内容,给读者 MINIX3 形成一个整体的 导读框架: 1 MINIX3 采用微内核结构,什么是微内核呢?MINIX3 设计者认为除了部分非

Linux0.11内核--进程调度分析之2.调度

上一篇说到进程调度归根结底是调用timer_interrupt函数,在system_call.s中: #### int32 -- (int 0x20) 时钟中断处理程序.中断频率被设置为100Hz(include/linux/sched.h,5), # 定时芯片8253/8254 是在(kernel/sched.c,406)处初始化的.因此这里jiffies 每10 毫秒加1. # 这段代码将jiffies 增1,发送结束中断指令给8259 控制器,然后用当前特权级作为参数调用 # C 函数do

Linux0.11内核--进程调度分析之1.初始化

首先看main.c里的初始化函数main函数里面有个函数是对进程调度的初始化,sched_init()函数,次函数在sched.c中实现: // 调度程序的初始化子程序. void sched_init (void) { int i; struct desc_struct *p; // 描述符表结构指针. if (sizeof (struct sigaction) != 16) // sigaction 是存放有关信号状态的结构. panic ("Struct sigaction MUST be

通过gdb跟踪进程调度分析进程切换的过程

作者:吴乐 山东师范大学 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 本实验目的:通过gdb在linux下对一个简单的命令行命令实现进程的过程进行跟踪,分析一般用户进程实现进程切换的过程,并进一步剖析进程调度的工作的原理. 一.实验过程 1.打开实验环境,并设置context_switch和pick_next_switch两个断点. 2.来到第二个断点处list(分析在第三部分) 3.到第一个断点处,在这里

Linux Kernel - Debug Guide (Linux内核调试指南 )

http://blog.csdn.net/blizmax6/article/details/6747601 linux内核调试指南 一些前言 作者前言 知识从哪里来 为什么撰写本文档 为什么需要汇编级调试 ***第一部分:基础知识*** 总纲:内核世界的陷阱 源码阅读的陷阱 代码调试的陷阱 原理理解的陷阱 建立调试环境 发行版的选择和安装 安装交叉编译工具 bin工具集的使用 qemu的使用 initrd.img的原理与制作 x86虚拟调试环境的建立 arm虚拟调试环境的建立 arm开发板调试环

linux内核调试指南

linux内核调试指南 一些前言 作者前言 知识从哪里来 为什么撰写本文档 为什么需要汇编级调试 ***第一部分:基础知识*** 总纲:内核世界的陷阱 源码阅读的陷阱 代码调试的陷阱 原理理解的陷阱 建立调试环境 发行版的选择和安装 安装交叉编译工具 bin工具集的使用 qemu的使用 initrd.img的原理与制作 x86虚拟调试环境的建立 arm虚拟调试环境的建立 arm开发板调试环境的建立 gdb基础 基本命令 gdb之gui gdb技巧 gdb宏 汇编基础--X86篇 用户手册 AT&

MINIX3 内核时钟分析

4.1 内核时钟概要 先想想为什么 OS 需要时钟?时钟是异步的一个非常重要的标志,设想一下,如 果我们的应用程序需要在多少秒后将触发某个程序或者进程,我们该怎么做到? 就需要一个时钟的鼎力相助才能完成这个项工作.我的意思非常明确,就是说内 核时钟就是一个非常重要的部件,它可以完成分时系统的调度功能,同时它也能 为应用程序提供一个非常方便的异步处理问题的功能 现在我们简要的来看下时钟芯片的构造基本原理,如果需要了解详细的时钟芯 片,可以参考 intel 时钟芯片的详细编程资料. 一般时钟芯片就是

对Minix3中进程模型的简要分析

简单介绍 Minix(Mini UNIX)原来是荷兰阿姆斯特丹的Vrije大学计算机科学系的Andrew S. Tanenbaum教授所发展的一个类Unix操作系统. 目前的Minix版本为Minix 3,是一个免费.开源的操作系统,设计目标是实现高可靠性.灵活性及安全性. 其系统主要包括在核心模式下运作的微核心和在用户模式下作为一系列独立.受保护的进程运行的其余所有操作系统组件. Minix3的整体认识 MINIX3本身就是一组进程的集合 第一层的主要功能是为上层驱动程序和服务器提供一组特权内

Linux内核设计第八周学习总结 理解进程调度时机跟踪分析进程调度与进程切换的过程

陈巧然 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 一.视频内容 Linux系统的一般执行过程 最一般的情况:正在运行的用户态进程X切换到运行用户态进程Y的过程 1. 正在运行的用户态进程X 2. 发生中断——save cs:eip/esp/eflags(current) to kernel stack, then load cs:eip(entry of a specific IS