3. OpenHarmony内核基础:LiteOS-M内核架构、任务创建与调度、任务优先级与状态管理、我在项目中遇到的任务堆栈溢出问题

3.1 LiteOS-M 内核架构概览

LiteOS-M 是 OpenHarmony 针对轻量级物联网设备打造的内核。说白了,它就是一个为资源受限的 MCU 量身定做的实时操作系统内核。我最早接触它是在一个智能传感器项目上,当时选型就看中它体积小、响应快。

它的架构非常精简,核心模块就那么几个:任务管理、内存管理、中断管理、时间管理,外加一个IPC(进程间通信)机制。你想想看,一个跑在 Cortex-M0 上的内核,能有多复杂?但麻雀虽小,五脏俱全。

核心设计理念: 静态资源分配 + 抢占式调度。所有任务栈、任务控制块(TCB)在编译时就确定好了,运行时不再动态分配。这样做的好处是——没有内存碎片,运行极其稳定。

我个人习惯把 LiteOS-M 的架构分成三层来看:

  • 硬件抽象层(HAL): 屏蔽不同 MCU 的差异,比如中断向量表、时钟配置。
  • 内核服务层: 任务、信号量、互斥锁、消息队列、软件定时器。
  • 扩展组件层: 文件系统、网络协议栈(LwIP)、Shell 交互。

下面这张图能帮你快速建立整体认知:

LiteOS-M 内核架构分层图 用户应用层(App Task) 扩展组件层(文件系统 / LwIP / Shell) 内核服务层 任务管理 | 内存管理 | 中断管理 | 时间管理 | IPC 硬件抽象层(HAL) MCU 硬件(Cortex-M / RISC-V)

3.2 任务创建与调度机制

任务,是 LiteOS-M 里最基本的执行单元。每个任务都有自己的栈空间、优先级和状态。创建任务时,你需要告诉内核三件事:任务入口函数、栈大小、优先级。

来看一个实际创建任务的代码片段:

#include "los_task.h"

UINT32 g_taskID;

VOID MyTaskEntry(VOID)
{
    while (1) {
        printf("Hello, LiteOS-M!\n");
        LOS_TaskDelay(1000);  // 延时1秒,主动挂起
    }
}

UINT32 CreateMyTask(VOID)
{
    TSK_INIT_PARAM_S taskParam = {0};

    taskParam.pfnTaskEntry  = (TSK_ENTRY_FUNC)MyTaskEntry;
    taskParam.usTaskPrio    = 5;      // 优先级,数值越小优先级越高
    taskParam.uwStackSize   = 0x400;  // 1KB 栈空间
    taskParam.pcName        = "MyTask";
    taskParam.uwResved      = LOS_TASK_STATUS_DETACHED;

    return LOS_TaskCreate(&g_taskID, &taskParam);
}

嗯,这里要注意:uwStackSize 的单位是字节,别填错了。我见过有人填了 0x100(256字节),结果任务一跑就崩。

调度策略:抢占式 + 时间片轮转

LiteOS-M 默认是抢占式调度。什么意思?高优先级的任务一旦就绪,立马抢走 CPU。低优先级的任务只能靠边站。如果两个任务优先级相同,那就轮着来,每人分一个时间片(默认 10ms)。

我的经验: 在实时性要求高的场景(比如电机控制),把控制任务优先级设到 3 以上。而像日志打印这种不紧急的任务,优先级设到 10 以下就行。别把所有任务都设成同一个优先级,那样调度器会频繁切换,白白浪费 CPU。

3.3 任务优先级与状态管理

LiteOS-M 支持 0-31 级优先级,0 最高,31 最低。系统内部会保留几个最高优先级给内核线程,用户任务一般从 4 开始用。

任务有四种基本状态:

状态 含义 触发条件
就绪(Ready) 任务已准备好,等待 CPU 创建完成、延时结束、等待的资源可用
运行(Running) 正在占用 CPU 调度器选中该任务
阻塞(Blocked) 等待某个事件或资源 调用 LOS_TaskDelay、等待信号量/队列
挂起(Suspended) 被主动暂停,不参与调度 调用 LOS_TaskSuspend

状态切换的典型路径是这样的:

任务状态迁移图 就绪 (Ready) 运行 (Running) 阻塞 (Blocked) 挂起 (Suspended) 调度选中 时间片用完/被抢占 等待资源 资源就绪 主动挂起 恢复

3.4 我在项目中遇到的任务堆栈溢出问题

说到堆栈溢出,这可是嵌入式开发者的“老朋友”了。我记得有一次做 NB-IoT 数据采集器,设备运行两三天后突然死机。排查了整整一天,最后发现是一个任务栈设小了。

堆栈溢出的典型症状:

  • 系统随机死机,毫无规律
  • 函数调用返回时跑飞(PC 指针异常)
  • 全局变量莫名其妙被修改
  • HardFault 异常频繁触发

为什么会这样?每个任务都有自己的栈空间,用来保存局部变量、函数调用上下文。如果栈不够用,就会往相邻的内存区域写数据——这通常就是其他任务的控制块或者全局变量区。数据一乱,系统就崩了。

如何定位堆栈溢出?

LiteOS-M 提供了栈水印检测机制。你可以在创建任务时开启:

taskParam.uwStackSize = 0x800;  // 2KB
// 开启栈水印检测(需要在编译时使能 LOSCFG_BASE_CORE_TSK_MONITOR)
LOS_TaskCreate(&g_taskID, &taskParam);

// 运行时检查栈使用情况
UINT32 peakUsage = LOS_TaskStackPeek(g_taskID);
printf("Task stack peak usage: %d bytes\n", peakUsage);

如果 peakUsage 接近你分配的栈大小,那就危险了。我一般留 30% 的余量。比如峰值用了 700 字节,我就分配 1KB 以上。

避坑指南:我曾经踩过的三个坑

  1. 递归函数: 在任务里写递归,栈消耗是指数级的。我后来一律改用循环。
  2. 大局部变量: 比如在函数里定义一个 char buf[1024],这 1KB 全在栈上。建议改用静态分配或堆分配。
  3. 中断嵌套: 中断服务函数里如果调用了可能阻塞的 API,栈会急剧膨胀。中断里只做标记,具体处理交给任务。

我的调试习惯: 项目初期把所有任务的栈都设大一点(比如 2KB),等功能稳定后再用 LOS_TaskStackPeek 查看实际使用量,然后逐个优化。这样既不会一开始就崩,也不会浪费内存。

最后说一句,堆栈溢出是“软故障”,最难复现。你永远不知道它什么时候冒出来。所以,从一开始就养成好习惯——合理估算栈大小,开启水印检测,定期检查峰值。别等到现场设备挂了才后悔。