跳转至

RTOS调度算法详解:理解任务如何被调度执行

概述

调度器(Scheduler)是RTOS的核心组件,它决定了在任何给定时刻哪个任务应该运行。理解调度算法是掌握RTOS的关键。完成本文学习后,你将能够:

  • 理解RTOS调度器的工作原理
  • 掌握抢占式调度和协作式调度的区别
  • 理解优先级调度的机制和策略
  • 掌握时间片轮转调度的原理
  • 了解不同调度策略的性能特点
  • 学会根据应用需求选择合适的调度策略

背景知识

什么是调度?

**调度(Scheduling)**是指操作系统决定哪个任务在何时运行的过程。

想象一个场景: - 你有3个任务要完成:写报告、回邮件、开会 - 如何安排这些任务的执行顺序? - 如果有紧急任务,是否要中断当前任务? - 如果任务优先级相同,如何公平分配时间?

这就是调度器要解决的问题。

为什么需要调度算法?

在多任务系统中,通常有多个任务需要执行,但CPU只有一个(或有限个)。调度算法的目标是:

  1. 保证实时性:高优先级任务能及时响应
  2. 提高效率:充分利用CPU资源
  3. 保证公平性:相同优先级任务公平分配时间
  4. 避免饥饿:低优先级任务也能得到执行机会

相关概念

就绪队列(Ready Queue):存储所有就绪态任务的数据结构,调度器从中选择任务执行。

上下文切换(Context Switch):保存当前任务的状态并恢复另一个任务状态的过程。

时间片(Time Slice):分配给任务的CPU时间单位,也称为时间量子(Time Quantum)。

调度点(Scheduling Point):触发调度器重新选择任务的时刻。

核心内容

调度器的基本工作流程

调度器的核心工作可以概括为三个步骤:

1. 选择任务 → 2. 上下文切换 → 3. 执行任务
     ↑                                    ↓
     └────────────── 调度触发 ←───────────┘

详细流程

// 调度器伪代码
void Scheduler(void) {
    while(1) {
        // 1. 从就绪队列中选择最高优先级任务
        Task *next_task = SelectHighestPriorityTask();

        // 2. 如果选中的任务不是当前任务,进行切换
        if(next_task != current_task) {
            // 保存当前任务上下文
            SaveContext(current_task);

            // 恢复新任务上下文
            RestoreContext(next_task);

            // 更新当前任务指针
            current_task = next_task;
        }

        // 3. 执行选中的任务
        // (实际上是返回到任务代码继续执行)
    }
}

抢占式调度 vs 协作式调度

RTOS的调度方式主要分为两大类:抢占式调度和协作式调度。

抢占式调度(Preemptive Scheduling)

定义:高优先级任务可以随时中断低优先级任务的执行。

特点: - 实时性好:高优先级任务能立即响应 - 系统响应快:不需要等待当前任务主动让出CPU - 复杂度高:需要处理任务切换和资源保护 - 适合硬实时系统

工作原理

时间轴:
0ms    10ms   20ms   30ms   40ms   50ms
|------|------|------|------|------|
低优先级任务运行
|████████|
        ↑ 高优先级任务就绪
        |██████|  ← 立即抢占
              ↑ 高优先级任务完成
              |████████████████|  ← 恢复低优先级

代码示例

// 低优先级任务
void LowPriorityTask(void *param) {
    while(1) {
        printf("Low priority task running\n");
        // 正在执行时可能被高优先级任务抢占
        DoLongWork();  // 长时间工作
        vTaskDelay(100);
    }
}

// 高优先级任务
void HighPriorityTask(void *param) {
    while(1) {
        // 等待事件
        xSemaphoreTake(event_sem, portMAX_DELAY);

        // 事件发生时立即抢占低优先级任务
        printf("High priority task running\n");
        HandleCriticalEvent();

        // 完成后,低优先级任务继续执行
    }
}

int main(void) {
    // 创建不同优先级的任务
    xTaskCreate(LowPriorityTask, "Low", 128, NULL, 1, NULL);
    xTaskCreate(HighPriorityTask, "High", 128, NULL, 5, NULL);

    vTaskStartScheduler();
    while(1);
}

协作式调度(Cooperative Scheduling)

定义:任务必须主动让出CPU,其他任务才能运行。

特点: - 实现简单:不需要复杂的任务切换机制 - 资源保护简单:任务不会被意外中断 - 实时性差:高优先级任务需要等待当前任务让出CPU - 适合软实时或非实时系统

工作原理

时间轴:
0ms    10ms   20ms   30ms   40ms   50ms
|------|------|------|------|------|
任务1运行
|████████████████|
                ↑ 主动让出CPU
                |████████████████|  ← 任务2运行
                                ↑ 主动让出CPU

代码示例

// 协作式调度示例
void Task1(void *param) {
    while(1) {
        printf("Task1 running\n");
        DoWork1();

        // 主动让出CPU
        taskYIELD();  // 或 vTaskDelay(0)
    }
}

void Task2(void *param) {
    while(1) {
        printf("Task2 running\n");
        DoWork2();

        // 主动让出CPU
        taskYIELD();
    }
}

// 注意:如果Task1不调用taskYIELD(),Task2永远无法运行

对比总结

特性 抢占式调度 协作式调度
任务切换 自动切换 手动切换
实时性
响应时间
实现复杂度
资源保护 需要互斥机制 相对简单
适用场景 硬实时系统 软实时/非实时系统
典型应用 FreeRTOS、RT-Thread 早期嵌入式系统

优先级调度

优先级调度是RTOS最常用的调度策略,它根据任务的优先级决定执行顺序。

固定优先级调度(Fixed Priority Scheduling)

原理:每个任务在创建时分配一个固定的优先级,调度器总是选择就绪队列中优先级最高的任务执行。

