第三章:任务创建与管理

任务管理,说白了就是RTOS的心脏。你想想看,一个实时系统里跑着十几个甚至几十个任务,怎么让它们井井有条地工作?这就得靠任务控制块、状态机、优先级这些核心机制来撑腰。

我个人习惯把任务管理比作一个公司的员工管理。每个员工(任务)都有自己的档案(任务控制块),有自己当前的状态(上班、休息、请假),还有自己的职级(优先级)。老板(内核)根据这些信息来调度大家干活。

3.1 任务控制块(TCB)—— 任务的身份证

任务控制块,简称TCB。每个任务创建时,内核都会给它分配一个TCB。这里面存了什么?我列给你看:

  • 任务栈指针:指向任务自己的栈空间,保存局部变量和函数调用信息
  • 任务状态:当前处于就绪、运行、阻塞还是挂起
  • 优先级:数字越小优先级越高,还是反过来?各家RTOS定义不同
  • 任务名称:调试时用的,方便你认出是哪个任务
  • 任务入口函数:任务启动后执行的函数指针
  • 延时计数器:任务要睡多久才醒

我在项目中遇到过一个问题:有个同事把TCB结构体定义得特别大,里面塞了一堆调试信息。结果呢?每个任务多占几十字节,系统里跑了50个任务,内存直接炸了。嗯,这里要注意——TCB要精简,够用就行。

下面是一个典型的TCB结构体定义:

typedef struct tcb {
    uint32_t    *stack_ptr;      // 当前栈指针
    uint32_t    *stack_base;     // 栈底地址
    uint32_t     stack_size;     // 栈大小(字节)
    uint8_t      priority;       // 任务优先级(0最高)
    uint8_t      state;          // 任务状态
    char         name[16];       // 任务名称
    void       (*entry)(void*);  // 入口函数
    void        *arg;            // 入口参数
    uint32_t     delay_ticks;    // 延时计数
    struct tcb  *next;           // 链表指针
} tcb_t;

3.2 任务状态机 —— 任务的一生

任务不是一直在跑的。它会在几个状态之间跳来跳去。我画了一张图,你看一眼就明白了:

就绪态 运行态 阻塞态 挂起态 调度 时间片用完 等待资源 资源就绪 挂起 恢复 任务状态机

这张图里,四个状态之间的转换逻辑很清楚。我重点说一下几个容易搞混的地方:

  • 就绪态 → 运行态:调度器选中了优先级最高的就绪任务
  • 运行态 → 就绪态:时间片到了,或者有更高优先级的任务抢占了
  • 运行态 → 阻塞态:任务主动调用延时、等待信号量、等待消息队列
  • 阻塞态 → 就绪态:等待的条件满足了,比如延时结束、信号量释放
  • 运行态 → 挂起态:被其他任务或中断强制挂起
  • 挂起态 → 就绪态:被其他任务恢复

重要提醒:挂起态和阻塞态不一样。阻塞态是任务自己主动等的,挂起态是被别人强制暂停的。阻塞态的任务还在等条件,挂起态的任务直接不参与调度了。

3.3 任务优先级 —— 谁先谁后

优先级是RTOS调度决策的核心依据。我见过太多人在这上面栽跟头了。

先说两种常见的优先级策略:

策略类型 说明 典型RTOS
固定优先级 每个任务创建时指定优先级,运行中不变 FreeRTOS、uC/OS-II
动态优先级 优先级可以运行时调整,防止优先级反转 VxWorks、RT-Thread

优先级设计有几个原则,我踩过的坑都列出来:

  • 实时性要求高的任务给高优先级:比如电机控制、传感器采集
  • 避免优先级反转:低优先级任务占着高优先级任务需要的资源,这事我遇到过好几次
  • 不要所有任务都用同一个优先级:那跟裸机轮询有什么区别?
  • 优先级数量要合理:32级够用,256级有点浪费

