Skip to content

第2章:操作系统的任务调度原理 ​

本章导读

ZigBee 程序里往往同时有好多件"待办的事":2 秒后开灯、3 秒后关灯、1 分钟后上报传感器数据……操作系统是怎么知道"什么时候该做哪件事"的?

本章用一个最简单的例子——2 秒开灯、3 秒关灯——把任务调度的核心机制拆开讲清楚。看懂这一章,后面理解 Z-Stack 的 OSAL(操作系统抽象层)调度框架就轻松多了。

先懂 5 个词:任务调度的基础理论 ​

系统做任务调度时,会反复用到 5 个概念。我们用生活化的方式先认识它们,再结合代码理解。

任务(Task) ​

要做的一件事。例如"1 秒后开灯"、"2 秒后关灯"。

生活类比:你给自己列的待办事项,比如"10 分钟后出门"。

任务池 ​

一个能存放多个任务的"缓冲区"(容器)。例如一个任务池里可以同时放着"1 秒后开灯"、"2 秒后关灯"、"3 秒后开灯"、"1 分钟后关灯"这几件事。系统会在到点后,依次执行任务池里的各个任务。

生活类比:你的待办清单 / 备忘录,上面记着好几件还没做的事。

优先级 ​

区分"同一时刻先做谁"的等级。同一时刻可能有多个任务需要执行,就需要定个先后次序:相同条件下系统优先处理高优先级任务,低优先级任务先等着;必要时,系统甚至可能中断低优先级任务,转而去处理高优先级任务。

生活类比:同时来了"接电话"和"扫地"两件事,接电话优先级高,先接电话。

轮询 ​

系统每隔一段时间,在任务池里检查一遍"有没有现在需要处理的任务",这个过程就叫轮询。

生活类比:每隔几分钟看一眼备忘录,看有没有到点该做的事。

操作系统调度周期 ​

"每隔一段时间"的具体时间长度,也就是任务的最小时间单位。例如系统调度周期是 1 秒,但有一个任务是"0.1 秒后关灯"——因为 0.1 秒小于调度周期,这个任务实际要到 1 秒后才会被执行。

生活类比:你定的是"每 5 分钟看一次备忘录",那么"3 分钟后的提醒"也要等到下一个 5 分钟节点才被看到。

5 个概念速查表

概念一句话解释生活类比
任务需要处理器做的一件事待办事项
任务池存放多个任务的容器待办清单
优先级同一时刻先做谁的等级重要事优先
轮询定时去任务池检查有没有到点的任务定时翻看备忘录
调度周期轮询的时间间隔,任务时间的最小粒度每 5 分钟看一次

动手实现:一个极简任务调度器 ​

理解了概念,我们用 C 语言亲手写一个"微型调度器",跑在 CC2530 开发板上:每 1 毫秒轮询一次任务池,到点就执行任务。

总体流程:main 函数做了什么 ​

c
void main() {
    initLed();      // 初始化 LED 灯
    taskListInit(); // 初始化任务池

    addTask(2000, TASK_LED_ON);  // 往任务池添加任务:2s 后打开 LED
    addTask(3000, TASK_LED_OFF); // 往任务池添加任务:3s 后关闭 LED

    // 每隔 1ms 轮询 1 次
    while (1) {
        delayMs(1); // 暂停 1ms
        polling();  // 轮询:查看有没有需要处理的任务
    }
}

主函数只做了三件事:

  1. 初始化:点亮 LED 需要引脚配置,任务池要先清空;
  2. 添加任务:往任务池里放两个任务——"2 秒后开灯"、"3 秒后关灯";
  3. 死循环轮询:delayMs(1) 让程序每 1 毫秒醒一次,polling() 去看看有没有到点的任务。

于是,这段代码就模拟出了一个调度周期为 1 毫秒的系统轮询。这就是操作系统调度的雏形——只是这里的"操作系统"是你在 main 里自己写的。

说明:这个例子目的是让读者更通俗地理解调度机制,代码做了简化,与真实 OSAL 相比不够严谨,但核心思想完全一致。

任务池的实现:数组 + 结构体 ​

任务池用什么数据结构实现?理论上队列(先进先出)、堆栈(先进后出)、树都可以;实现方式可以是静态数组,也可以是动态链表。这里选用最简单的静态数组来讲解。

第一步:定义任务结构体

c
/** @brief 任务结构体的定义 */
typedef struct task_t {
    bool occupy;      // 是否已占用:true=当前有任务;false=当前没有任务
    uint16_t task;    // 任务内容
    uint16_t expire;  // 等待时间(还剩多少时间到期)
} task_t;

每个任务占一个"格子",格子里记着三样东西:这个格子有没有被占用、任务是什么、还有多久到期。

第二步:定义任务池(结构体数组)

c
/** @brief 任务池的定义 */
#define TASK_LIST_SIZE 5
static task_t taskList_g[TASK_LIST_SIZE];

一个长度为 5 的任务池,最多同时放 5 个任务。

第三步:4 个 API 操作任务池

