第九章 执行绪在队列之间的转换分析
第三章列出了Windows CE执行绪在排程时的几个主要函数,在本章中将借着分析这些函数的流程,来了解执行绪在排程过程中的行为。本章所节选的程序代码全部来自[CEROOT]/PRIVATE/WINCEOS/COREOS/NK/KERNEL目录下的 schedule.c档,由于程序代码在第三章已详细列出,所以本章除了一些重要的分析之外,其它程序代码均省略。
9.1概述
Windows CE是一个实时的操作系统 (real-time operating system),在这里我们将会讨论实时执行绪抢占处理器的情况。当一个优先权较高的执行绪处于可执行状态时,它将抢占处理器资源;也就是说,正在执行的执行绪将转到可执行状态,并且不再占有处理器资源,而由优先权高的执行绪占用,直到这个高优先权的执行绪执行一定的时间后,如time slice用完,或者优先权降低,才会重新考虑让先前让出处理器资源的执行绪重新执行。实际上,执行绪有以下状态:
RUNSTATE_RUNNING 正在执行
RUNSTATE_RUNNABLE 可以执行
RUNSTATE_BLOCKED 可执行态的停滞态,可能是自愿进入停滞态
RUNSTATE_NEEDSRUN 即将进入可执行状态
WAITSTATE_SIGNALLED 等待某个信号的唤醒
WAITSTATE_PROCESSING 重新处理等待态
WAITSTATE_BLOCKED 等待状态的停滞态
大致上可以分为,执行态、可执行态、停滞态(睡眠态),上面是再细分的状态。如果不考虑细分的状态,我们可以得到执行绪排程的基本切换条件,如图9.1所示:
图9.1 执行绪的状态切换
我们可以用书中介绍的侦错工具在程序代码中设定断点,然后一步一步追踪在处理程序排程和切换过程中Windows CE程序代码的执行流程,也会得到类似的状态图。
上面介绍的是概略的状态切换图,那么在实际切换过程中,例如从一种状态转换到另一种状态时,实际上必须要做哪些事情呢?下面我们将结合程序代码来做具体的说明。
9.2 具体分析
在状态转换的过程中,执行绪在不同的队列之间切换,一个执行绪从一个队列切换到另外一个队列,也就是从前一个队列中删除掉,而加到另外一个队列中去,在什么情况下会导致执行绪从队列中删除呢?结合上面的状态转换图,可以看出,如果执行绪被排程,也就是说当执行绪可以取得处理器资源,那么该执行绪便从执行队列中删除。
同样的,在睡眠队列中的执行绪一定是等待某一个条件或某个事件发生才能执行的执行绪,如果这个执行绪从睡眠队列中删除,说明这个执行绪所等待的条件已被满足,或者是等待的某个事件已经发生,因此执行绪可以就绪,伺机取得处理器资源了。这个执行绪自队列中被删除后,将处于可执行状态。
9.2.1 MakeRun()函数分析
当一个执行绪被建立后,首先呼叫MakeRun这个函数来判断是否执行,流程图如图9.2所示:
否 |
MakeRun() |
执行绪的暂停 次数是否为 0 |
没有可执行的执行绪或本身就是最高优先权的执行绪 |
是否有其它的执行绪 正在执行或者此执行 绪的优先权是最高的 |
目前优先权等 级的可执行伫 列是否为空 |
优先权小于可执行 队列开头的执行绪 |
将目前执行绪放入可执行伫 列的第一位置并更新杂凑表
|
找到前面非0的杂凑表 入口 (hash table entry) |
插入可执行队列 并更新杂凑表 |
重新排程 |
是 |
是 |
是 |
是 |
否 |
停滞 (blocked) |
否 |
找到适当的位置插入 |
否 |
图9.2 MakeRun 函数流程图
在这个函数中首先检查暂停次数,也就是一函数,流程图如下pth->bSuspendCnt这个参数,判断系统中是否已有等待要执行的执行绪。如果有,表示目前有执行绪正在执行,那么就停滞这个请求执行的执行绪。如果暂停次数为0,再进一步判断是否有可执行的执行绪,或者执行绪本身即为系统中具有最高优先权的执行绪。如果执行绪本身就是最高优先权的执行绪,那么就把请求的执行绪设为可执行,并且放置在可执行队列的最前面,更新相应的数据。接下来,进一步判断是否有别的执行绪正在执行,并确认目前的执行绪确实是系统中优先权最高的执行绪。函数中对应程序代码如下:
if (!(pth2 = RunList.pRunnable) || (prio < GET_CPRIO(pth2))) {
//没有其它可执行的执行绪或本身就是优先权最高的执行绪
//使目前执行绪位于可执行队列的开头并更新hash table
pth->pPrevSleepRun = 0;
if (pth->pNextSleepRun = pth2) {
pth2->pPrevSleepRun = pth;
}
pth->pUpRun = pth->pDownRun = RunList.pHashThread[prio2] = pth;
RunList.pRunnable = pth;
// 判断是否需要重新排程
if (!RunList.pth || (prio < GET_CPRIO(RunList.pth)))
SetReschedule();
}
最后一步应该考虑到的是:在设定之后是否有中断发生,是否改变了系统的状态。如果没有别的执行绪在执行,而此执行绪确实是系统中优先权最高的执行绪,则更新对该执行绪的排程策略,使该执行绪可以立刻执行。当然如果目前有别的执行绪正在执行,或者请求的执行绪并不是系统中优先权最高的执行绪,则把执行绪插入相应的队列。
9.2.2 RunqDequeue()函数分析
正在执行的执行绪什么时候让出CPU呢? 答案是当分配到的time slice用完,或者有更高优先权的执行绪进行抢占时。让出CPU的执行绪会被排到执行队列的最后面,下面我们主要观察一个执行绪在执行队列和睡眠队列之间的变化情况。当要把一个执行绪从执行队列中删除的时候,必须经过以下步骤。首先判断要删除的执行绪的pDown指标是否为NULL,对应的程序代码是函数中最开头的部分:流程图如图9.3所示:
目前执行绪和 pDown 所指的执行绪 优先等级是否相同 |
RunqDequeue |
pDown 指标是 否不为NULL |
关于 proxy 的 一些操作 |
把执行绪从可执行队列中移除 并对队列做必要的修改 |
是 |
是 |
否 |
否 |
呼叫 MakeRun |
否 |
以 pDown 替换执行绪所占的 slot 并设定 pDown 为可执行状态 |
是 |
pDown 指的执行绪 是否可执行 |
图9.3 RunDequeue 函数流程图
如果pDown与执行绪的pDownSleep指标所指的执行绪不相同,也就是说pth->pDownSleep为NULL,则直接删除执行绪,同时对队列作一些必要的修改。
pDown = pth->pDownRun;
pNext = pth->pNextSleepRun;
if (RunList.pHashThread[prio] == pth) {
RunList.pHashThread[prio] = ((pDown != pth) ? pDown :
(pNext && (GET_CPRIO(pNext)/PRIORITY_LEVELS_HASHSCALE == (WORD)prio)) ?
pNext : 0);
}
if (pDown == pth) {
if (!pth->pPrevSleepRun) {
DEBUGCHK(RunList.pRunnable == pth);
if (RunList.pRunnable = pNext)
pNext->pPrevSleepRun = 0;
} else {
if (pth->pPrevSleepRun->pNextSleepRun = pNext)
pNext->pPrevSleepRun = pth->pPrevSleepRun;
}
} else {
pDown->pUpRun = pth->pUpRun;
pth->pUpRun->pDownRun = pDown;
if (pth->pPrevSleepRun) {
pth->pPrevSleepRun->pNextSleepRun = pDown;
pDown->pPrevSleepRun = pth->pPrevSleepRun;
goto FinishDequeue;
其中FinishDequeue对应的是以下的程序代码:
if (pNext) {
pNext->pPrevSleepRun = pDown;
pDown->pNextSleepRun = pNext;
}
如果相等,则说明需要唤醒所指向的执行绪。下一步要做的就是判断pDown是否是可执行的,如果不是,则同上一步,直接删除要删除的执行绪,作必要的修改即可。如果不能执行,则处理一些与proxy相关的操作。
如果不是纯粹睡眠,则建立proxy以便之后的删除动作。DEBUGCHK开头的程序代码为侦错时使用,正式编译时可以去除这些程序代码
DEBUGCHK(GET_RUNSTATE(pDown)==RUNSTATE_BLOCKED);
if (pDown->lpce) { // not set if purely sleeping
pDown->lpce->base = pDown->lpProxy;
pDown->lpce->size = (DWORD)pDown->lpPendProxy;
pDown->lpProxy = 0;
} else if (pDown->lpProxy) {
// must be an interrupt event - special case it!
LPPROXY pprox = pDown->lpProxy;
LPEVENT lpe = (LPEVENT)pprox->pObject;
DEBUGCHK(pprox->bType == HT_MANUALEVENT);
DEBUGCHK(pprox->pQDown == pprox);
DEBUGCHK(pprox->pQPrev == (LPPROXY)lpe);
DEBUGCHK(pprox->dwRetVal == WAIT_OBJECT_0);
lpe->pProxList = 0;
pprox->pQDown = 0;
pDown->lpProxy = 0;
判断pDown所指之执行绪的优先权是不是和目前执行绪的优先权相同,如果相同,则以该执行绪取代目前执行绪所占的位置,并且把它设定为可执行的状态(RUNNABLE),如下所示:
if (RunList.pHashThread[prio] == pth)
RunList.pHashThread[prio] = pDown;
SET_RUNSTATE (pDown, RUNSTATE_RUNNABLE
反之,如果不相等,则以pDown作为参数,呼叫MakeRun (参见上面对MakeRun函数的解释),接着对执行队列做一定的修改,流程图如图9.3所示。
9.2.3 SleepqDequeue()函数分析
接下来我们将讨论从等待队列中删除一个执行绪 (pth) 的时机,首先判断pth->pUpSleep参数 (指向上一个执行绪,它执行完后执行绪本身才被唤醒) 是否为NULL,对应程序代码为下面程序代码的第一行:
if (pth2 = pth->pUpSleep) {
……
} else if (pth2 = pth->pDownSleep) {
……
} else if (pth2 = pth->pPrevSleepRun) {
……
} else {
……
}
如果为NULL,更新要删除执行绪的pUpSleep以及pDownSleep (指向等待此执行绪执行完后才唤醒的执行绪) 这两个指标,然后直接呼叫CLEAR_SLEEPING(pth) 即可:
if (pth2->pDownSleep = pth->pDownSleep) {
pth2->pDownSleep->pUpSleep = pth2;
pth->pDownSleep = 0;
}
pth->pUpSleep = 0;
……
CLEAR_SLEEPING(pth); //此行在函数的最后一行
如果不为NULL,接着判断pth->pDownSleep参数是否为NULL,即下面所示的程序代码:
} else if (pth2 = pth->pDownSleep) { …… }
pth->pDownSleep如果为NULL,类似上一步,更新要删除之执行绪的pPrevSleepRun以及pNextSleepRun这两个指标,然后直接呼叫CLEAR_SLEEPING(pth)即可。如果不为NULL,判断pth->pPrevSleepRun参数 (上一个睡眠的或可执行的执行绪) 是否为NULL,即下面所示的程序代码:
} else if (pth2 = pth->pPrevSleepRun) { …… }
如果为NULL,更新NextSleepRun参数,让下一个执行绪的pPrevSleepRun指向目前的执行绪,也就是执行以下的程序代码:
if (pth2->pNextSleepRun = pth->pNextSleepRun) {
pth2->pNextSleepRun->pPrevSleepRun = pth2;
}
(注:pth2是为了判断执行绪需要比较的参数是否为NULL而设定的一个局部变量,详细情况请参看NK/KERNEL/schudule.c中的SleepqDequeue函数),接着也是直接呼叫CLEAR_SLEEPING(pth)。如果这一步也不相等,那么更新睡眠队列和排程时间相关的一些信息,
} else {
DEBUGCHK (pth == SleepList);
//更新睡眠队列 (SleepList) 和dwReschedTime
dwReschedTime = dwPrevReschedTime
+((RunList.pth&&RunList.pth->dwQuantum)?RunList.pth->dwQuantLeft : 0x7fffffff);
if (SleepList = pth->pNextSleepRun) {
SleepList->pPrevSleepRun = 0;
if ((int) (dwReschedTime - SleepList->dwWakeupTime) > 0) {
dwReschedTime = SleepList->dwWakeupTime;
}
}
}
然后呼叫CLEAR_SLEEPING(pth)删除pth。流程图如图9.4所示。
执行绪的 pDownSleep 是否为空 |
更新睡眠队列和 dwreschedTime |
从从睡眠队列中清除 |
否 |
是 |
更新 NextSleepRun 串行 |
SleepqDequeue |
更新 DownSleep 和 UpSleep 串行 |
执行绪的 pUpSleep 是否为空 |
执执行绪的 pPrevSleepRun 是否为空 |
否 |
是 |
否 |
是 |
更新 pNextSleepRun 和 pPrevSleepRun 指标 |
图9.4 SleepDequeue 函数流程图