一、概述
? 前面的两篇介绍了队列结构、定长存储块的管理。这一篇将以此为基础介绍 eos 内核最核心、最基础的部分,即任务的创建、任务的调度。
? 我们知道操作系统内核与与一般的前后台式系统的最大区别在于前者能并发的运行多个任务,同时会提供相关的一些服务机制,诸如任务间同步与通信、存储管理、文件系统、设备管理等,使得应用编程能够在操作系统平台上进行而相对要简单的多。在嵌入式系统中受限于硬件资源的限制,实现的内核不可能包含太多的功能。特别是像 FreeRtos, uc/os 这样的内核, 仅包含一组简单的机制。 如果做得最精简化, 那么只需要像运行于 51 单片机上的rtxtiny 那样仅包含基本的任务管理机制即可。即使包含如此简单的机制,应用操作系统在易用性上还是比传统的后台+中断的方式要容易的多。
? 这一篇只说明任务是如何创建和启动的。我想,这个问题也是最核心的。一旦理解了,那么对整个系统的运行机理也就了解的差不多了。
? 本篇大致分为如下几个部分:
? 1、eos 中任务所有的状态及状态的变迁;
? 2、任务的创建过程;
? 3、多任务的启动过程分析;
? 4、任务间切换以及调度算法说明;
二、任务的状态及状态变迁
? 如上图所示,任务的初始状态即为删除状态。所谓的删除,即指只有任务的要执行的代
码存在,而没有任务控制块( task_struct )结构,OS 没有为任务分配其它任务资源。
实际的任务状态变迁为:
1、起始态 ----- 删除态:
? 此状态下仅存在任务执行所需的指令代码, 没有为任务的运行分配相应的控制块, OS堆栈等资源。
? 目的态 ------ 就绪态;
? 当调用 task_create()后,OS 为任务分配任务控制块和堆栈,并按要求进行初始化。
? 这样任务具备了运行的条件进入就绪态。
2、起始态 ---- 就绪态
? 此状态下,任务具备运行条件,没有等待任务事件和其它的资源,仅等待其它任务放弃cpu 后开始执行。
? 至运行态:当正在执行的任务运行时间片到或等待其它资源,也或者因为被强制删除、挂起等待外部事件发生时放弃 cpu,调度器分派 cpu 给该任务,任务此时执行自己的代码,此时由就绪态转为运行态。
? 至删除:任务被其它任务或自己强制删除、相应的资源被回收;
? 至挂起:当调用 task_suspend()时,被强制从就绪队列移除,并不再参与调度
3、起始态 ---- 挂起态
? 此状态下,任务具务运行条件,也不等待外部事件的发生,但由于不再被调度,因为不
能运行。
? 至就绪态:被其它任务调用 task_wakeup()唤醒。
? 至删除态:任务被其它任务或自己强制删除、相应的资源被回收;
4、起始态 ---- 运行态
? 此状态下,cpu 正执行该任务的代码
? 至就绪态, 当系统有更高优先级的任务就绪后, 调度器会强制中止当前运行任务, cpu 将分派给高优先级任务。
? 至删除态:任务被其它任务或自己强制删除、相应的资源被回收;
? 至阻塞态:任务因为等待外部事件或资源(如信号量) ,而暂时停止执行
5、阻塞态 ---- 阻塞态
? 此状态下,任务因为等待外部事件或资源(如信号量) ,而暂时停止执行
? 至就绪态:外部事件发生或资源可用,但优先级比当前正在运行的任务优先级更低,任务进行就绪态;
? 至运行态:外部事件发生或资源可用,且优先级比当前正在运行的任务优先级更低,任务进行就绪态;
? 至删除态:任务被其它任务或自己强制删除、相应的资源被回收;
? 任务的状态变迁,实际上不并太难理解。可以将其视为任务从“出生”到“死亡”的活
? 动过程。最初自学操作系统原理读到有关任务管理的章节时,对任务的状态图非常困惑,虽? 然能看懂,但总感觉太抽像。前面的叙述也太抽像,还必须结合代码来理解。这里,先简单? 的给个描述。整个任务的活动过程,可以简单的认为是给任务分配堆栈空间、控制块,然后? 将控制块在不同的队列中插入和删除,同时将储存与恢复处理器内寄存器的内容。不同的队? 列可能对应于不同的状态,比如说,任务控制块在就绪队列中即为就绪态,在信号量队列中? 即处理阻塞态;当任务的堆栈空间、控制块被回收了,任务也就消亡了。
? 这里再介绍下 task_struct 结构:
? /* task_t 控制块
struct _task_struct */
{
? /* 任务栈顶
? stack_t * stk_top; */
? /* 延时链接
? node_t tmo_node; */
? /* 事件链接
? node_t wait_node; */
? /* 任务当前状态 */
? uint8 state;
? /* 任务优先级
? uint8 prio; */
? /* 任务延时值
? uint16 delay; */
? /* 任务运行时间片
? uint8 slice; */
? /* 任务运行总的时间片
? uint8 total_slice; */
#if OS_TASK_SET_SUSPEND == 1
? /* 任务挂起的次数
? uint8 suspend_cnt; */
#endif
#if OS_TASK_SET_DESTORY_FUN == 1
? /* 任务删除析构函数 */
? void ( * destroy )( task_t task, void * pdata );
? void * pdata;
#endif
? /* 等待状态
? uint8 wait_state; */
? /* 任务等待的事件
? void * event; */
};
? task_struct 结构中包含了或干域,在这里我们只需要关注.prio 及之前的各项域,其
? 能已在注释中说明。
? 其中 state 域保存了当前任务的状态,.state 域对应的 8 位字节中有若干标志位,标志任? 务运行的标志。
? /* 任务状态: 延时中 */
? #define OS_TASK_STATE_DELAY 0x2
? /* 任务状态: 被挂起 */
? #define OS_TASK_STATE_SUSPEND 0x4
? * 任务状态: 阻塞等待*/
? #define OS_TASK_STATE_WAITING 0x8
? /* 等待状态:多级队列*/
? #define OS_TASK_WAIT_MLIST 0x10
? 当.state 值为 0 时,表明任务处于就绪队列中,可能为就绪态,也可能为运行态。
三、任务的创建
? 在 eos 中,创建任务的过程非常简单,即分配和初始化 task_struct 结构和任务堆栈。
? task_struct 结构中保存了任务运行的状态信息,因为处理器只有一个,而要实现并发的? 任务运行,任务必须保存运行信息,以等待恢复。可以说,task_struct 结构的存在标识了任? 务的存在。堆栈用于保存处理器相关的信息,包含寄存器的值及发生的各种调用返回值,参? 数等。关于堆栈的初始化过程,这里暂不作介绍。
? 任务的创建由 task_create()完成。
task_t task_create( task_entry_t task_entry, task_data_t pdata, stack_t * stk_base, uint8 prio )
{
? task_t task;
? --------------参数检查 略 -------------
? task = mpool_get( MPOOL_TYPE_TASK );
? if( task != (task_t )0 )
? {
? task->state = 0;
? task->prio = prio;
? ----------- 其它域初始化 -----------------
? /* 初始化任务链接结点
? task_node_init( task ); */
? /* 初始化任务堆栈
? task->stk_top = context_init( task_entry, pdata, stk_base ); */
? OS_ARCH_PROTECT();
? /* 任务加入就绪队列
? mlist_add_task( &OSRdyQ, task ); */
? OS_ARCH_UNPROTECT();
? /* 进行任务调度
? scheduler(); */
? }
? return task;
}
? 首先由 mpool_get()分配一个 task_struct 的存储块,然后初始化各项,其中 context_init()? 负责初始化任务堆栈,栈顶指针保存在 task->stk_top 域。再将任务插入就绪队列,就绪队? 列的定义为:
? mlist_t OSRdyQ;
? 当 task_struct 结构初始化后,且处于 OSRdyQ 后,任务就算是处于就绪态了.task->state? 值为0,表明任务处理就绪态。再运行 scheduler()调度器,检查任务是否是最高优先级的,? 如果是,则抢占 cpu 运行创建的任务。
? 这里的任务创建实现很简单, 因为 eos 包含的功能本能就比较少。 这样,反而易于理解 。
四、eos 的初始化与启动
? eos? 初始化过程直接由 main()函数完成。目前 eos 运行于 Atmega32 上,不需要像 ecos 那样需要嵌入式的 bootloader.。一旦代码编程完成后,编译,烧写至 mega32 内的 flash,立即可以执行。默认的代码入口即为 main()。其代码如下:
? int main( void )
? {
? /* 初始化系统时钟 */
? kernel_ticks = 0;
? /* 初始化中断嵌套计数 */
? irq_cnt = 0;
? /* 初始化调度锁计数 */
? sched_lock = 0;
? /* 初始化就绪队列
? slist_init( &OSDelayQ ); */
? /* 初始化延时队列
? mlist_init( &OSRdyQ ); */
? /* 初始化定长内存池模块 */
? mpool_init();
? /* 初始化中断系统
? irq_init(); */
? /* 调度上锁
? scheduler_lock(); */
? /* 安装时钟中断处理函数 */
? irq_attach( OS_TICK_VECT, time_tick_handler );
? task_idle = task_create( (task_entry_t)idle_task, (task_data_t)0, /* 创建空闲任务 */
? &idle_stk[IDLE_TASK_STK_LEN-1],OS_PRIO_MAX );
? eos_start();
? kernel_start();
? return 0;
? }
? 首先初始的是内核的一些诸如 kernel_ticks 的计数,再者初始化的为就绪、延时队列结构,其次是初始化一些模块,创建空闲任务。初始化完成后,调用 eos_start(),该函数是由用户定义的函数,可在其中完成硬件的初始化,任务的创建,最后调用 kernel_start()启动多任务。而 kernel_start()的实现也非常简单,代码如下:
? static void kernel_start( void )
? {
? /* 重置调度锁计数
? sched_lock = 0; */
? /* 取首个任务的 task_t */
? OSTaskCur = task_rdy_prio_max();
? OSPrioCur = OSTaskCur->prio;
? /* 初始化系统时钟
? kernel_tick_init(); */
? task_start();
? }
? kernel_start()完成的功能只是取首个任务的 task_struct 结构,初始化系统时钟后,由
task_start()恢复任务堆栈内的处理器上下文信息,该函数并不会返回到 kernel_start()函 数 ,而是直接返回至 OSTaskCur 对应的代码的入口地址。这样就由初始化函数,切换至首个任务,并开始执行。
五、如何在单处理器上实现并发的多任务?
? 在 Atmega32 上,有且仅有一块 cpu,不可能实现并行多任务,而只能够实现并发的多任务。多个任务按照某种策略轮流使用 CPU,以从整体运行结果上来实现多个任务的同时运行,但在任意时刻,仅有一个任务占用 cpu,那么如何实现这种伪并行执行?
? 首先考虑的是既然要实现多个任务并替使用 cpu,而一个任务在大多数情况下,不可能在放弃 cpu 前已经执行完毕其代码。这样,任务就需要保存运行的状态信息,其备再次执行时恢复这些信息, 使得就如同执行中没有发生中断。 任务的状态信息, 除了保存在 task_struct结构中的信息外, 还包括执行代码执止时的代码地址, 执行各种运行的局部变量, 参数等待 ,这些一般都保存在处理器内的寄存器以及堆栈中。 Atmega32 为例: Atmega32 有 32 个通用 以寄存器 R0-R31,状态寄存器 SREG, 按照使用的 AVR-GCC 编译器的说明,局部变量、参数传递的参数一般保存在通用寄存器中,当数量过多时会保存在堆栈中。所以针对 Atemga32,要使得任务暂停运行后能恢复,需要保存任务的返回地址, R0-R31, SREG。所以 eos 在创建任务时,初始化这些信息保存于堆栈中,由 contex_init()完成初始化:
? stack_t * context_init( task_entry_t task_entry, task_data_t pdata, stack_t * stk )
? {
? stack_t * ptos = stk;
? *ptos-- = ( (uint16)task_entry ) & 0xff;
? *ptos-- = ( (uint16)task_entry >> 8 ) & 0xff;
? *ptos-- = 0;
? /* ------------------------------ */
? *ptos-- = (uint16)pdata & 0xff;
? *ptos-- = ((uint16)pdata>>8) & 0xff;
? *ptos-- = 26;
? /* ---------------------------- */
? *ptos-- = 0x80;
? return ptos;
? }
? 而在 kernel_start()中的 task-start()执行时,则是从堆栈中取这些值,并恢复至 cpu 寄存器中。
? 任务切换时,当前任务自行保存执行状态,同时恢复另一任务的执行状态,实现任务切换。
? 这里,我并不想对其详细的工作机制作更进一步的说明。已经说过,这个内核是仿照uc/os 实现的,所以这里有关堆栈初始化和任务切换的工作机制只作了简单的介绍,其原理和 uc/os 的一样。有兴趣的读者可自行参考<<嵌入式实时操作系统 uc/os ii>>中关于移植的章节,其中对这部分有深入的讲解。
任务调度算法:
? 调度器由 scheduler()实现,算法也非常简单,即从就绪表取最高优先级任务运行。
? 代码如下:
?void scheduler( void )
?{
? task_t task;
? OS_ARCH_VAR;
? OS_ARCH_PROTECT();
? /* 判断是否允许调度
? if( sched_lock == 0 && irq_cnt == 0 ) */
? {
? if( ( task = task_rdy_prio_max() ) != OSTaskCur )
? /* 取优先级更高任务
? { */
? * 进行任务切换
? OSTaskPre = OSTaskCur; */
? OSTaskCur = task;
? OSPrioCur = task->prio;
? TaskYield();
? }
? }
? OS_ARCH_UNPROTECT();
?}
? 处于运行状态的任务,总是处于就绪队列。task_rdy_prio_max()并不将最高优先级的任务从就绪表移除,而仅仅是返回 task_struct()结构,这样能简化实现。反之,如果从就绪队列移除,那么当运行的任务执行 task_rdy_prio_max()时,取得就绪队列中最高优先级的任务后还需要与自己的优先级进行比较,显然操作要复杂些。
? 该调度器并不能实现任务主动放弃 cpu 给同优先级的其它任务。
? TaskYield()的功能即实现调用 scheduler()的任务的运行状态保存,同时恢复取得的 task的运行状态,以实现任务的切换。
? 调度算法和 uc/os 的基本一致,也相当的简单。虽然之前在一些嵌入式操作系统中看过一些调度算法,但对我而言实现起来比较复杂。毕竟自己不是搞理论的,又是一个简单的内核。简单一点就OK!
六、总结与思考
? 1、上述讨论的代码,仅涉及内核的初始化、任务启动与多任务调度。基本流程同 uc/os一致。其要点就在于如何实现任务的启动和任务间的切换。在最初阅读 uc/os 源码时,我对此过程十分不解。后来通过写了一个简单的任务切换程序,保存与恢复寄存器,实现了在多个函数间切换执行后,才得以理解,进而能够比较顺利的将 uc/os 移植到 amtega 处理器上。所以,有句话说得好, “纸上得来终觉浅,觉知此事要躬行”!
七、源码与资料
涉及源码文件:
? core.c / core.h ------------ 内核核心代码
文章评论(0条评论)
登录后参与讨论