3. OpenHarmony源码获取与编译

任务管理,说白了就是操作系统的“心脏”。你写的所有业务逻辑,最终都要跑在任务里。我刚开始接触RTOS时,总觉得任务管理就是创建几个线程跑一跑,直到有一次产品出现优先级反转导致死机,才明白这里面的门道有多深。

OpenHarmony的LiteOS-M内核,任务管理做得相当精巧。咱们今天就把任务状态、切换、优先级、调度策略,还有任务间通信的三大件——事件、信号量、互斥锁,一次性讲透。

核心认知:任务管理不是“创建-运行-删除”这么简单。真正的难点在于:如何让多个任务高效协作,既不互相阻塞,又不抢占过度。这需要你对调度机制和同步原语有深刻理解。

3.1 任务状态与切换

任务在生命周期里,会经历几种状态。我习惯把它们比作人的一天:

  • 就绪态(Ready)——早上起床,准备好干活,等CPU分配时间片
  • 运行态(Running)——正在干活,CPU归你
  • 阻塞态(Blocked)——等某个资源,比如等信号量、等事件、等延时
  • 挂起态(Suspended)——被强制暂停,不参与调度
  • 终止态(Terminated)——任务跑完了,或者被删了

状态切换的路径,我画了张图,你一看就明白:

任务状态切换图 任务状态切换图 就绪态 (Ready) 运行态 (Running) 阻塞态 (Blocked) 挂起态 (Suspended) 终止态 (Terminated) 调度器选中 时间片用完/被抢占 等待资源 资源就绪 主动挂起 恢复 任务结束 说明:任务切换由 PendSV 异常触发,保存当前任务上下文(寄存器、栈指针),恢复新任务上下文。 切换时机:时间片耗尽、更高优先级任务就绪、任务主动阻塞或挂起。

任务切换,本质上是上下文切换。每次切换,CPU都要保存当前任务的寄存器、栈指针,再恢复新任务的。这个开销虽然不大,但频繁切换会浪费CPU。我见过有人把任务切得特别细,结果性能反而下降了。

我的经验:任务切换频率控制在1-10ms一次比较合理。太频繁,切换开销占比高;太慢,实时性又不够。具体看你的业务场景。

3.2 任务优先级与调度策略

OpenHarmony的LiteOS-M内核,用的是基于优先级的抢占式调度。说白了就是:谁优先级高,谁先跑。同等优先级,轮流跑(时间片轮转)。

优先级范围是0-31,数值越小,优先级越高。0是最高,31是最低。这个设计跟FreeRTOS相反,你刚接触时容易搞混。我刚开始就踩过这个坑——把高优先级任务设成了大数值,结果它老抢不到CPU。

优先级 说明 典型用途
0-3 最高优先级,实时性要求极高 中断处理、时间关键型任务
4-15 高优先级,硬实时任务 传感器数据采集、控制算法
16-23 中优先级,普通实时任务 通信协议处理、UI刷新
24-31 低优先级,后台任务 日志记录、统计上报

调度策略就两种:

  • 抢占式调度:高优先级任务就绪时,立即抢占当前运行的低优先级任务。这是默认行为。
  • 时间片轮转:同优先级任务,每个任务运行一个时间片(默认10ms),然后切换到下一个。

你想想看,如果两个任务优先级相同,一个跑死循环,另一个就永远没机会。时间片轮转就是解决这个问题的。

注意:优先级不要设太多层级。我建议最多用5-6个优先级就够了。层级太多,调度器维护就绪列表的开销会变大,而且容易出优先级反转问题。

3.3 任务创建与删除API实战

光说不练假把式。咱们直接看代码。OpenHarmony的任务API,核心就几个:

// 任务创建
UINT32 LOS_TaskCreate(UINT32 *taskID, TSK_INIT_PARAM_S *taskInitParam);

// 任务删除
UINT32 LOS_TaskDelete(UINT32 taskID);

// 任务挂起
UINT32 LOS_TaskSuspend(UINT32 taskID);

// 任务恢复
UINT32 LOS_TaskResume(UINT32 taskID);

// 获取当前任务ID
UINT32 LOS_CurTaskIDGet(VOID);

任务初始化参数结构体,我习惯这样配:

TSK_INIT_PARAM_S taskParam = {
    .pfnTaskEntry = (TSK_ENTRY_FUNC)TaskFunction,  // 任务函数指针
    .uwStackSize = 0x1000,                          // 栈大小,4KB
    .pcName = "MyTask",                             // 任务名,调试用
    .usTaskPrio = 5,                                // 优先级,数值越小优先级越高
    .uwResved = LOS_TASK_STATUS_DETACHED,           // 任务属性
};

来个完整的例子:

#include "los_task.h"

// 任务函数
VOID TaskFunction(VOID)
{
    while (1) {
        printf("Task running, ID: %u\n", LOS_CurTaskIDGet());
        LOS_TaskDelay(1000);  // 延时1秒,主动让出CPU
    }
}

UINT32 CreateExampleTask(VOID)
{
    UINT32 taskID;
    TSK_INIT_PARAM_S taskParam = {0};

    taskParam.pfnTaskEntry = (TSK_ENTRY_FUNC)TaskFunction;
    taskParam.uwStackSize = 0x1000;
    taskParam.pcName = "ExampleTask";
    taskParam.usTaskPrio = 10;
    taskParam.uwResved = LOS_TASK_STATUS_DETACHED;

    UINT32 ret = LOS_TaskCreate(&taskID, &taskParam);
    if (ret != LOS_OK) {
        printf("Task create failed, ret: %u\n", ret);
        return ret;
    }

    printf("Task created, ID: %u\n", taskID);
    return LOS_OK;
}

