3. OpenHarmony源码获取与编译
任务管理,说白了就是操作系统的“心脏”。你写的所有业务逻辑,最终都要跑在任务里。我刚开始接触RTOS时,总觉得任务管理就是创建几个线程跑一跑,直到有一次产品出现优先级反转导致死机,才明白这里面的门道有多深。
OpenHarmony的LiteOS-M内核,任务管理做得相当精巧。咱们今天就把任务状态、切换、优先级、调度策略,还有任务间通信的三大件——事件、信号量、互斥锁,一次性讲透。
核心认知:任务管理不是“创建-运行-删除”这么简单。真正的难点在于:如何让多个任务高效协作,既不互相阻塞,又不抢占过度。这需要你对调度机制和同步原语有深刻理解。
3.1 任务状态与切换
任务在生命周期里,会经历几种状态。我习惯把它们比作人的一天:
- 就绪态(Ready)——早上起床,准备好干活,等CPU分配时间片
- 运行态(Running)——正在干活,CPU归你
- 阻塞态(Blocked)——等某个资源,比如等信号量、等事件、等延时
- 挂起态(Suspended)——被强制暂停,不参与调度
- 终止态(Terminated)——任务跑完了,或者被删了
状态切换的路径,我画了张图,你一看就明白:
任务切换,本质上是上下文切换。每次切换,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 避坑总结
做任务管理,有几个坑我反复踩过,今天一并告诉你:
- 栈空间别省:任务栈设太小,跑着跑着就栈溢出,系统崩溃。我一般给每个任务至少2KB,复杂任务4KB起步。
- 优先级别乱设:优先级层级太多,调度开销大。5-6个层级足够。
- 死锁要预防:两个任务互相等对方释放锁,就死锁了。我习惯用“锁顺序”策略——所有任务申请锁的顺序保持一致。
- 超时别设永久:LOS_WAIT_FOREVER要慎用。万一资源一直不来,任务就永远卡死。
- 任务函数别return:任务函数退出前,记得调用LOS_TaskDelete清理资源。
任务管理这块,理论不难,难的是实践中的权衡。你多写几个项目,自然就找到感觉了。