嵌入式开发基础之任务管理(线程管理)

2022-12-03 09:30:21 浏览数 (2)

引言

RTOS 系统的核心是任务管理,而在实时操作系统中,任务和线程在概念上其实是一样的。所以任务管理也可以叫做线程管理。初步上手 RTOS 系统首先必须掌握的也是任务的创建、删除、挂起和恢复等操作,由此可见任务管理的重要性。在日常生活中,我们要完成一个大任务,一般会将它分解成多个简单、容易解决的小问题,小问题逐个被解决,大问题也就随之解决了。在多线程操作系统中,也同样需要开发人员把一个复杂的应用分解成多个小的、可调度的、序列化的程序单元,当合理地划分任务并正确地执行时,这种设计能够让系统满足实时系统的性能及时间的要求。本文中使用的例子,多是参考与FreeRTOS和RT-Thread。

介绍

多任务系统

多任务系统会把一个大问题(应用)“分而治之”,把大问题划分成很多个小问题,逐步的把小问题解决掉,大问题也就随之解决了,这些小问题可以单独的作为一个小任务来处理。这些小任务是并发处理的,注意,并不是说同一时刻一起执行很多个任务,而是由于每个任务执行的时间很短,导致看起来像是同一时刻执行了很多个任务一样。多个任务带来了一个新的问题, 究竟哪个任务先运行,哪个任务后运行呢?完成这个功能的东西在 RTOS 系统中叫做任务调度器。不同的系统其任务调度器的实现方法也不同。线程管理的主要功能是对线程进行管理和调度,系统中总共存在两类线程,分别是系统线程和用户线程,系统线程是由 RTOS内核创建的线程,用户线程是由应用程序创建的线程,这两类线程都会从内核对象容器中分配线程对象,当线程被删除时,也会被从对象容器中删除。 在多任务系统中,根据程序的功能,我们把这个程序主体分割成一个个独立的,无限循环且不能返回的小程序,这个小程序我们称之为任务。每个任务都是独立的,互不干扰的,且具备自身的优先级,它由操作系统调度管理。 高优先级的任务可以打断低优先级任务的运行而取得 CPU 的使用权,这样 就保证了那些紧急任务的运行。这样我们就可以为那些对实时性要求高的任务设置一个很高的优先级,比如自动驾驶中的障碍物检测任务等。高优先级的任务执行完成以后重新把 CPU 的使用权归还给低优先级的任务,这个就是抢占式多任务系统的基本原理。

任务

什么是任务?

在裸机系统中,系统的主体就是 main 函数里面顺序执行的无限循环,这个无限循环里面 CPU 按照顺序完成各种事情。在多任务系统中,我们根据功能的不同,把整个系统分割成一个个独立的且无法返回的函数,这个函数我们称为任务。在使用 RTOS 的时候一个实时应用可以作为一个独立的任务。每个任务都有自己的运行环境,不依赖于系统中其他的任务或者 RTOS 调度器。任何一个时间点只能有一个任务运行,具体运行哪个任务是由 RTOS 调度器来决定的,RTOS 调度器因此就会重复的开启、关闭每个任务。任务不需要了解 RTOS 调度器的具体行为,RTOS 调度器的职责是确保当一个任务开始执行的时候其上下文环境(寄存器值,堆栈内容等)和任务上一次退出的时候相同。为了做到这一点,每个任务都必须有个堆栈,当任务切换的时候将上下文环境保存在堆栈中,这样当任务再次执行的时候就可以从堆栈中取出上下文环境,任务恢复运行。 任务的大概形式具体见如下代码:

代码语言:javascript复制
void task_entry(void *pvParameters){
    /*任务主体,无限循环且不能返回*/
    while()
    {
    //任务主体代码
    }}

任务状态