我曾经...在一个项目中把所有通信任务都设成了最高优先级。结果呢?低优先级的控制任务根本跑不起来,电机抖得像筛子。后来我把通信任务降了一级,控制任务升了一级,世界清净了。

3.4 任务创建与删除

任务创建,就是告诉内核:嘿,我要跑一个新任务了。内核会分配TCB、分配栈空间、把任务加入就绪队列。

创建任务的典型流程:

  1. 分配TCB内存(静态或动态)
  2. 分配任务栈空间
  3. 初始化栈帧(模拟一次中断返回的现场)
  4. 设置任务优先级、名称等属性
  5. 把任务加入就绪队列

代码示例:

// 任务入口函数
void led_task(void *arg) {
    while(1) {
        GPIO_Toggle(LED_PIN);
        delay_ms(500);
    }
}

// 创建任务
tcb_t *task_tcb;
uint32_t task_stack[512];

task_tcb = task_create(
    "led_task",          // 任务名称
    led_task,            // 入口函数
    NULL,                // 参数
    task_stack,          // 栈空间
    sizeof(task_stack),  // 栈大小
    5                    // 优先级
);

if (task_tcb == NULL) {
    // 创建失败,处理错误
    error_handler();
}

删除任务呢?就是把任务从所有队列中移除,回收TCB和栈空间。但要注意:

我个人习惯:不到万不得已不要动态删除任务。删除任务容易造成内存碎片,而且如果任务正持有某个资源,删除后资源就泄漏了。我一般用挂起代替删除,或者让任务自己退出。

3.5 任务挂起与恢复

挂起和恢复,是任务管理的两个重要操作。挂起让任务暂停运行,恢复让它重新参与调度。

挂起操作的内部流程:

  • 检查任务是否在运行态或就绪态
  • 如果是运行态,触发一次调度,让出CPU
  • 把任务状态改为挂起态
  • 从就绪队列中移除

恢复操作的内部流程:

  • 检查任务是否在挂起态
  • 把任务状态改为就绪态
  • 加入就绪队列
  • 如果恢复的任务优先级更高,触发调度

代码示例:

// 挂起任务
void task_suspend(tcb_t *tcb) {
    if (tcb->state == STATE_RUNNING || tcb->state == STATE_READY) {
        tcb->state = STATE_SUSPENDED;
        remove_from_ready_queue(tcb);
        
        // 如果挂起的是当前任务,触发调度
        if (tcb == current_task) {
            schedule();
        }
    }
}

// 恢复任务
void task_resume(tcb_t *tcb) {
    if (tcb->state == STATE_SUSPENDED) {
        tcb->state = STATE_READY;
        add_to_ready_queue(tcb);
        
        // 如果恢复的任务优先级更高,触发调度
        if (tcb->priority < current_task->priority) {
            schedule();
        }
    }
}

注意:挂起操作可以嵌套吗?有些RTOS支持多次挂起,需要恢复同样次数才能回到就绪态。我个人觉得这个设计有点多余,增加复杂度不说,还容易搞混。我建议一次挂起对应一次恢复,简单明了。

3.6 实战经验总结

讲了这么多,我总结几条实战中容易踩的坑:

  • 栈空间别给太少:我见过一个任务栈溢出,把隔壁任务的TCB给覆盖了,查了两天才找到原因
  • 优先级别设太高:中断服务程序里别创建任务,优先级反转要处理
  • 删除任务要小心:确保任务没有持有互斥锁、信号量等资源
  • 挂起任务别太久:挂起时间太长,其他任务等它的资源会饿死

嗯,任务创建与管理这块,说白了就是三板斧:TCB存信息、状态机管状态、优先级定顺序。把这三点吃透了,RTOS的任务管理你就掌握了八成。

最后分享一个小技巧:调试时把每个任务的TCB信息打印出来,看看栈使用率、状态变化、运行次数。这些数据能帮你快速定位问题。我每次做项目都会加这个调试功能,省了不少事。

专注资料整理