2. VxWorks任务模型与状态机

VxWorks 的实时性建立在“任务”这一基本调度单位之上。理解任务模型,核心是理解三件事:内核用什么数据结构描述任务、任务在生命周期中如何迁移状态、以及任务栈如何被创建、使用和检测溢出。这三者共同决定了系统能否在确定的时间内完成关键响应。

任务控制块(TCB)结构

每个任务在内核中都对应一个任务控制块(Task Control Block,TCB)。TCB 是任务的“身份证”加“运行档案”,内核调度、切换、统计、异常处理都围绕它展开。典型 TCB 包含以下关键字段:

  • 任务标识:任务 ID(TID)与任务名,用于 windTaskId 查找与调试识别。
  • 调度信息:优先级、任务选项(如 VX_FP_TASK、VX_UNBREAKABLE)、调度策略相关标志。
  • 上下文保存区:CPU 寄存器组、程序计数器、状态字等,任务切换时保存与恢复。
  • 栈信息:栈基址、栈顶指针、栈大小、栈边界哨兵值,用于溢出检测。
  • 状态与等待信息:当前状态、等待对象(信号量、消息队列、事件)、延迟计数值。
  • 统计与钩子:CPU 占用时间、切换次数、任务钩子函数指针。

TCB 由内核在任务创建时从系统内存池分配,删除任务时回收。因此任务数量受内存池大小限制,而不是无限增长。理解 TCB 字段,有助于解释为什么任务优先级、栈大小、选项必须在创建时确定,而不是运行中随意更改。

任务状态转换

VxWorks 任务状态机是理解调度的关键。任务并非只有“运行”和“不运行”两种状态,而是包含就绪(READY)、运行(RUNNING)、阻塞(PEND)、延迟(DELAY)、挂起(SUSPEND)等状态。状态之间由内核 API 和事件驱动迁移。

VxWorks 任务状态转换图 就绪 READY 在就绪队列等待CPU 运行 RUNNING 占用CPU执行 阻塞 PEND 等待信号量/消息/事件 延迟 DELAY taskDelay 定时等待 挂起 SUSPEND 被 taskSuspend 冻结 调度器选中 被抢占/时间片到 semTake/msgQReceive 资源可用/超时 taskDelay(n) 延迟到期 taskSuspend taskResume 删除 DELETE taskDelete / 任务退出 状态说明: 就绪=可运行但未获CPU;运行=当前占用CPU;阻塞=等待资源;延迟=定时等待;挂起=被强制冻结 任一状态均可被 taskDelete 删除;挂起可叠加在阻塞/延迟之上,需先恢复才能继续

状态迁移的驱动来源主要有三类:调度器行为(抢占、时间片)、任务自身调用(taskDelay、semTake)、外部干预(taskSuspend、taskResume、taskDelete)。需要特别注意:挂起状态可以叠加在阻塞或延迟之上。一个任务可能同时处于“阻塞+挂起”,此时即使等待的资源可用,它也不会进入就绪队列,必须先执行 taskResume 才能继续。这是调试中常见的“任务不跑”原因之一。

任务创建与删除 API

VxWorks 提供 taskSpawn 和 taskInit/taskActivate 两种创建路径。taskSpawn 一步完成创建与激活,适合大多数场景:

#include <taskLib.h>

int taskId = taskSpawn(
    "tSensor",          /* 任务名 */
    100,                /* 优先级:数值越小优先级越高 */
    VX_FP_TASK,         /* 选项:使用浮点单元 */
    4096,               /* 栈大小(字节) */
    (FUNCPTR)sensorTask,/* 入口函数 */
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0  /* 最多10个参数 */
);
if (taskId == ERROR) {
    /* 创建失败:内存不足或参数非法 */
}

taskInit 只创建 TCB 和栈但不激活,随后可用 taskActivate 激活,适合需要先设置任务变量再启动的场景。删除任务使用 taskDelete(taskId),它会释放 TCB 和栈。若任务持有信号量或互斥量,删除前应先释放,否则可能造成资源永久占用。任务入口函数正常 return 也会触发删除,但更推荐显式 taskDelete(0) 删除自身,语义更清晰。

任务栈管理与溢出检测

每个任务拥有独立栈,栈大小在创建时确定。栈用于保存局部变量、函数调用返回地址、中断嵌套上下文。栈太小会导致溢出,破坏相邻内存,表现为随机死机或数据错乱;栈太大则浪费内存,影响可创建任务数量。

VxWorks 提供两种溢出检测机制。第一种是栈哨兵:taskSpawn 时在栈边界写入特定模式(如 0xEEEEEEEE),通过 checkStack 检查哨兵是否被改写。第二种是 MMU 保护:若启用,栈边界可设为不可访问页,溢出立即触发异常,定位更精确。

/* 检查任务栈使用情况 */
STATUS status = checkStack(taskId, 0);
/* 返回 OK 表示哨兵完好;ERROR 表示可能溢出 */

/* 查看栈高水位:剩余未使用字节数 */
int unused = taskId->stackRemaining; /* 调试器或 taskInfoGet 获取 */

实际工程中,栈大小估算可按“局部变量总和 + 调用深度 × 每层开销 + 中断嵌套余量”计算。例如一个任务局部变量约 200 字节,调用深度 8 层,每层约 64 字节,中断嵌套预留 512 字节,则基础需求约 200 + 8×64 + 512 = 1224 字节。考虑对齐和编译器优化,建议取 2 倍余量,即 4096 字节是常见安全起点。若使用浮点、递归或大数组,需单独评估。

调试建议:在任务入口和出口打印栈指针差值,或使用 taskInfoGet 获取栈使用峰值,运行一段时间后观察峰值是否接近栈上限。若峰值超过 80%,应增大栈或减少局部变量。
常见误区:把栈大小写成 0 或过小值,taskSpawn 可能返回成功但运行后随机崩溃;另一个误区是任务删除后继续使用其 TID,导致访问已释放 TCB。删除后应将 TID 置为 ERROR 或 NULL。