调度规则: 1. 高优先级任务优先执行 2. 相同优先级任务按照FIFO或时间片轮转 3. 低优先级任务只有在高优先级任务阻塞时才能运行

示例场景

// 定义优先级
#define PRIORITY_CRITICAL   5  // 关键任务(最高)
#define PRIORITY_HIGH       4  // 高优先级
#define PRIORITY_NORMAL     3  // 普通优先级
#define PRIORITY_LOW        2  // 低优先级
#define PRIORITY_IDLE       1  // 空闲任务(最低)

// 关键任务:安全监控(最高优先级)
void SafetyTask(void *param) {
    while(1) {
        // 监控系统安全状态
        if(CheckSafetyCondition()) {
            // 发现异常,立即处理
            HandleSafetyIssue();
        }
        vTaskDelay(10);  // 每10ms检查一次
    }
}

// 高优先级任务:数据采集
void DataAcquisitionTask(void *param) {
    while(1) {
        // 采集传感器数据
        float sensor_data = ReadSensor();

        // 发送到处理队列
        xQueueSend(data_queue, &sensor_data, 0);

        vTaskDelay(100);  // 每100ms采集一次
    }
}

// 普通优先级任务:数据处理
void DataProcessingTask(void *param) {
    float data;
    while(1) {
        // 从队列接收数据
        if(xQueueReceive(data_queue, &data, portMAX_DELAY)) {
            // 处理数据
            ProcessData(data);
        }
    }
}

// 低优先级任务:日志记录
void LoggingTask(void *param) {
    while(1) {
        // 记录系统日志
        WriteLog();
        vTaskDelay(1000);  // 每秒记录一次
    }
}

int main(void) {
    // 创建不同优先级的任务
    xTaskCreate(SafetyTask, "Safety", 256, NULL, PRIORITY_CRITICAL, NULL);
    xTaskCreate(DataAcquisitionTask, "DataAcq", 256, NULL, PRIORITY_HIGH, NULL);
    xTaskCreate(DataProcessingTask, "DataProc", 512, NULL, PRIORITY_NORMAL, NULL);
    xTaskCreate(LoggingTask, "Logging", 256, NULL, PRIORITY_LOW, NULL);

    vTaskStartScheduler();
    while(1);
}

执行时序图

时间轴:
0ms    10ms   20ms   30ms   40ms   50ms   60ms
|------|------|------|------|------|------|

优先级5(Safety):
|██|      |██|      |██|      |██|

优先级4(DataAcq):
  |████|      |████|      |████|

优先级3(DataProc):
      |██████|      |██████|

优先级2(Logging):
            |████████████████████|

说明:
- Safety任务每10ms运行一次,立即抢占其他任务
- DataAcq任务在Safety空闲时运行
- DataProc任务在更高优先级任务空闲时运行
- Logging任务优先级最低,只在所有高优先级任务空闲时运行

动态优先级调度(Dynamic Priority Scheduling)

原理:任务的优先级可以在运行时动态调整。

应用场景: - 避免优先级反转 - 实现优先级继承 - 根据系统状态调整任务重要性

代码示例

TaskHandle_t worker_task_handle;

// 工作任务
void WorkerTask(void *param) {
    while(1) {
        UBaseType_t current_priority = uxTaskPriorityGet(NULL);
        printf("Worker task priority: %d\n", current_priority);

        DoWork();
        vTaskDelay(100);
    }
}

// 管理任务
void ManagerTask(void *param) {
    bool emergency_mode = false;

    while(1) {
        // 检测系统状态
        if(DetectEmergency()) {
            if(!emergency_mode) {
                // 进入紧急模式:提升工作任务优先级
                printf("Emergency detected! Boosting priority\n");
                vTaskPrioritySet(worker_task_handle, 5);
                emergency_mode = true;
            }
        } else {
            if(emergency_mode) {
                // 恢复正常模式:降低工作任务优先级
                printf("Emergency cleared! Restoring priority\n");
                vTaskPrioritySet(worker_task_handle, 2);
                emergency_mode = false;
            }
        }

        vTaskDelay(50);
    }
}

int main(void) {
    // 创建任务
    xTaskCreate(WorkerTask, "Worker", 256, NULL, 2, &worker_task_handle);
    xTaskCreate(ManagerTask, "Manager", 256, NULL, 3, NULL);

    vTaskStartScheduler();
    while(1);
}

时间片轮转调度

当多个任务具有相同优先级时,RTOS使用时间片轮转(Round-Robin)策略公平分配CPU时间。

工作原理

基本概念: - 每个任务分配一个固定的时间片(如10ms) - 任务运行完一个时间片后,切换到下一个相同优先级的任务 - 所有相同优先级的任务轮流执行

时序图

时间片 = 10ms

时间轴:
0ms    10ms   20ms   30ms   40ms   50ms   60ms
|------|------|------|------|------|------|

任务A(优先级2):
|██████|            |██████|            |██████|

任务B(优先级2):
      |██████|            |██████|

任务C(优先级2):
            |██████|            |██████|

说明:
- 三个任务优先级相同,轮流执行
- 每个任务运行一个时间片(10ms)
- 按照A→B→C→A的顺序循环

配置时间片

// FreeRTOSConfig.h 配置文件

// 1. 启用时间片轮转
#define configUSE_TIME_SLICING  1

// 2. 配置时钟节拍频率(影响时间片精度)
#define configTICK_RATE_HZ  1000  // 1ms一个节拍

// 时间片大小 = 1个时钟节拍
// 如果需要更大的时间片,可以在任务中使用延时

代码示例

// 三个相同优先级的任务
void Task1(void *param) {
    uint32_t counter = 0;
    while(1) {
        printf("Task1: %d\n", counter++);
        // 不使用延时,让时间片轮转生效
        // 每个时间片后自动切换到Task2
    }
}

