BLOG8
跳转至

时间片轮询调度算法:实现简单的多任务调度

概述

时间片轮询调度(Time Slice Round-Robin Scheduling)是一种简单而有效的任务调度算法,它为每个任务分配固定的执行时间,让多个任务轮流执行,从而在裸机环境下实现"多任务"的效果。完成本文学习后,你将能够:

  • 理解时间片调度的基本概念和工作原理
  • 掌握轮询调度算法的实现方法
  • 学会设计和实现任务切换机制
  • 了解如何处理任务优先级
  • 能够分析调度系统的性能特征

背景知识

什么是时间片?

时间片(Time Slice) 是分配给每个任务的固定执行时间。就像轮流使用操场,每个班级有固定的时间段,时间到了就换下一个班级。

核心概念: - 时间片长度:每个任务连续执行的时间,通常为1-100ms - 时间片用完:当前任务的时间片耗尽,需要切换到下一个任务 - 任务轮转:所有任务按顺序轮流获得CPU时间

为什么需要时间片调度?

在裸机程序中,我们经常遇到需要"同时"执行多个任务的情况:

问题场景:

// 没有调度的代码:任务阻塞
while(1) {
    ReadSensor();      // 可能需要100ms
    UpdateDisplay();   // 可能需要50ms
    CheckButton();     // 需要及时响应
    ProcessData();     // 可能需要200ms
}

问题分析: - 如果ReadSensor()阻塞100ms,其他任务都要等待 - CheckButton()无法及时响应用户操作 - 任务执行时间不均衡,影响系统响应性

使用时间片调度后:

// 每个任务最多执行10ms
Task1() { ReadSensor(); }      // 分多次完成
Task2() { UpdateDisplay(); }   // 分多次完成
Task3() { CheckButton(); }     // 快速响应
Task4() { ProcessData(); }     // 分多次完成

// 调度器每10ms切换一次任务
// Task1 → Task2 → Task3 → Task4 → Task1 → ...

轮询调度的特点

优点: - 简单易实现,代码量小 - 公平性好,每个任务都能获得执行机会 - 响应性可预测,最大延迟 = 任务数 × 时间片长度 - 不需要复杂的优先级管理

缺点: - 所有任务优先级相同,无法区分重要性 - 时间片固定,不够灵活 - 任务切换有开销 - 不适合实时性要求极高的场景

核心内容

时间片调度的基本原理

时间片调度的核心思想是:将CPU时间分成固定长度的时间片,让每个任务轮流执行一个时间片。

调度流程

[任务1执行] → 时间片到 → [任务2执行] → 时间片到 → [任务3执行] → 时间片到 → [任务1执行] → ...
    10ms                      10ms                      10ms                      10ms

关键步骤: 1. 初始化:设置时间片长度,创建任务列表 2. 启动调度:从第一个任务开始执行 3. 时间片计时:使用定时器跟踪当前任务的执行时间 4. 任务切换:时间片到期时,保存当前任务状态,切换到下一个任务 5. 循环执行:重复步骤3-4

基础实现:简单的时间片调度器

让我们从最简单的实现开始,逐步构建一个完整的调度系统。

方法1:函数指针数组实现

这是最简单的实现方式,适合任务数量固定的场景:

#include <stdint.h>
#include <stdbool.h>

// 任务函数类型定义
typedef void (*TaskFunction_t)(void);

// 任务定义
#define MAX_TASKS 4
#define TIME_SLICE_MS 10  // 时间片长度:10ms

// 任务函数数组
TaskFunction_t task_list[MAX_TASKS];
uint8_t task_count = 0;
uint8_t current_task = 0;

// 时间片计数器
volatile uint32_t time_slice_counter = 0;

// 注册任务
void Scheduler_RegisterTask(TaskFunction_t task) {
    if(task_count < MAX_TASKS) {
        task_list[task_count++] = task;
    }
}

// SysTick中断(1ms)
void SysTick_Handler(void) {
    time_slice_counter++;
}

