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 实时事件调度

实时事件调度,说白了就是保证高优先级事件在规定时间内得到处理。我常用的调度策略有三种:

  1. 固定优先级抢占式调度:高优先级事件随时打断低优先级。简单粗暴,适合硬实时。
  2. 时间片轮转:同优先级事件轮流执行。适合软实时,保证公平性。
  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翻转,用示波器一看就知道调度延迟有没有超标。嗯,实践出真知,光靠理论推导是不够的。

事件优先级与调度核心架构 事件源 高优先级队列 (0-31) 中优先级队列 (32-127) 低优先级队列 (128-255) 事件调度器 位图法 O(1) 查找 事件处理函数 优先级反转处理 • 优先级继承 • 优先级天花板 实时调度策略 • 固定优先级抢占 • 时间片轮转 / 截止时间优先 调度延迟 中断响应 + 上下文切换 + 调度决策时间 事件优先级与调度核心架构图

公众号:蓝海资料掘金营,微信 deep3321