void Task2(void *param) {
    uint32_t counter = 0;
    while(1) {
        printf("Task2: %d\n", counter++);
        // 不使用延时
    }
}

void Task3(void *param) {
    uint32_t counter = 0;
    while(1) {
        printf("Task3: %d\n", counter++);
        // 不使用延时
    }
}

int main(void) {
    // 创建三个相同优先级的任务
    xTaskCreate(Task1, "Task1", 128, NULL, 2, NULL);
    xTaskCreate(Task2, "Task2", 128, NULL, 2, NULL);
    xTaskCreate(Task3, "Task3", 128, NULL, 2, NULL);

    vTaskStartScheduler();
    while(1);
}

运行结果

Task1: 0
Task1: 1
Task1: 2
...(运行一个时间片)
Task2: 0
Task2: 1
Task2: 2
...(运行一个时间片)
Task3: 0
Task3: 1
Task3: 2
...(运行一个时间片)
Task1: 3
Task1: 4
...(循环继续)

时间片大小的影响

时间片太小: - 优点:任务响应快,看起来更"并发" - 缺点:上下文切换频繁,CPU开销大

时间片太大: - 优点:上下文切换少,CPU效率高 - 缺点:任务响应慢,用户体验差

选择建议

// 典型配置
#define configTICK_RATE_HZ  1000  // 1ms时钟节拍

// 对于不同应用:
// - 实时性要求高:1-5ms时间片
// - 一般应用:10-20ms时间片
// - 后台任务:50-100ms时间片

调度策略对比

常见调度策略总结

调度策略 特点 优点 缺点 适用场景
固定优先级抢占 高优先级立即抢占 实时性好 低优先级可能饥饿 硬实时系统
时间片轮转 相同优先级轮流执行 公平性好 实时性一般 多任务公平调度
协作式调度 任务主动让出CPU 实现简单 实时性差 简单系统
动态优先级 运行时调整优先级 灵活性高 复杂度高 复杂系统

混合调度策略

实际的RTOS通常结合多种策略:

// FreeRTOS的调度策略:
// 1. 优先级抢占式调度(主要策略)
// 2. 相同优先级使用时间片轮转
// 3. 支持动态优先级调整

// 示例:混合调度
void HighPriorityTask(void *param) {
    while(1) {
        // 高优先级任务,抢占式执行
        HandleCriticalEvent();
        vTaskDelay(100);
    }
}

void NormalTask1(void *param) {
    while(1) {
        // 相同优先级任务1,时间片轮转
        DoWork1();
        // 不延时,让时间片轮转生效
    }
}

void NormalTask2(void *param) {
    while(1) {
        // 相同优先级任务2,时间片轮转
        DoWork2();
        // 不延时,让时间片轮转生效
    }
}

int main(void) {
    // 高优先级任务(抢占式)
    xTaskCreate(HighPriorityTask, "High", 256, NULL, 5, NULL);

    // 相同优先级任务(时间片轮转)
    xTaskCreate(NormalTask1, "Normal1", 256, NULL, 2, NULL);
    xTaskCreate(NormalTask2, "Normal2", 256, NULL, 2, NULL);

    vTaskStartScheduler();
    while(1);
}

调度触发时机

调度器在以下情况下会重新选择任务:

1. 时钟节拍中断(Tick Interrupt)

原理:每个时钟节拍都会触发调度器检查是否需要切换任务。

// 时钟节拍中断处理函数(简化版)
void SysTick_Handler(void) {
    // 1. 更新系统时钟计数
    system_tick_count++;

    // 2. 检查延时任务是否到期
    CheckDelayedTasks();

    // 3. 时间片轮转:切换到下一个相同优先级任务
    if(configUSE_TIME_SLICING) {
        RotateReadyList();
    }

    // 4. 触发任务调度
    if(NeedReschedule()) {
        TriggerPendSV();  // 触发上下文切换
    }
}

配置

// FreeRTOSConfig.h
#define configTICK_RATE_HZ  1000  // 1000Hz = 1ms一次中断

// 时钟节拍频率的影响:
// - 频率高:时间精度高,但中断开销大
// - 频率低:中断开销小,但时间精度低
// 典型值:100Hz-1000Hz

2. 任务主动让出CPU

场景: - 调用延时函数:vTaskDelay() - 等待信号量:xSemaphoreTake() - 等待队列:xQueueReceive() - 主动让出:taskYIELD()

void Task1(void *param) {
    while(1) {
        DoWork();

        // 主动让出CPU,触发调度
        vTaskDelay(100);  // 延时100ms
        // 或
        taskYIELD();      // 立即让出
    }
}

3. 高优先级任务就绪

场景: - 中断中释放信号量 - 中断中发送队列消息 - 中断中设置事件标志

// 中断服务函数
void EXTI_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    // 释放信号量,可能唤醒高优先级任务
    xSemaphoreGiveFromISR(event_sem, &xHigherPriorityTaskWoken);

    // 如果唤醒了更高优先级的任务,触发调度
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

// 高优先级任务
void HighPriorityTask(void *param) {
    while(1) {
        // 等待信号量
        xSemaphoreTake(event_sem, portMAX_DELAY);

        // 信号量到来时,立即抢占低优先级任务
        HandleEvent();
    }
}

4. 任务优先级改变

void ManagerTask(void *param) {
    while(1) {
        if(NeedBoost()) {
            // 提升任务优先级,触发调度
            vTaskPrioritySet(worker_handle, 5);
            // 如果新优先级高于当前任务,立即切换
        }

        vTaskDelay(100);
    }
}

上下文切换

上下文切换是调度的核心操作,它保存当前任务的状态并恢复新任务的状态。

上下文包含的内容