// 调度器主循环
void Scheduler_Run(void) {
    uint32_t last_switch_time = 0;

    while(1) {
        // 检查是否需要切换任务
        if((time_slice_counter - last_switch_time) >= TIME_SLICE_MS) {
            last_switch_time = time_slice_counter;

            // 切换到下一个任务
            current_task = (current_task + 1) % task_count;
        }

        // 执行当前任务
        if(task_count > 0) {
            task_list[current_task]();
        }
    }
}

// 示例任务
void Task_LED(void) {
    static uint32_t counter = 0;
    counter++;
    if(counter >= 50) {  // 500ms
        counter = 0;
        LED_Toggle();
    }
}

void Task_Button(void) {
    if(Button_IsPressed()) {
        HandleButtonPress();
    }
}

void Task_Sensor(void) {
    static uint32_t counter = 0;
    counter++;
    if(counter >= 100) {  // 1000ms
        counter = 0;
        ReadSensor();
    }
}

void Task_Display(void) {
    UpdateDisplay();
}

// 主函数
int main(void) {
    SystemInit();
    SysTick_Init();  // 配置1ms中断

    // 注册任务
    Scheduler_RegisterTask(Task_LED);
    Scheduler_RegisterTask(Task_Button);
    Scheduler_RegisterTask(Task_Sensor);
    Scheduler_RegisterTask(Task_Display);

    // 启动调度器
    Scheduler_Run();

    return 0;
}

代码说明: - 任务注册:使用函数指针数组存储任务 - 时间片计时:使用SysTick中断提供1ms时基 - 任务切换:时间片到期时,切换到下一个任务 - 任务执行:每次循环执行当前任务一次

关键点: - 每个任务函数应该快速返回,不能阻塞 - 任务内部使用计数器实现定时功能 - 时间片长度影响系统响应性和开销

方法2:任务控制块(TCB)实现

使用任务控制块可以存储更多任务信息,实现更灵活的调度:

#include <stdint.h>
#include <stdbool.h>

// 任务状态
typedef enum {
    TASK_STATE_READY,      // 就绪
    TASK_STATE_RUNNING,    // 运行
    TASK_STATE_SUSPENDED   // 挂起
} TaskState_t;

// 任务控制块
typedef struct {
    TaskFunction_t function;    // 任务函数
    TaskState_t state;          // 任务状态
    uint32_t time_slice;        // 时间片长度(ms)
    uint32_t elapsed_time;      // 已执行时间(ms)
    uint32_t total_time;        // 总执行时间(统计)
    uint32_t run_count;         // 运行次数(统计)
    char name[16];              // 任务名称
} TaskControlBlock_t;

// 任务列表
#define MAX_TASKS 8
TaskControlBlock_t task_table[MAX_TASKS];
uint8_t task_count = 0;
uint8_t current_task_index = 0;

// 系统时钟(1ms)
volatile uint32_t system_ticks = 0;

// SysTick中断
void SysTick_Handler(void) {
    system_ticks++;
}

// 创建任务
bool Scheduler_CreateTask(TaskFunction_t function, 
                          uint32_t time_slice,
                          const char *name) {
    if(task_count >= MAX_TASKS) {
        return false;
    }

    TaskControlBlock_t *task = &task_table[task_count];
    task->function = function;
    task->state = TASK_STATE_READY;
    task->time_slice = time_slice;
    task->elapsed_time = 0;
    task->total_time = 0;
    task->run_count = 0;
    strncpy(task->name, name, sizeof(task->name) - 1);

    task_count++;
    return true;
}

// 挂起任务
void Scheduler_SuspendTask(uint8_t task_id) {
    if(task_id < task_count) {
        task_table[task_id].state = TASK_STATE_SUSPENDED;
    }
}

// 恢复任务
void Scheduler_ResumeTask(uint8_t task_id) {
    if(task_id < task_count) {
        task_table[task_id].state = TASK_STATE_READY;
    }
}