RTOS的任务状态大致可分为:新建态、运行态、就绪态、阻塞态(有的操作系统也称为挂起态,有的操作系统同时有阻塞态和挂起态)和终止态。

  1. 新建态 当线程刚开始创建还没开始运行时就处于初始状态;在初始状态下,线程不参与调度。
  2. 运行态 当一个任务正在运行时,那么就说这个任务处于运行态,处于运行态的任务就是当前正在使用处理器的任务。如果使用的是单核处理器的话那么不管在任何时刻永远都只有一个任务处于运行态。
  3. 就绪态 处于就绪态的任务是那些已经准备就绪(这些任务没有被阻塞或者挂起),可以运行的任务,但是处于就绪态的任务还没有运行,因为有一个同优先级或者更高优先级的任务正在运行!
  4. 阻塞态(挂起态) 阻塞态也称挂起态,它可能因为资源不可用而挂起等待,或线程主动延时一段时间而挂起。在挂起状态下,线程不参与调度。 如果一个任务当前正在等待某个外部事件的话就说它处于阻塞态,比如说如果某个任务调用了函数 vTaskDelay()的话就会进入阻塞态,直到延时周期完成。任务在等待队列、信号量、事件组、通知或互斥信号量的时候也会进入阻塞态。任务进入阻塞态会有一个超时时间,当超过这个超时时间任务就会退出阻塞态,即使所等待的事件还没有来临!
  5. 终止态 线程运行结束时将处于关闭状态。关闭状态的线程不参与线程的调度。

任务优先级

线程的优先级是表示线程被调度的优先程度。每个线程都具有优先级,线程越重要,赋予的优先级就应越高,线程被调度的可能才会越大。对于 ARM Cortex-M 系列,普遍采用 32 个优先级。最低优先级默认分配给空闲线程使用,用户一般不使用。在系统中,当有比当前线程优先级更高的线程就绪时,当前线程将立刻被换出,高优先级线程抢占处理器运行。优先级数字越低表示任务的优先级越低,0 的优先级最低。RTOS 调度器确保处于就绪态或运行态的高优先级的任务获取处理器使用权,换句话说就是处于就绪态的最高优先级的任务才会运行。处于就绪态的优先级相同的任务就会使用时间片轮转调度器获取运行时间。

任务控制块

的每个任务都有一些属性需要存储,RTOS 把这些属性集合到一起用一个结构体来表示,这个结构体叫做任务控制块(TCB)。任务控制块就相当于任务的身份证,里面存有任务的所有信息,比如任务的栈指针,任务名称,任务的形参等。有了这个任务控制块之后,以后系统对任务的全部操作都可以通过这个任务控制块来实现。

任务堆栈

RTOS 之所以能正确的恢复一个任务的运行就是因为有任务堆栈在保驾护航,(如果是在有进程的操作系统中,保存和恢复现场是通过PCB完成)任务调度器在进行任务切换的时候会将当前任务的现场(CPU 寄存器值等)保存在此任务的任务堆栈中,等到此任务下次运行的时候就会先用堆栈中保存的值来恢复现场,恢复现场以后任务就会接着从上次中断的地方开始运行。创建任务的时候需要给任务指定堆栈。线程栈还用来存放函数中的局部变量:函数中的局部变量从线程栈空间中申请;函数中局部变量初始时从寄存器中分配(ARM 架构),当这个函数再调用另一个函数时,这些局部变量将放入栈中。

使用方法

创建和删除任务

创建线程

一个线程要成为可执行的对象,就必须由操作系统的内核来为它创建一个线程。 一般情况,创建线程都会分为两种方式,分别是动态创建和静态创建。 比如FreeRTOS的线程创建就是分为xTaskCreate( 使用动态的方法创建一个任务)和xTaskCreateStatic( 使用静态的方法创建一个任务)。 动态创建任务的堆栈由系统分配,而静态创建任务的堆栈由用户自己传递。 新创建的任务默认就是就绪态的,如果当前没有比它更高优先级的任务运行那么此任务就会立即进入运行态开始运行,不管在任务调度器启动前还是启动后,都可以创建任务。 我们均以FreeRTOS为例。

动态创建

xTaskCreate()此函数用来动态创建一个任务,任务需要 RAM 来保存与任务有关的状态信息(任务控制块),任务也需要一定的 RAM 来作为任务堆栈。如果使用函数 xTaskCreate()来创建任务的话那么这些所需的 RAM 就会自动的从 FreeRTOS 的堆中分配,因此必须提供内存管理文件,默认我们使用heap_4.c 这个内存管理文件