// 任务上下文(ARM Cortex-M为例)
typedef struct {
    // 1. 通用寄存器
    uint32_t R0;
    uint32_t R1;
    uint32_t R2;
    uint32_t R3;
    uint32_t R4;
    uint32_t R5;
    uint32_t R6;
    uint32_t R7;
    uint32_t R8;
    uint32_t R9;
    uint32_t R10;
    uint32_t R11;
    uint32_t R12;

    // 2. 特殊寄存器
    uint32_t SP;   // 栈指针
    uint32_t LR;   // 链接寄存器
    uint32_t PC;   // 程序计数器
    uint32_t PSR;  // 程序状态寄存器

    // 3. 浮点寄存器(如果使用FPU)
    float S0_S31[32];
    uint32_t FPSCR;
} TaskContext_t;

上下文切换过程

// 上下文切换伪代码
void ContextSwitch(Task *old_task, Task *new_task) {
    // 1. 保存当前任务上下文
    old_task->SP = GetCurrentSP();
    SaveRegisters(old_task);

    // 2. 恢复新任务上下文
    RestoreRegisters(new_task);
    SetCurrentSP(new_task->SP);

    // 3. 返回到新任务继续执行
    // (通过修改PC寄存器实现)
}

实际实现(ARM Cortex-M)

// PendSV中断处理函数(上下文切换)
__asm void PendSV_Handler(void) {
    // 1. 保存当前任务上下文
    MRS     R0, PSP         // 获取任务栈指针
    STMDB   R0!, {R4-R11}   // 保存R4-R11到栈

    // 2. 保存栈指针到TCB
    LDR     R1, =pxCurrentTCB
    LDR     R2, [R1]
    STR     R0, [R2]        // 保存SP到TCB

    // 3. 选择新任务
    BL      vTaskSwitchContext

    // 4. 恢复新任务上下文
    LDR     R1, =pxCurrentTCB
    LDR     R2, [R1]
    LDR     R0, [R2]        // 从TCB加载SP

    LDMIA   R0!, {R4-R11}   // 从栈恢复R4-R11
    MSR     PSP, R0         // 恢复任务栈指针

    // 5. 返回到新任务
    BX      LR
}

上下文切换的开销

时间开销: - Cortex-M3/M4:约1-3微秒 - Cortex-M0:约5-10微秒 - 包含FPU:增加50%-100%

影响因素: - CPU频率 - 是否使用FPU - 中断嵌套深度 - 缓存命中率

优化建议

// 1. 减少不必要的任务切换
void Task(void *param) {
    while(1) {
        DoWork();

        // ❌ 不好:频繁切换
        vTaskDelay(1);  // 每1ms切换一次

        // ✅ 更好:减少切换频率
        vTaskDelay(100);  // 每100ms切换一次
    }
}

// 2. 合理设置优先级,避免不必要的抢占
// 3. 使用任务通知代替信号量(更快)
// 4. 禁用FPU(如果不需要浮点运算)

适用场景分析

如何选择调度策略?

根据应用需求选择合适的调度策略:

硬实时系统

需求: - 严格的时间约束 - 高优先级任务必须立即响应 - 可预测的响应时间

推荐策略: - 固定优先级抢占式调度 - 禁用时间片轮转(避免不确定性) - 精心设计任务优先级

// 硬实时系统配置
#define configUSE_PREEMPTION        1  // 启用抢占
#define configUSE_TIME_SLICING      0  // 禁用时间片
#define configMAX_PRIORITIES        8  // 足够的优先级级别

// 任务优先级分配
#define PRIORITY_SAFETY     7  // 安全关键任务
#define PRIORITY_CONTROL    5  // 控制任务
#define PRIORITY_DATA       3  // 数据处理
#define PRIORITY_LOG        1  // 日志记录

软实时系统

需求: - 一般的时间约束 - 偶尔超时可以接受 - 需要公平性

推荐策略: - 优先级抢占式调度 - 启用时间片轮转 - 适度的优先级级别

// 软实时系统配置
#define configUSE_PREEMPTION        1  // 启用抢占
#define configUSE_TIME_SLICING      1  // 启用时间片
#define configMAX_PRIORITIES        5  // 适度的优先级

// 任务优先级分配
#define PRIORITY_HIGH       4  // 重要任务
#define PRIORITY_NORMAL     2  // 普通任务
#define PRIORITY_LOW        1  // 后台任务

多任务公平调度

需求: - 多个任务需要公平分配CPU - 没有严格的实时性要求 - 避免任务饥饿

推荐策略: - 所有任务使用相同优先级 - 启用时间片轮转 - 较大的时间片

// 公平调度配置
#define configUSE_PREEMPTION        1
#define configUSE_TIME_SLICING      1
#define configTICK_RATE_HZ          100  // 10ms时间片

// 所有任务相同优先级
xTaskCreate(Task1, "Task1", 256, NULL, 2, NULL);
xTaskCreate(Task2, "Task2", 256, NULL, 2, NULL);
xTaskCreate(Task3, "Task3", 256, NULL, 2, NULL);

深入理解

调度器的实现原理

就绪队列的数据结构

RTOS通常使用多级队列存储不同优先级的任务:

// 就绪队列结构(简化版)
typedef struct {
    List_t ready_lists[configMAX_PRIORITIES];  // 每个优先级一个链表
    uint32_t ready_bitmap;                      // 位图标记哪些优先级有就绪任务
} ReadyQueue_t;

// 查找最高优先级任务(O(1)时间复杂度)
Task* GetHighestPriorityTask(ReadyQueue_t *queue) {
    // 1. 从位图中找到最高优先级
    int highest_priority = FindHighestBit(queue->ready_bitmap);

    // 2. 从对应链表中取出第一个任务
    Task *task = GetFirstTask(&queue->ready_lists[highest_priority]);

    return task;
}

// 使用位操作快速查找
int FindHighestBit(uint32_t bitmap) {
    // ARM Cortex-M提供CLZ指令(Count Leading Zeros)
    // 可以在1个时钟周期内完成
    return 31 - __CLZ(bitmap);
}