// 获取下一个就绪任务
uint8_t Scheduler_GetNextTask(void) {
    uint8_t start = current_task_index;

    do {
        current_task_index = (current_task_index + 1) % task_count;

        if(task_table[current_task_index].state == TASK_STATE_READY) {
            return current_task_index;
        }
    } while(current_task_index != start);

    return 0xFF;  // 没有就绪任务
}

// 调度器运行
void Scheduler_Run(void) {
    uint32_t last_tick = system_ticks;

    while(1) {
        // 计算时间增量
        uint32_t current_tick = system_ticks;
        uint32_t delta = current_tick - last_tick;
        last_tick = current_tick;

        if(task_count == 0) {
            continue;
        }

        TaskControlBlock_t *current_task = &task_table[current_task_index];

        // 更新当前任务的执行时间
        current_task->elapsed_time += delta;
        current_task->total_time += delta;

        // 检查时间片是否用完
        if(current_task->elapsed_time >= current_task->time_slice) {
            // 时间片用完,切换任务
            current_task->elapsed_time = 0;
            current_task->state = TASK_STATE_READY;

            // 获取下一个任务
            uint8_t next_task = Scheduler_GetNextTask();
            if(next_task != 0xFF) {
                current_task_index = next_task;
                current_task = &task_table[current_task_index];
            }
        }

        // 执行当前任务
        if(current_task->state == TASK_STATE_READY) {
            current_task->state = TASK_STATE_RUNNING;
            current_task->run_count++;

            // 调用任务函数
            current_task->function();

            // 任务执行完毕,恢复就绪状态
            if(current_task->state == TASK_STATE_RUNNING) {
                current_task->state = TASK_STATE_READY;
            }
        }
    }
}

// 打印任务统计信息
void Scheduler_PrintStats(void) {
    printf("Task Statistics:\n");
    printf("%-16s %8s %12s %10s\n", "Name", "State", "Total Time", "Run Count");

    for(uint8_t i = 0; i < task_count; i++) {
        TaskControlBlock_t *task = &task_table[i];
        printf("%-16s %8d %12lu %10lu\n",
               task->name,
               task->state,
               task->total_time,
               task->run_count);
    }
}

改进说明: - 任务控制块:存储任务的完整信息 - 任务状态管理:支持就绪、运行、挂起状态 - 灵活的时间片:每个任务可以有不同的时间片长度 - 统计功能:记录任务执行时间和次数 - 任务挂起/恢复:可以动态控制任务的执行

任务切换机制

任务切换是调度器的核心功能,需要考虑以下几个方面:

1. 时间片到期检测

// 方法1:轮询检测
void Scheduler_Loop(void) {
    while(1) {
        if(IsTimeSliceExpired()) {
            SwitchToNextTask();
        }
        ExecuteCurrentTask();
    }
}

// 方法2:中断驱动
void Timer_IRQHandler(void) {
    // 时间片到期中断
    time_slice_expired = true;
}

void Scheduler_Loop(void) {
    while(1) {
        if(time_slice_expired) {
            time_slice_expired = false;
            SwitchToNextTask();
        }
        ExecuteCurrentTask();
    }
}

2. 任务上下文保存

在简单的协作式调度中,任务主动返回,不需要保存上下文:

// 协作式任务(主动返回)
void Task_Example(void) {
    // 执行一小段工作
    DoSomeWork();

    // 主动返回,让出CPU
    return;
}

在抢占式调度中,需要保存任务的执行状态(寄存器、栈指针等)。这在裸机环境下较为复杂,通常需要汇编代码支持。

3. 任务选择策略

// 轮询选择(Round-Robin)
uint8_t SelectNextTask_RoundRobin(void) {
    uint8_t next = (current_task + 1) % task_count;

    // 跳过挂起的任务
    while(task_table[next].state == TASK_STATE_SUSPENDED) {
        next = (next + 1) % task_count;
    }

    return next;
}