代码语言:javascript复制
BaseType_t xTaskCreate(  TaskFunction_t pxTaskCode,
              const char * const pcName,
              const uint16_t usStackDepth,
              void * const pvParameters,
              UBaseType_t uxPriority,
              TaskHandle_t * const pxCreatedTask )

参数:

名称

作用

pxTaskCode

任务函数。

pcName

任务名字,一般用于追踪和调试,任务名字长度不能超过configMAX_TASK_NAME_LEN。

usStackDepth

任务堆栈大小,注意实际申请到的堆栈是 usStackDepth 的 4 倍。其中空闲任务的任务堆栈大小为 configMINIMAL_STACK_SIZE。

pvParameters

传递给任务函数的参数。

uxPriotiry:

任务优先级,范围 0~ configMAX_PRIORITIES-1。

pxCreatedTask

任务句柄,任务创建成功以后会返回此任务的任务句柄,这个句柄其实就是任务的任务堆栈。此参数就用来保存这个任务句柄。其他 API 函数可能会使用到这个句柄。

返回值:

名称

含义

pdPASS

任务创建成功。

errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY

任务创建失败,因为堆内存不足!

静态创建

静态创建任务使用xTaskCreateStatic(),但是使用此函数创建的任务所需 的 RAM 需 要 用 用 户 来 提 供 。

代码语言:javascript复制
TaskHandle_t xTaskCreateStatic(  TaskFunction_t pxTaskCode,
                  const char * const pcName,
                  const uint32_t ulStackDepth,
                  void * const pvParameters,
                  UBaseType_t uxPriority,
                  StackType_t * const puxStackBuffer,
                  StaticTask_t * const pxTaskBuffer )

参数:

名称

作用

pxTaskCode

任务函数。

pcName

任务名字,一般用于追踪和调试,任务名字长度不能超过。

configMAX_TASK_NAME_LEN。

usStackDepth

任务堆栈大小,由于本函数是静态方法创建任务,所以任务堆栈由用户给出,一般是个数组,此参数就是这个数组的大小。

pvParameters

传递给任务函数的参数。

uxPriotiry

任务优先级,范围 0~ configMAX_PRIORITIES-1。

puxStackBuffer

任务堆栈,一般为数组,数组类型要为 StackType_t 类型。

pxTaskBuffe

任务控制块。

返回值:

名称

含义

NULL

任务创建失败,puxStackBuffer 或 pxTaskBuffer 为 NULL 的时候会导致这个错误的发生。

其他值

任务创建成功,返回任务的任务句柄。

删除线程

被删除了的任务不再存在,也就是说再也不会进入运行态。任务被删除以后就不能再使用此任务的句柄!如果此任务是使用动态方法创建的,也就是使用函数 xTaskCreate()创建的,那么在此任务被删除以后此任务之前申请的堆栈和控制块内存会在空闲任务中被释放掉,因此当调用函数 vTaskDelete()删除任务以后必须给空闲任务一定的运行时间。只有那些由内核分配给任务的内存才会在任务被删除以后自动的释放掉,用户分配给任务的内存需要用户自行释放掉。

代码语言:javascript复制
vTaskDelete( TaskHandle_t xTaskToDelete )

参数: xTaskToDelete: 要删除的任务的任务句柄。

挂起和恢复线程

有时候我们需要暂停某个任务的运行,过一段时间以后在重新运行。这个时候要是使用任务删除和重建的方法的话那么任务中变量保存的值肯定丢失了!RTOS 给我们提供了解决这种问题的方法,那就是任务挂起和恢复,当某个任务要停止运行一段时间的话就将这个任务挂起,当要重新运行这个任务的话就恢复这个任务的运行。 下面还以FreeRTOS为例:

挂起线程

在FreeRTOS中,vTaskSuspend()此函数用于将某个任务设置为挂起态,进入挂起态的任务永远都不会进入运行态。退出挂起态的唯一方法就是调用任务恢复函数 vTaskResume()或 xTaskResumeFromISR()。

代码语言:javascript复制
void vTaskSuspend( TaskHandle_t xTaskToSuspend );