优势: - 查找最高优先级任务:O(1)时间复杂度 - 添加/删除任务:O(1)时间复杂度 - 内存占用小

调度器的状态机

// 调度器状态
typedef enum {
    SCHEDULER_NOT_STARTED,  // 未启动
    SCHEDULER_RUNNING,      // 运行中
    SCHEDULER_SUSPENDED     // 挂起
} SchedulerState_t;

// 调度器控制
void vTaskStartScheduler(void) {
    // 1. 创建空闲任务
    CreateIdleTask();

    // 2. 启动时钟节拍
    StartSysTick();

    // 3. 选择第一个任务
    Task *first_task = GetHighestPriorityTask();

    // 4. 启动第一个任务
    StartFirstTask(first_task);

    // 永远不会返回
}

void vTaskSuspendAll(void) {
    // 挂起调度器(不会切换任务)
    scheduler_state = SCHEDULER_SUSPENDED;
}

void xTaskResumeAll(void) {
    // 恢复调度器
    scheduler_state = SCHEDULER_RUNNING;

    // 检查是否需要调度
    if(NeedReschedule()) {
        TriggerPendSV();
    }
}

调度延迟分析

**调度延迟**是指从事件发生到任务开始执行的时间,包括:

总延迟 = 中断延迟 + 调度延迟 + 上下文切换时间

中断延迟:从中断发生到ISR开始执行
调度延迟:ISR执行时间 + 调度器选择任务时间
上下文切换:保存/恢复上下文的时间

典型值(Cortex-M4 @ 168MHz)

// 中断延迟:12个时钟周期(硬件固定)
// = 12 / 168MHz ≈ 0.07微秒

// 调度延迟:取决于ISR复杂度
// 简单ISR:1-5微秒
// 复杂ISR:10-50微秒

// 上下文切换:约2微秒

// 总延迟:3-57微秒

优化方法

