1、任务状态及转换
AliOS Things是一款支持单处理器上运行多个任务的实时操作系统。对于多任务系统,在单个处理器上任意时刻只能有一个任务在运行,其他任务均处于非运行状态。所以这里可以简单的认为任务有两种状态:运行和非运行,如图1。
图1 任务状态和转换的顶层模型
当任务处于运行状态时,处理器被用来执行该任务的代码;当任务处于非运行状态时,其运行上下文被保存,并在下一次进入运行状态时被恢复执行。当任务恢复执行时,它会从上一次离开运行状态之前要执行的指令开始执行。那任务什么时候又是为什么发生非运行状态与运行状态之间的切换,主要依据系统的调度策略、可引起调度的系统事件以及特定的函数调用。
实时操作系统任务调度依赖于任务优先级,对于那些实时性要求比较高的任务被赋予高的优先级,一旦高优先级的任务准备就绪,系统就会将正在运行的低优先级任务换出,然后将处理器的执行权交给高优先级任务。而对于那些因为某种原因暂时不需要执行而处于非运行状态的任务,只有等到特定事件的发生才会被再次调度执行。所以可以将任务的非运行状态划分为两个子状态:就绪和挂起,如图2。
图2 任务状态和转换的基础模型
如图2所示,只有当任务处于就绪状态时,才能被系统调度进入运行状态;而处于运行状态的任务可以通过两种方式退出运行:挂起或被抢占。当任务执行完成或者因为某种原因不能继续运行(如等待一定时间段的延迟)则进入一种暂时停止运行的状态即挂起状态。当任务进入挂起状态,它释放处理器资源给其他任务。任务可以因为完成相应操作或自身程序产生信号来主动放弃处理器;任务也会因其他任务抢占、时间到期或系统事件被系统强制换出而被动的放弃处理器资源,任务放弃处理器资源的原因可以参见图3。
图3 任务挂起的原因
注意,对于工作还未完成被其他任务抢占被强行放弃处理器的任务,不会被挂起而是处于就绪状态,等待被系统再次调度。
结合上述任务挂起的原因分析可以得出任务状态的通用模型如图4所示,其中任务从挂起状态进入就绪状态的条件,包括事件、时间间隔到期、事件和时间间隔到期的组合。
图4 任务状态和转换的通用模型
2、实现
任务状态是反映当前系统中任务所处的状况,操作系统内核需要维护所有任务的当前状态。由于引发任务状态切换的条件和因素较多,所以不同的操作系统在定义系统任务状态的种类和迁移变化上是有差异的,如图5、图6、图7分别是FreeRTOS、RT-Thread、AliOS Things的任务状态图,它们是在任务状态的通用模型上对挂起状态进行了细化,但细化子类定义是不同的,并且由同类事件引发的任务状态迁移的变化也是有差异的。
2.1、FreeRTOS
FreeRTOS将通用模型中的挂起状态细化为两种状态:阻塞和挂起。任务正在等待事件的状态称为阻塞状态。任务进入阻塞状态等待的事件类型有两种:
(1) 时间相关的事件
延迟间隔的到期或绝对时间的到达。如任务通过调用vTaskDelay()延迟10ms再允许被调度。
(2) 同步事件
其他任务产生的事件或中断。如任务调用xQueueReceive()等待数据到达队列。
处于挂起状态的任务将不再被调度,任务只有被其他任务解除挂起状态才会被调度。处于就绪状态、运行状态或者阻塞状态的任务都可以通过调用vTaskSuspend()进入挂起状态,当有任务调用vTaskResume()将其唤醒时,任务进入就绪状态。
对于新创建的任务因为无需等待任何资源而处于就绪状态,当系统发生调度时,若无更高优先级任务处于就绪状态,则该任务将会被调度进入运行状态。
图5 FreeRTOS任务状态和转换
2.2、RT-Thread
RT-Thread是在任务状态基础模型上引入了两个新的状态:初始状态和关闭状态。任务在创建完成后进入初始状态,初始状态的任务可以通过调用rt_thread_startup() 函数进入到就绪状态。当任务运行结束执行rt_thread_exit()函数时或被其他任务调用rt_thread_delete/detach() 函数将其删除或销毁时,任务进入关闭状态。处于运行状态的任务可以通过调用 rt_thread_delay(),rt_sem_take(),rt_mutex_take(),rt_mb_recv() 等函数,进入到挂起状态;处于挂起状态的线程,如果等待超时依然未能获得资源或由于其他线程释放了资源,那么它将返回到就绪状态。
图6 RT-Thread 任务状态和转换
2.3、AliOS Things
AliOS Things为了充分描述任务在系统中所处的状态以及引发状态迁移的条件差异,在上述任务状态的基础模型上新增任务删除状态并对挂起状态进行了细化,具体分为挂起状态、休眠状态和阻塞状态,如图7。
图7 AliOS Things任务状态和转换
(1)阻塞状态是指因等待资源而处于等待状态,如调用aos_mutex_lock()获取互斥量时互斥量已经被锁定、调用aos_queue_recv()获取队列数据时队列为空、调用aos_sem_wait()等待信号量时信号量计数为0、调用aos_evnet_get()获取事件时,事件还未发生;
(2)挂起状态是因任务被其他或自身调用挂起函数aos_task_suspend()后,将无条件地停止运行,被挂起的任务只能通过其他任务调用恢复函数aos_task_resume()使其恢复到就绪状态;
(3)休眠状态是因任务在调用任务休眠函数aos_msleep()后,进入一种延迟执行的状态,直到休眠时间到达任务被重新调度恢复运行。
(4)删除状态是因任务运行完成调用任务退出函数aos_task_exit()或被调用任务删除函数aos_task_del()时被设置的一种状态。
与RT-Thread和FreeRTOS相比,AliOS Things对新创建任务的状态给予更多的灵活性。当任务通过调用aos_task_new()函数被创建时,任务状态为就绪状态。若应用程序不要求任务在创建后立即被调度执行,可以通过调用aos_task_new_ext()函数并设置参数autorun为0来创建任务,这时任务处于挂起状态,当应用程序需要任务执行时,可以通过调用aos_task_resume()函数将任务切换至就绪状态。处于就绪状态的任务,在系统发生调度时,优先级最高的任务将会获得处理器的控制权,进入运行状态。若此时有其他更高优先级的任务处于就绪状态,该任务将会被抢占,重新放回到就绪状态。
AliOS Things允许任务处于组合状态:阻塞挂起状态或休眠挂起状态:
(1) 任务在阻塞状态下,被其他任务挂起,则进入阻塞挂起状态。该状态下,若任务被恢复则保持阻塞状态;若任务解除阻塞则保持挂起状态。
(2) 任务在休眠状态下,被其他任务挂起,则进入休眠挂起状态。该状态下,若任务被恢复则保持休眠状态;若任务休眠到期则保持挂起状态。
注意,虽然FreeRTOS中处于阻塞状态的任务可以被挂起,但是被挂起后任务不再阻塞,即使任务等待的事件未发生但任务一旦被恢复,将进入就绪状态。而AliOS Things中任务因等待某个事件进入阻塞状态,而此时又被其他任务将其挂起,该任务仍然是处于阻塞状态,如果在此过程中等待的事件发生了,则任务会解除阻塞进入挂起状态;如果事件未发生,则任务恢复状态后仍然处于阻塞状态。这样设计的好处是可以保证任务解除阻塞是等待的特定事件发生了而引起的,而非因为任务挂起恢复后消除了等待状态造成的。例如,任务A和任务B同时访问资源M,由于任务B先拿到互斥量导致任务A因未获得互斥量而进入阻塞状态,之后任务C将任务A挂起,然后恢复,此时任务A仍处于阻塞状态并无法访问资源M,这样能够保证资源M的互斥访问。如果采用FreeRTOS的任务状态切换方式,任务A在挂起恢复后将直接进入就绪状态,这样任务A可能会在任务B还未操作完成时就访问了资源M,使得互斥量未起到互斥的作用。
3、示例
下面以一个应用示例来说明AliOS Things中任务状态切换过程,如图8所示:
(1) 在t0时刻,任务task1、task2分别通过aos_task_new()和aos_task_new_ext()函数调用被创建,之后task1进入就绪状态,而task2处于挂起状态。
(2) Task1得到运行后,在t1时刻调用aos_task_resume()将task2恢复,task2进入就绪状态,之后task1通过调用aos_msleep()进入休眠状态,task2因为task1休眠而获得CPU执行权,task2运行后因等待信号量进入阻塞状态。
(3) Task1在t2时刻因延迟到期得到运行,并调用aos_task_suspend()将task2挂起,task2此时的状态为阻塞挂起。之后task1通过调用aos_msleep()进入休眠状态。
(4) Task1在t3时刻因延迟到期得到运行,并调用aos_task_resume()将task2恢复,此时task2的状态为阻塞状态。之后task1通过调用aos_msleep()进入休眠状态。
(5) Task1在t4时刻因延迟到期得到运行,并调用aos_sem_signal()释放信号量,这时task2因等到信号量而进入就绪状态。待到task1再次进入休眠转改后task2得到运行,进入运行状态。
图8 AliOS Things任务状态和切换应用示例
根据图8描述的应用示例,编写代码如下:
include <aos/kernel.h>
static aos_sem_t g_testsync_sem;
static aos_task_t handle_task2 = {NULL};
void task1_entry()
{
printf("task1 is running!\n");
printf("task1 resume task2!\n");
if (0 != aos_task_resume(&handle_task2))
{
printf("task1 resume task2 failed!\n");
}
printf("task1 start to sleep and release CPU!\n");
aos_msleep(10000);
printf("task1 suspend task2!\n");
if (0 != aos_task_suspend(&handle_task2))
{
printf("task1 resume task2 failed!\n");
}
printf("task1 start to sleep and release CPU!\n");
aos_msleep(10000);
printf("task1 resume task2 again!\n");
if (0 != aos_task_resume(&handle_task2))
{
printf("task1 resume task2 failed!\n");
}
printf("task1 start to sleep and release CPU!\n");
aos_msleep(10000);
printf("task1 signal a semphone!\n");
aos_sem_signal(&g_testsync_sem);
printf("task1 start to sleep and release CPU!\n");
aos_msleep(10000);
aos_task_exit(0);
}
void task2_entry()
{
printf("task2 is running!\n");
if (0 != aos_sem_wait(&g_testsync_sem, AOS_WAIT_FOREVER))
{
printf("task2 wait semphone failed!\n");
}
printf("task2 get semphone and is running!\n");
aos_task_exit(0);
}
int task_test(void)
{
int retn;
retn = aos_sem_new(&g_testsync_sem, 0);
if (retn != 0)
{
printf("%s:%d sem new failed, err=%d\n", __func__, __LINE__, retn);
return -1;
}
if (0 != aos_task_new("task1", task1_entry, NULL, 4096))
{
aos_sem_free(&g_testsync_sem);
return -1;
}
if (0 != aos_task_new_ext(&handle_task2, "task2", task2_entry, NULL, 4096, 30, 0))
{
aos_sem_free(&g_testsync_sem);
return -1;
}
return 0;
}
上述代码的运行结果如图9。
图9 应用示例代码运行结果
图9中,task2在运行之后因为未信号量计数值为0而进入阻塞状态,此时通过tasklist命令查看task2的任务状态为“PEND”,这里的“PEND”代表阻塞状态;之后task1将task2挂起,此时查看到的task2任务状态为“PEND_SUS”代表阻塞挂起状态;最后task1恢复task2,此时查到task2任务状态为“PEND”,说明代码运行结果与AlisOS Things任务状态图中的切换过程是一致的。
【2】https://www.rt-thread.org/document/site/programming-manual/thread/thread/#_12
【3】《Real-time Operating Systems》Jim Cooling.
5、开发者技术支持
如需更多技术支持,可加入钉钉开发者群,或者关注微信公众号