参数: xTaskToSuspend:要挂起的任务的任务句柄,创建任务的时候会为每个任务分配一个任务句柄。如果使用函数 xTaskCreate()创建任务的话那么函数的参数pxCreatedTask 就是此任务的任务句柄,如果使用函数 xTaskCreateStatic()创建任务的话那么函数的返回值就是此任务的任务句柄。也可以通过函数 xTaskGetHandle()来根据任务名字来获取某个任务的任务句柄。注意!如果参数为 NULL 的话表示挂起任务自己。

恢复线程

在FreeRTOS中,vTaskResume()此函数用于将某一个任务从挂起态恢复到就绪态。

代码语言:javascript复制
void vTaskResume( TaskHandle_t xTaskToResume)

参数: xTaskToResume:要恢复的任务的任务句柄。

任务调度器

我们想要任务能够进行调度,就必须依赖于任务调度器,在FreeROTS中调度器开始使用的是vTaskStartScheduler();,这个函数的功能就是开启任务调度器。 开启后我们才可以进行任务调度。

空闲任务

空闲任务就是空闲的时候运行的任务,也就是系统中其他的任务由于各种原因不能运行的时候空闲任务就在运行。空闲任务是 RTOS 系统自动创建的,不需要用户手动创建。任务调度器启动以后就必须有一个任务运行!但是空闲任务不仅仅是为了满足任务调度器启动以后至少有一个任务运行而创建的,空闲任务中还会去做一些其他的事情,如下:

  1. 判断系统是否有任务删除,如果有的话就在空闲任务中释放被删除任务的任务堆栈和任务控制块的内存。
  2. 运行用户设置的空闲任务钩子函数。
  3. 判断是否开启低功耗 tickless 模式,如果开启的话还需要做相应的处理 空闲任务的任务优先级是最低的,为 0. 空闲线程是一个线程状态永远为就绪态的线程.

应用实例

光看枯燥的知识可能不太容易理解,下面我们来举个例子。 下面我们的项目,设计 4 个任务:start_task、key_task、task1_task 和 task2_task,这四个任务的任务功能如下: start_task:用来创建其他 3 个任务。 key_task:按键服务任务,检测按键的按下结果,根据不同的按键结果执行不同的操作。 task1_task:应用任务 1。 task2_task: 应用任务 2。 实验需要四个按键,KEY0、KEY1、KEY2 和 KEY_UP,这四个按键的功能如下: KEY0: 此按键为中断模式,在中断服务函数中恢复任务 2 的运行。 KEY1: 此按键为输入模式,用于恢复任务 1 的运行。 KEY2: 此按键为输入模式,用于挂起任务 2 的运行。 KEY_UP: 此按键为输入模式,用于挂起任务 1 的运行。 (1)、start_task 任务,用于创建其他 3 个任务。 (2)、在 key_tssk 任务里面,KEY_UP 被按下,调用函数 vTaskSuspend()挂起任务 1。 (3)、KEY1 被按下,调用函数 vTaskResume()恢复任务 1 的运行。 (4)、KEY2 被按下,调用函数 vTaskSuspend()挂起任务 2。 (5)、任务 1 的任务函数,用于观察任务挂起和恢复的过程。 (6)、任务 2 的任务函数,用于观察任务挂起和恢复的过程(中断方式)。

