2.2 任务(task)
2.2.1 概念
现代操作系统都建立在任务的基础上,任务是Rhino中代码的一个基本执行环境,有的操作系统也称之为线程(thread)。多任务的运行环境提供了一个基本机制让上层应用软件来控制或反馈真实的或离散的外部世界,从宏观上可以看作单个CPU执行单元上同时执行多个任务;从微观上看,CPU通过快速的切换任务来执行。Rhino实时操作系统支持多任务机制。
每个任务都具有上下文(context)。上下文是指当任务被调度执行时此任务可见的CPU资源和系统资源,当发生任务切换时,任务的上下文被保存在任务控制块(ktask_t)中,这些上下文包括当前任务的CPU指令地址(PC指针)、当前任务的栈空间、当前任务的CPU寄存器状态等。
2.2.2 系统task
依据不同的初始配置,Rhino内核将在系统启动阶段创建一些默认的task,且这些task将会一直运行而不退出。常见的默认系统task包括:
(1)timer_task:定时器任务。当有需要把在处理的工作推迟一些时间时,可以启动一个定时器,指定延迟时间和工作内容,并将此工作加入timer_task的内部队列,当定时器到了定的时间时,timer_task将会执行此工作内容。从另一个方面看,Rhino内核的定时器执行上下文是任务上下文,而不是中断上下文,也有某些操作系统将定时器的执行上下文放在中断上下文来处理。配置定时器任务的系统选项是RHINO_CONFIG_TIMER,此任务默认优先级是5,可用系统选项RHINO_CONFIG_TIMER_TASK_PRI来控制。
(2)DEFAULT-WORKQUEUE:工作队列。在当前代码执行上下文无法完成某些工作时,可以把此工作排入工作队列,由工作队列在任务上下文中执行。配置此任务的系统选项是RHINO_CONFIG_WORKQUEUE。
(3)cli:rhino kernel shell任务。此任务提供一个shell界面,用户可通过此shell界面来运行命令与OS交互,比如查看当前系统任务列表、空余内存,查看任务堆栈信息、debug,重启系统等。
(4)idle_task:空闲任务。当CPU没有需要执行的指令时,则切入此task执行,此task执行一个while循环,直到有任何一个其他task需要被调度。
(5)dyn_mem_proc_task:动态释放内存。配置此任务的系统选项是RHINO_CONFIG_KOBJ_DYN_ALLOC。
2.2.3 task状态和迁移
在kernel shell中使用命令tasklist可以列出当前系统中所有task的信息,包含任务的状态信息,如表2-1所示。
表2-1 当前系统中所有task的信息
表2-2给出了Rhino内核维护的任务状态符号对应的意义。注意,task的状态具有累加性,一个任务可在同一时刻具有多个状态。例如,一个处于PEND状态的task可以迁移到状态PEND_SUSPENDED。当PEND状态解除后,任务状态迁移为SUSPENDED。
表2-2 Rhino内核维护的任务状态符号对应的意义
如图2-2所示是一个简单的任务状态迁移示例。使用参数autorun=1创建的task直接进入RDY状态,使用autorun=0创建的task直接进入SUSPENDED状态。此外,系统还包括PEND状态和SLEEP状态,图中的箭头表示了状态之间可能的切换。表2-3所示为图2-2中对应各个状态变化及引起该变化的操作。注意,图2-2没有列出所有的任务状态,比如上面讨论的PEND_SUSPENDED/SLEEP_SUSPENDED状态。
图2-2 任务状态迁移示例
表2-3 状态变化以及引起该变化的操作
下面我们对task状态及其转移中的任务调度和ready queue做进一步阐述。
1.任务调度
Rhino支持两种调度模式:①基于优先级的抢占式调度;②Round-Robin,基于时间片的轮转调度。
(1)基于优先级的抢占式调度。
Rhino对每一个不同的任务优先级,都维护了一个FIFO模式的ready queue,里面包含了当前所有可以运行的task列表。此列表中的task状态都处于RDY状态。当CPU可用时,最高优先级的ready queue里面排在第一个的task将得到CPU,并开始执行。
当有一个task就绪且优先级高于当前task,那么OS将立即切换到高优先级的task执行,并在切换之前保存当前task的上下文。
在实际应用中,应该按照任务实际要处理事件的紧急程度来安排task的优先级,避免将所有task都安排在同一个优先级。比如在航空系统中,用于飞行控制的task应该给予高优先级,用来处理机载娱乐系统的任务则给予低优先级。这种调度机制有一个潜在问题是,如果当前所有task都属于同一优先级,此时其中一个task一直运行且不放弃CPU,那么其他task则会一直得不到CPU资源来执行指令。Round-Robin调度机制可以避免这个问题。
(2)Round-Robin调度机制。
Round-Robin调度机制可以通过系统配置选项RHINO_CONFIG_SCHED_RR来开启。在Round-Robin调度机制下,同一优先级的task依次获得CPU,而不会因为某一个“野蛮”task不放弃CPU而导致其他task“饿死”。
Round-Robin调度机制会在同一优先级的task开始时共同分享CPU,每个task至多可以运行的时间片(time slice)是固定的,当某个task的时间片用完以后,此task将被放到此优先级对应的ready queue的末尾,然后调度ready queue上排在第一个位置的task来运行。这种机制是依靠Rhino内核机制来保证的,不论被切出的task是否愿意放弃CPU。
注意,Rhino内核的Round-Robin是考虑了task的优先级的,如果有一个高优先级的task就绪了,不论当前task的时间片是否用完,CPU都会立即去执行高优先级的task,当被中断的task恢复执行时,它将继续执行剩下的时间片。
Rhino默认为所有task分配默认的时间片(RHINO_CONFIG_TIME_SLICE_DEFAULT),也支持给特定的task设定特定的时间片,函数名是krhino_task_time_slice_set()。
当调度模式为基于优先级的抢占模式时,是不能动态调整任务的调度机制的。当调度模式为Round-Robin模式时,可以动态地设定某一个task脱离时间片分片的机制,即如果此task获得CPU后,可以一直运行指令不受时间片的影响,除非有更高优先级task就绪或者被某些资源阻塞。注意,这里的调整也只针对某一个单一task,系统整体调度机制没有发生改变。
2.ready queue
Rhino系统支持最大256个优先级(0-255),用户可以根据应用需求配置系统支持的最大优先级数。对于每个优先级,Rhino维护了一个ready queue,对应优先级的所有处于RDY状态的task都处于ready queue中。当CPU可用时,最高优先级的ready queue中排在第一个位置的task将得到执行,所以task在ready queue中的位置直接关系到此task的执行预期。一个task在ready queue中的位置是会变化的,task也可以在不同优先级的ready queue中迁移。
一些可能发生位置变化的情况如下:
(1)当前task的任务优先级发生变化时,当前task将从当前优先级的ready queue中脱离,进入新优先级的ready queue,并被排在新优先级的第一位置。
(2)当前task被抢占,CPU转去执行更高优先级的task,当前task在当前优先级的ready queue的位置不变。
2.2.4 task创建
Rhino内核支持两种创建task的方式:
(1)静态创建,krhino_task_create()。
(2)动态创建,krhino_task_dyn_create()。
在Rhino系统内核启动阶段,如果内存管理功能还没有完成初始化,但又需要创建task,可以使用krhino_task_create()来完成。此时,用于任务管理的数据空间ktask_t(有些操作系统也将此空间称为任务控制块(task control block, TCB))和任务栈空间均来自于预先在代码中定义好的全局或局部变量,视编译链接工具的不同,这段空间可位于DATA段或者BSS段(BSS段通常是指用来存放程序中未初始化的全局变量和静态变量的一块内存区域)。
静态创建task的代码示例如下:
ktask_t g_timer_task; cpu_stack_t g_timer_task_stack[RHINO_CONFIG_TIMER_TASK_STACK_SIZE]; krhino_task_create(&g_timer_task,"timer_task",NULL, RHINO_CONFIG_TIMER_TASK_PRI,0u,g_timer_task_stack, RHINO_CONFIG_TIMER_TASK_STACK_SIZE,timer_task,1u);
当内存管理功能完成初始化以后,Rhino支持动态地为要创建的task分配ktask_t空间和任务栈空间。这里的任务控制管理块(ktask_t)和任务栈空间都从系统内存堆中动态分配。
动态创建task的代码示例如下:
ktask_t *g_aos_init; krhino_task_dyn_create(&g_aos_init, "aos-init",0 , AOS_DEFAULT_APP_PRI,0, AOS_START_STACK, sys_init,1);
2.2.5 任务栈
每个任务的栈空间是在创建任务时就分配好的,每个任务都必须有自己的任务栈空间,为了防止任务栈溢出,且踩到其他区域,导致更严重的系统问题,操作系统需要采取适当措施将损失降到最小。
严格地讲,没有方法可以保证当前任务栈不溢出,操作系统所能做的和需要做的事情有两个:第一个是侦测任务栈溢出;第二个是悬起栈溢出的task,防止此task给系统带来更大危害。
1.栈溢出侦测
栈溢出侦测的方法一般有以下两种:
(1)在支持内存管理单元MMU(memory manage unit)的操作系统中,可以给任务栈上下各自加入一个警戒区,一般是一个页的空间,并且设置此警戒区为不可访问,一旦有任务栈上溢出或者下溢出,那么将会导致一个硬件异常,从而可以在异常处理中捕获这个错误,并定位是哪个task导致的,从而做出进一步处理。
(2)在不支持MMU的操作系统中,我们可以在任务栈的边缘写入一个特定的初始数值,然后检测该数值是否有变化,如果数值发生了变化,则说明发生了栈溢出,因为栈的边缘被修改了。
基于当前的Rhino内核,因为还不支持MMU,所以采取了第二种方法来侦测任务栈的溢出。任务栈的边缘在创建任务时被初始化成一个固定值RHINO_TASK_STACK_OVF_MAGIC(0xdeadbeaf)。
Rhino内核提供了一个函数void krhino_stack_ovf_check(void),可以用来检测当前task是否有栈溢出情况。如果有栈溢出的情况发生,则调用k_err_proc()来处理。k_err_proc()会调用BSP注册的g_err_proc()来处理,比如进一步打印栈,用户可以依据栈回溯内容来分析问题。
2.栈空间检测
在编写应用程序时,一般很难知道应用程序所在task的精确栈空间耗费尺寸数据。为了防止栈溢出,可以在开始时把任务栈的空间分配得大一些,在程序运行以后,可以通过Rhino内核提供的任务栈空间剩余检测功能来查看任务实际剩余栈空间大小,以此来评估程序实际耗费的栈空间大小,然后再调整任务栈空间的大小。相关函数如下。
检测任务栈空间历史最小剩余:
kstat_t krhino_task_stack_min_free(ktask_t *task, size_t *free)
检测当前时刻,任务栈空间剩余大小:
kstat_t krhino_task_stack_cur_free(ktask_t *task, size_t *free)
获取任务的调度机制:
kstat_t krhino_sched_pol icy_get(ktask_t *task, uint8_t *pol icy)
获取任务关联的信息:
kstat_t krhino_task_info_get(ktask_t *task, size_t idx, void **info)
2.2.6 任务执行控制
任务执行控制主要包括任务睡眠、任务避让、任务悬起、任务继续、任务强行放弃等待。下面对每个任务执行控制分别加以阐述。
(1)任务睡眠:kstat_t krhino_task_sleep(tick_t ticks)。
将当前任务推迟一些时间再继续执行。这个函数会将当前task从ready queue中删除,并插入g_tick_head队列,当给定的时间过去了,如果没有其他更高优先级的任务需要处理,则继续执行被推迟的task。注意,g_tick_head是一个任务队列,被延迟的任务将按照延迟时间长短插入到该队列中。当系统tick往前走时,系统将查询该队列中的任务,并唤醒那些延时到期的任务。
(2)任务避让:kstat_t krhino_task_yield(void)。
将当前task从ready queue中取出,并重新排入ready queue的尾部,目的是让当前task让出CPU,让其他task得到执行。
(3)任务悬起:kstat_t task_suspend(ktask_t *task)。
将当前task悬起。悬起当前task后,OS将调度新的task继续执行。
(4)任务继续:kstat_t task_resume(ktask_t *task)。
将悬起的任务插入ready queue队列,且将任务状态改为RDY。
(5)任务强行放弃等待:kstat_t krhino_task_wait_abort(ktask_t *task)。
将处于等待状态(包含SLEEP、SLEEP _SUSPENDED、PEND、PEND_SUSPENDED)的任务从相应等待资源队列中删除,并插入ready queue的尾部,标记task的状态为RDY。如果目标任务正在睡眠,那么可以使用此功能使目标task立即苏醒;如果目标任务正在等待某个信号量或者消息且处于PEND状态,那么此函数可以立即解锁目标task,且恢复执行,但这样有可能破坏之前由信号量保护的临界区。这种方法要慎用,除非你非常清楚此功能会带来的逻辑问题。
2.2.7 调度机制的控制
Rhino内核支持两种内核调度机制:基于优先级的抢占模式和Round-Robin模式。当调度模式为基于优先级的抢占模式时,是不能动态调整任务的调度机制的,即一旦系统设定为此模式,那么系统将一直运行此调度模式,无法改变。
当调度模式为Round-Robin模式时,则可以动态地设定某一个task脱离时间片分片的机制,即如果此task获得CPU后,可以一直运行指令不受时间片的影响,除非有更高优先级task就绪或者被某些资源阻塞。注意,这里的调整也只针对某一个单一task,系统整体调度机制没有发生改变。
设定某个task的调度机制的接口函数是:
kstat_t krhino_sched_policy_set(ktask_t *task, uint8_t pol icy)