第3章:VxWorks内核基础——任务(Task)概念、任务状态机与任务控制块
好,咱们今天聊聊VxWorks里最核心的东西——任务。说白了,任务就是VxWorks里能独立运行的一段代码,一个执行流。你想想看,一个实时系统要同时干好几件事,比如一边采集传感器数据,一边控制电机,还得跟人机界面交互。这怎么做到?就是靠任务。
我刚开始接触VxWorks时,有个误区:以为任务就是函数调用。后来被坑过一次才明白,任务是有“生命”的,它有自己的状态、自己的栈、自己的优先级。嗯,咱们今天就把这事彻底说清楚。
3.1 任务到底是什么?
任务(Task),在VxWorks里是调度的基本单位。每个任务都对应一个独立的执行线程,拥有自己的上下文。你可以把它想象成一个“小工人”,专门负责干某一类活。
举个例子,一个无人机飞控系统里,我通常会创建这么几个任务:
- 传感器采集任务:负责读取IMU、GPS数据,优先级最高
- 姿态解算任务:根据传感器数据算出当前姿态,优先级次高
- 控制输出任务:根据目标姿态和当前姿态,输出PWM给电机
- 通信任务:跟地面站收发数据,优先级最低
每个任务各司其职,互不干扰。这就是多任务的好处。
核心要点:任务不是函数,函数是被调用的,任务是被调度的。任务有独立的栈空间和TCB(任务控制块),函数没有。
3.2 任务状态机——任务的“生老病死”
任务不是一直运行的。它会在几个状态之间来回切换。VxWorks的任务状态机,我习惯把它分成四个核心状态:
| 状态 | 英文 | 说明 |
|---|---|---|
| 就绪 | READY | 任务已准备好运行,只等CPU分配给它 |
| 阻塞 | PEND | 任务在等待某个资源(信号量、消息队列等) |
| 挂起 | SUSPEND | 任务被强制暂停,不参与调度 |
| 延迟 | DELAY | 任务主动休眠一段时间 |
这几个状态之间怎么切换?我画个简单的逻辑:
- 任务创建后,进入就绪状态
- 调度器选中它,它就运行(运行态其实可以看作就绪态的一个子集)
- 运行中如果等信号量,就进入阻塞状态
- 运行中调用taskDelay(),就进入延迟状态
- 其他任务调用taskSuspend(),它就进入挂起状态
我的经验:调试时最常用的就是挂起状态。我曾经遇到一个任务跑飞了,整个系统卡死。我通过taskSuspend()把可疑任务一个个挂起,最后定位到是某个中断服务程序里调用了阻塞函数。嗯,这个坑大家以后要注意——ISR里千万别调用pend类的函数。
3.3 任务控制块(TCB)——任务的“身份证”
每个任务都有一个TCB。TCB里存了什么?说白了,就是任务的所有“个人信息”。VxWorks的TCB结构体定义在taskLib.h里,我挑几个关键字段说说:
typedef struct windTcb {
OBJ_CORE objCore; // 对象核心,用于对象管理
WIND_TCB *pTcbNext; // 链表指针,指向下一个TCB
WIND_TCB *pTcbPrev; // 链表指针,指向上一个TCB
UINT16 status; // 任务状态(就绪、阻塞等)
UINT16 priority; // 任务优先级(0-255,0最高)
FUNCPTR entry; // 任务入口函数地址
char name[WIND_MAX_NAME]; // 任务名称
void *pStackBase; // 栈基址
void *pStackLimit; // 栈限制地址
UINT stackSize; // 栈大小
...
} WIND_TCB;
你看,TCB里记录了任务的优先级、状态、栈信息、入口函数等。调度器就是靠这些信息来决定“下一个该谁跑”。
避坑指南:我曾经接手过一个项目,系统运行几天后莫名其妙死机。查了三天,最后发现是某个任务的栈溢出了,把相邻任务的TCB给覆盖了。从那以后,我每个任务都会预留至少20%的栈余量,并且在调试阶段打开栈溢出检测。
3.4 任务创建与删除——实战代码
光说不练假把式。咱们直接看代码。创建一个任务,用taskSpawn()函数:
#include <vxWorks.h>
#include <taskLib.h>
/* 任务入口函数 */
void myTask(int param1, int param2)
{
while (1) {
printf("Task running, param1=%d, param2=%d\n", param1, param2);
taskDelay(100); /* 延迟100个tick */
}
}
/* 创建任务 */
STATUS createMyTask(void)
{
int taskId;
taskId = taskSpawn("tMyTask", /* 任务名 */
100, /* 优先级,100 */
0, /* 选项,默认0 */
4096, /* 栈大小,4KB */
(FUNCPTR)myTask, /* 入口函数 */
10, /* 参数1 */
20, /* 参数2 */
0, 0, 0, 0, 0, 0, 0); /* 剩余参数填0 */
if (taskId == ERROR) {
printf("Task spawn failed!\n");
return ERROR;
}
printf("Task created, ID=%d\n", taskId);
return OK;
}
删除任务用taskDelete():
STATUS deleteMyTask(int taskId)
{
if (taskDelete(taskId) == ERROR) {
printf("Delete task %d failed!\n", taskId);
return ERROR;
}
return OK;
}
个人习惯:我一般不用taskDelete()直接删任务,而是用信号量或标志位让任务自己退出。为什么呢?因为taskDelete()如果删了一个正在使用共享资源的任务,可能导致死锁。我曾经在项目里吃过这个亏,后来就改成“优雅退出”的方式了。
3.5 任务状态查询——调试利器
调试时最常用的命令是i(显示所有任务信息)和ti(显示指定任务信息)。在VxWorks Shell里输入:
-> i
NAME ENTRY TID PRI STATUS PC SP ERRNO
---------- ----------- ------ ---- ----------- -------- ------- -----
tMyTask myTask 3f8a00 100 READY 3f8a10 3f8a00 0
tExcTask excTask 3f8b00 0 PEND 3f8b10 3f8b00 0
tLogTask logTask 3f8c00 0 PEND 3f8c10 3f8c00 0
tShell shellTask 3f8d00 1 READY 3f8d10 3f8d00 0
你看,STATUS列显示的就是任务当前状态。如果任务卡住了,先看看它是不是在PEND状态,等什么资源。如果是SUSPEND状态,看看是谁把它挂起了。
嗯,这里要注意:任务状态是动态变化的。你查一次是一个样,过一会儿可能就变了。所以调试时我习惯用watchdog定时打印任务状态,这样能抓到状态切换的瞬间。
3.6 本章小结
咱们今天把任务的三个核心概念捋了一遍:
- 任务:VxWorks里独立执行的线程,有独立栈和TCB
- 任务状态机:就绪、阻塞、挂起、延迟四个状态,调度器负责切换
- 任务控制块:任务的“身份证”,记录了优先级、栈、入口等关键信息
下一章咱们聊聊任务调度——VxWorks是怎么决定“下一个该谁跑的”。这个很有意思,因为VxWorks用的是基于优先级的抢占式调度,跟Linux的CFS完全不一样。到时候我会拿一个实际项目里的调度问题来举例,保证让你印象深刻。