一、OneOS任务基础知识
RTOS 系统的核心就是任务管理,OneOS 操作系统也不例外,而且大多数学习RTOS系统的工程师主要就是为了使用 RTOS 的多任务处理功能,初步上手 RTOS 系统首先必须掌握的也是任务的创建、删除、挂起和恢复等操作,由此可见任务管理的重要性。
1.1、什么是多任务系统?
51、AVR、STM32单片机等裸机(未使用系统)的时候一般都是在main 函数里面用while(1)做一个大循环来完成所有的处理,即应用程序是一个无限的循环,循环中调用相应的函数完成所需的处理。有时候我们也需要中断中完成一些处理。相对于多任务系统而言,这个就是单任务系统,也称作前后台系统,中断服务函数作为前台程序,大循环while(1)作为后台程序,如下图所示:
前后台系统的实时性差,系统各个任务(应用程序)都是排队轮流执行,不管程序是否紧急,相当于所有任务的优先级都一样。但前后台系统简单,资源消耗也少。如果在稍微大一点的嵌入式应用中,前后台系统就明显力不从心了,此时就需要多任务系统出面了。
首先我们需要知道在操作系统的角度,任务是什么,比如在一个系统上,一部分程序负责收集数据,一部分程序负责处理数据,这两部分程序相互独立运行,并且会分时占用 CPU 交替运行,事实上,我们的系统会存在很多这样的程序片段,这些程序片段就可以被抽象为任务。任务是操作系统中最小的运行单位,也是最基本的调度单位,而系统把每个任务都抽象为任务控制块,通过任务控制块对任务进行管理。 任务调度就是如何选取任务投入运行的策略。支持基于优先级的抢占式调度;同时也支持多个具有相同优先级的任务时间片轮转调度。
多任务系统会把一个大问题(应用)“分而治之”,把大问题划分成很多个小问题,通过解决小问题来解决大问题,这些小问题可以单独的作为一个小任务来处理。这些小任务是并发处理的,注意,并不是说同一时刻一起执行很多个任务,而是由于每个任务执行的时间很短,导致看起来像是同一时刻执行了很多个任务一样。多个任务带来了一个新的问题,究竟哪个任务先运行,哪个任务后运行呢?完成这个功能的东西在RTOS系统中叫做任务调度器。不同的系统其任务调度器的实现方法也不同,比如 OneOS 是一个抢占式的实时多任务系统,那么其任务调度器也是抢占式的,运行过程如下图所示:
在上图中,高优先级的任务可以打断低优先级任务的运行从而取得CPU的使用权,这样就保证了那些紧急任务的运行。这样我们就可以为那些对实时性要求高的任务设置一个很高的优先级,比如自动驾驶中的障碍物检测任务等。高优先级的任务执行完成以后重新把 CPU 的使用权归还给低优先级的任务,这个就是抢占式多任务系统的基本原理。
1.2、任务调度管理实现
1.2.1、任务管理
OneOS的任务管理基于队列实现,通俗来讲就是基于链表实现的,如下表所示:
基础任务状态 | 描述 |
任务资源队列 | 用于管理任务资源,除关闭态的任务,其它状态下的任务都以资源 的方式挂在此队列。 |
任务资源回收队列 | 处于关闭态任务但资源没有回收的情况下,任务资源会挂到此队列上,待回收任务回收资源。 |
就绪队列 | 就绪态的任务会挂在此队列上。为提高性能,处于运行态的任务也会挂在此队列上。 |
睡眠队列 | 睡眠或者有限等待的阻塞任务会挂到此队列上。 |
阻塞队列 | 任务在等待的特定条件未满足时,任务会被挂在此队列上。阻塞队列会分布在其它功能模块上,如互斥锁、信号量、消息队列等模块,各存在1个阻塞队列。 |
任务管理就是实现对任务的各种操作。任务创建时,设置任务控制块的各个成员变量,例如入口函数,栈地址,优先级等,初始化任务的栈空间,并且将任务插入到任务资源队列。任务启动时,将任务插入到就绪队列,任务控制块和栈占用的内存空间可以是静态定义的,也可以是从内存堆动态分配的。任务销毁时,将任务从资源队列和就绪队列移除,释放任务占用的资源。任务睡眠和阻塞时,将任务插入到睡眠队列,如果睡眠时间到期,就从睡眠队列移除并且插入到就绪队列。任务挂起时,设置任务状态,从就绪队列移除。任务唤醒时,插入任务到就绪队列。任务管理示意图如下所示。
1.2.2、任务状态
OneOS 操作系统的任务永远处于下面7种状态:
(1) 初始态:任务创建完成,被启动之前处于初始态。
(2) 就绪态:初始态任务被启动之后,状态切换为就绪态,加入就绪队列,等待调度器调度运行。
(3) 运行态:正在运行的任务处于运行态。
(4) 睡眠态:正在运行的任务睡眠后会处于睡眠态,睡眠时间到后,会被唤醒。
(5) 阻塞态:任务运行条件未满足需要等待时进入阻塞态,当运行条件满足后被唤醒。
(6) 挂起态:任务被挂起后进入挂起态,不参与调度,直到被恢复。
(7) 关闭态:任务被销毁。
OneOS有些状态是可以组合的,例如任务在阻塞态中被挂起、任务处于睡眠态中等等,如下表所示:
有效组合态 | 描述 |
阻塞态+挂起态 | 处在阻塞态的任务被挂起 |
阻塞态+睡眠态 | 任务有限时间等待某种条件满足时,此时被唤醒的方式有2种,超时或者条件满足 |
睡眠态+挂起态 | 处于睡眠态的任务被挂起 |
阻塞态+睡眠态+挂起态 | 处于阻塞态+睡眠态的任务被挂起 |
上述表中,有效组合状态都是基于OneOS操作7个状态为基础延申的,本质上OneOS只具备7种状态,如下图所示:
从上图中可知:
(1) 创建的任务被启动,既调用函数 os_task_startup()让任务由初始态进入就绪态。
(2) 挂起态任务被解挂恢复,既调用函数 os_task_resume ()变成就绪态,可以参与任务调度。
(3) 调用函数 os_task_suspend()让任务在就绪态任务被挂起。
(4) 正在运行的任务被抢占或是时间片用完。
(5) 就绪态的任务被调度而运行。
(6) 处于阻塞状态的任务所等待的条件被满足,任务可以参与调度。
(7) 正在运行的任务由于某种条件不满足而需要无限等待,此时会挂到阻塞队列。
(8) 任务睡眠时间已到。
(9) 正在运行的任务主动睡眠。
(10)处于阻塞态的任务任务主动睡眠。
(11)处于阻塞态的任务任务被挂起。
(12)任务睡眠时被挂起。
1.2.3、任务优先级
每个任务都可以分配一个从0~(OS_TASK_PRIORITY_MAX-1)的优先级,那么宏OS_TASK_PRIORITY_MAX在文件oneos_config.h中有定义,前面我们讲解OneOS系统配置的时候已经讲过了。在 OneOS中,规定宏OS_TASK_PRIORITY_MAX可设置的最大值为256,但是在实际情况中宏 OS_TASK_PRIORITY_MAX默认设置为32,也就是可分配的优先级为0~31。OneOS操作系统的优先级数字越低表示任务的优先级越高,0的优先级最高,OS_TASK_PRIORITY_MAX -1 的优先级最低。空闲任务的优先级设置为最低优先级。OneOS调度器确保处于就绪态或运行态的高优先级的任务获取处理器使用权,换句话说就是处于就绪态的最高优先级的任务才会运行。优先级数量越多,消耗RAM越大(RAM:随机存储器,是计算机存储器的一种,特点就是数据可以随机存取)。OneOS可以创建多个任务且任务可以共用一个优先级,数量不限。此时处于就绪态的优先级相同的任务就会使用时间片轮转调度器获取运行时间。
1.2.4、任务实现
在进行任务的各种操作的时候我们需要使用到任务的一些基本API函数。基本的API函数如下表。
命令 | 说明 |
os_task_init() | 静态方式创建任务 |
os_task_deinit() | 删除静态方式创建的任务 |
os_task_create() | 动态方式创建任务 |
os_task_destroy() | 删除动态方式创建的任务 |
os_task_suspend () | 挂起一个任务 |
os_task_resume () | 恢复一个任务的运行 |
具体如何使用在第二章的实验部分会有详细讲解。
1.2.5、任务控制块
OneOS的每个任务都有一些属性需要存储,OneOS 把这些属性集合到一起用一个结构体来表示,这个结构体叫做任务控制块:os_task_t,在使用函数os_task_create()创建任务的时候就会自动的给每个任务分配一个任务控制块,此结构体在文件os_tasks.c 中均有定义。
注:任务的CPU使用权被剥夺时,任务控制块保存该任务的状态等等信息。当任务重新得到CPU使用权时,任务控制块能确保任务从当时被中断的那一点丝毫不差地继续执行。
1.2.6、任务堆栈
OneOS之所以能正确的恢复一个任务的运行就是因为有任务堆栈在保驾护航,任务调度器在进行任务切换的时候会将当前任务的现场(任务堆栈中可以保存CPU寄存器值、执行的代码的地址和任务参数等等)保存在此任务的任务堆栈中,等到此任务下次运行的时候就会先用堆栈中保存的值来恢复现场,恢复现场以后任务就会接着从上次中断的地方开始运行。创建任务的时候需要给任务指定堆栈,如果使用的函数 os_task_create()创建任务(动态方法)的话那么任务堆栈就会由函数os_task_create()自动创建。如果使用函数 os_task_init()创建任务(静态方法)的话就需要程序员自行定义任务堆栈,然后堆栈首地址作为函数的参数 stack_begin 传递给函数。
二、OneOS任务实验
2.1、任务创建与删除
2.1.1、实验目的
学习 os_task_create()和 os_task_destroy()这两个函数的使用。
2.1.2、实验设置
本实验设计两个任务: task1_task 和 task2_task ,这两个任务的任务功能如下:
task1_task:串口输出task1运行次数,
task1_task运行3次删除task2_task并串口输出“del task2_task”。
task2_task:串口输出task2运行次数。
注意:当代码载入到开发板之后可以用正点原子串口调试助手来查看串口的输出情况!
2.1.3、代码解释和实验分析
#include <board.h>
#include <os_task.h>
#define TASK1_PRIO 3
#define TASK1_STK 512
static uint8_t task_static_stack1 [TASK1_STK];/*定义一个堆栈块的大小(这也是动态任务与静态的区别,静态任务需要自己来分配任务栈空间和任务控制块指针,而动态不用)*/
static os_task_dummy_t task_static1;/*任务控制块指针*/
void task1_task(void *parameter);/*任务函数1*/
static os_task_id task1=0;
#define TASK2_PRIO 3
#define TASK2_STK 512
static uint8_t task_static_stack2 [TASK2_STK];
static os_task_dummy_t task_static2;
void task1_task(void *parameter);
static os_task_id task2=0;
/*任务1的任务函数*/
void static_task1_task(void *parameter)
{
parameter = parameter;
int task1_num = 0;
while(1)
{
task1_num ++;
os_kprintf("task1 run :%d\r\n",task1_num);
if(task1_num == 3)
{
if(task1 != OS_NULL)
{
os_task_destroy(task2);
task2 =OS_NULL;
os_kprintf("task2 leave :\r\n");
}
}
os_task_msleep(500);
}
}
/*任务2的任务函数*/
void static_task2_task(void *parameter)
{
parameter = parameter;
int task2_num = 0;
while(1)
{
task2_num ++;
os_kprintf("task2 run :%d\r\n",task2_num);
os_task_msleep(500);
}
}
int main(void)
{
// os_task_id task1;
task1 = os_task_create (&task_static1, task_static_stack1, TASK1_STK , "task1", static_task1_task, OS_NULL, TASK1_PRIO);
// OS_ASSERT(task1);
os_task_startup(task1);
// os_task_id task2;
task2 = os_task_create(&task_static2, task_static_stack2, TASK2_STK , "task2", static_task2_task, OS_NULL, TASK2_PRIO);
// OS_ASSERT(task2);
os_task_startup(task2);
return 0;
}
(1)首先,定义了任务1的优先级和堆栈大小。任务优先级是3,堆栈大小是512字节。为任务1定义了一个静态堆栈数组、一个任务控制块、任务函数声明、以及任务名称。
(2)任务函数定义
首先定义了任务1的函数 static_task1_task,它无限循环地递增 task1_num 变量,打印一条消息到控制台,并在 task1_num 达到3时尝试使用os_task_destroy()销毁任务2。然后定义了任务2的函数 static_task2_task,它的工作方式与任务1类似,但是没有销毁其他任务的逻辑。
(3)Main函数
在 main 函数中,首先创建任务1,使用 os_task_create 函数。这个函数接受任务控制块、堆栈内存、任务名称、任务函数和优先级等参数。然后调用 os_task_startup 函数启动任务1。
(4)输出结果和原因分析
任务1在其函数中有一个逻辑,当 task1_num 达到3时,会尝试销毁任务2。两个任务优先级相同,所以结果大概率是任务1和任务2交替输出,在任务1输出了三次之后,任务2被删除,打印被删除的信息。那么任务2的输出将停止,而任务1将继续输出,直到它自然结束或被其他逻辑改变。最终的实验结果也符合预期。
2.2、任务挂起与恢复
2.2.1、实验目的
学习 os_task_suspend ()和 os_task_resume ()这两个函数的使用。
2.2.2、实验设置
本实验设计两个任务: task1_task 和 task2_task ,这两个任务的任务功能如下:
task1_task:串口输出task1运行次数,
task1_task运行第3次挂起task2_task并串口输出“suspend task2”,
task1_task运行第5次恢复task2_task并串口输出“resume task2”,
task2_task:串口输出task2运行次数。
2.2.3、代码解释和实验分析
#include <board.h>
#include <os_task.h>
#include <os_types.h>
#include <os_errno.h>
#define TASK1_PRIO 3
#define TASK1_STK 512
static uint8_t task_static_stack1 [TASK1_STK];
static os_task_dummy_t task_static1;/*任务控制块指针*/
void task1_task(void *parameter);/*任务函数1*/
static os_task_id task1=0;
#define TASK2_PRIO 4
#define TASK2_STK 512
static uint8_t task_static_stack2 [TASK2_STK];
static os_task_dummy_t task_static2;
void task1_task(void *parameter);
static os_task_id task2=0;
/*任务1的任务函数*/
void static_task1_task(void *parameter)
{
parameter = parameter;
int task1_num = 0;
while(1)
{
task1_num ++;
os_kprintf("task1 run :%d\r\n",task1_num);
if(task1_num == 3)
{
if(task1 != OS_NULL)
{
os_err_t t1 =1;
t1 = os_task_suspend(task2);
if(t1 == OS_SUCCESS)
{
os_kprintf("suspend task2 \r\n");
}
}
}
if(task1_num == 5)
{
if(task1 != OS_NULL)
{
os_err_t t2 =1 ;
t2 = os_task_resume(task2);
if(t2 == OS_SUCCESS)
{
os_kprintf("resume task2 \r\n");
}
}
}
os_task_msleep(500);
}
}
/*任务2的任务函数*/
void static_task2_task(void *parameter)
{
parameter = parameter;
int task2_num = 0;
while(1)
{
task2_num ++;
os_kprintf("task2 run :%d\r\n",task2_num);
os_task_msleep(500);
}
}
int main(void)
{
task1 = os_task_create (&task_static1, task_static_stack1, TASK1_STK , "task1", static_task1_task, OS_NULL, TASK1_PRIO);
os_task_startup(task1);
task2 = os_task_create(&task_static2, task_static_stack2, TASK2_STK , "task2", static_task2_task, OS_NULL, TASK2_PRIO);
os_task_startup(task2);
return 0;
}
主要讲解任务1的任务函数
任务1中定义了一个变量task1_num,当无限循环地递增的task1_num变量到达3的时候,调用os_task_suspend函数将任务2挂起,并打印出suspend task2。当task1_num变量到达5的时候,调用os_task_resume函数恢复任务2,并且打印resume task2。
实验结果推测 当task1_num小于3的时候 两个任务交替打印task1 run和task2 run,当打印到task1 run:3的时候,任务2被挂起 打印suspend task2,然后直到打印到task1 run:5的时候,任务2被恢复,然后两个任务又才继续交替打印。实验结果符合预期。