// 优先级选择(带优先级的轮询)
uint8_t SelectNextTask_Priority(void) {
    // 首先查找高优先级任务
    for(uint8_t i = 0; i < task_count; i++) {
        if(task_table[i].priority == PRIORITY_HIGH &&
           task_table[i].state == TASK_STATE_READY) {
            return i;
        }
    }

    // 然后查找普通优先级任务
    for(uint8_t i = 0; i < task_count; i++) {
        if(task_table[i].priority == PRIORITY_NORMAL &&
           task_table[i].state == TASK_STATE_READY) {
            return i;
        }
    }

    return 0;  // 默认返回空闲任务
}

优先级处理

虽然基本的时间片调度不考虑优先级,但我们可以通过以下方法引入优先级:

方法1:不同的时间片长度

// 高优先级任务获得更长的时间片
Scheduler_CreateTask(Task_Important, 20, "Important");  // 20ms
Scheduler_CreateTask(Task_Normal, 10, "Normal");        // 10ms
Scheduler_CreateTask(Task_Low, 5, "Low");               // 5ms

方法2:优先级队列

typedef enum {
    PRIORITY_HIGH = 0,
    PRIORITY_NORMAL = 1,
    PRIORITY_LOW = 2,
    PRIORITY_LEVELS = 3
} TaskPriority_t;

// 每个优先级一个任务队列
TaskControlBlock_t *priority_queues[PRIORITY_LEVELS][MAX_TASKS];
uint8_t queue_sizes[PRIORITY_LEVELS] = {0};

// 调度时优先选择高优先级队列
uint8_t SelectNextTask_WithPriority(void) {
    // 从高到低遍历优先级
    for(uint8_t prio = 0; prio < PRIORITY_LEVELS; prio++) {
        if(queue_sizes[prio] > 0) {
            // 在该优先级队列中轮询
            return GetNextTaskFromQueue(prio);
        }
    }
    return 0xFF;  // 无任务
}

方法3:动态优先级调整

// 防止低优先级任务饥饿
void Scheduler_AdjustPriority(void) {
    for(uint8_t i = 0; i < task_count; i++) {
        TaskControlBlock_t *task = &task_table[i];

        // 如果任务长时间未执行,提升优先级
        if(task->wait_time > STARVATION_THRESHOLD) {
            if(task->priority < PRIORITY_HIGH) {
                task->priority--;  // 提升优先级
            }
        }

        // 任务执行后,恢复原始优先级
        if(task->state == TASK_STATE_RUNNING) {
            task->priority = task->base_priority;
            task->wait_time = 0;
        } else {
            task->wait_time++;
        }
    }
}

性能分析

理解调度系统的性能特征对于优化设计非常重要。

1. 响应时间分析

最坏情况响应时间:

最大响应延迟 = (任务数 - 1) × 时间片长度

示例: - 4个任务,每个时间片10ms - 最坏情况:某个任务刚执行完,需要等待其他3个任务 - 最大延迟 = 3 × 10ms = 30ms

优化方法: - 减少任务数量 - 缩短时间片长度 - 使用优先级调度

2. CPU利用率

// 计算CPU利用率
typedef struct {
    uint32_t total_time;      // 总时间
    uint32_t idle_time;       // 空闲时间
    uint32_t task_time;       // 任务执行时间
    uint32_t switch_time;     // 切换开销时间
} CPUStats_t;

CPUStats_t cpu_stats = {0};

float GetCPUUtilization(void) {
    if(cpu_stats.total_time == 0) {
        return 0.0f;
    }

    float utilization = (float)(cpu_stats.task_time) / cpu_stats.total_time * 100.0f;
    return utilization;
}

float GetSwitchOverhead(void) {
    if(cpu_stats.total_time == 0) {
        return 0.0f;
    }

    float overhead = (float)(cpu_stats.switch_time) / cpu_stats.total_time * 100.0f;
    return overhead;
}

3. 时间片长度选择

时间片长度的选择需要权衡:

时间片太短: - 优点:响应快,任务切换频繁 - 缺点:切换开销大,CPU利用率低

时间片太长: - 优点:切换开销小,CPU利用率高 - 缺点:响应慢,类似超级循环

推荐值: - 简单系统:10-50ms - 中等复杂度:5-20ms - 高响应要求:1-10ms

计算公式:

