在Hello China的实现中,一个全局对象KernelThreadManager(核心线程管理对象)用来完成对整个操作系统线程的管理,包括线程的组织、创建、销毁、修改优先级等操作,该对象的定义如下(为了描述方便,删除了部分注释):
这是一个比较大的对象,其中,四个优先级队列和一个优先级队列数组用于对系统中的线程进行管理,每个队列的用途见表4-1。
表4-1 Hello China的线程队列
①对于单CPU的情况,有且只有一个线程处于运行状态(即任何时刻,只有一个线程获得CPU资源,处于运行状态),这种情况下,该队列未被使用。但在多处理器情况下,任何一个时刻,有与系统中CPU数量相同的线程在运行,这样为了便于管理,设置此队列,用于管理多CPU情况下的运行态线程。
这些线程的管理队列比较容易理解,处于某一状态的核心线程,会被放入对应状态的队列。线程在不同状态之间的转换,实际上就是在不同队列之间的转移操作。比如,线程状态从睡眠转到就绪,则核心线程对象会被从睡眠队列中删除,然后加入就绪队列。需要注意的是,就绪队列不是一个单一的队列,而是一个包含16个优先队列对象的队列数组,数组中的每个元素,对应相应优先级的核心线程队列。图4-1说明了这种关系。
图4-1 Hello China的核心线程队列
缺省情况下,Hello China采用的是严格基于线程优先级的调度方式。优先级为0的核心线程的调度优先级最高,如果优先级为0的就绪队列中存在就绪的核心线程,那么优先级是1或者大于1的核心线程,将没有机会得到调度。相同优先级的核心线程,按照轮转方式调度,即首先调度队列中的第一个核心线程,运行一个时间片后,第一个线程会被重新插入就绪队列的尾部,然后再调度队列中的第二个核心线程,这样依此类推。这种调度方式符合大部分应用场景下的需求,尤其是嵌入式系统的需求。但存在低优先级线程饿死的问题。不过可通过修改调度算法,来避免这个问题。
在Hello China V1.75的实现中,线程调度算法被封装到两个函数中——GetSchedule KernelThread和AddReadyKernelThread。第一个函数的功能是从就绪队列数组中,提取一个优先级最高的就绪状态的核心线程。操作系统的调度程序会调用这个函数,选择一个就绪状态的核心线程去执行。而AddReadyKernelThread的作用则相反,用于向就绪队列数组中增加一个状态为READY的核心线程。在核心线程状态转换的时候,比如从睡眠状态转换到就绪状态,转换代码会调用该函数,把就绪线程加入就绪队列数组中。在当前的实现中,这两个函数都是按照严格优先级的调度方式实现的。下面看一下其代码:
函数代码比较简单,无非是按照从低到高的顺序,依次检查就绪队列数组,试图从优先级最高的就绪队列中提取一个核心线程。如果提取成功(GetHeaderElement返回核心线程的指针),则直接返回这个核心线程对象,否则继续检查下一个优先级的就绪队列。如果所有就绪队列中都没有核心线程对象,则返回NULL。(www.xing528.com)
下面是AddReadyKernelThread的实现代码:
上述函数首先检查核心线程对象优先级的合法性,这是非常有必要的,因为当前的实现是把线程的优先级作为索引,来直接索引就绪队列数组的。因此,万一核心线程对象的优先级超出预定范围,会导致数组越界访问。当然,优先级超出预定范围的核心线程对象,也必然是一个非法的核心线程对象。
另外,在核心线程管理对象的初始化函数(Initialize)中,需要对就绪队列数组进行初始化,即创建每一个就绪队列数组对象。
需要注意的是,还有一种线程状态——阻塞状态没有体现在上述队列中。因为线程的阻塞状态一般是因为该线程要请求一个共享资源,而该共享资源又不可用(被其他线程占用),这时候线程进入阻塞状态。进入阻塞状态的线程会被暂时存放在共享资源的阻塞队列中,因此没有必要专门设置一个全局队列来管理阻塞状态的线程。详细信息在介绍同步对象的时候会提到。
该对象还提供了大量的接口,用于完成对线程的操作。表4-2给出了操作动作和对应的操作函数。
表4-2 核心线程管理对象提供的接口
上述函数可以被应用程序直接调用,来完成对Hello China线程的操作。另外的几个函数,比如ScheduleFromProc、ScheduleFromInt等,是操作系统完成线程切换的功能函数。这些函数被操作系统核心代码调用,一般不建议应用程序直接调用这些函数,但为了简便起见,把这些函数也纳入KernelThreadManager的管理范围。
另外,dwNextWakeupTick变量用于管理线程的睡眠功能。该变量记录了需要最早唤醒的线程和应该唤醒的时刻(tick数目)。比如,当前的时钟tick是1000,有三个线程,分别调用了Sleep函数,示意代码如下:
并假设这三个线程对Sleep函数的调用发生在同一个时间片内,这样需要最早唤醒的线程应该是Thread2。于是,Sleep函数在执行的时候,把以毫秒(ms)为单位的参数,转换为时钟tick数,然后与当前的系统tick数(System对象维护,详细信息请参考第8章)相加,并赋给dwNextWakeupTick变量。
这样每次时钟中断处理的时候,操作系统把dwNextWakeupTick与当前的时钟tick数进行比较,若一致,则说明已经到达唤醒线程的时刻,于是从睡眠队列(lpSleepingQueue)中取出所有需要唤醒的线程,插入就绪队列数组。
另外需要注意的是,KernelThreadManager是一个全局对象,整个系统中只有一个这样的对象,因此,该对象没有从__COMMON_OBJECT对象派生。对于该对象提供的功能函数(比如CreateKernelThread等),为了保持面向对象的语义,其参数列表也与其他对象一样,第一个参数是lpThis(一个指向自己的指针),实际上,可以不用这个参数,而直接引用KernelThreadManager对象。
免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。