// 1. 简化ISR
void EXTI_IRQHandler(void) {
    // ✅ 好:只做必要的操作
    xSemaphoreGiveFromISR(event_sem, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

void EXTI_IRQHandler_Bad(void) {
    // ❌ 不好:在ISR中做复杂处理
    ProcessData();  // 耗时操作
    UpdateDisplay();
    WriteLog();
}

// 2. 使用任务通知代替信号量(更快)
void EXTI_IRQHandler(void) {
    vTaskNotifyGiveFromISR(task_handle, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

// 3. 降低中断优先级(减少中断嵌套)
NVIC_SetPriority(EXTI0_IRQn, 5);  // 较低的中断优先级

空闲任务

RTOS会自动创建一个空闲任务(Idle Task),它在所有其他任务都阻塞时运行。

空闲任务的作用

// 空闲任务伪代码
void IdleTask(void *param) {
    while(1) {
        // 1. 清理被删除任务的资源
        CleanupDeletedTasks();

        // 2. 执行用户定义的钩子函数
        if(configUSE_IDLE_HOOK) {
            vApplicationIdleHook();
        }

        // 3. 进入低功耗模式(可选)
        if(configUSE_TICKLESS_IDLE) {
            EnterLowPowerMode();
        }
    }
}

空闲任务钩子函数

// 在FreeRTOSConfig.h中启用
#define configUSE_IDLE_HOOK  1

// 实现钩子函数
void vApplicationIdleHook(void) {
    // 在空闲任务中执行的代码
    // 注意:不能阻塞或挂起

    // 示例1:进入低功耗模式
    __WFI();  // 等待中断

    // 示例2:执行后台任务
    CheckSystemHealth();

    // 示例3:喂看门狗
    FeedWatchdog();

    // 示例4:统计CPU使用率
    UpdateCPUUsage();
}

注意事项: - 空闲任务优先级最低(通常为0) - 空闲任务永远不能阻塞或挂起 - 空闲任务钩子函数必须快速返回 - 如果所有任务都阻塞,空闲任务会一直运行

常见问题

Q1: 为什么高优先级任务会"饿死"低优先级任务?

A: 这是优先级调度的固有特性。

问题场景

// 高优先级任务一直运行
void HighPriorityTask(void *param) {
    while(1) {
        DoWork();
        // 没有延时,一直占用CPU
    }
}

// 低优先级任务永远无法运行
void LowPriorityTask(void *param) {
    while(1) {
        DoWork();  // 永远不会执行
        vTaskDelay(100);
    }
}

解决方法

// 方法1:高优先级任务添加延时
void HighPriorityTask(void *param) {
    while(1) {
        DoWork();
        vTaskDelay(10);  // 让出CPU
    }
}

// 方法2:使用相同优先级 + 时间片轮转
xTaskCreate(Task1, "Task1", 256, NULL, 2, NULL);
xTaskCreate(Task2, "Task2", 256, NULL, 2, NULL);

// 方法3:动态调整优先级
void ManagerTask(void *param) {
    while(1) {
        // 定期提升低优先级任务
        vTaskPrioritySet(low_task_handle, 5);
        vTaskDelay(100);
        vTaskPrioritySet(low_task_handle, 1);
        vTaskDelay(1000);
    }
}

Q2: 时间片轮转何时生效?

A: 只有在以下条件同时满足时才生效:

  1. 启用时间片轮转:configUSE_TIME_SLICING = 1
  2. 有多个相同优先级的就绪任务
  3. 任务不主动让出CPU(不调用延时函数)
// ✅ 时间片轮转生效
void Task1(void *param) {
    while(1) {
        DoWork();
        // 不延时,让时间片轮转生效
    }
}

// ❌ 时间片轮转不生效
void Task2(void *param) {
    while(1) {
        DoWork();
        vTaskDelay(100);  // 主动让出CPU,不需要时间片
    }
}

Q3: 如何确定任务的优先级?

A: 根据以下原则分配优先级:

原则1:根据实时性要求

// 硬实时任务:最高优先级
#define PRIORITY_SAFETY     7  // 安全关键
#define PRIORITY_CONTROL    5  // 实时控制

// 软实时任务:中等优先级
#define PRIORITY_DATA       3  // 数据处理

// 非实时任务:低优先级
#define PRIORITY_LOG        1  // 日志记录

原则2:根据响应时间要求

// 需要快速响应:高优先级
xTaskCreate(InterruptHandler, "IRQ", 256, NULL, 6, NULL);

// 周期性任务:中等优先级
xTaskCreate(PeriodicTask, "Periodic", 256, NULL, 3, NULL);

// 后台任务:低优先级
xTaskCreate(BackgroundTask, "BG", 256, NULL, 1, NULL);

原则3:避免过多优先级级别

// ✅ 好:3-5个优先级级别
#define PRIORITY_CRITICAL   5
#define PRIORITY_HIGH       4
#define PRIORITY_NORMAL     3
#define PRIORITY_LOW        2
#define PRIORITY_IDLE       1

// ❌ 不好:过多的优先级级别
// 难以管理,容易出错

Q4: 上下文切换的开销有多大?

A: 上下文切换的开销取决于多个因素:

典型值

// Cortex-M3/M4 @ 168MHz
// 不使用FPU:约2微秒
// 使用FPU:约3-4微秒

// Cortex-M0 @ 48MHz
// 约5-10微秒

测量方法

void MeasureContextSwitchTime(void) {
    uint32_t start, end;

    // 创建两个高优先级任务
    xTaskCreate(Task1, "Task1", 128, NULL, 5, NULL);
    xTaskCreate(Task2, "Task2", 128, NULL, 5, NULL);

    // 在任务中测量
    void Task1(void *param) {
        while(1) {
            start = GetCycleCount();
            taskYIELD();  // 触发切换
        }
    }

    void Task2(void *param) {
        while(1) {
            end = GetCycleCount();
            uint32_t cycles = end - start;
            uint32_t time_us = cycles / (CPU_FREQ_MHZ);
            printf("Context switch time: %d us\n", time_us);
            taskYIELD();
        }
    }
}

优化建议: - 减少任务切换频率 - 禁用FPU(如果不需要) - 使用任务通知代替信号量 - 合理设置任务优先级

Q5: 如何避免优先级反转?

A: 优先级反转是指低优先级任务持有高优先级任务需要的资源,导致高优先级任务被阻塞。

问题场景

// 低优先级任务持有互斥量
void LowPriorityTask(void *param) {
    while(1) {
        xSemaphoreTake(mutex, portMAX_DELAY);
        // 持有互斥量,执行长时间操作
        DoLongWork();
        xSemaphoreGive(mutex);
        vTaskDelay(100);
    }
}

// 高优先级任务等待互斥量
void HighPriorityTask(void *param) {
    while(1) {
        // 被低优先级任务阻塞!
        xSemaphoreTake(mutex, portMAX_DELAY);
        DoWork();
        xSemaphoreGive(mutex);
    }
}

解决方法:使用优先级继承互斥量

// 创建支持优先级继承的互斥量
SemaphoreHandle_t mutex = xSemaphoreCreateMutex();

// 当低优先级任务持有互斥量时
// 如果高优先级任务请求该互斥量
// 低优先级任务的优先级会临时提升到高优先级
// 避免被中等优先级任务抢占

详细内容请参考:优先级反转问题与解决

性能分析

调度器性能指标

1. 调度延迟(Scheduling Latency)

定义:从任务就绪到开始执行的时间。

影响因素: - 中断延迟 - 当前任务的剩余时间片 - 调度器算法复杂度 - 上下文切换时间

测量方法

void MeasureSchedulingLatency(void) {
    uint32_t event_time, start_time;

    // 在中断中记录事件时间
    void ISR_Handler(void) {
        event_time = GetTimestamp();
        xSemaphoreGiveFromISR(sem, &xHigherPriorityTaskWoken);
        portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
    }

    // 在任务中记录开始时间
    void Task(void *param) {
        while(1) {
            xSemaphoreTake(sem, portMAX_DELAY);
            start_time = GetTimestamp();

            uint32_t latency = start_time - event_time;
            printf("Scheduling latency: %d us\n", latency);
        }
    }
}

2. 吞吐量(Throughput)

定义:单位时间内完成的任务数量。

影响因素: - 上下文切换频率 - 任务执行效率 - 调度策略

优化方法

// 减少上下文切换
// 方法1:增加任务延时
vTaskDelay(100);  // 而不是 vTaskDelay(1)

// 方法2:批量处理
void Task(void *param) {
    while(1) {
        // 一次处理多个数据
        for(int i = 0; i < 10; i++) {
            ProcessData();
        }
        vTaskDelay(100);
    }
}

3. CPU利用率

定义:CPU实际工作时间占总时间的百分比。

测量方法

// 使用空闲任务钩子统计
uint32_t idle_count = 0;
uint32_t total_count = 0;

void vApplicationIdleHook(void) {
    idle_count++;
}

void MonitorTask(void *param) {
    while(1) {
        vTaskDelay(1000);  // 每秒统计一次

        total_count = GetTotalTicks();
        float cpu_usage = (1.0f - (float)idle_count / total_count) * 100;

        printf("CPU Usage: %.1f%%\n", cpu_usage);

        idle_count = 0;
        total_count = 0;
    }
}

调度策略性能对比

策略 调度延迟 吞吐量 CPU利用率 公平性 实时性
固定优先级抢占
时间片轮转
协作式调度
动态优先级

选择建议: - 硬实时系统:固定优先级抢占 - 软实时系统:优先级抢占 + 时间片轮转 - 公平调度:时间片轮转 - 简单系统:协作式调度

实践示例

示例1:多优先级任务系统

创建一个包含不同优先级任务的完整系统:

#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"

// 定义优先级
#define PRIORITY_CRITICAL   5
#define PRIORITY_HIGH       4
#define PRIORITY_NORMAL     3
#define PRIORITY_LOW        2

// 全局变量
SemaphoreHandle_t event_sem;
uint32_t critical_counter = 0;
uint32_t high_counter = 0;
uint32_t normal_counter = 0;
uint32_t low_counter = 0;

// 关键任务(最高优先级)
void CriticalTask(void *param) {
    while(1) {
        // 等待紧急事件
        xSemaphoreTake(event_sem, portMAX_DELAY);

        // 立即处理
        printf("[CRITICAL] Handling emergency event %d\n", critical_counter++);
        HAL_GPIO_TogglePin(LED_CRITICAL_GPIO_Port, LED_CRITICAL_Pin);

        // 快速完成
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

// 高优先级任务
void HighPriorityTask(void *param) {
    while(1) {
        printf("[HIGH] Processing data %d\n", high_counter++);

        // 模拟数据处理
        for(volatile int i = 0; i < 10000; i++);

        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

// 普通优先级任务
void NormalPriorityTask(void *param) {
    while(1) {
        printf("[NORMAL] Running task %d\n", normal_counter++);

        // 模拟工作
        for(volatile int i = 0; i < 5000; i++);

        vTaskDelay(pdMS_TO_TICKS(200));
    }
}

// 低优先级任务
void LowPriorityTask(void *param) {
    while(1) {
        printf("[LOW] Background task %d\n", low_counter++);

        // 后台工作
        for(volatile int i = 0; i < 1000; i++);

        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

// 模拟事件触发
void EventSimulatorTask(void *param) {
    while(1) {
        // 每3秒触发一次紧急事件
        vTaskDelay(pdMS_TO_TICKS(3000));

        printf("\n*** Emergency Event Triggered ***\n");
        xSemaphoreGive(event_sem);
    }
}

int main(void) {
    // 系统初始化
    HAL_Init();
    SystemClock_Config();

    // 创建信号量
    event_sem = xSemaphoreCreateBinary();

    // 创建任务(按优先级从高到低)
    xTaskCreate(CriticalTask, "Critical", 256, NULL, PRIORITY_CRITICAL, NULL);
    xTaskCreate(HighPriorityTask, "High", 256, NULL, PRIORITY_HIGH, NULL);
    xTaskCreate(NormalPriorityTask, "Normal", 256, NULL, PRIORITY_NORMAL, NULL);
    xTaskCreate(LowPriorityTask, "Low", 256, NULL, PRIORITY_LOW, NULL);
    xTaskCreate(EventSimulatorTask, "EventSim", 256, NULL, PRIORITY_NORMAL, NULL);

    // 启动调度器
    printf("Starting scheduler...\n");
    vTaskStartScheduler();

    // 永远不会执行到这里
    while(1);
}

运行效果

[HIGH] Processing data 0
[NORMAL] Running task 0
[LOW] Background task 0
[HIGH] Processing data 1
[NORMAL] Running task 1

*** Emergency Event Triggered ***
[CRITICAL] Handling emergency event 0  ← 立即抢占
[HIGH] Processing data 2
[NORMAL] Running task 2
...

示例2:时间片轮转演示

演示相同优先级任务的时间片轮转:

#include "FreeRTOS.h"
#include "task.h"

// 任务1
void Task1(void *param) {
    uint32_t counter = 0;
    while(1) {
        printf("Task1: %d\n", counter++);

        // 不使用延时,让时间片轮转生效
        // 每个时间片后会自动切换到Task2

        // 模拟工作负载
        for(volatile int i = 0; i < 100000; i++);
    }
}

// 任务2
void Task2(void *param) {
    uint32_t counter = 0;
    while(1) {
        printf("Task2: %d\n", counter++);

        // 不使用延时
        for(volatile int i = 0; i < 100000; i++);
    }
}

// 任务3
void Task3(void *param) {
    uint32_t counter = 0;
    while(1) {
        printf("Task3: %d\n", counter++);

        // 不使用延时
        for(volatile int i = 0; i < 100000; i++);
    }
}

// 监控任务(高优先级)
void MonitorTask(void *param) {
    while(1) {
        printf("\n=== Monitor Report ===\n");
        printf("Free heap: %d bytes\n", xPortGetFreeHeapSize());
        printf("Tick count: %d\n", xTaskGetTickCount());

        vTaskDelay(pdMS_TO_TICKS(5000));
    }
}

int main(void) {
    SystemInit();

    // 创建三个相同优先级的任务(时间片轮转)
    xTaskCreate(Task1, "Task1", 128, NULL, 2, NULL);
    xTaskCreate(Task2, "Task2", 128, NULL, 2, NULL);
    xTaskCreate(Task3, "Task3", 128, NULL, 2, NULL);

    // 创建监控任务(高优先级)
    xTaskCreate(MonitorTask, "Monitor", 256, NULL, 3, NULL);

    vTaskStartScheduler();
    while(1);
}

运行效果

Task1: 0
Task1: 1
Task1: 2
...(运行一个时间片)
Task2: 0
Task2: 1
Task2: 2
...(运行一个时间片)
Task3: 0
Task3: 1
Task3: 2
...(运行一个时间片)
Task1: 3
Task1: 4
...(循环继续)

=== Monitor Report ===  ← 高优先级任务抢占
Free heap: 15360 bytes
Tick count: 5000

示例3:动态优先级调整

演示根据系统状态动态调整任务优先级:

#include "FreeRTOS.h"
#include "task.h"

TaskHandle_t sensor_task_handle;
TaskHandle_t process_task_handle;

// 传感器任务
void SensorTask(void *param) {
    uint32_t counter = 0;
    while(1) {
        UBaseType_t priority = uxTaskPriorityGet(NULL);
        printf("Sensor task (priority %d): reading %d\n", priority, counter++);

        // 模拟传感器读取
        float sensor_value = ReadSensor();

        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

// 数据处理任务
void ProcessTask(void *param) {
    uint32_t counter = 0;
    while(1) {
        UBaseType_t priority = uxTaskPriorityGet(NULL);
        printf("Process task (priority %d): processing %d\n", priority, counter++);

        // 模拟数据处理
        ProcessData();

        vTaskDelay(pdMS_TO_TICKS(200));
    }
}

// 系统管理任务
void SystemManagerTask(void *param) {
    typedef enum {
        MODE_NORMAL,
        MODE_HIGH_SPEED,
        MODE_LOW_POWER
    } SystemMode_t;

    SystemMode_t current_mode = MODE_NORMAL;
    uint32_t cycle = 0;

    while(1) {
        cycle++;

        // 每5秒切换一次模式
        if(cycle % 5 == 0) {
            current_mode = (current_mode + 1) % 3;

            switch(current_mode) {
                case MODE_NORMAL:
                    printf("\n*** NORMAL MODE ***\n");
                    vTaskPrioritySet(sensor_task_handle, 2);
                    vTaskPrioritySet(process_task_handle, 2);
                    break;

                case MODE_HIGH_SPEED:
                    printf("\n*** HIGH SPEED MODE ***\n");
                    // 提升传感器任务优先级
                    vTaskPrioritySet(sensor_task_handle, 5);
                    vTaskPrioritySet(process_task_handle, 4);
                    break;

                case MODE_LOW_POWER:
                    printf("\n*** LOW POWER MODE ***\n");
                    // 降低所有任务优先级
                    vTaskPrioritySet(sensor_task_handle, 1);
                    vTaskPrioritySet(process_task_handle, 1);
                    break;
            }
        }

        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

int main(void) {
    SystemInit();

    // 创建任务
    xTaskCreate(SensorTask, "Sensor", 256, NULL, 2, &sensor_task_handle);
    xTaskCreate(ProcessTask, "Process", 256, NULL, 2, &process_task_handle);
    xTaskCreate(SystemManagerTask, "Manager", 256, NULL, 3, NULL);

    vTaskStartScheduler();
    while(1);
}

总结

调度算法是RTOS的核心,理解调度原理对于开发高效的嵌入式系统至关重要。

核心要点

  1. 调度方式
  2. 抢占式调度:高优先级任务可以随时抢占低优先级任务
  3. 协作式调度:任务必须主动让出CPU
  4. 现代RTOS主要使用抢占式调度

  5. 优先级调度

  6. 固定优先级:任务优先级在创建时确定
  7. 动态优先级:运行时可以调整任务优先级
  8. 高优先级任务优先执行

  9. 时间片轮转

  10. 相同优先级任务公平分配CPU时间
  11. 每个任务运行一个时间片后切换
  12. 需要启用配置:configUSE_TIME_SLICING = 1

  13. 调度触发时机

  14. 时钟节拍中断
  15. 任务主动让出CPU
  16. 高优先级任务就绪
  17. 任务优先级改变

  18. 上下文切换

  19. 保存当前任务状态
  20. 恢复新任务状态
  21. 开销:1-10微秒(取决于CPU和配置)

最佳实践

  • 根据实时性要求选择调度策略
  • 合理分配任务优先级(3-5个级别)
  • 避免高优先级任务长时间占用CPU
  • 使用时间片轮转实现公平调度
  • 定期检查系统性能指标
  • 使用优先级继承避免优先级反转

配置建议

// FreeRTOSConfig.h 推荐配置

// 启用抢占式调度
#define configUSE_PREEMPTION        1

// 启用时间片轮转
#define configUSE_TIME_SLICING      1

// 时钟节拍频率(1ms)
#define configTICK_RATE_HZ          1000

// 优先级级别(5个足够)
#define configMAX_PRIORITIES        5

// 启用空闲任务钩子
#define configUSE_IDLE_HOOK         1

// 启用任务统计
#define configGENERATE_RUN_TIME_STATS  1

性能优化

  • 减少任务切换频率
  • 简化中断服务函数
  • 使用任务通知代替信号量
  • 合理配置时间片大小
  • 避免不必要的优先级调整

下一步学习

  • 学习任务间通信机制(信号量、队列、事件组)
  • 理解优先级反转问题及解决方案
  • 学习实时性分析和可调度性理论
  • 实践完整的多任务项目

延伸阅读

推荐进一步学习的资源:

参考资料

  1. "Real-Time Systems" - Jane W. S. Liu
  2. "Mastering the FreeRTOS Real Time Kernel" - Richard Barry
  3. "The Definitive Guide to ARM Cortex-M3 and Cortex-M4 Processors" - Joseph Yiu
  4. FreeRTOS官方文档 - https://www.freertos.org/
  5. "Operating System Concepts" - Abraham Silberschatz
  6. "Real-Time Concepts for Embedded Systems" - Qing Li, Caroline Yao

练习题

  1. 基础练习
  2. 创建三个不同优先级的任务,观察它们的执行顺序
  3. 实现时间片轮转,让三个相同优先级的任务轮流执行

  4. 进阶练习

  5. 测量上下文切换的时间开销
  6. 实现一个动态优先级调整系统
  7. 分析不同调度策略对系统性能的影响

  8. 综合练习

  9. 设计一个多任务系统,包含不同优先级的任务
  10. 实现CPU使用率统计功能
  11. 优化系统性能,减少上下文切换开销

  12. 调试练习

  13. 故意制造优先级反转问题,观察系统行为
  14. 使用调试工具分析任务调度情况
  15. 测量和优化系统的调度延迟

下一步:建议学习 信号量使用实战,掌握任务间同步和通信机制。