时间片长度 = 最大可接受延迟 / 任务数

实践示例

示例1:LED和按键的多任务系统

实现一个包含LED闪烁、按键检测和串口通信的多任务系统:

#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>

// 系统配置
#define TIME_SLICE_MS 10
#define MAX_TASKS 4

// 全局变量
volatile uint32_t system_ticks = 0;
TaskControlBlock_t task_table[MAX_TASKS];
uint8_t task_count = 0;
uint8_t current_task = 0;

// SysTick中断(1ms)
void SysTick_Handler(void) {
    system_ticks++;
}

// 任务1:LED闪烁
void Task_LED_Blink(void) {
    static uint32_t last_toggle = 0;

    if((system_ticks - last_toggle) >= 500) {
        last_toggle = system_ticks;
        LED_Toggle();
    }
}

// 任务2:按键检测
void Task_Button_Check(void) {
    static uint8_t button_state = 0;
    static uint32_t debounce_timer = 0;

    bool button_pressed = Button_Read();

    switch(button_state) {
        case 0:  // 空闲
            if(button_pressed) {
                debounce_timer = system_ticks;
                button_state = 1;
            }
            break;

        case 1:  // 消抖
            if((system_ticks - debounce_timer) >= 20) {
                if(button_pressed) {
                    // 按键确认
                    OnButtonPressed();
                    button_state = 2;
                } else {
                    button_state = 0;
                }
            }
            break;

        case 2:  // 等待释放
            if(!button_pressed) {
                debounce_timer = system_ticks;
                button_state = 3;
            }
            break;

        case 3:  // 释放消抖
            if((system_ticks - debounce_timer) >= 20) {
                if(!button_pressed) {
                    button_state = 0;
                }
            }
            break;
    }
}

// 任务3:串口通信
void Task_UART_Process(void) {
    // 检查接收缓冲区
    if(UART_DataAvailable()) {
        uint8_t data = UART_ReadByte();
        ProcessReceivedData(data);
    }

    // 检查发送缓冲区
    if(UART_TxReady() && HasDataToSend()) {
        uint8_t data = GetNextByteToSend();
        UART_SendByte(data);
    }
}

// 任务4:系统监控
void Task_System_Monitor(void) {
    static uint32_t last_report = 0;

    if((system_ticks - last_report) >= 1000) {
        last_report = system_ticks;

        // 打印系统状态
        printf("System uptime: %lu s\n", system_ticks / 1000);
        printf("CPU usage: %.1f%%\n", GetCPUUtilization());

        // 打印任务统计
        Scheduler_PrintStats();
    }
}

// 主函数
int main(void) {
    // 硬件初始化
    SystemInit();
    LED_Init();
    Button_Init();
    UART_Init();
    SysTick_Init();

    // 创建任务
    Scheduler_CreateTask(Task_LED_Blink, 10, "LED");
    Scheduler_CreateTask(Task_Button_Check, 10, "Button");
    Scheduler_CreateTask(Task_UART_Process, 10, "UART");
    Scheduler_CreateTask(Task_System_Monitor, 10, "Monitor");

    // 启动调度器
    printf("Scheduler started\n");
    Scheduler_Run();

    return 0;
}

示例说明: - 任务1:LED每500ms闪烁一次 - 任务2:按键检测,包含消抖处理 - 任务3:串口数据收发 - 任务4:系统监控,每秒打印统计信息

运行效果: - 所有任务"同时"运行 - LED稳定闪烁 - 按键响应及时(最大延迟40ms) - 串口通信流畅 - 定期输出系统状态

示例2:带优先级的数据采集系统

实现一个数据采集系统,包含高优先级的实时采集和低优先级的数据处理:

#include <stdint.h>
#include <stdbool.h>

// 优先级定义
typedef enum {
    PRIORITY_HIGH = 0,
    PRIORITY_NORMAL = 1,
    PRIORITY_LOW = 2
} Priority_t;