删除任务更简单:

// 删除指定任务
LOS_TaskDelete(taskID);

// 删除当前任务(任务自杀)
LOS_TaskDelete(LOS_CurTaskIDGet());

避坑指南:我曾经在任务函数里直接return,以为任务会自动退出。结果呢?任务确实退出了,但资源没释放,栈空间泄漏了。正确的做法是:任务退出前调用LOS_TaskDelete删除自己,或者用LOS_TaskStatusSet设置终止状态。

3.4 任务间通信:事件、信号量、互斥锁

任务间通信,是嵌入式开发的灵魂。三个核心原语,各有各的用途。

3.4.1 事件(Event)

事件机制,说白了就是“发信号”。一个任务可以等一个或多个事件,另一个任务可以设置事件来唤醒它。适合一对多、多对一的同步场景。

// 事件创建
UINT32 LOS_EventInit(PEVENT_CB_S eventCB);

// 等待事件
UINT32 LOS_EventRead(PEVENT_CB_S eventCB, UINT32 eventMask, UINT32 mode, UINT32 timeout);

// 设置事件
UINT32 LOS_EventWrite(PEVENT_CB_S eventCB, UINT32 events);

// 清除事件
UINT32 LOS_EventClear(PEVENT_CB_S eventCB, UINT32 events);

事件等待模式有三种:

模式 说明 示例
LOS_WAITMODE_AND 所有指定事件都发生才返回 等A和B都到齐
LOS_WAITMODE_OR 任一指定事件发生就返回 等A或B任意一个
LOS_WAITMODE_CLR 事件读取后自动清除 边沿触发模式

3.4.2 信号量(Semaphore)

信号量,我把它理解成“资源计数器”。二值信号量(0或1)用于互斥和同步,计数信号量用于管理多个资源。

// 信号量创建
UINT32 LOS_SemCreate(UINT16 count, UINT32 *semHandle);

// 申请信号量(P操作)
UINT32 LOS_SemPend(UINT32 semHandle, UINT32 timeout);

// 释放信号量(V操作)
UINT32 LOS_SemPost(UINT32 semHandle);

// 删除信号量
UINT32 LOS_SemDelete(UINT32 semHandle);

举个典型场景:生产者-消费者模型。

UINT32 g_semHandle;

// 生产者任务
VOID ProducerTask(VOID)
{
    while (1) {
        // 生产数据...
        LOS_SemPost(g_semHandle);  // 释放信号量,通知消费者
        LOS_TaskDelay(500);
    }
}

// 消费者任务
VOID ConsumerTask(VOID)
{
    while (1) {
        LOS_SemPend(g_semHandle, LOS_WAIT_FOREVER);  // 等信号量
        // 消费数据...
    }
}

注意:信号量的Pend操作,timeout参数不要轻易设成LOS_WAIT_FOREVER。万一信号量一直不来,任务就永远卡死了。我一般设一个超时值,比如1000ms,超时后做异常处理。

3.4.3 互斥锁(Mutex)

互斥锁,专门解决优先级反转问题。它跟二值信号量很像,但多了优先级继承机制。

什么是优先级反转?举个例子:

  • 任务A(高优先级)和任务C(低优先级)共享一个资源
  • 任务B(中优先级)不共享资源
  • C先拿到锁,然后被B抢占,B一直跑
  • A等锁,但锁在C手里,C又被B抢着,A只能干等

这就是优先级反转。互斥锁的优先级继承机制,会在A等锁时,临时把C的优先级提升到A的级别,让C尽快跑完释放锁。

// 互斥锁创建
UINT32 LOS_MuxCreate(UINT32 *muxHandle);

// 申请互斥锁
UINT32 LOS_MuxPend(UINT32 muxHandle, UINT32 timeout);

// 释放互斥锁
UINT32 LOS_MuxPost(UINT32 muxHandle);

// 删除互斥锁
UINT32 LOS_MuxDelete(UINT32 muxHandle);

使用互斥锁的典型模式:

UINT32 g_muxHandle;

VOID SafeTask(VOID)
{
    UINT32 ret = LOS_MuxPend(g_muxHandle, 1000);  // 等锁,最多等1秒
    if (ret != LOS_OK) {
        printf("Lock timeout!\n");
        return;
    }

    // 临界区代码,操作共享资源
    // ...

    LOS_MuxPost(g_muxHandle);  // 释放锁
}

关键区别:保护共享资源时,优先用互斥锁而不是二值信号量。互斥锁的优先级继承机制,能有效避免优先级反转。二值信号量没有这个机制,只适合做简单的同步。

3.5 避坑总结

做任务管理,有几个坑我反复踩过,今天一并告诉你:

  1. 栈空间别省:任务栈设太小,跑着跑着就栈溢出,系统崩溃。我一般给每个任务至少2KB,复杂任务4KB起步。
  2. 优先级别乱设:优先级层级太多,调度开销大。5-6个层级足够。
  3. 死锁要预防:两个任务互相等对方释放锁,就死锁了。我习惯用“锁顺序”策略——所有任务申请锁的顺序保持一致。
  4. 超时别设永久:LOS_WAIT_FOREVER要慎用。万一资源一直不来,任务就永远卡死。
  5. 任务函数别return:任务函数退出前,记得调用LOS_TaskDelete清理资源。

任务管理这块,理论不难,难的是实践中的权衡。你多写几个项目,自然就找到感觉了。