3. VxWorks内核与任务管理
VxWorks 的实时性并不来自某个单独的“快函数”,而是来自 Wind 内核把 CPU 时间切成可预测的片段,并按优先级把片段交给正确的任务。理解这一点的关键,是同时看清三件事:调度器在什么时机重新选任务、任务在什么条件下改变状态、以及工业控制中周期任务与事件任务如何分工。下面从 Wind 内核的调度机制出发,逐步落到任务创建、优先级设计和状态迁移的实际做法。
Wind 内核调度机制
Wind 内核采用基于优先级的抢占式调度,并支持同优先级任务的时间片轮转。系统为每个优先级维护一个就绪队列,调度器通过就绪位图快速找到当前最高优先级的就绪任务。只要有一个更高优先级任务进入就绪态,内核立即保存当前任务的上下文,切换到高优先级任务,这就是“抢占”。抢占发生的典型时机包括:高优先级任务被释放、信号量被释放、消息到达、中断服务程序退出等。
需要区分两个概念:任务优先级决定谁先运行,时间片只决定同优先级任务之间轮转多久。在工业控制中,通常把关键控制回路放在高优先级,把通信、日志、界面刷新放在较低优先级,避免非关键工作干扰控制周期。Wind 内核还提供轮转调度(round-robin)开关,但周期控制任务一般不建议依赖时间片,而应让每个任务在自身周期内主动阻塞,把 CPU 让给其他任务。
任务创建与删除
VxWorks 中任务创建的核心接口是 taskSpawn(),它一次性完成任务控制块分配、栈分配、入口函数绑定和就绪入队。一个典型的周期任务创建如下:
#include <vxWorks.h>
#include <taskLib.h>
#define CTRL_TASK_PRI 50
#define CTRL_TASK_STACK 8192
int ctrlTaskId;
void ctrlLoop(void)
{
while (1) {
/* 读取传感器、执行控制律、输出执行器 */
taskDelay(10); /* 10 tick 周期,示例中 1 tick = 1 ms */
}
}
void startCtrl(void)
{
ctrlTaskId = taskSpawn("tCtrl", CTRL_TASK_PRI, 0,
CTRL_TASK_STACK, (FUNCPTR)ctrlLoop,
0,0,0,0,0,0,0,0,0,0);
if (ctrlTaskId == ERROR) {
logMsg("ctrl task spawn failed\n", 0,0,0,0,0,0);
}
}
参数含义需要明确:任务名用于调试识别;优先级数值越小优先级越高;选项位 0 表示默认属性;栈大小要按函数调用深度和局部变量估算,控制任务通常留 4~8 KB。删除任务使用 taskDelete(),它会释放任务控制块和栈。若任务持有互斥量或正在写共享缓冲区,直接删除可能造成资源泄漏或数据不一致,因此更安全的做法是设置一个退出标志,让任务自行清理后调用 taskExit()。
任务优先级与状态迁移
任务状态主要包括:就绪(READY)、运行(RUNNING)、阻塞(PEND)、延迟(DELAY)、挂起(SUSPEND)。状态迁移由内核调用和外部事件共同驱动。例如,任务调用 semTake() 等待信号量时从运行态进入阻塞态;信号量被释放后回到就绪态;调用 taskDelay() 进入延迟态,延时到期后回到就绪态;被 taskSuspend() 挂起后即使就绪也不能运行,必须 taskResume() 才能恢复。
从图中可以看到,任务并不是“创建后一直运行”,而是在就绪、运行、阻塞、延迟、挂起之间反复迁移。调度器只在就绪队列中选任务,因此让任务及时进入阻塞或延迟态,是保证高优先级任务获得 CPU 的关键。工业控制中常见的错误是:低优先级通信任务在循环里忙等,导致高优先级控制任务被延迟,控制周期抖动增大。
周期任务与事件任务的设计
工业控制软件通常把任务分成两类。周期任务按固定时间间隔执行,例如每 10 ms 采样一次温度、执行一次 PID、输出一次 PWM。它的设计要点是:用 taskDelay() 或看门狗定时器保证周期稳定,任务体尽量短,避免在周期任务中做文件写入、网络等待等不确定操作。事件任务由外部事件触发,例如按键、报警输入、通信报文到达,通常通过信号量或消息队列唤醒,优先级根据事件紧急程度设定。
下面给出一个周期任务与事件任务协同的示例。周期任务负责控制回路,事件任务负责处理报警:
SEM_ID alarmSem;
void alarmIsr(void)
{
semGive(alarmSem); /* 中断中释放信号量,唤醒事件任务 */
}
void alarmTask(void)
{
while (1) {
semTake(alarmSem, WAIT_FOREVER);
/* 读取报警源、记录时间戳、执行安全动作 */
}
}
void ctrlTask(void)
{
while (1) {
/* 采样、PID 计算、输出 */
taskDelay(10); /* 固定 10 ms 周期 */
}
}
设计时需要检查两个指标:周期抖动和事件响应延迟。周期抖动指实际执行间隔与设定周期的偏差,若抖动超过控制周期的 10%~20%,控制质量可能明显下降。事件响应延迟指事件发生到事件任务开始执行的时间,它受中断延迟、高优先级任务占用和调度开销影响。可以用 tickGet() 在任务入口和出口打时间戳,统计多次执行的最大值和平均值,作为判断依据。
最后,任务删除和状态迁移要与资源管理一起考虑。删除任务前应确认它没有持有互斥量、没有正在访问共享内存、没有未释放的消息缓冲区。对于周期任务,推荐用退出标志加 taskDelay() 的方式让任务自然退出,而不是从外部强制删除。这样既能保证控制过程完整结束,也能避免系统在运行中留下难以排查的隐患。