// 扩展的任务控制块
typedef struct {
    TaskFunction_t function;
    TaskState_t state;
    Priority_t priority;
    uint32_t time_slice;
    uint32_t elapsed_time;
    char name[16];
} TaskControlBlock_Ex_t;

// 数据缓冲区
#define BUFFER_SIZE 128
uint16_t data_buffer[BUFFER_SIZE];
volatile uint8_t write_index = 0;
volatile uint8_t read_index = 0;

// 任务1:高优先级 - ADC采样
void Task_ADC_Sample(void) {
    // 读取ADC
    uint16_t adc_value = ADC_Read();

    // 写入缓冲区
    data_buffer[write_index] = adc_value;
    write_index = (write_index + 1) % BUFFER_SIZE;

    // 检查缓冲区溢出
    if(write_index == read_index) {
        // 缓冲区满,记录错误
        LogError("Buffer overflow");
    }
}

// 任务2:普通优先级 - 数据处理
void Task_Data_Process(void) {
    // 检查是否有数据
    if(read_index != write_index) {
        // 读取数据
        uint16_t data = data_buffer[read_index];
        read_index = (read_index + 1) % BUFFER_SIZE;

        // 处理数据(滤波、计算等)
        float filtered = ApplyFilter(data);
        float result = CalculateResult(filtered);

        // 存储结果
        StoreResult(result);
    }
}

// 任务3:低优先级 - 数据上传
void Task_Data_Upload(void) {
    static uint32_t last_upload = 0;

    // 每5秒上传一次
    if((system_ticks - last_upload) >= 5000) {
        last_upload = system_ticks;

        // 上传数据到云端
        if(HasDataToUpload()) {
            UploadData();
        }
    }
}

// 任务4:低优先级 - 显示更新
void Task_Display_Update(void) {
    static uint32_t last_update = 0;

    // 每100ms更新一次显示
    if((system_ticks - last_update) >= 100) {
        last_update = system_ticks;
        UpdateDisplay();
    }
}

// 带优先级的调度器
void Scheduler_Run_WithPriority(void) {
    uint32_t last_tick = system_ticks;

    while(1) {
        uint32_t current_tick = system_ticks;
        uint32_t delta = current_tick - last_tick;
        last_tick = current_tick;

        // 查找最高优先级的就绪任务
        int8_t next_task = -1;
        Priority_t highest_priority = PRIORITY_LOW + 1;

        for(uint8_t i = 0; i < task_count; i++) {
            TaskControlBlock_Ex_t *task = &task_table_ex[i];

            if(task->state == TASK_STATE_READY &&
               task->priority < highest_priority) {
                highest_priority = task->priority;
                next_task = i;
            }
        }

        if(next_task >= 0) {
            TaskControlBlock_Ex_t *task = &task_table_ex[next_task];

            // 更新时间
            task->elapsed_time += delta;

            // 检查时间片
            if(task->elapsed_time >= task->time_slice) {
                task->elapsed_time = 0;

                // 执行任务
                task->state = TASK_STATE_RUNNING;
                task->function();
                task->state = TASK_STATE_READY;
            }
        }
    }
}

// 主函数
int main(void) {
    SystemInit();
    ADC_Init();
    Display_Init();
    Network_Init();
    SysTick_Init();

    // 创建任务(优先级不同)
    Scheduler_CreateTask_Ex(Task_ADC_Sample, 5, PRIORITY_HIGH, "ADC");
    Scheduler_CreateTask_Ex(Task_Data_Process, 10, PRIORITY_NORMAL, "Process");
    Scheduler_CreateTask_Ex(Task_Data_Upload, 20, PRIORITY_LOW, "Upload");
    Scheduler_CreateTask_Ex(Task_Display_Update, 20, PRIORITY_LOW, "Display");

    // 启动调度器
    Scheduler_Run_WithPriority();

    return 0;
}

示例说明: - 高优先级任务:ADC采样,确保数据不丢失 - 普通优先级任务:数据处理,及时处理采集的数据 - 低优先级任务:数据上传和显示更新,不影响关键任务

优先级效果: - ADC采样任务优先执行,保证实时性 - 数据处理任务在采样间隙执行 - 上传和显示任务在系统空闲时执行