任务池就像一张"有 5 个格子的登记表",接下来 4 个函数分别负责:清空登记表、登记新任务、扫描到点任务、执行任务。

① taskListInit() —— 清空任务池

c
/*
 * 任务池初始化
 */
static void taskListInit() {
    // 把所有任务都设置为未使用
    for (uint16_t i = 0; i < TASK_LIST_SIZE; i++)
        taskList_g[i].occupy = false;
}

把 5 个格子的"占用标志"全部置为 false,表示所有格子都空着。

② addTask(expire, task) —— 登记新任务

c
/*
 * 往任务池中添加任务
 * @param expire 延迟多久执行
 * @param task 任务内容
 */
static void addTask(uint16_t expire, uint16_t task) {
    for (uint16_t i = 0; i < TASK_LIST_SIZE; i++) {
        if (taskList_g[i].occupy) continue;  // 这个格子被占了,看下一个

        taskList_g[i].task = task;   // 记录任务内容
        taskList_g[i].expire = expire; // 记录还要等多久
        taskList_g[i].occupy = true; // 标记为已占用

        break;  // 登记完成,退出
    }
}

从头到尾找一个空着的格子(occupy == false),把任务内容和等待时间写进去,然后标记"已占用"。

③ polling() —— 扫描到点任务并执行

c
/*
 * 轮询
 */
static void polling() {
    for (uint16_t i = 0; i < (sizeof(taskList_g) / sizeof(taskList_g[0])); i++)
        if (taskList_g[i].occupy && (--taskList_g[i].expire == 0)) {
            // 执行任务
            taskHandler(taskList_g[i].task);
            // 释放任务池中的空间
            taskList_g[i].occupy = false;
        }
}

这是调度的心脏。每执行一次轮询:

  • 遍历任务池里每一个被占用的格子;
  • 把它的 expire 减 1(等待时间又少了一个调度周期);
  • 当 expire 减到 0,说明"到点了",调用 taskHandler() 执行任务;
  • 执行完,把格子标记为 occupy = false,释放出来给新任务用。

注意一个细节:越靠前的任务越先被遍历、越先被检查,因此任务池里越靠前的位置,"优先级"越高。

④ taskHandler(task) —— 真正执行任务

c
/*
 * 执行指定的任务
 * @param task 任务
 */
static void taskHandler(uint16_t task) {
    /* 根据指定的任务,执行对应的操作 */
    if (task == TASK_LED_ON) {
        LED = LED_ON;              // 开灯
        printf("Set Led On!\n");
        // 执行完任务后,重新往任务池中添加一个开灯任务
        addTask(2000, TASK_LED_ON);
    }
    else if (task == TASK_LED_OFF) {
        LED = LED_OFF;             // 关灯
        printf("Set Led Off!\n");
        // 执行完任务后,重新往任务池中添加一个关灯任务
        addTask(2000, TASK_LED_OFF);
    }
}

任务处理函数根据"任务内容"做对应的事。这里有一个关键设计:执行完任务后,又往任务池里添加了一个新任务(开灯后安排 2 秒后再开灯、关灯后安排 2 秒后再关灯)——这样任务"自我延续",LED 就形成了周期性闪烁的效果。这正是后面 Z-Stack 里周期任务的雏形。

调试仿真:看调度器跑起来 ​

代码写好后,在 IAR 里仿真运行,观察效果。

第一步:进入仿真模式

打开工程 scheduling,单击 IAR 10.10.1 工具栏的 Download and Debug 按钮(快捷键 Ctrl+D),完成编译、链接、下载并进入仿真模式:

IAR 工程与 Download and Debug 按钮

第二步:运行程序

进入仿真模式后,单击 Go 按钮(快捷键 F5)运行程序:

IAR Go 运行按钮

第三步:观察输出

程序运行后,在 Terminal I/O 窗口中会交替输出 Set Led On! 和 Set Led Off!——这正是"2 秒开灯 → 3 秒关灯 → 又安排 2 秒后开灯……"的调度结果,说明你的微型调度器已经跑起来了:

Terminal I/O 交替输出

工程代码结构:

调度工程代码结构

小结:一次完整的调度循环 ​

把这一章串起来,一次任务调度就是这样的循环:

text
初始化(清空任务池)
      ↓
往任务池添加任务(登记"2s 开灯"、"3s 关灯")
      ↓
死循环里每 1ms 轮询一次(delayMs + polling)
      ↓
polling 把每个任务的等待时间减 1
      ↓
时间减到 0?──否──→ 继续等待下一次轮询
      ↓ 是
执行任务(开灯/关灯)
      ↓
释放格子 + 重新登记新任务(形成周期任务)
      ↓
回到轮询……(循环往复)

核心思想一句话:操作系统调度 = 任务池里存任务 + 按周期轮询扫时间 + 到期执行并释放。这个极简模型就是 Z-Stack OSAL 任务调度机制的地基,下一章你会看到它在真实协议栈里是如何实现的。


内容来源:紫甲 IoT《ZigBee 3.0 开发指南》第三部分·第2章,经本站整理优化以便于理解;原站链接

aioteer · 物联网专业课程群

💬AI 伴学配置