代码语言:javascript复制
//任务优先级#define START_TASK_PRIO    1//任务堆栈大小  #define START_STK_SIZE     128  //任务句柄TaskHandle_t StartTask_Handler;//任务函数void start_task(void *pvParameters);//任务优先级#define KEY_TASK_PRIO    2//任务堆栈大小  #define KEY_STK_SIZE     128  //任务句柄TaskHandle_t KeyTask_Handler;//任务函数void key_task(void *pvParameters);//任务优先级#define TASK1_TASK_PRIO    3//任务堆栈大小  #define TASK1_STK_SIZE     128  //任务句柄TaskHandle_t Task1Task_Handler;//任务函数void task1_task(void *pvParameters);//任务优先级#define TASK2_TASK_PRIO    4//任务堆栈大小  #define TASK2_STK_SIZE     128  //任务句柄TaskHandle_t Task2Task_Handler;//任务函数void task2_task(void *pvParameters);int main(void){
  NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4   
  delay_init();              //延时函数初始化   
  uart_init(115200);          //初始化串口
  LED_Init();                //初始化LED
  KEY_Init();              //初始化按键
  EXTIX_Init();            //初始化外部中断
  
  //创建开始任务
    xTaskCreate((TaskFunction_t )start_task,            //任务函数
                (const char*    )"start_task",          //任务名称
                (uint16_t       )START_STK_SIZE,        //任务堆栈大小
                (void*          )NULL,                  //传递给任务函数的参数
                (UBaseType_t    )START_TASK_PRIO,       //任务优先级
                (TaskHandle_t*  )&StartTask_Handler);   //任务句柄              
    vTaskStartScheduler();          //开启任务调度}//开始任务任务函数void start_task(void *pvParameters){
    taskENTER_CRITICAL();           //进入临界区
  //创建KEY任务
  xTaskCreate((TaskFunction_t )key_task,             
                (const char*    )"key_task",           
                (uint16_t       )KEY_STK_SIZE,        
                (void*          )NULL,                  
                (UBaseType_t    )KEY_TASK_PRIO,        
                (TaskHandle_t*  )&KeyTask_Handler);  
    //创建TASK1任务
    xTaskCreate((TaskFunction_t )task1_task,             
                (const char*    )"task1_task",           
                (uint16_t       )TASK1_STK_SIZE,        
                (void*          )NULL,                  
                (UBaseType_t    )TASK1_TASK_PRIO,        
                (TaskHandle_t*  )&Task1Task_Handler);   
    //创建TASK2任务
    xTaskCreate((TaskFunction_t )task2_task,     
                (const char*    )"task2_task",   
                (uint16_t       )TASK2_STK_SIZE,
                (void*          )NULL,
                (UBaseType_t    )TASK2_TASK_PRIO,
                (TaskHandle_t*  )&Task2Task_Handler); 
    vTaskDelete(StartTask_Handler); //删除开始任务
    taskEXIT_CRITICAL();            //退出临界区}//key任务函数void key_task(void *pvParameters){
  u8 key,statflag=0;
  while(1)
  {
    key=KEY_Scan(0);
    switch(key)
    {
      case WKUP_PRES:
        statflag=!statflag;
        if(statflag==1)
        {
          vTaskSuspend(Task1Task_Handler);//挂起任务
          printf("挂起任务1的运行!rn");
        }
        else if(statflag==0)
        {
          vTaskResume(Task1Task_Handler);  //恢复任务1
          printf("恢复任务1的运行!rn");
        }    
        break;
      case KEY1_PRES:
        vTaskSuspend(Task2Task_Handler);//挂起任务2
        printf("挂起任务2的运行!rn");
        break;
    }
    vTaskDelay(10);      //延时10ms 
  }}//task1任务函数void task1_task(void *pvParameters){
  u8 task1_num=0;
  printf("Task1 Run:000");
  while(1)
  {
    task1_num  ;  //任务执1行次数加1 注意task1_num1加到255的时候会清零!!
    LED0=!LED0;
    printf("任务1已经执行:%d次rn",task1_num);
    printf("%d",task1_num);  //显示任务执行次数
        vTaskDelay(1000);                           //延时1s,也就是1000个时钟节拍  
  }}//task2任务函数void task2_task(void *pvParameters){
  u8 task2_num=0;
  printf("Task2 Run:000");
  while(1)
  {
    task2_num  ;  //任务2执行次数加1 注意task1_num2加到255的时候会清零!!
        LED1=!LED1;
    printf("任务2已经执行:%d次rn",task2_num);
        vTaskDelay(1000);                           //延时1s,也就是1000个时钟节拍  
  }}

一开始任务 1 和任务 2 都正常运行,当挂起任务 1 或者任务 2 以后,任务 1 或者任务 2 就会停止运行,直到下一次重新恢复任务 1 或者任务 2 的运行。重点是,保存任务运行次数的变量都没有发生数据丢失,如果用任务删除和重建的方法这些数据必然会丢失掉的

后续

如果想了解更多物联网、智能家居项目知识,可以关注我的公众号了解更多。

编写不易,感谢支持。

0 人点赞