深入理解

调度开销分析

任务切换会带来一定的开销,主要包括:

  1. 时间开销:
  2. 保存当前任务状态:几微秒到几十微秒
  3. 选择下一个任务:取决于任务数量和算法
  4. 恢复新任务状态:几微秒到几十微秒

  5. 空间开销:

  6. 任务控制块:每个任务约20-100字节
  7. 任务栈:每个任务几百字节到几KB

测量切换开销:

void MeasureSwitchOverhead(void) {
    uint32_t start, end;

    // 测量切换时间
    start = GetCycleCount();
    SwitchToNextTask();
    end = GetCycleCount();

    uint32_t cycles = end - start;
    float time_us = (float)cycles / (CPU_FREQ_MHZ);

    printf("Task switch overhead: %.2f us\n", time_us);
}

与RTOS的对比

时间片调度器是RTOS的简化版本,主要区别:

特性 时间片调度器 RTOS
任务切换 协作式或简单抢占 完全抢占式
上下文保存 简单或无 完整的寄存器保存
优先级 简单或无 多级优先级
同步机制 无或简单 信号量、互斥量、消息队列
内存管理 静态分配 动态内存管理
代码复杂度 低 高
资源占用 小(<1KB) 大(几KB到几十KB)

何时升级到RTOS: - 任务数量超过5-10个 - 需要复杂的任务间通信 - 需要严格的实时性保证 - 需要使用第三方中间件

常见陷阱和最佳实践

陷阱1:任务阻塞

错误示例:

void Task_Bad(void) {
    // 错误:阻塞等待
    while(!DataReady()) {
        // 等待数据
    }
    ProcessData();
}

正确做法:

void Task_Good(void) {
    // 使用状态机,不阻塞
    static uint8_t state = 0;

    switch(state) {
        case 0:
            if(DataReady()) {
                state = 1;
            }
            break;
        case 1:
            ProcessData();
            state = 0;
            break;
    }
}

陷阱2:共享资源竞争

错误示例:

// 全局变量,多个任务访问
uint32_t shared_counter = 0;

void Task1(void) {
    shared_counter++;  // 不安全
}

void Task2(void) {
    shared_counter++;  // 不安全
}

正确做法:

// 方法1:关中断保护
void Task1(void) {
    __disable_irq();
    shared_counter++;
    __enable_irq();
}

// 方法2:使用标志位
volatile bool resource_locked = false;

void Task1(void) {
    if(!resource_locked) {
        resource_locked = true;
        shared_counter++;
        resource_locked = false;
    }
}

陷阱3:时间片过短

问题:

// 时间片太短(1ms),切换开销大
#define TIME_SLICE_MS 1

// 如果切换开销为100us,则:
// 开销比例 = 100us / 1000us = 10%

解决方案:

// 根据任务特性选择合适的时间片
#define TIME_SLICE_MS 10  // 10ms,开销比例降至1%

最佳实践

  1. 任务设计原则:
  2. 任务函数应该快速返回
  3. 使用状态机处理复杂逻辑
  4. 避免长时间循环和阻塞

  5. 时间片选择:

  6. 根据响应要求选择
  7. 考虑切换开销
  8. 不同任务可以有不同时间片

  9. 优先级设置:

  10. 实时任务高优先级
  11. 后台任务低优先级
  12. 避免优先级反转

  13. 资源保护:

  14. 关中断保护临界区
  15. 使用标志位同步
  16. 最小化临界区长度

  17. 性能监控:

  18. 记录任务执行时间
  19. 监控CPU利用率
  20. 检测任务饥饿

常见问题

Q1: 时间片调度和超级循环有什么区别?

A: 主要区别在于任务执行的公平性和可预测性:

超级循环: - 任务顺序执行,一个任务执行完才执行下一个 - 如果某个任务耗时长,其他任务等待时间不确定 - 响应时间取决于所有任务的总执行时间

