14. 事件优先级与调度:事件优先级定义、优先级反转处理、实时事件调度
事件驱动架构里,优先级是个绕不开的话题。说白了,就是当多个事件同时到达时,谁先被处理的问题。我早年做的一个工业控制项目,就因为在优先级上偷了懒,结果现场出了大问题——一个低优先级的温度报警事件,硬生生把高优先级的急停事件堵在了后面。嗯,那次教训让我彻底明白了优先级设计的分量。
14.1 事件优先级定义
优先级不是随便拍脑袋定的。我个人习惯把事件分成三个等级:
| 优先级等级 | 数值范围 | 典型事件 | 响应时间要求 |
|---|---|---|---|
| 高优先级 | 0-31 | 急停、故障、看门狗 | 微秒级 |
| 中优先级 | 32-127 | 数据采集、通信帧 | 毫秒级 |
| 低优先级 | 128-255 | 日志、界面刷新 | 秒级 |
数值越小,优先级越高。这是RTOS里常见的做法。你想想看,如果反过来,数值越大优先级越高,那默认初始化的0值就会变成最高优先级,容易出bug。
核心原则:优先级数值必须单调递增或递减,且预留足够的扩展空间。我建议至少留出32个等级给未来扩展。
代码实现上,我通常这样定义:
/* 事件优先级定义 */
typedef enum {
PRIO_CRITICAL = 0, /* 最高优先级 */
PRIO_HIGH = 10,
PRIO_NORMAL = 50,
PRIO_LOW = 100,
PRIO_IDLE = 255 /* 最低优先级 */
} EventPriority_t;
/* 事件结构体 */
typedef struct {
uint32_t eventId;
EventPriority_t priority;
void* data;
uint32_t timestamp; /* 用于同优先级FIFO排序 */
} Event_t;
这里有个细节:同优先级的事件怎么处理?我采用FIFO顺序。timestamp字段就是干这个用的——谁先来谁先走,公平合理。
14.2 优先级反转处理
优先级反转,这个词听起来挺吓人,其实场景很常见。举个例子:
- 任务A(高优先级)等待一个互斥锁
- 任务B(中优先级)正在运行,不涉及那个锁
- 任务C(低优先级)持有那个锁,但被B抢占了CPU
结果呢?A被C阻塞,C被B抢占。A明明优先级最高,却要等B跑完才能轮到C释放锁。这就是优先级反转。
我曾经在一个多传感器融合项目里踩过这个坑。高优先级的碰撞检测任务迟迟得不到响应,差点导致机器人撞墙。排查了两天才发现是优先级反转在作祟。
解决优先级反转,业界有三种主流方案:
| 方案 | 原理 | 适用场景 | 缺点 |
|---|---|---|---|
| 优先级继承 | 低优先级任务临时提升到高优先级 | 短临界区 | 实现复杂 |
| 优先级天花板 | 锁的优先级设为所有可能请求者的最高值 | 锁使用固定 | 可能过度提升 |
| 禁止抢占 | 持有锁时禁止任务切换 | 临界区极短 | 影响实时性 |
我个人偏爱优先级继承。虽然实现起来麻烦点,但效果最自然。下面是一个简化版的实现思路:
/* 优先级继承示例 */
void mutex_lock(Mutex_t* mutex, Task_t* task) {
if (mutex->owner == NULL) {
/* 无人持有,直接获取 */
mutex->owner = task;
return;
}
/* 检查是否需要优先级继承 */
if (task->priority < mutex->owner->priority) {
/* 高优先级任务在等待,提升持有者优先级 */
mutex->owner->original_priority = mutex->owner->priority;
mutex->owner->priority = task->priority;
}
/* 将当前任务加入等待队列 */
enqueue_waiting(task, &mutex->waiting_list);
}
嗯,这里要注意:释放锁的时候,记得把优先级恢复回去。不然低优先级任务就一直顶着高优先级跑了,那其他中优先级任务就遭殃了。
14.3 实时事件调度
实时事件调度,说白了就是保证高优先级事件在规定时间内得到处理。我常用的调度策略有三种:
- 固定优先级抢占式调度:高优先级事件随时打断低优先级。简单粗暴,适合硬实时。
- 时间片轮转:同优先级事件轮流执行。适合软实时,保证公平性。
- 截止时间优先:谁快超时谁先跑。适合有明确deadline的场景。
实际项目中,我通常把固定优先级抢占式调度作为基础,再结合时间片轮转处理同优先级事件。来看一个调度器的核心逻辑:
/* 事件调度器主循环 */
void event_scheduler_run(void) {
Event_t* event;
while (1) {
/* 从最高优先级队列取事件 */
event = dequeue_highest_priority();
if (event == NULL) {
/* 没有事件,进入低功耗模式 */
enter_sleep_mode();
continue;
}
/* 检查截止时间 */
if (is_deadline_missed(event)) {
/* 记录超时,但还是要处理 */
log_event_timeout(event);
}
/* 分发事件到对应的处理函数 */
dispatch_event(event);
/* 处理完毕后释放事件内存 */
free_event(event);
}
}
小技巧:调度器里一定要加看门狗喂狗操作。我曾经见过一个调度器死循环,整个系统卡死,现场工程师只能断电重启。有了看门狗,至少能自动恢复。
实时调度还有一个关键点:调度延迟。从事件触发到开始处理,中间的时间损耗包括:
- 中断响应时间(硬件延迟)
- 上下文切换时间(保存/恢复寄存器)
- 调度器决策时间(遍历优先级队列)
我建议用位图法来加速优先级查找。256个优先级,用32位变量就能表示,配合__builtin_clz指令,O(1)就能找到最高优先级。
/* 位图法快速查找最高优先级 */
static uint32_t priority_bitmap[8]; /* 256个优先级 */
static inline int get_highest_priority(void) {
int i;
for (i = 0; i < 8; i++) {
if (priority_bitmap[i] != 0) {
/* 找到非空的位图块 */
int bit = __builtin_clz(priority_bitmap[i]);
return i * 32 + (31 - bit);
}
}
return -1; /* 没有就绪事件 */
}
这个实现,我在Cortex-M3上实测过,一次查找只需要几十个时钟周期。对于微秒级的实时要求,完全够用。
避坑指南:实时调度里最容易忽略的是中断嵌套。高优先级中断可以打断低优先级中断,但如果中断处理函数里又触发了事件,就要小心优先级反转了。我建议中断里只做最轻量的事件标记,真正的处理放到任务级去完成。
最后说一句,事件优先级和调度不是一锤子买卖。系统跑起来后,要用示波器或者逻辑分析仪抓一下关键事件的响应时间。我习惯在事件处理函数入口和出口各放一个GPIO翻转,用示波器一看就知道调度延迟有没有超标。嗯,实践出真知,光靠理论推导是不够的。