1. 多核SMP实时调度工程导论
VxWorks 从单核实时内核演进到多核 SMP 内核,并不是简单地把任务“平均分到几个 CPU 上跑”。它改变的是一整套调度语义:任务可以在任意 CPU 上被调度,内核数据结构需要并发保护,中断、锁、缓存与优先级共同决定一个高优先级任务能否在确定的时间内拿到 CPU。本章先建立全局图景,说明 VxWorks SMP 的架构演进、多核实时调度的核心挑战,以及本课程要交付的工程目标与学习路线。
一、从单核到 SMP:VxWorks 内核的架构演进
早期 VxWorks(如 5.x、6.x 的 UP 版本)是典型的单核实时内核:只有一个就绪队列、一个当前任务指针、一套全局中断锁。所有任务共享同一个 CPU,调度器只需在“任务切换点”和“中断返回点”决定下一个运行的任务,优先级抢占语义非常直接——高优先级任务一旦就绪,立即抢占低优先级任务。
VxWorks 6.9 之后引入 SMP 支持,7.x 进一步成熟。SMP 内核的关键变化包括:
- 多核共享一个内核镜像:所有 CPU 运行同一份内核代码,共享全局任务表、就绪队列、对象容器等数据结构。
- 任务不再绑定 CPU:默认情况下任务可在任意 CPU 上运行,由调度器动态分配,只有通过
taskCpuAffinitySet()才能绑定到指定核。 - 引入细粒度内核锁:用自旋锁(spinlock)和中断锁(intLock)替代单核时代的全局中断禁用,保护共享数据结构。
- 调度器本身多核化:每个 CPU 有本地就绪队列,同时存在全局调度决策,任务可能在不同 CPU 间迁移。
这些变化带来性能提升,也带来新的不确定性。下图给出 UP 与 SMP 内核在任务调度路径上的结构对比,帮助建立直观认识。
二、多核实时调度的核心挑战
在单核上,实时性主要取决于优先级设计和中断延迟。到了 SMP,问题维度明显增加,工程上最常遇到的挑战可以归纳为四类。
第一,优先级反转在多核下更隐蔽。单核中经典的优先级反转可以通过优先级继承(priority inheritance)缓解。但在 SMP 中,一个低优先级任务持有自旋锁,可能同时阻塞多个 CPU 上的高优先级任务,形成“一对多”的阻塞链。如果锁持有时间过长,高优先级任务的响应时间会被显著拉长。
第二,缓存一致性与伪共享。多核各自有 L1/L2 缓存,共享数据被修改后需要缓存一致性协议同步。如果两个 CPU 频繁读写同一缓存行中的不同变量(伪共享,false sharing),即使逻辑上无冲突,也会产生大量缓存行来回迁移,导致性能抖动。实时系统中这种抖动会直接表现为任务执行时间不稳定。
第三,任务迁移带来的不确定性。SMP 调度器为了负载均衡,可能把任务从 CPU0 迁移到 CPU1。迁移本身有开销,且迁移后任务的缓存热度丢失,最坏执行时间(WCET)会变大。对于硬实时任务,通常需要用 CPU 亲和性绑定来消除迁移抖动。
第四,中断与调度器的并发。中断可以在任意 CPU 上到达,中断服务程序(ISR)可能唤醒任务,触发调度决策。如果 ISR 与调度器同时访问就绪队列,就需要锁保护。锁的粒度、持锁时间和中断屏蔽范围,直接决定系统的最坏中断延迟。
工程提醒:在 SMP 下评估实时性,不能只看平均响应时间,必须关注最坏情况下的响应时间(WCRT)。锁竞争、缓存迁移和任务迁移都会让 WCRT 明显大于单核场景。
三、课程工程目标
本课程不是泛泛介绍 VxWorks SMP 的概念,而是围绕一个可落地的工程目标展开:在 VxWorks SMP 平台上,构建一套可分析、可测量、可调优的多核实时调度方案。具体包括:
- 能读懂 VxWorks SMP 的调度器结构,理解就绪队列、优先级位图、CPU 亲和性等关键机制。
- 能针对给定任务集,合理设置优先级、CPU 亲和性和锁策略,避免优先级反转和伪共享。
- 能用
taskCpuAffinitySet()、spinLockCreate()、intLock()等接口完成实际配置。 - 能通过
windView、taskShow、spy等工具观察任务迁移、锁竞争和中断延迟,判断调度是否达标。 - 能对典型问题(如高优先级任务抖动、锁竞争导致延迟)进行定位和调优。
四、学习路线
课程按“机制—配置—测量—调优”的主线推进,建议按以下顺序学习:
- 机制篇:SMP 调度器结构、就绪队列、优先级位图、CPU 亲和性、自旋锁与中断锁。
- 配置篇:任务创建与优先级设置、CPU 绑定、锁的创建与使用、中断亲和性配置。
- 测量篇:使用系统自带工具观察任务迁移、锁等待、中断延迟,采集 WCRT 数据。
- 调优篇:针对优先级反转、伪共享、迁移抖动等问题,给出可操作的调优手段。
- 综合案例:以一个多任务实时采集与处理系统为例,完成从设计到验证的完整闭环。
下面给出本课程的学习路线与各阶段产出的关系图,便于把握整体节奏。
五、一个最小示例:观察 SMP 下的任务迁移
为了建立感性认识,可以在目标板上运行一个简单实验:创建两个同优先级任务,分别打印自己的 CPU 编号,观察它们是否会在不同 CPU 上运行。示例代码如下(参数为教学示例,实际需根据目标板调整):
#include <vxWorks.h>
#include <taskLib.h>
#include <stdio.h>
void workerTask(int id)
{
int cpu;
while (1) {
cpu = taskCpuGet(); /* 获取当前运行 CPU 编号 */
printf("task %d running on CPU %d\n", id, cpu);
taskDelay(sysClkRateGet() / 2); /* 延时 0.5 秒 */
}
}
void startDemo(void)
{
taskSpawn("t1", 100, 0, 0x2000, (FUNCPTR)workerTask, 1,0,0,0,0,0,0,0,0,0);
taskSpawn("t2", 100, 0, 0x2000, (FUNCPTR)workerTask, 2,0,0,0,0,0,0,0,0,0);
}
运行后如果看到两个任务交替出现在 CPU0、CPU1、CPU2 上,说明任务在迁移;如果希望它们固定,可以调用 taskCpuAffinitySet() 绑定。这个实验虽小,但已经触及 SMP 调度的核心:任务不再属于某个 CPU,而是由调度器动态分配。
学习建议:每学完一个机制,尽量在目标板或模拟环境上做一次最小验证。SMP 调度问题往往不是“看不懂”,而是“没观察过”。把 taskShow、spy 等工具用起来,比单纯读文档更有效。