时间片调度: - 每个任务最多执行固定时间 - 所有任务轮流获得执行机会 - 响应时间可预测:最大延迟 = (任务数-1) × 时间片

Q2: 如何选择合适的时间片长度?

A: 考虑以下因素:

  1. 响应要求:
  2. 如果需要快速响应,选择较短的时间片(1-5ms)
  3. 如果响应要求不高,可以选择较长的时间片(20-50ms)

  4. 切换开销:

  5. 切换开销通常为几十微秒
  6. 时间片应该远大于切换开销(至少100倍)
  7. 推荐:时间片 > 100 × 切换时间

  8. 任务特性:

  9. 如果任务执行时间短,时间片可以短一些
  10. 如果任务需要连续执行,时间片应该长一些

经验公式:

时间片长度 = 最大可接受延迟 / 任务数量

Q3: 时间片调度能实现真正的多任务吗?

A: 不能。时间片调度只是模拟多任务:

  • 单核CPU:同一时刻只有一个任务在执行
  • 时间分片:通过快速切换造成"同时"执行的假象
  • 并发不是并行:任务是交替执行,不是真正的同时执行

真正的多任务需要: - 多核CPU(硬件并行) - 或者使用RTOS(软件多任务)

Q4: 如何调试时间片调度系统?

A: 常用的调试方法:

  1. 任务执行跟踪:

    void Task_Example(void) {
        GPIO_Set(DEBUG_PIN);  // 任务开始
    
        // 任务代码
        DoWork();
    
        GPIO_Clear(DEBUG_PIN);  // 任务结束
    }
    // 使用逻辑分析仪观察DEBUG_PIN
    

  2. 串口日志:

    void Scheduler_Log(void) {
        printf("[%lu] Task %s running\n", 
               system_ticks, 
               task_table[current_task].name);
    }
    

  3. 统计信息:

    void PrintTaskStats(void) {
        for(uint8_t i = 0; i < task_count; i++) {
            printf("Task %s: %lu ms, %lu runs\n",
                   task_table[i].name,
                   task_table[i].total_time,
                   task_table[i].run_count);
        }
    }
    

  4. 断言检查:

    void Scheduler_Check(void) {
        // 检查任务是否超时
        if(task_table[current_task].elapsed_time > MAX_TASK_TIME) {
            ASSERT(0);  // 任务超时
        }
    }
    

总结

时间片轮询调度是一种简单而实用的任务调度方法,适合在裸机环境下实现基本的多任务功能:

核心要点: - 时间片:为每个任务分配固定的执行时间 - 轮询调度:任务按顺序轮流执行 - 任务切换:时间片到期时切换到下一个任务 - 优先级:可以通过不同的时间片长度或优先级队列实现

适用场景: - 3-10个简单任务 - 响应要求不是特别严格(几十毫秒级) - 资源受限,无法使用RTOS - 学习操作系统原理

优势: - 实现简单,代码量小(<500行) - 资源占用少(<1KB) - 响应时间可预测 - 易于理解和调试

局限: - 不支持复杂的任务间通信 - 优先级支持有限 - 不适合大规模应用 - 实时性不如RTOS

下一步学习: - 如果需要更强大的功能,学习RTOS(如FreeRTOS) - 如果需要更好的任务管理,学习协作式调度器 - 如果需要更精确的时序控制,学习中断驱动架构

延伸阅读

推荐进一步学习的资源:

参考资料

  1. "Operating Systems: Three Easy Pieces" - Remzi H. Arpaci-Dusseau
  2. "Real-Time Systems" - Jane W. S. Liu
  3. "Embedded Systems Architecture" - Tammy Noergaard
  4. FreeRTOS Documentation - https://www.freertos.org/

练习题:

  1. 实现一个包含3个任务的时间片调度系统:LED闪烁、按键检测、串口通信
  2. 为调度器添加任务挂起和恢复功能
  3. 实现一个带优先级的调度器,支持高、中、低三个优先级
  4. 测量你的调度器的任务切换开销,并计算CPU利用率

下一步:建议学习 软件定时器实现,为调度系统添加更精确的时间管理功能。