1. 任务调度基础:什么是任务调度、任务状态模型、任务控制块(TCB)结构解析

大家好,欢迎来到《鸿蒙实时操作系统任务调度实战》的第一章。

咱们直接开门见山。任务调度,说白了就是操作系统决定「下一个该谁干活」的过程。你想想看,一个CPU同一时刻只能跑一个任务,但系统里可能同时挂着十几个、几十个任务在等。谁先上?谁后上?上多久下来?这就是调度要解决的问题。

我刚开始接触RTOS那会儿,总觉得调度是个很玄乎的东西。后来自己动手写了个简单的调度器,才明白——其实没那么复杂。核心就三件事:谁就绪了、谁优先级高、该不该抢当前的任务

1.1 什么是任务调度

任务调度,是实时操作系统的核心功能之一。它负责按照某种策略,从就绪任务队列中选出一个任务,把CPU的控制权交给它。

在鸿蒙轻内核(LiteOS)中,调度器是内核的心脏。每次时钟中断来了,或者任务主动让出CPU,调度器就会跑一遍,看看有没有更合适的任务要切换。

我个人习惯把调度理解成一个「交通指挥员」:

  • 任务就是路上的车
  • CPU就是路口
  • 调度器决定哪辆车先通过

嗯,这个比喻虽然简单,但很管用。我在项目中遇到过不少新手,把调度想得太复杂,其实抓住这个核心就够了。

1.2 任务状态模型

任务不是一直都能跑的。它有生命周期。鸿蒙的任务状态模型,我总结为「三态一特殊」:

状态 含义 典型场景
就绪态 任务已准备好,只等CPU 刚创建、等待超时结束、信号量释放后
运行态 任务正在占用CPU 调度器选中了它,正在执行代码
阻塞态 任务在等某个事件 等待信号量、消息队列、延时
挂起态 任务被主动暂停 调试时、系统休眠时

为什么会这样设计?你想想看,如果所有任务都在抢CPU,那系统就乱套了。阻塞态的存在,就是为了让任务在「无事可做」的时候别浪费CPU。

我曾经在做一个物联网网关项目时,有个任务一直在轮询等待串口数据,占着CPU不放。结果其他任务全饿死了。后来改成阻塞等待,CPU利用率直接从95%降到了15%。这就是状态模型的价值。

核心要点:任务状态切换是调度器工作的基础。只有就绪态的任务才能被调度。阻塞态和挂起态的任务,调度器根本不会看它们一眼。

1.3 任务控制块(TCB)结构解析

每个任务在鸿蒙内核里,都有一个对应的数据结构来管理它。这个结构就叫任务控制块(Task Control Block,TCB)

你可以把TCB理解成任务的「身份证+档案袋」。里面存了任务的所有关键信息。

鸿蒙的TCB结构大致长这样(我简化了,但核心字段都在):

typedef struct {
    UINT32          taskID;         // 任务ID,唯一标识
    CHAR            *taskName;      // 任务名,调试用
    UINT32          priority;       // 优先级,数值越小优先级越高
    UINT32          status;         // 当前状态(就绪/运行/阻塞/挂起)
    UINTPTR         stackPointer;   // 栈指针,保存CPU现场
    UINT32          stackSize;      // 栈大小
    UINT32          timeSlice;      // 时间片(轮转调度用)
    LOS_DL_LIST     pendList;       // 挂起链表,用于阻塞队列
    UINT32          eventMask;      // 事件掩码,等待的事件类型
    UINT32          eventMode;      // 事件模式(与/或)
    void            *mutex;         // 当前持有的互斥锁
    UINT32          errorNum;       // 错误码
} TCB;

我来挑几个重点字段说说:

  • taskID:每个任务唯一的编号。我习惯在调试时打印这个ID,一眼就能看出是哪个任务在跑。
  • priority:优先级。鸿蒙里0是最高优先级,数值越大优先级越低。这个和很多RTOS是反的,注意别搞混了。
  • stackPointer:这个太重要了。任务切换时,CPU的寄存器值全部压栈,然后stackPointer指向栈顶。下次切回来时,直接从栈里恢复现场。这就是「上下文切换」的本质。
  • timeSlice:时间片。同优先级任务轮转调度时,每个任务能跑多久。我一般设10ms,太短了切换开销大,太长了实时性差。

个人经验:调试任务问题时,我第一件事就是打印TCB内容。看status字段是不是预期状态,看stackPointer有没有越界。很多死机问题,都是栈溢出导致的,看stackPointer和stackSize一对比就明白了。

1.4 调度器是怎么工作的?

调度器的工作流程,我总结成三步:

  1. 触发调度:时钟中断、任务主动让出、信号量释放等事件触发调度
  2. 选任务:从就绪队列中选出优先级最高的任务
  3. 切换上下文:保存当前任务的CPU现场,恢复新任务的CPU现场

鸿蒙默认用的是优先级抢占式调度。什么意思?就是高优先级任务一旦就绪,立马抢走CPU。低优先级任务?不好意思,你先等着。

同优先级任务之间呢?用的是时间片轮转。每个任务跑一个时间片,时间到了就换下一个。这样大家都有机会。

避坑指南:我曾经在一个项目中,把两个关键任务的优先级设成了相同的值。结果它们互相抢时间片,导致一个需要及时响应的任务经常被延迟。后来把其中一个优先级调高了一级,问题就解决了。记住:实时性要求高的任务,优先级一定要高

1.5 小结

这一章咱们把任务调度的基础打好了。总结一下:

  • 任务调度就是「谁该用CPU」的决策过程
  • 任务有就绪、运行、阻塞、挂起四种状态
  • TCB是任务的核心数据结构,存了任务的所有信息
  • 鸿蒙用优先级抢占+时间片轮转的调度策略

下一章,咱们会深入调度器的源码,看看鸿蒙到底是怎么选任务的。到时候我会带着大家一行一行地读代码。准备好了吗?


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