您可以自由地:
分享 在任何媒介以任何形式复制、发行本文档
演绎 修改、转换或以本文档为基础进行创作
只要你遵守许可协议条款,许可人就无法收回你的这些权利
惟须遵守下列条件:
署名 您必须提供适当的证书,提供一个链接到许可证,并指示是否作出更改。您可以以任何合理的方式这样做,但不是以任何方式表明,许可方赞同您或您的使用。
非商业性使用 您不得将本文档用于商业目的。
相同方式共享 如果您的修改、转换,或以本文档为基础进行创作,仅得依本素材的授权条款来散布您的贡献作品。
没有附加限制 您不能增设法律条款或科技措施,来限制别人依授权条款本已许可的作为。
声明:
当您使用本素材中属于公众领域的元素,或当法律有例外或限制条款允许您的使用,则您不需要遵守本授权条款。
未提供保证。本授权条款未必能完全提供您预期用途所需要的所有许可。例如:形象权、隐私权、著作人格权等其他权利,可能限制您如何使用本素材。
须知:
为了方便用户理解,这是协议的概述。可以访问网址https://creativecommons.org/licenses/by-nc-sa/3.0/legalcode了解完整协议内容。
目的
本文档介绍Huawei LiteOS的体系结构,并介绍如何进行开发和调试。
读者对象
本文档主要适用于Huawei LiteOS的开发者,主要适用于以下对象:
- 物联网端侧软件开发工程师
- 物联网架构设计师
符号约定
在本文中可能出现下列标志,它们所代表的含义如下。
Huawei LiteOS是轻量级的实时操作系统,具备轻量级、低功耗、快速启动、组件丰富等关键能力。
图 1 Huawei LiteOS Kernel的基本框架图
Huawei LiteOS基础内核包括不可裁剪的极小内核和可裁剪的其他模块。极小内核包含任务管理、内存管理、中断管理、异常管理和系统时钟。可裁剪的模块包括信号量、互斥锁、队列管理、事件管理、软件定时器等。 Huawei LiteOS支持 UP(单核)与 SMP(多核)模式,即支持在单核或者多核的环境上运行。
Huawei LiteOS Kernel遵循BSD-3开源许可协议。
任务管理
提供任务的创建、删除、延迟、挂起、恢复等功能,以及锁定和解锁任务调度。支持任务按优先级高低的抢占调度以及同优先级时间片轮转调度。
内存管理
- 提供静态内存和动态内存两种算法,支持内存申请、释放。目前支持的内存管理算法有固定大小的BOX算法、动态申请的bestfit算法和bestfit_little算法。
- 提供内存统计、内存越界检测功能。
硬件相关
提供中断管理、异常管理、系统时钟等功能。
- 中断管理:提供中断的创建、删除、使能、禁止、请求位的清除功能。
- 异常管理:系统运行过程中发生异常后,跳转到异常处理模块,打印当前发生异常的函数调用栈信息,或者保存当前系统状态。
- Tick :Tick是操作系统调度的基本时间单位,对应的时长由每秒Tick数决定,由用户配置。
IPC通信
提供消息队列、事件、信号量和互斥锁功能。
- 消息队列:支持消息队列的创建、删除、发送和接收功能。
- 事件:支持读事件和写事件功能。
- 信号量:支持信号量的创建、删除、申请和释放功能。
- 互斥锁:支持互斥锁的创建、删除、申请和释放功能。
软件定时器
软件定时器提供了定时器的创建、删除、启动、停止功能。
自旋锁
多核场景下,支持自旋锁的初始化、申请、释放功能。
低功耗
- Run-stop:即休眠唤醒,是Huawei LiteOS提供的保存系统现场镜像以及从系统现场镜像中恢复运行的机制。
- Tickless:Tickless机制通过计算下一次有意义的时钟中断的时间,来减少不必要的时钟中断,从而降低系统功耗。打开Tickless功能后,系统会在CPU空闲时启动Tickless机制。
维测
- CPU占用率:可以获取系统或者指定任务的CPU占用率。
- Trace事件跟踪:实时获取事件发生的上下文,并写入缓冲区。支持自定义缓冲区,跟踪指定模块的事件,开启/停止Trace,清除/输出trace缓冲区数据等。
- LMS:实时检测内存操作合法性,LMS能够检测的内存问题包括缓冲区溢出(buffer overflow),释放后使用(use after free),多重释放(double free)和释放野指针(wild pointer)。
- Shell:Huawei LiteOS Shell使用串口接收用户输入的命令,通过命令的方式调用、执行相应的应用程序。Huawei LiteOS Shell支持常用的基本调试功能,同时支持用户添加自定义命令。
C++支持
Huawei LiteOS支持部分STL特性、异常和RTTI特性,其他特性由编译器支持。
- Huawei LiteOS提供一套自有OS接口,同时也支持POSIX和CMSIS接口。请勿混用这些接口。混用接口可能导致不可预知的错误,例如:用POSIX接口申请信号量,但用Huawei LiteOS接口释放信号量。
- 开发驱动程序只能用Huawei LiteOS的接口,上层APP建议用POSIX接口。
从系统角度看,任务是竞争系统资源的最小运行单元。任务可以使用或等待CPU、使用内存空间等系统资源,并独立于其它任务运行。
Huawei LiteOS的任务模块可以给用户提供多个任务,实现任务间的切换,帮助用户管理业务程序流程。Huawei LiteOS的任务模块具有如下特性:
- 支持多任务。
- 一个任务表示一个线程。
- 抢占式调度机制,高优先级的任务可打断低优先级任务,低优先级任务必须在高优先级任务阻塞或结束后才能得到调度。
- 相同优先级任务支持时间片轮转调度方式。
- 共有32个优先级[0-31],最高优先级为0,最低优先级为31。
任务状态
Huawei LiteOS系统中的任务有多种运行状态。系统初始化完成后,创建的任务就可以在系统中竞争一定的资源,由内核进行调度。
任务状态通常分为以下四种:
- 就绪(Ready):该任务在就绪队列中,只等待CPU。
- 运行(Running):该任务正在执行。
- 阻塞(Blocked):该任务不在就绪队列中。包含任务被挂起(suspend状态)、任务被延时(delay状态)、任务正在等待信号量、读写队列或者等待事件等。
- 退出态(Dead):该任务运行结束,等待系统回收资源。
任务状态迁移
任务状态迁移说明:
-
就绪态→运行态
任务创建后进入就绪态,发生任务切换时,就绪队列中最高优先级的任务被执行,从而进入运行态,但此刻该任务依旧在就绪队列中。
-
运行态→阻塞态
正在运行的任务发生阻塞(挂起、延时、读信号量等)时,该任务会从就绪队列中删除,任务状态由运行态变成阻塞态,然后发生任务切换,运行就绪队列中最高优先级任务。
-
阻塞态→就绪态(阻塞态→运行态)
阻塞的任务被恢复后(任务恢复、延时时间超时、读信号量超时或读到信号量等),此时被恢复的任务会被加入就绪队列,从而由阻塞态变成就绪态;此时如果被恢复任务的优先级高于正在运行任务的优先级,则会发生任务切换,该任务由就绪态变成运行态。
-
就绪态→阻塞态
任务也有可能在就绪态时被阻塞(挂起),此时任务状态由就绪态变为阻塞态,该任务从就绪队列中删除,不会参与任务调度,直到该任务被恢复。
-
运行态→就绪态
有更高优先级任务创建或者恢复后,会发生任务调度,此刻就绪队列中最高优先级任务变为运行态,那么原先运行的任务由运行态变为就绪态,依然在就绪队列中。
-
运行态→退出态
运行中的任务运行结束,任务状态由运行态变为退出态。退出态包含任务运行结束的正常退出状态以及Invalid状态。例如,任务运行结束但是没有自删除,对外呈现的就是Invalid状态,即退出态。
-
阻塞态→退出态
阻塞的任务调用删除接口,任务状态由阻塞态变为退出态。
任务ID
任务ID,在任务创建时通过参数返回给用户,是任务的重要标识。系统中的ID号是唯一的。用户可以通过任务ID对指定任务进行任务挂起、任务恢复、查询任务名等操作。
任务优先级
优先级表示任务执行的优先顺序。任务的优先级决定了在发生任务切换时即将要执行的任务,就绪队列中最高优先级的任务将得到执行。
任务入口函数
新任务得到调度后将执行的函数。该函数由用户实现,在任务创建时,通过任务创建结构体设置。
任务栈
每个任务都拥有一个独立的栈空间,我们称为任务栈。栈空间里保存的信息包含局部变量、寄存器、函数参数、函数返回地址等。
任务上下文
任务在运行过程中使用的一些资源,如寄存器等,称为任务上下文。当这个任务挂起时,其他任务继续执行,可能会修改寄存器等资源中的值。如果任务切换时没有保存任务上下文,可能会导致任务恢复后出现未知错误。
因此,Huawei LiteOS在任务切换时会将切出任务的任务上下文信息,保存在自身的任务栈中,以便任务恢复后,从栈空间中恢复挂起时的上下文信息,从而继续执行挂起时被打断的代码。
任务控制块TCB
每个任务都含有一个任务控制块(TCB)。TCB包含了任务上下文栈指针(stack pointer)、任务状态、任务优先级、任务ID、任务名、任务栈大小等信息。TCB可以反映出每个任务运行情况。
任务切换
任务切换包含获取就绪队列中最高优先级任务、切出任务上下文保存、切入任务上下文恢复等动作。
用户创建任务时,系统会初始化任务栈,预置上下文。此外,系统还会将“任务入口函数”地址放在相应位置。这样在任务第一次启动进入运行态时,将会执行“任务入口函数”。
任务创建后,内核可以执行锁任务调度,解锁任务调度,挂起,恢复,延时等操作,同时也可以设置任务优先级,获取任务优先级。
Huawei LiteOS 的任务管理模块提供下面几种功能,接口详细信息可以查看API参考。
- 可以通过make menuconfig配置LOSCFG_KERNEL_SMP使能多核模式。在SMP的子菜单中,还可以设置核的数量、使能多任务的核间同步、使能函数跨核调用。
- 在多核模式下,创建任务时可以传入usCpuAffiMask来配置任务的CPU亲和性,该标志位采用1bit-1core的对应方式,详细可见TSK_INIT_PARAM_S结构体。
- 各个任务的任务栈大小,在创建任务时可以进行针对性的设置,若设置为0,则使用默认任务栈大小(LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE)作为任务栈大小。
Huawei LiteOS任务的大多数状态由内核维护,唯有自删除状态对用户可见,需要用户在创建任务时传入:
用户在调用创建任务接口时,可以将创建任务的TSK_INIT_PARAM_S参数的uwResved域设置为LOS_TASK_STATUS_DETACHED,即自删除状态,设置成自删除状态的任务会在运行完成后执行自删除操作。
须知: 自删除状态受LOSCFG_COMPAT_POSIX开关影响。
- LOSCFG_COMPAT_POSIX打开,只有将任务状态设置为LOS_TASK_STATUS_DETACHED才能实现自删除,否则任务完成时不会自删除。
- LOSCFG_COMPAT_POSIX关闭,任务完成时都会自删除,不管TSK_INIT_PARAM_S参数的uwResved域是否设置为LOS_TASK_STATUS_DETACHED。
创建任务、删除任务、挂起任务、恢复任务、延时任务等操作存在失败的可能,失败时会返回对应的错误码,以便快速定位错误原因。
如果错误发生在LiteOS启动过程中的任务初始化,还可以通过减少系统支持的最大任务数来解决;如果错误发生在任务创建过程中,也可以减小任务栈大小来解决 |
||||
|
||||
- 错误码定义见错误码简介。8~15位的所属模块为任务模块,值为0x02。
- 任务模块中的错误码序号 0x16、0x1c,未被定义,不可用。
以创建任务为例,讲解开发流程。
-
通过make menuconfig配置任务模块。
使能后,任务参数使用旧方式UINTPTR auwArgs[4],否则使用新的任务参数VOID *pArgs。建议关闭此开关,使用新的任务参数
-
锁任务调度LOS_TaskLock,防止高优先级任务调度。
-
创建任务LOS_TaskCreate,或静态创建任务LOS_TaskCreateStatic(需要打开LOSCFG_TASK_STATIC_ALLOCATION宏)。
-
解锁任务LOS_TaskUnlock,让任务按照优先级进行调度。
-
延时任务LOS_TaskDelay,任务延时等待。
-
挂起指定的任务LOS_TaskSuspend,任务挂起等待恢复操作。
-
恢复挂起的任务LOS_TaskResume。
无。
-
执行Idle任务时,会对之前已删除任务的任务控制块和任务栈进行回收。
-
任务名是指针,并没有分配空间,在设置任务名时,禁止将局部变量的地址赋值给任务名指针。
-
任务栈的大小按16字节大小或者sizeof(UINTPTR) * 2对齐。确定任务栈大小的原则是,够用就行,多了浪费,少了任务栈溢出。
-
挂起当前任务时,如果任务已经被锁定,则无法挂起。
-
Idle任务及软件定时器任务不能被挂起或者删除。
-
在中断处理函数中或者在锁任务的情况下,执行LOS_TaskDelay会失败。
-
锁任务调度,并不关中断,因此任务仍可被中断打断。
-
锁任务调度必须和解锁任务调度配合使用。
-
设置任务优先级时可能会发生任务调度。
-
可配置的系统最大任务数是指:整个系统的任务总个数,而非用户能使用的任务个数。例如:系统软件定时器多占用一个任务资源,那么用户能使用的任务资源就会减少一个。
-
LOS_CurTaskPriSet和LOS_TaskPriSet接口不能在中断中使用,也不能用于修改软件定时器任务的优先级。
-
LOS_TaskPriGet接口传入的task ID对应的任务未创建或者超过最大任务数,统一返回0xffff。
-
在删除任务时要保证任务申请的资源(如互斥锁、信号量等)已被释放。
-
在多核模式下,锁任务调度只能锁住当前核的调度器,其他核仍然能正常调度。
-
在多核模式下,由于跨核间任务的删除或挂起是异步执行的,因此操作的返回值并不代表最终操作的结果,仅代表上述请求已经发出。同时执行完成会存在延时。
-
在多核模式下,如果开启任务跨核删除同步的功能(LOSCFG_KERNEL_SMP_TASK_SYNC选项),则跨核删除任务时,需要等待目标任务删除后才会返回结果,如果在设定的时间内未成功将任务删除,则会返回LOS_ERRNO_TSK_MP_SYNC_FAILED错误。开启该功能后,每个任务会增加1个信号量的开销。
本实例介绍基本的任务操作方法,包含2个不同优先级任务的创建、任务延时、任务锁与解锁调度、挂起和恢复等操作,阐述任务优先级调度的机制以及各接口的应用。
前提条件:在menuconfig菜单中完成任务模块的配置。
UINT32 g_taskHiId;
UINT32 g_taskLoId;
#define TSK_PRIOR_HI 4
#define TSK_PRIOR_LO 5
UINT32 Example_TaskHi(VOID)
{
UINT32 ret;
printf("Enter TaskHi Handler.\r\n");
/* 延时2个Tick,延时后该任务会挂起,执行剩余任务中最高优先级的任务(g_taskLoId任务) */
ret = LOS_TaskDelay(2);
if (ret != LOS_OK) {
printf("Delay Task Failed.\r\n");
return LOS_NOK;
}
/* 2个Tick时间到了后,该任务恢复,继续执行 */
printf("TaskHi LOS_TaskDelay Done.\r\n");
/* 挂起自身任务 */
ret = LOS_TaskSuspend(g_taskHiId);
if (ret != LOS_OK) {
printf("Suspend TaskHi Failed.\r\n");
return LOS_NOK;
}
printf("TaskHi LOS_TaskResume Success.\r\n");
return ret;
}
/* 低优先级任务入口函数 */
UINT32 Example_TaskLo(VOID)
{
UINT32 ret;
printf("Enter TaskLo Handler.\r\n");
/* 延时2个Tick,延时后该任务会挂起,执行剩余任务中最高优先级的任务(背景任务) */
ret = LOS_TaskDelay(2);
if (ret != LOS_OK) {
printf("Delay TaskLo Failed.\r\n");
return LOS_NOK;
}
printf("TaskHi LOS_TaskSuspend Success.\r\n");
/* 恢复被挂起的任务g_taskHiId */
ret = LOS_TaskResume(g_taskHiId);
if (ret != LOS_OK) {
printf("Resume TaskHi Failed.\r\n");
return LOS_NOK;
}
printf("TaskHi LOS_TaskDelete Success.\r\n");
return ret;
}
/* 任务测试入口函数,创建两个不同优先级的任务 */
UINT32 Example_TskCaseEntry(VOID)
{
UINT32 ret;
TSK_INIT_PARAM_S initParam;
/* 锁任务调度,防止新创建的任务比本任务高而发生调度 */
LOS_TaskLock();
printf("LOS_TaskLock() Success!\r\n");
initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_TaskHi;
initParam.usTaskPrio = TSK_PRIOR_HI;
initParam.pcName = "TaskHi";
initParam.uwStackSize = LOSCFG_TASK_MIN_STACK_SIZE;
initParam.uwResved = LOS_TASK_STATUS_DETACHED;
/* 创建高优先级任务,由于锁任务调度,任务创建成功后不会马上执行 */
ret = LOS_TaskCreate(&g_taskHiId, &initParam);
if (ret != LOS_OK) {
LOS_TaskUnlock();
printf("Example_TaskHi create Failed!\r\n");
return LOS_NOK;
}
printf("Example_TaskHi create Success!\r\n");
initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_TaskLo;
initParam.usTaskPrio = TSK_PRIOR_LO;
initParam.pcName = "TaskLo";
initParam.uwStackSize = LOSCFG_TASK_MIN_STACK_SIZE;
initParam.uwResved = LOS_TASK_STATUS_DETACHED;
/* 创建低优先级任务,由于锁任务调度,任务创建成功后不会马上执行 */
ret = LOS_TaskCreate(&g_taskLoId, &initParam);
if (ret != LOS_OK) {
LOS_TaskUnlock();
printf("Example_TaskLo create Failed!\r\n");
return LOS_NOK;
}
printf("Example_TaskLo create Success!\r\n");
/* 解锁任务调度,此时会发生任务调度,执行就绪队列中最高优先级任务 */
LOS_TaskUnlock();
return LOS_OK;
}
编译运行得到的结果为:
LOS_TaskLock() Success!
Example_TaskHi create Success!
Example_TaskLo create Success!
Enter TaskHi Handler.
Enter TaskLo Handler.
TaskHi LOS_TaskDelay Done.
TaskHi LOS_TaskSuspend Success.
TaskHi LOS_TaskResume Success.
TaskHi LOS_TaskDelete Success.
本实例介绍基本的任务操作方法,包含任务创建、任务延时、任务锁与解锁调度、挂起和恢复等操作,阐述任务优先级调度的机制以及各接口的应用。
- 创建了2个任务:TaskHi和TaskLo。
- TaskHi为高优先级任务, 绑定在当前测试任务的CPU上。
- TaskLo为低优先级任务,不设置亲和性即不绑核。
须知: 由于TaskLo未设置亲和性,LOS_TaskLock对其没有锁任务的效果,因此"Example_TaskLo create Success!" 与 " Enter TaskLo Handler." 这两句打印并没有严格的先后顺序。
前提条件:在menuconfig菜单中完成任务模块的配置和SMP模式使能。
UINT32 g_taskLoId;
UINT32 g_taskHiId;
#define TSK_PRIOR_HI 4
#define TSK_PRIOR_LO 5
UINT32 Example_TaskHi(VOID)
{
UINT32 ret;
printf("[cpu%d] Enter TaskHi Handler.\r\n", ArchCurrCpuid());
/* 延时2个Tick,延时后该任务会挂起,执行剩余任务中最高优先级的任务(g_taskLoId任务) */
ret = LOS_TaskDelay(2);
if (ret != LOS_OK) {
printf("Delay Task Failed.\r\n");
return LOS_NOK;
}
/* 2个Tick后,该任务恢复,继续执行 */
printf("TaskHi LOS_TaskDelay Done.\r\n");
/* 挂起自身任务 */
ret = LOS_TaskSuspend(g_taskHiId);
if (ret != LOS_OK) {
printf("Suspend TaskHi Failed.\r\n");
return LOS_NOK;
}
printf("TaskHi LOS_TaskResume Success.\r\n");
return ret;
}
/* 低优先级任务入口函数 */
UINT32 Example_TaskLo(VOID)
{
UINT32 ret;
printf("[cpu%d] Enter TaskLo Handler.\r\n", ArchCurrCpuid());
/* 延时2个Tick,延时后该任务会挂起,执行剩余任务中就高优先级的任务(背景任务) */
ret = LOS_TaskDelay(2);
if (ret != LOS_OK) {
printf("Delay TaskLo Failed.\r\n");
return LOS_NOK;
}
printf("TaskHi LOS_TaskDelete Success.\r\n");
return ret;
}
/* 任务测试入口函数,创建两个不同优先级的任务 */
UINT32 Example_TskCaseEntry(VOID)
{
UINT32 ret;
TSK_INIT_PARAM_S initParam = {0};
/* 锁任务调度 */
LOS_TaskLock();
printf("LOS_TaskLock() Success on cpu%d!\r\n", ArchCurrCpuid());
initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_TaskHi;
initParam.usTaskPrio = TSK_PRIOR_HI;
initParam.pcName = "TaskHi";
initParam.uwStackSize = LOSCFG_TASK_MIN_STACK_SIZE;
initParam.uwResved = LOS_TASK_STATUS_DETACHED;
#ifdef LOSCFG_KERNEL_SMP
/* 绑定高优先级任务到CPU1运行 */
initParam.usCpuAffiMask = CPUID_TO_AFFI_MASK(ArchCurrCpuid());
#endif
/* 创建高优先级任务,由于CPU1的调度器被锁,任务创建成功后不会马上执行 */
ret = LOS_TaskCreate(&g_taskHiId, &initParam);
if (ret != LOS_OK) {
LOS_TaskUnlock();
printf("Example_TaskHi create Failed!\r\n");
return LOS_NOK;
}
printf("Example_TaskHi create Success!\r\n");
initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_TaskLo;
initParam.usTaskPrio = TSK_PRIOR_LO;
initParam.pcName = "TaskLo";
initParam.uwStackSize = LOSCFG_TASK_MIN_STACK_SIZE;
initParam.uwResved = LOS_TASK_STATUS_DETACHED;
#ifdef LOSCFG_KERNEL_SMP
/* 低优先级任务不设置CPU亲和性 */
initParam.usCpuAffiMask = 0;
#endif
/* 创建低优先级任务,尽管锁任务调度,但是由于该任务没有绑定该处理器,任务创建成功后可以马上在其他CPU执行 */
ret = LOS_TaskCreate(&g_taskLoId, &initParam);
if (ret != LOS_OK) {
LOS_TaskUnlock();
printf("Example_TaskLo create Failed!\r\n");
return LOS_NOK;
}
printf("Example_TaskLo create Success!\r\n");
/* 解锁任务调度,此时会发生任务调度,执行就绪列表中最高优先级任务 */
LOS_TaskUnlock();
return LOS_OK;
}
编译运行得到的结果为:
LOS_TaskLock() success on cpu1!
Example_TaskHi create Success!
Example_TaskLo create Success!
[cpu2] Enter TaskLo Handler.
[cpu1] Enter TaskHi Handler.
TaskHi LOS_TaskDelete Success.
TaskHi LOS_TaskDelay Done.
内存管理模块管理系统的内存资源,它是操作系统的核心模块之一,主要包括内存的初始化、分配以及释放。
在系统运行过程中,内存管理模块通过对内存的申请/释放来管理用户和OS对内存的使用,使内存的利用率和使用效率达到最优,同时最大限度地解决系统的内存碎片问题。
Huawei LiteOS的内存管理分为静态内存管理和动态内存管理,提供内存初始化、分配、释放等功能。
-
动态内存:在动态内存池中分配用户指定大小的内存块。
- 优点:按需分配。
- 缺点:内存池中可能出现碎片。
-
静态内存:在静态内存池中分配用户初始化时预设(固定)大小的内存块。
- 优点:分配和释放效率高,静态内存池中无碎片。
- 缺点:只能申请到初始化预设大小的内存块,不能按需申请。
动态内存管理,即在内存资源充足的情况下,根据用户需求,从系统配置的一块比较大的连续内存(内存池,也是堆内存)中分配任意大小的内存块。当用户不需要该内存块时,又可以释放回系统供下一次使用。
与静态内存相比,动态内存管理的优点是按需分配,缺点是内存池中容易出现碎片。
LiteOS动态内存支持bestfit(也称为dlink)和bestfit_little两种内存管理算法。
-
bestfit。
bestfit内存管理结构如下图所示:
-
第一部分
堆内存(也称内存池)的起始地址及堆区域总大小。
-
第二部分
本身是一个数组,每个元素是一个双向链表,所有free节点的控制头都会被分类挂在这个数组的双向链表中。
假设内存允许的最小节点为2min字节,则数组的第一个双向链表存储的是所有size为2min<size< 2min+1的free节点,第二个双向链表存储的是所有size为2min+1<size< 2min+2的free节点,依次类推第n个双向链表存储的是所有size为2min+n-1<size< 2min+n的free节点。每次申请内存的时候,会从这个数组检索最合适大小的free节点以分配内存。每次释放内存时,会将该内存作为free节点存储至这个数组以便下次再使用。
-
第三部分
占用内存池极大部分的空间,是用于存放各节点的实际区域。以下是LosMemDynNode节点结构体申明以及简单介绍:
typedef struct { union { LOS_DL_LIST freeNodeInfo; /* Free memory node */ struct { UINT32 magic; UINT32 taskId : 16; }; }; struct tagLosMemDynNode *preNode; UINT32 sizeAndFlag; } LosMemCtlNode; typedef struct tagLosMemDynNode { LosMemCtlNode selfNode; } LosMemDynNode;
当申请到的节点包含的数据空间首地址不符合对齐要求时需要进行对齐,通过增加Gap域确保返回的指针符合对齐要求。
-
-
bestfit_little。
bestfit_little算法是在最佳适配算法的基础上加入slab机制形成的算法。最佳适配算法使得每次分配内存时,都会选择内存池中最小最适合的内存块进行分配,而slab机制可以用于分配固定大小的内存块,从而减小产生内存碎片的可能性。
Huawei LiteOS内存管理中的slab机制支持配置slab class数目及每个class的最大空间。
现以内存池中共有4个slab class,每个slab class的最大空间为512字节为例说明slab机制。这4个slab class是从内存池中按照最佳适配算法分配出来的。第一个slab class被分为32个16字节的slab块,第二个slab class被分为16个32字节的slab块,第三个slab class被分为8个64字节的slab块,第四个slab class被分为4个128字节的slab块。
初始化内存模块时,首先初始化内存池,然后在初始化后的内存池中按照最佳适配算法申请4个slab class,再逐个按照slab内存管理机制初始化4个slab class。
每次申请内存时,先在满足申请大小的最佳slab class中申请(比如用户申请20字节内存,就在slab块大小为32字节的slab class中申请),如果申请成功,就将slab内存块整块返回给用户,释放时整块回收。需要注意的是,如果满足条件的slab class中已无可以分配的内存块,则从内存池中按照最佳适配算法申请,而不会继续从有着更大slab块空间的slab class中申请。释放内存时,先检查释放的内存块是否属于slab class,如果是则还回对应的slab class中,否则还回内存池中。
静态内存实质上是一个静态数组,静态内存池内的块大小在初始化时设定,初始化后块大小不可变更。
静态内存池由一个控制块和若干相同大小的内存块构成。控制块位于内存池头部,用于内存块管理。内存块的申请和释放以块大小为粒度。
动态内存管理的主要工作是动态分配并管理用户申请到的内存区间。
动态内存管理主要用于用户需要使用大小不等的内存块的场景。当用户需要使用内存时,可以通过操作系统的动态内存申请函数索取指定大小的内存块,一旦使用完毕,通过动态内存释放函数归还所占用内存,使之可以重复使用。
Huawei LiteOS系统中的动态内存管理模块为用户提供下面几种功能,接口详细信息可以查看API参考。
- 上述接口中,通过宏开关控制的都是内存调测功能相关的接口。
- 对于bestfit_little算法,只支持宏开关LOSCFG_MEM_MUL_POOL控制的多内存池相关接口和宏开关LOSCFG_BASE_MEM_NODE_INTEGRITY_CHECK控制的内存合法性检查接口,不支持其他内存调测功能。
- 通过LOS_MemAllocAlign/LOS_MemMallocAlign申请的内存进行LOS_MemRealloc/LOS_MemMrealloc操作后,不能保障新的内存首地址保持对齐。
- 对于bestfit_little算法,不支持对LOS_MemAllocAlign申请的内存进行LOS_MemRealloc操作,否则将返回失败。
本节介绍使用动态内存的典型场景开发流程。
-
在los_config.h文件中配置项动态内存池起始地址与大小。
- OS_SYS_MEM_ADDR:一般使用默认值即可。
- OS_SYS_MEM_SIZE:一般使用默认值即可。
-
通过make menuconfig配置动态内存管理模块。
-
初始化LOS_MemInit。
初始一个内存池后如图,生成一个 EndNode,并且剩余的内存全部被标记为FreeNode节点。注:EndNode作为内存池末尾的节点,size为0。
-
申请任意大小的动态内存LOS_MemAlloc。
判断动态内存池中是否存在申请量大小的空间,若存在,则划出一块内存块,以指针形式返回,若不存在,返回NULL。
调用三次LOS_MemAlloc函数可以创建三个节点,假设分别为UsedA,UsedB,UsedC,大小分别为sizeA,sizeB,sizeC。因为刚初始化内存池的时候只有一个大的FreeNode,所以这些内存块是从这个FreeNode中切割出来的。
当内存池中存在多个FreeNode的时候进行malloc,将会适配最合适大小的FreeNode用来新建内存块,减少内存碎片。若新建的内存块不等于被使用的FreeNode的大小,则在新建内存块后,多余的内存又会被标记为一个新的FreeNode。
-
释放动态内存LOS_MemFree。
回收内存块,供下一次使用。
假设调用LOS_MemFree释放内存块UsedB,则会回收内存块UsedB,并且将其标记为FreeNode。在回收内存块时,相邻的FreeNode会自动合并。
无。
- 由于动态内存管理需要管理控制块数据结构来管理内存,这些数据结构会额外消耗内存,故实际用户可使用内存总量小于配置项OS_SYS_MEM_SIZE的大小。
- 对齐分配内存接口LOS_MemAllocAlign/LOS_MemMallocAlign因为要进行地址对齐,可能会额外消耗部分内存,故存在一些遗失内存,当系统释放该对齐内存时,同时回收由于对齐导致的遗失内存。
- 重新分配内存接口LOS_MemRealloc/LOS_MemMrealloc如果分配成功,系统会自己判定是否需要释放原来申请的内存,并返回重新分配的内存地址。如果重新分配失败,原来的内存保持不变,并返回NULL。禁止使用pPtr = LOS_MemRealloc(pool, pPtr, uwSize),即:不能使用原来的旧内存地址pPtr变量来接收返回值。
- 对同一块内存多次调用LOS_MemFree/LOS_MemMfree时,第一次会返回成功,但对同一块内存多次重复释放会导致非法指针操作,结果不可预知。
- 由于动态内存管理的内存节点控制块结构体LosMemDynNode中,成员sizeAndFlag的数据类型为UINT32,高两位为标志位,余下的30位表示内存结点大小,因此用户初始化内存池的大小不能超过1G,否则会出现不可预知的结果。
前提条件:在menuconfig菜单中完成动态内存的配置。
本实例执行以下步骤:
- 初始化一个动态内存池。
- 从动态内存池中申请一个内存块。
- 在内存块中存放一个数据。
- 打印出内存块中的数据。
- 释放该内存块。
#define TEST_POOL_SIZE (2*1024*1024)
UINT8 g_testPool[TEST_POOL_SIZE];
VOID Example_DynMem(VOID)
{
UINT32 *mem = NULL;
UINT32 ret;
ret = LOS_MemInit(g_testPool, TEST_POOL_SIZE);
if (LOS_OK == ret) {
dprintf("内存池初始化成功!\n");
} else {
dprintf("内存池初始化失败!\n");
return;
}
/*分配内存*/
mem = (UINT32 *)LOS_MemAlloc(g_testPool, 4);
if (NULL == mem) {
dprintf("内存分配失败!\n");
return;
}
dprintf("内存分配成功\n");
/*赋值*/
*mem = 828;
dprintf("*mem = %d\n", *mem);
/*释放内存*/
ret = LOS_MemFree(g_testPool, mem);
if (LOS_OK == ret) {
dprintf("内存释放成功!\n");
} else {
dprintf("内存释放失败!\n");
}
return;
}
输出结果如下:
内存池初始化成功!
内存分配成功
*mem = 828
内存释放成功!
当用户需要使用固定长度的内存时,可以通过静态内存分配的方式获取内存,一旦使用完毕,通过静态内存释放函数归还所占用内存,使之可以重复使用。
Huawei LiteOS的静态内存管理主要为用户提供以下功能,接口详细信息可以查看API参考。
打印指定静态内存池所有节点信息(打印等级是LOS_INFO_LEVEL),包括内存池起始地址、内存块大小、总内存块数量、每个空闲内存块的起始地址、所有内存块的起始地址 |
本节介绍使用静态内存的典型场景开发流程。
-
通过make menuconfig配置静态内存管理模块。
-
规划一片内存区域作为静态内存池。
-
调用LOS_MemboxInit初始化静态内存池。
初始化会将入参指定的内存区域分割为N块(N值取决于静态内存总大小和块大小),将所有内存块挂到空闲链表,在内存起始处放置控制头。
-
调用LOS_MemboxAlloc接口分配静态内存。
系统将会从空闲链表中获取第一个空闲块,并返回该内存块的起始地址。
-
调用LOS_MemboxClr接口。
将入参地址对应的内存块清零。
-
调用LOS_MemboxFree接口。
将该内存块加入空闲链表。
无。
静态内存池区域,如果是通过动态内存分配方式获得的,在不需要静态内存池时,需要释放该段内存,避免发生内存泄露。
前提条件:在menuconfig菜单中完成静态内存的配置。
本实例执行以下步骤:
- 初始化一个静态内存池。
- 从静态内存池中申请一块静态内存。
- 在内存块存放一个数据。
- 打印出内存块中的数据。
- 清除内存块中的数据。
- 释放该内存块。
VOID Example_StaticMem(VOID)
{
UINT32 *mem = NULL;
UINT32 blkSize = 10;
UINT32 boxSize = 100;
UINT32 boxMem[1000];
UINT32 ret;
ret = LOS_MemboxInit(&boxMem[0], boxSize, blkSize);
if(ret != LOS_OK) {
printf("内存池初始化失败!\n");
return;
} else {
printf("内存池初始化成功!\n");
}
/*申请内存块*/
mem = (UINT32 *)LOS_MemboxAlloc(boxMem);
if (NULL == mem) {
printf("内存分配失败!\n");
return;
}
printf("内存分配成功\n");
/*赋值*/
*mem = 828;
printf("*mem = %d\n", *mem);
/*清除内存内容*/
LOS_MemboxClr(boxMem, mem);
printf("清除内存内容成功\n *mem = %d\n", *mem);
/*释放内存*/
ret = LOS_MemboxFree(boxMem, mem);
if (LOS_OK == ret) {
printf("内存释放成功!\n");
} else {
printf("内存释放失败!\n");
}
return;
}
输出结果如下:
内存池初始化成功!
内存分配成功
*mem = 828
清除内存内容成功
*mem = 0
内存释放成功!
中断是指出现需要时,CPU暂停执行当前程序,转而执行新程序的过程。即在程序运行过程中,出现了一个必须由CPU立即处理的事务。此时,CPU暂时中止当前程序的执行转而处理这个事务,这个过程就叫做中断。
外设可以在没有CPU介入的情况下完成一定的工作,但某些情况下也需要CPU为其执行一定的工作。通过中断机制,在外设不需要CPU介入时,CPU可以执行其它任务,而当外设需要CPU时,将通过产生中断信号使CPU立即中断当前任务来响应中断请求。这样可以使CPU避免把大量时间耗费在等待、查询外设状态的操作上,大大提高系统实时性以及执行效率。
Huawei LiteOS的中断特性:
- 中断共享,且可配置。
- 中断嵌套,即高优先级的中断可抢占低优先级的中断,且可配置。
- 使用独立中断栈,可配置。
- 可配置支持的中断优先级个数。
- 可配置支持的中断数。
与中断相关的硬件可以划分为三类:设备、中断控制器、CPU本身。
-
设备
发起中断的源,当设备需要请求CPU时,产生一个中断信号,该信号连接至中断控制器。
-
中断控制器
中断控制器是CPU众多外设中的一个,它一方面接收其它外设中断引脚的输入,另一方面,它会发出中断信号给CPU。可以通过对中断控制器编程来打开和关闭中断源、设置中断源的优先级和触发方式。常用的中断控制器有VIC(Vector Interrupt Controller)和GIC(General Interrupt Controller)。在ARM Cortex-M系列中使用的中断控制器是NVIC(Nested Vector Interrupt Controller)。在ARM Cortex-A7中使用的中断控制器是GIC。
-
CPU
CPU会响应中断源的请求,中断当前正在执行的任务,转而执行中断处理程序。
中断号
每个中断请求信号都会有特定的标志,使得计算机能够判断是哪个设备提出的中断请求,这个标志就是中断号。
中断请求
“紧急事件”需向CPU提出申请(发一个电脉冲信号),要求中断,及要求CPU暂停当前执行的任务,转而处理该“紧急事件”,这一申请过程称为中断请求。
中断优先级
为使系统能够及时响应并处理所有中断,系统根据中断时间的重要性和紧迫程度,将中断源分为若干个级别,称作中断优先级。
中断处理程序
当外设产生中断请求后,CPU暂停当前的任务,转而响应中断申请,即执行中断处理程序。产生中断的每个设备都有相应的中断处理程序。
中断嵌套
中断嵌套也称为中断抢占,指的是正在执行一个中断处理程序时,如果有另一个优先级更高的中断源提出中断请求,这时会暂时终止当前正在执行的优先级较低的中断源的中断处理程序,转而去处理更高优先级的中断请求,待处理完毕,再返回到之前被中断的处理程序中继续执行。
中断触发
中断源向中断控制器发送中断信号,中断控制器对中断进行仲裁,确定优先级,将中断信号送给CPU。中断源产生中断信号的时候,会将中断触发器置“1”,表明该中断源产生了中断,要求CPU去响应该中断。
中断触发类型
外部中断申请通过一个物理信号发送到NVIC/GIC,可以是电平触发或边沿触发。
中断向量
中断服务程序的入口地址。
中断向量表
存储中断向量的存储区,中断向量与中断号对应,中断向量在中断向量表中按照中断号顺序存储。
中断共享
当外设较少时,可以实现一个外设对应一个中断号,但为了支持更多的硬件设备,可以让多个设备共享一个中断号,共享同一个中断号的中断处理程序形成一个链表。当外部设备产生中断申请时,系统会遍历执行中断号对应的中断处理程序链表直到找到对应设备的中断处理程序。在遍历执行过程中,各中断处理程序可以通过检测设备ID,判断是否是这个中断处理程序对应的设备产生的中断。
核间中断
对于多核系统,中断控制器允许一个CPU的硬件线程去中断其他CPU的硬件线程,这种方式被称为核间中断。核间中断的实现基础是多CPU内存共享,采用核间中断可以减少某个CPU负荷过大,有效提升系统效率。目前只有GIC中断控制器支持。
-
Huawei LiteOS的中断机制支持中断共享:
中断共享的实现依赖于链表,对应每一个中断号创建一个链表,链表节点中包含注册的中断处理函数和函数入参。当对同一中断号多次创建中断时,将中断处理函数和函数入参添加到中断号对应的链表中,因此当硬件产生中断时,通过中断号查找到其对应的链表,遍历执行链表直到找到对应设备的中断处理函数。
-
Huawei LiteOS的中断嵌套:
- GIC与NVIC的中断嵌套由硬件实现。
- RISC-V的中断嵌套实现机制为:中断嵌套下,中断A触发后会将当前的操作进行压栈,调用中断处理程序前,将MIE设置为1,允许新的中断被响应。在A执行中断处理程序的过程中,如果有更高优先级的中断B被触发,B 会将当前的操作即中断A相关的操作进行压栈,然后执行B的中断处理程序。待B的中断处理程序执行完后,会暂时的将mstatus寄存器中的MIE域置为0,关闭中断响应,将中断A相关的操作进行出栈,将MIE设置为1,允许处理器再次响应中断,中断B结束,继续执行中断A。
当有中断请求产生时,CPU暂停当前的任务,转而去响应外设请求。根据需要,用户通过中断申请,注册中断处理程序,可以指定CPU响应中断请求时所执行的具体操作。
Huawei LiteOS 的中断模块为用户提供下面几种功能,接口详细信息可以查看API参考。
对存在失败可能性的操作返回对应的错误码,以便快速定位错误原因。
须知: 错误码定义见错误码简介。8~15位的所属模块为中断模块,值为0x09。
-
通过make menuconfig配置中断模块,菜单路径为:Kernel ---> Hardware Interrupt。
-
调用中断创建接口LOS_HwiCreate创建中断。
-
如果是SMP模式,调用LOS_HwiSetAffinity设置中断的亲和性,否则直接进入步骤4。
-
调用LOS_HwiEnable接口使能指定中断。
-
调用LOS_HwiTrigger接口触发指定中断(该接口通过写中断控制器的相关寄存器模拟外部中断,一般的外设设备,不需要执行这一步)。
-
调用LOS_HwiDisable接口屏蔽指定中断,此接口根据实际情况使用,判断是否需要屏蔽中断。
-
调用LOS_HwiDelete接口删除指定中断,此接口根据实际情况使用,判断是否需要删除中断。
- 根据具体硬件,配置支持的最大中断数及可设置的中断优先级个数。
- 中断共享机制,支持不同的设备使用相同的中断号注册同一中断处理程序,但中断处理程序的入参pDevId(设备号)必须唯一,代表不同的设备。即同一中断号,同一dev只能挂载一次;但同一中断号,同一中断处理程序,dev不同则可以重复挂载。
- 中断处理程序耗时不能过长,否则会影响CPU对中断的及时响应。
- 中断响应过程中不能执行引起调度的函数。
- 中断恢复LOS_IntRestore()的入参必须是与之对应的LOS_IntLock()的返回值(即关中断之前的CPSR值)。
- Cortex-M系列处理器中0-15中断为内部使用,Cortex-A7中0-31中断为内部使用,因此不建议用户去申请和创建。
本实例实现如下功能:
- 创建中断。
- 设置中断亲和性。
- 使能中断。
- 触发中断。
- 屏蔽中断。
- 删除中断。
前提条件:menuconfig菜单中配置中断使用最大数、配置可设置的中断优先级个数。
代码实现如下:
#include "los_hwi.h"
#include "los_typedef.h"
#include "los_task.h"
STATIC VOID HwiUsrIrq(VOID)
{
printf("\n in the func HwiUsrIrq \n");
}
/* cpu0 trigger, cpu0 response */
UINT32 It_Hwi_001(VOID)
{
UINT32 ret;
UINT32 irqNum = 26; /* ppi */
UINT32 irqPri = 0x3;
ret = LOS_HwiCreate(irqNum, irqPri, 0, (HWI_PROC_FUNC)HwiUsrIrq, 0);
if (ret != LOS_OK) {
return LOS_NOK;
}
#ifdef LOSCFG_KERNEL_SMP
ret = LOS_HwiSetAffinity(irqNum, CPUID_TO_AFFI_MASK(ArchCurrCpuid()));
if (ret != LOS_OK) {
return LOS_NOK;
}
#endif
ret = LOS_HwiEnable(irqNum);
if (ret != LOS_OK) {
return LOS_NOK;
}
ret = LOS_HwiTrigger(irqNum);
if (ret != LOS_OK) {
return LOS_NOK;
}
LOS_TaskDelay(1);
ret = LOS_HwiDisable(irqNum);
if (ret != LOS_OK) {
return LOS_NOK;
}
ret = LOS_HwiDelete(irqNum, NULL);
if (ret != LOS_OK) {
return LOS_NOK;
}
return LOS_OK;
}
异常接管是操作系统对运行期间发生的异常情况(芯片硬件异常)进行处理的一系列动作,例如打印异常发生时当前函数的调用栈信息、CPU现场信息、任务的堆栈情况等。
异常接管作为一种调测手段,可以在系统发生异常时给用户提供有用的异常信息,譬如异常类型、发生异常时的系统状态等,方便用户定位分析问题。
Huawei LiteOS的异常接管,在系统发生异常时的处理动作为:显示异常发生时正在运行的任务信息(包括任务名、任务号、堆栈大小等),以及CPU现场等信息。
每个函数都有自己的栈空间,称为栈帧。调用函数时,会创建子函数的栈帧,同时将函数入参、局部变量、寄存器入栈。栈帧从高地址向低地址生长。
以ARM32 CPU架构为例,每个栈帧中都会保存PC、LR、SP和FP寄存器的历史值。
堆栈分析
-
LR寄存器(Link Register),链接寄存器,指向函数的返回地址。
-
R11:可以用作通用寄存器,在开启特定编译选项时可以用作帧指针寄存器FP,用来实现栈回溯功能。
GNU编译器(gcc)默认将R11作为存储变量的通用寄存器,因而默认情况下无法使用FP的栈回溯功能。为支持调用栈解析功能,需要在编译参数中添加-fno-omit-frame-pointer选项,提示编译器将R11作为FP使用。
-
FP寄存器(Frame Point),帧指针寄存器,指向当前函数的父函数的栈帧起始地址。利用该寄存器可以得到父函数的栈帧,从栈帧中获取父函数的FP,就可以得到祖父函数的栈帧,以此类推,可以追溯程序调用栈,得到函数间的调用关系。
当系统发生异常时,系统打印异常函数的栈帧中保存的寄存器内容,以及父函数、祖父函数的栈帧中的LR、FP寄存器内容,用户就可以据此追溯函数间的调用关系,定位异常原因。
堆栈分析原理如下图所示,实际堆栈信息根据不同CPU架构有所差异,此处仅做示意。
图中不同颜色的寄存器表示不同的函数。可以看到函数调用过程中,寄存器的保存。通过FP寄存器,栈回溯到异常函数的父函数,继续按照规律对栈进行解析,推出函数调用关系,方便用户定位问题。
异常接管对系统运行期间发生的芯片硬件异常进行处理,不同芯片的异常类型存在差异,具体异常类型可以查看芯片手册。
异常接管一般的定位步骤如下:
- 打开编译后生成的镜像反汇编(asm)文件。
- 搜索PC指针(指向当前正在执行的指令)在asm中的位置,找到发生异常的函数。
- 根据LR值查找异常函数的父函数。
- 重复步骤3,得到函数间的调用关系,找到异常原因。
具体的定位方法会在实例中举例说明。
要查看调用栈信息,必须添加编译选项宏-fno-omit-frame-pointer支持stack frame,否则编译时FP寄存器是关闭的。
在某ARM32平台上通过错误释放内存,触发系统异常。系统异常被挂起后,能在串口中看到异常调用栈打印信息和关键寄存器信息,如下所示,其中excType表示异常类型,此处值为4表示数据终止异常,其它数值可以查看芯片手册。通过这些信息可以定位到异常所在函数和其调用栈关系,为分析异常原因提供第一手资料。
excType: 4
taskName = MNT_send
taskId = 6
task stackSize = 12288
excBuffAddr pc = 0x8034d3cc
excBuffAddr lr = 0x8034d3cc
excBuffAddr sp = 0x809ca358
excBuffAddr fp = 0x809ca36c
*******backtrace begin*******
traceback 0 -- lr = 0x803482fc
traceback 0 -- fp = 0x809ca38c
traceback 1 -- lr = 0x80393e34
traceback 1 -- fp = 0x809ca3a4
traceback 2 -- lr = 0x8039e0d0
traceback 2 -- fp = 0x809ca3b4
traceback 3 -- lr = 0x80386bec
traceback 3 -- fp = 0x809ca424
traceback 4 -- lr = 0x800a6210
traceback 4 -- fp = 0x805da164
定位步骤如下:
-
打开编译后生成的 asm 文件,默认是 vs_server.asm(默认生成在Huawei_LiteOS/out/<platform>目录下,其中的platform为具体的平台名)。
-
搜索PC指针 8034d3cc 在asm文件中的位置(去掉0x)。
PC地址指向发生异常时程序正在执行的指令。在当前执行的二进制文件对应的asm文件中,查找PC值8034d3cc,找到当前CPU正在执行的指令行,得到如下图所示结果。
从图中可以看到:
- 异常时CPU正在执行的指令是ldrh r2, [r4, #-4]。
- 异常发生在函数osSlabMemFree中。
结合ldrh指令分析,此指令是从内存的(r4-4)地址中读值,将其load到寄存器r2中。再结合异常时打印的寄存器信息,查看此时r4的值。下图是异常时打印的寄存器信息,可以看到,r4此时值是0xffffffff。
显然,r4的值超出了内存范围,故CPU执行到该指令时发生了数据终止异常。根据汇编知识,从asm文件可以看到,r4是从r1 mov过来,而r1是函数第二个入参,于是可以确认,在调用osSlabMemFree时传入了0xffffffff(或-1)这样一个错误入参。
接下来,需要查找谁调用了osSlabMemFree函数。
-
根据 LR链接寄存器值 查找调用栈。
从异常信息的backtrace begin开始,打印的是调用栈信息。在asm文件中查找backtrace 0对应的LR,如下图所示。
可见,是LOS_MemFree调用了osSlabMemFree。依此方法,可得到异常时函数调用关系如下:MNT_buf_send(业务函数) -> free -> LOS_MemFree -> osSlabMemFree。
最终,通过排查业务中MNT_buf_send实现,发现其中存在错误使用指针的问题,导致free了一个错误地址,引发上述异常。
错误处理指程序运行错误时,调用错误处理模块的接口函数,上报错误信息,并调用注册的钩子函数进行特定处理,保存现场以便定位问题。
通过错误处理,可以控制和提示程序中的非法输入,防止程序崩溃。
错误处理是一种机制,用于处理异常状况。当程序出现错误时,会显示相应的错误码。此外,如果注册了相应的错误处理函数,则会执行这个函数。
调用API接口时可能会出现错误,此时接口会返回对应的错误码,以便快速定位错误原因。
错误码是一个32位的无符号整型数,31~24位表示错误等级,23~16位表示错误码标志(当前该标志值为0),15~8位代表错误码所属模块,7~0位表示错误码序号。
表 1 错误码中的错误等级
例如将任务模块中的错误码LOS_ERRNO_TSK_NO_MEMORY定义为FATAL级别的错误,模块ID为LOS_MOD_TSK,错误码序号为0,其定义如下:
#define LOS_ERRNO_TSK_NO_MEMORY LOS_ERRNO_OS_FATAL(LOS_MOD_TSK, 0x00)
#define LOS_ERRNO_OS_FATAL(MID, ERRNO) \
(LOS_ERRTYPE_FATAL | LOS_ERRNO_OS_ID | ((UINT32)(MID) << 8) | ((UINT32)(ERRNO)))
说明: LOS_ERRTYPE_FATAL:错误等级为FATAL,值为0x03000000 LOS_ERRNO_OS_ID:错误码标志,值为0x000000 MID:所属模块,LOS_MOD_TSK的值为0x2 ERRNO:错误码序号 所以LOS_ERRNO_TSK_NO_MEMORY的值为0x03000200
有时只靠错误码不能快速准确的定位问题,为方便用户分析错误,错误处理模块支持注册错误处理的钩子函数,发生错误时,用户可以调用LOS_ErrHandle接口以执行错误处理函数。
Huawei LiteOS 的错误处理模块为用户提供下面几个接口,接口详细信息可以查看API参考。
须知: 系统内部会在某些难以定位的错误处,主动调用注册的钩子函数(目前只在互斥锁模块和信号量模块中主动调用了钩子函数)。
系统中只有一个错误处理的钩子函数。当多次注册钩子函数时,最后一次注册的钩子函数会覆盖前一次注册的函数。
在下面的例子中,演示如下功能:
- 注册错误处理钩子函数。
- 执行错误处理函数。
代码实现如下:
#include "los_err.h"
#include "los_typedef.h"
#include <stdio.h>
void Test_ErrHandle(CHAR *fileName, UINT32 lineNo, UINT32 errorNo, UINT32 paraLen, VOID *para)
{
printf("err handle ok\n");
}
static UINT32 TestCase(VOID)
{
UINT32 errNo = 0;
UINT32 ret;
UINT32 errLine = 16;
LOS_RegErrHandle(Test_ErrHandle);
ret = LOS_ErrHandle("os_unspecific_file", errLine, errNo, 0, NULL);
if (ret != LOS_OK) {
return LOS_NOK;
}
return LOS_OK;
}
编译运行得到的结果为:
Huawei LiteOS # err handle ok
队列又称消息队列,是一种常用于任务间通信的数据结构。队列接收来自任务或中断的不固定长度消息,并根据不同的接口确定传递的消息是否存放在队列空间中。
任务能够从队列里面读取消息,当队列中的消息为空时,挂起读取任务;当队列中有新消息时,挂起的读取任务被唤醒并处理新消息。任务也能够往队列里写入消息,当队列已经写满消息时,挂起写入任务;当队列中有空闲消息节点时,挂起的写入任务被唤醒并写入消息。如果将读队列和写队列的超时时间设置为0,则不会挂起任务,接口会直接返回,这就是非阻塞模式。
消息队列提供了异步处理机制,允许将一个消息放入队列,但不立即处理。同时队列还有缓冲消息的作用。
Huawei LiteOS中使用队列实现任务异步通信,具有如下特性:
- 消息以先进先出的方式排队,支持异步读写。
- 读队列和写队列都支持超时机制。
- 每读取一条消息,就会将该消息节点设置为空闲。
- 发送消息类型由通信双方约定,可以允许不同长度(不超过队列的消息节点大小)的消息。
- 一个任务能够从任意一个消息队列接收和发送消息。
- 多个任务能够从同一个消息队列接收和发送消息。
- 创建队列时所需的队列空间,默认支持接口内系统自行动态申请内存的方式,同时也支持将用户分配的队列空间作为接口入参传入的方式。
队列控制块
typedef enum {
OS_QUEUE_READ =0,
OS_QUEUE_WRITE =1,
OS_QUEUE_N_RW =2
} QueueReadWrite;
/**
* Queue information block structure
*/
typedef struct
{
UINT8 *queueHandle; /* 队列指针 */
UINT8 queueState; /* 队列状态 */
UINT8 queueMemType; /* 创建队列时内存分配的方式 */
UINT16 queueLen; /* 队列中消息节点个数,即队列长度 */
UINT16 queueSize; /* 消息节点大小 */
UINT32 queueID; /* 队列ID */
UINT16 queueHead; /* 消息头节点位置(数组下标)*/
UINT16 queueTail; /* 消息尾节点位置(数组下标)*/
UINT16 readWriteableCnt[OS_QUEUE_N_RW]; /* 数组下标0的元素表示队列中可读消息数,
数组下标1的元素表示队列中可写消息数 */
LOS_DL_LIST readWriteList[OS_QUEUE_N_RW]; /* 读取或写入消息的任务等待链表,
下标0:读取链表,下标1:写入链表 */
LOS_DL_LIST memList; /* CMSIS-RTOS中的MailBox模块使用的内存块链表 */
} LosQueueCB;
每个队列控制块中都含有队列状态,表示该队列的使用情况:
- OS_QUEUE_UNUSED:队列没有被使用。
- OS_QUEUE_INUSED:队列被使用中。
每个队列控制块中都含有创建队列时的内存分配方式:
- OS_QUEUE_ALLOC_DYNAMIC:创建队列时所需的队列空间,由系统自行动态申请内存获取。
- OS_QUEUE_ALLOC_STATIC:创建队列时所需的队列空间,由接口调用者自行申请后传入接口。
队列运作原理
创建队列时,创建队列成功会返回队列ID。
在队列控制块中维护着一个消息头节点位置Head和一个消息尾节点位置Tail来,用于表示当前队列中消息的存储情况。Head表示队列中被占用的消息节点的起始位置。Tail表示被占用的消息节点的结束位置,也是空闲消息节点的起始位置。队列刚创建时,Head和Tail均指向队列起始位置。
写队列时,根据readWriteableCnt[1]判断队列是否可以写入,不能对已满(readWriteableCnt[1]为0)队列进行写操作。写队列支持两种写入方式:向队列尾节点写入,也可以向队列头节点写入。尾节点写入时,根据Tail找到起始空闲消息节点作为数据写入对象,如果Tail已经指向队列尾部则采用回卷方式。头节点写入时,将Head的前一个节点作为数据写入对象,如果Head指向队列起始位置则采用回卷方式。
读队列时,根据readWriteableCnt[0]判断队列是否有消息需要读取,对全部空闲(readWriteableCnt[0]为0)队列进行读操作会引起任务挂起。如果队列可以读取消息,则根据Head找到最先写入队列的消息节点进行读取。如果Head已经指向队列尾部则采用回卷方式。
删除队列时,根据队列ID找到对应队列,把队列状态置为未使用,把队列控制块置为初始状态。如果是通过系统动态申请内存方式创建的队列,还会释放队列所占内存。
上图对读写队列做了示意,图中只画了尾节点写入方式,没有画头节点写入,但是两者是类似的。
队列用于任务间通信,可以实现消息的异步处理。同时消息的发送方和接收方不需要彼此联系,两者间是解耦的。
Huawei LiteOS中的队列模块提供下面几种功能,接口详细信息可以查看API参考。
获取指定队列的信息,包括队列ID、队列长度、消息节点大小、头节点、尾节点、可读节点数量、可写节点数量、等待读操作的任务、等待写操作的任务、等待mail操作的任务 |
对存在失败可能性的操作返回对应的错误码,以便快速定位错误原因。
系统支持的最大队列数应该大于0。如果不使用队列模块,则将队列模块静态裁剪开关LOSCFG_BASE_IPC_QUEUE设置为NO |
||||
- 错误码定义见错误码简介。8~15位的所属模块为队列模块,值为0x06。
- 队列模块中的错误码序号0x11、0x14未被定义,不可用。
使用队列模块的典型流程如下:
-
通过make menuconfig配置队列模块。
-
创建队列。创建成功后,可以得到队列ID。
-
写队列。
-
读队列。
-
获取队列信息。
-
删除队列。
- 系统支持的最大队列数是指:整个系统的队列资源总个数,而非用户能使用的个数。例如:系统软件定时器多占用一个队列资源,那么用户能使用的队列资源就会减少一个。
- 创建队列时传入的队列名和flags暂时未使用,作为以后的预留参数。
- 队列接口函数中的入参timeout是相对时间。
- LOS_QueueReadCopy和LOS_QueueWriteCopy及LOS_QueueWriteHeadCopy是一组接口,LOS_QueueRead和LOS_QueueWrite及LOS_QueueWriteHead是一组接口,两组接口需要配套使用。
- 鉴于LOS_QueueWrite和LOS_QueueWriteHead和LOS_QueueRead这组接口实际操作的是数据地址,用户必须保证调用LOS_QueueRead获取到的指针所指向的内存区域在读队列期间没有被异常修改或释放,否则可能导致不可预知的后果。
- 鉴于LOS_QueueWrite和LOS_QueueWriteHead和LOS_QueueRead这组接口实际操作的是数据地址,也就意味着实际写和读的消息长度仅仅是一个指针数据,因此用户使用这组接口之前,需确保创建队列时的消息节点大小,为一个指针的长度,避免不必要的浪费和读取失败。
- 当队列使用结束后,如果存在动态申请的内存,需要及时释放这些内存。
创建一个队列,两个任务。任务1调用写队列接口发送消息,任务2通过读队列接口接收消息。
- 通过LOS_TaskCreate创建任务1和任务2。
- 通过LOS_QueueCreate创建一个消息队列。
- 在任务1 send_Entry中发送消息。
- 在任务2 recv_Entry中接收消息。
- 通过LOS_QueueDelete删除队列。
前提条件:在menuconfig菜单中完成队列模块的配置。
#include "los_task.h"
#include "los_queue.h"
static UINT32 g_queue;
#define BUFFER_LEN 50
VOID send_Entry(VOID)
{
UINT32 i = 0;
UINT32 ret = 0;
CHAR abuf[] = "test is message x";
UINT32 len = sizeof(abuf);
while (i < 5) {
abuf[len -2] = '0' + i;
i++;
ret = LOS_QueueWriteCopy(g_queue, abuf, len, 0);
if(ret != LOS_OK) {
dprintf("send message failure, error: %x\n", ret);
}
LOS_TaskDelay(5);
}
}
VOID recv_Entry(VOID)
{
UINT32 ret = 0;
CHAR readBuf[BUFFER_LEN] = {0};
UINT32 readLen = BUFFER_LEN;
while (1) {
ret = LOS_QueueReadCopy(g_queue, readBuf, &readLen, 0);
if(ret != LOS_OK) {
dprintf("recv message failure, error: %x\n", ret);
break;
}
dprintf("recv message: %s\n", readBuf);
LOS_TaskDelay(5);
}
while (LOS_OK != LOS_QueueDelete(g_queue)) {
LOS_TaskDelay(1);
}
dprintf("delete the queue success!\n");
}
UINT32 Example_CreateTask(VOID)
{
UINT32 ret = 0;
UINT32 task1, task2;
TSK_INIT_PARAM_S initParam;
initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)send_Entry;
initParam.usTaskPrio = 9;
initParam.uwStackSize = LOS_TASK_MIN_STACK_SIZE;
initParam.pcName = "sendQueue";
#ifdef LOSCFG_KERNEL_SMP
initParam.usCpuAffiMask = CPUID_TO_AFFI_MASK(ArchCurrCpuid());
#endif
initParam.uwResved = LOS_TASK_STATUS_DETACHED;
LOS_TaskLock();
ret = LOS_TaskCreate(&task1, &initParam);
if(ret != LOS_OK) {
dprintf("create task1 failed, error: %x\n", ret);
return ret;
}
initParam.pcName = "recvQueue";
initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)recv_Entry;
ret = LOS_TaskCreate(&task2, &initParam);
if(ret != LOS_OK) {
dprintf("create task2 failed, error: %x\n", ret);
return ret;
}
ret = LOS_QueueCreate("queue", 5, &g_queue, 0, BUFFER_LEN);
if(ret != LOS_OK) {
dprintf("create queue failure, error: %x\n", ret);
}
dprintf("create the queue success!\n");
LOS_TaskUnlock();
return ret;
}
create the queue success!
recv message: test is message 0
recv message: test is message 1
recv message: test is message 2
recv message: test is message 3
recv message: test is message 4
recv message failure, error: 200061d
delete the queue success!
事件(Event)是一种任务间通信的机制,可用于任务间的同步。
多任务环境下,任务之间往往需要同步操作,一个等待即是一个同步。事件可以提供一对多、多对多的同步操作。
- 一对多同步模型:一个任务等待多个事件的触发。可以是任意一个事件发生时唤醒任务处理事件,也可以是几个事件都发生后才唤醒任务处理事件。
- 多对多同步模型:多个任务等待多个事件的触发。
Huawei LiteOS提供的事件具有如下特点:
- 任务通过创建事件控制块来触发事件或等待事件。
- 事件间相互独立,内部实现为一个32位无符号整型,每一位标识一种事件类型。第25位不可用,因此最多可支持31种事件类型。
- 事件仅用于任务间的同步,不提供数据传输功能。
- 多次向事件控制块写入同一事件类型,在被清零前等效于只写入一次。
- 多个任务可以对同一事件进行读写操作。
- 支持事件读写超时机制。
事件控制块
/**
* Event control structure
*/
typedef struct tagEvent {
UINT32 uwEventID; /* 事件ID,每一位标识一种事件类型 */
LOS_DL_LIST stEventList; /* 读取事件的任务链表 */
} EVENT_CB_S, *PEVENT_CB_S;
uwEventID:用于标识该任务发生的事件类型,其中每一位表示一种事件类型(0表示该事件类型未发生、1表示该事件类型已经发生),一共31种事件类型,第25位系统保留。
事件读取模式
在读事件时,可以选择读取模式。读取模式如下:
- 所有事件(LOS_WAITMODE_AND):逻辑与,基于接口传入的事件类型掩码eventMask,只有这些事件都已经发生才能读取成功,否则该任务将阻塞等待或者返回错误码。
- 任一事件(LOS_WAITMODE_OR):逻辑或,基于接口传入的事件类型掩码eventMask,只要这些事件中有任一种事件发生就可以读取成功,否则该任务将阻塞等待或者返回错误码。
- 清除事件(LOS_WAITMODE_CLR):这是一种附加读取模式,需要与所有事件模式或任一事件模式结合使用(LOS_WAITMODE_AND | LOS_WAITMODE_CLR或 LOS_WAITMODE_OR | LOS_WAITMODE_CLR)。在这种模式下,当设置的所有事件模式或任一事件模式读取成功后,会自动清除事件控制块中对应的事件类型位。
任务在调用LOS_EventRead接口读事件时,可以根据入参事件掩码类型eventMask读取事件的单个或者多个事件类型。事件读取成功后,如果设置LOS_WAITMODE_CLR会清除已读取到的事件类型,反之不会清除已读到的事件类型,需显式清除。可以通过入参选择读取模式,读取事件掩码类型中所有事件还是读取事件掩码类型中任意事件。
任务在调用LOS_EventWrite接口写事件时,对指定事件控制块写入指定的事件类型,可以一次同时写多个事件类型。写事件会触发任务调度。
任务在调用LOS_EventClear接口清除事件时,根据入参事件和待清除的事件类型,对事件对应位进行清0操作。
事件可应用于多种任务同步场景,在某些同步场景下可替代信号量。
Huawei LiteOS 的事件模块为用户提供下面几种功能,接口详细信息可以查看API参考。
对存在失败可能性的操作返回对应的错误码,以便快速定位错误原因。
须知: 错误码定义见错误码简介。8~15位的所属模块为事件模块,值为0x1c。
使用事件模块的典型流程如下:
-
通过make menuconfig配置事件。
-
调用事件初始化LOS_EventInit接口,初始化事件等待队列。
-
写事件LOS_EventWrite,写入指定的事件类型。
-
读事件LOS_EventRead,可以选择读取模式。
-
清除事件LOS_EventClear,清除指定的事件类型。
无。
- 在系统初始化之前不能调用读写事件接口。如果调用,系统运行会不正常。
- 在中断中,可以对事件对象进行写操作,但不能进行读操作。
- 在锁任务调度状态下,禁止任务阻塞于读事件。
- LOS_EventClear 入参值是:要清除的指定事件类型的反码(~events)。
- 为了区别LOS_EventRead接口返回的是事件还是错误码,事件掩码的第25位不能使用。
示例中,任务Example_TaskEntry创建一个任务Example_Event,Example_Event读事件阻塞,Example_TaskEntry向该任务写事件。可以通过示例日志中打印的先后顺序理解事件操作时伴随的任务切换。
- 在任务Example_TaskEntry创建任务Example_Event,其中任务Example_Event优先级高于Example_TaskEntry。
- 在任务Example_Event中读事件0x00000001,阻塞,发生任务切换,执行任务Example_TaskEntry。
- 在任务Example_TaskEntry向任务Example_Event写事件0x00000001,发生任务切换,执行任务Example_Event。
- Example_Event得以执行,直到任务结束。
- Example_TaskEntry得以执行,直到任务结束。
前提条件:在menuconfig菜单中完成事件模块的配置。
代码实现如下:
#include "los_event.h"
#include "los_task.h"
#include "securec.h"
/* 任务ID */
UINT32 g_testTaskId;
/* 事件控制结构体 */
EVENT_CB_S g_exampleEvent;
/* 等待的事件类型 */
#define EVENT_WAIT 0x00000001
/* 用例任务入口函数 */
VOID Example_Event(VOID)
{
UINT32 ret;
UINT32 event;
/* 超时等待方式读事件,超时时间为100 ticks, 若100 ticks后未读取到指定事件,读事件超时,任务直接唤醒 */
printf("Example_Event wait event 0x%x \n", EVENT_WAIT);
event = LOS_EventRead(&g_exampleEvent, EVENT_WAIT, LOS_WAITMODE_AND, 100);
if (event == EVENT_WAIT) {
printf("Example_Event,read event :0x%x\n", event);
} else {
printf("Example_Event,read event timeout\n");
}
}
UINT32 Example_TaskEntry(VOID)
{
UINT32 ret;
TSK_INIT_PARAM_S task1;
/* 事件初始化 */
ret = LOS_EventInit(&g_exampleEvent);
if (ret != LOS_OK) {
printf("init event failed .\n");
return -1;
}
/* 创建任务 */
(VOID)memset_s(&task1, sizeof(TSK_INIT_PARAM_S), 0, sizeof(TSK_INIT_PARAM_S));
task1.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_Event;
task1.pcName = "EventTsk1";
task1.uwStackSize = OS_TSK_DEFAULT_STACK_SIZE;
task1.usTaskPrio = 5;
ret = LOS_TaskCreate(&g_testTaskId, &task1);
if (ret != LOS_OK) {
printf("task create failed .\n");
return LOS_NOK;
}
/* 写g_testTaskId 等待事件 */
printf("Example_TaskEntry write event .\n");
ret = LOS_EventWrite(&g_exampleEvent, EVENT_WAIT);
if (ret != LOS_OK) {
printf("event write failed .\n");
return LOS_NOK;
}
/* 清标志位 */
printf("EventMask:%d\n", g_exampleEvent.uwEventID);
LOS_EventClear(&g_exampleEvent, ~g_exampleEvent.uwEventID);
printf("EventMask:%d\n", g_exampleEvent.uwEventID);
/* 删除任务 */
ret = LOS_TaskDelete(g_testTaskId);
if (ret != LOS_OK) {
printf("task delete failed .\n");
return LOS_NOK;
}
return LOS_OK;
}
编译运行得到的结果为:
Example_Event wait event 0x1
Example_TaskEntry write event .
Example_Event,read event :0x1
EventMask:1
EventMask:0
信号量(Semaphore)是一种实现任务间通信的机制,可以实现任务间同步或共享资源的互斥访问。
一个信号量的数据结构中,通常有一个计数值,用于对有效资源数的计数,表示剩下的可被使用的共享资源数,其值的含义分两种情况:
- 0,表示该信号量当前不可获取,因此可能存在正在等待该信号量的任务。
- 正值,表示该信号量当前可被获取。
以同步为目的的信号量和以互斥为目的的信号量在使用上有如下不同:
- 用作互斥时,初始信号量计数值不为0,表示可用的共享资源个数。在需要使用共享资源前,先获取信号量,然后使用一个共享资源,使用完毕后释放信号量。这样在共享资源被取完,即信号量计数减至0时,其他需要获取信号量的任务将被阻塞,从而保证了共享资源的互斥访问。另外,当共享资源数为1时,建议使用二值信号量,一种类似于互斥锁的机制。
- 用作同步时,初始信号量计数值为0。任务1获取信号量而阻塞,直到任务2或者某中断释放信号量,任务1才得以进入Ready或Running态,从而达到了任务间的同步。
信号量控制块
/**
* Semaphore control structure.
*/
typedef struct {
UINT8 semStat; /* 是否使用标志位 */
UINT8 semType; /* 信号量类型 */
UINT16 semCount; /* 信号量计数 */
UINT32 semId; /* 信号量索引号 */
LOS_DL_LIST semList; /* 挂接阻塞于该信号量的任务 */
} LosSemCB;
信号量运作原理
信号量初始化,为配置的N个信号量申请内存(N值可以由用户自行配置,通过LOSCFG_BASE_IPC_SEM_LIMIT宏实现),并把所有信号量初始化成未使用,加入到未使用链表中供系统使用。
信号量创建,从未使用的信号量链表中获取一个信号量,并设定初值。
信号量申请,若其计数器值大于0,则直接减1返回成功。否则任务阻塞,等待其它任务释放该信号量,等待的超时时间可设定。当任务被一个信号量阻塞时,将该任务挂到信号量等待任务队列的队尾。
信号量释放,若没有任务等待该信号量,则直接将计数器加1返回。否则唤醒该信号量等待任务队列上的第一个任务。
信号量删除,将正在使用的信号量置为未使用信号量,并挂回到未使用链表。
信号量允许多个任务在同一时刻访问共享资源,但会限制同一时刻访问此资源的最大任务数目。当访问资源的任务数达到该资源允许的最大数量时,会阻塞其他试图获取该资源的任务,直到有任务释放该信号量。
在多任务系统中,信号量是一种非常灵活的同步方式,可以运用在多种场合中,实现锁、同步、资源计数等功能,也能方便的用于任务与任务,中断与任务的同步中。信号量常用于协助一组相互竞争的任务访问共享资源。
Huawei LiteOS 的信号量模块为用户提供下面几种功能,接口详细信息可以查看API参考。
说明: 信号量有三种申请模式:无阻塞模式、永久阻塞模式、定时阻塞模式。
- 无阻塞模式:即任务申请信号量时,入参timeout等于0。若当前信号量计数值不为0,则申请成功,否则立即返回申请失败。
- 永久阻塞模式:即任务申请信号量时,入参timeout等于0xFFFFFFFF。若当前信号量计数值不为0,则申请成功。否则该任务进入阻塞态,系统切换到就绪任务中优先级最高者继续执行。任务进入阻塞态后,直到有其他任务释放该信号量,阻塞任务才会重新得以执行。
- 定时阻塞模式:即任务申请信号量时,0<timeout<0xFFFFFFFF。若当前信号量计数值不为0,则申请成功。否则,该任务进入阻塞态,系统切换到就绪任务中优先级最高者继续执行。任务进入阻塞态后,超时前如果有其他任务释放该信号量,则该任务可成功获取信号量继续执行,若超时前未获取到信号量,接口将返回超时错误码。
对存在失败可能性的操作返回对应的错误码,以便快速定位错误原因。
调整OS_SYS_MEM_SIZE以确保有足够的内存供信号量使用,或减小系统支持的最大信号量数LOSCFG_BASE_IPC_SEM_LIMIT |
||||
须知: 错误码定义见错误码简介。8~15位的所属模块为信号量模块,值为0x07。
信号量的开发典型流程:
-
通过make menuconfig配置信号量模块。
-
创建信号量LOS_SemCreate,若要创建二值信号量则调用LOS_BinarySemCreate。
-
申请信号量LOS_SemPend。
-
释放信号量LOS_SemPost。
-
删除信号量LOS_SemDelete。
无。
由于中断不能被阻塞,因此不能在中断中使用阻塞模式申请信号量。
本实例实现如下功能:
- 测试任务Example_TaskEntry创建一个信号量,锁任务调度,创建两个任务Example_SemTask1、Example_SemTask2,Example_SemTask2优先级高于Example_SemTask1,两个任务中申请同一信号量,解锁任务调度后两任务阻塞,测试任务Example_TaskEntry释放信号量。
- Example_SemTask2得到信号量,被调度,然后任务休眠20Tick,Example_SemTask2延迟,Example_SemTask1被唤醒。
- Example_SemTask1定时阻塞模式申请信号量,等待时间为10Tick,因信号量仍被Example_SemTask2持有,Example_SemTask1挂起,10Tick后仍未得到信号量,Example_SemTask1被唤醒,试图以永久阻塞模式申请信号量,Example_SemTask1挂起。
- 20Tick后Example_SemTask2唤醒, 释放信号量后,Example_SemTask1得到信号量被调度运行,最后释放信号量。
- Example_SemTask1执行完,40Tick后任务Example_TaskEntry被唤醒,执行删除信号量,删除两个任务。
前提条件:在menuconfig菜单中完成信号量的配置。
代码实现如下:
#include "los_sem.h"
#include "securec.h"
/* 任务ID */
static UINT32 g_testTaskId01;
static UINT32 g_testTaskId02;
/* 测试任务优先级 */
#define TASK_PRIO_TEST 5
/* 信号量结构体id */
static UINT32 g_semId;
VOID Example_SemTask1(VOID)
{
UINT32 ret;
printf("Example_SemTask1 try get sem g_semId ,timeout 10 ticks.\n");
/* 定时阻塞模式申请信号量,定时时间为10ticks */
ret = LOS_SemPend(g_semId, 10);
/* 申请到信号量 */
if (ret == LOS_OK) {
LOS_SemPost(g_semId);
return;
}
/* 定时时间到,未申请到信号量 */
if (ret == LOS_ERRNO_SEM_TIMEOUT) {
printf("Example_SemTask1 timeout and try get sem g_semId wait forever.\n");
/*永久阻塞模式申请信号量*/
ret = LOS_SemPend(g_semId, LOS_WAIT_FOREVER);
printf("Example_SemTask1 wait_forever and get sem g_semId .\n");
if (ret == LOS_OK) {
LOS_SemPost(g_semId);
return;
}
}
}
VOID Example_SemTask2(VOID)
{
UINT32 ret;
printf("Example_SemTask2 try get sem g_semId wait forever.\n");
/* 永久阻塞模式申请信号量 */
ret = LOS_SemPend(g_semId, LOS_WAIT_FOREVER);
if (ret == LOS_OK) {
printf("Example_SemTask2 get sem g_semId and then delay 20ticks .\n");
}
/* 任务休眠20 ticks */
LOS_TaskDelay(20);
printf("Example_SemTask2 post sem g_semId .\n");
/* 释放信号量 */
LOS_SemPost(g_semId);
return;
}
UINT32 ExampleTaskEntry(VOID)
{
UINT32 ret;
TSK_INIT_PARAM_S task1;
TSK_INIT_PARAM_S task2;
/* 创建信号量 */
LOS_SemCreate(0,&g_semId);
/* 锁任务调度 */
LOS_TaskLock();
/* 创建任务1 */
(VOID)memset_s(&task1, sizeof(TSK_INIT_PARAM_S), 0, sizeof(TSK_INIT_PARAM_S));
task1.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_SemTask1;
task1.pcName = "TestTsk1";
task1.uwStackSize = OS_TSK_DEFAULT_STACK_SIZE;
task1.usTaskPrio = TASK_PRIO_TEST;
ret = LOS_TaskCreate(&g_testTaskId01, &task1);
if (ret != LOS_OK) {
printf("task1 create failed .\n");
return LOS_NOK;
}
/* 创建任务2 */
(VOID)memset_s(&task2, sizeof(TSK_INIT_PARAM_S), 0, sizeof(TSK_INIT_PARAM_S));
task2.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_SemTask2;
task2.pcName = "TestTsk2";
task2.uwStackSize = OS_TSK_DEFAULT_STACK_SIZE;
task2.usTaskPrio = (TASK_PRIO_TEST - 1);
ret = LOS_TaskCreate(&g_testTaskId02, &task2);
if (ret != LOS_OK) {
printf("task2 create failed .\n");
return LOS_NOK;
}
/* 解锁任务调度 */
LOS_TaskUnlock();
ret = LOS_SemPost(g_semId);
/* 任务休眠40 ticks */
LOS_TaskDelay(40);
/* 删除信号量 */
LOS_SemDelete(g_semId);
/* 删除任务1 */
ret = LOS_TaskDelete(g_testTaskId01);
if (ret != LOS_OK) {
printf("task1 delete failed .\n");
return LOS_NOK;
}
/* 删除任务2 */
ret = LOS_TaskDelete(g_testTaskId02);
if (ret != LOS_OK) {
printf("task2 delete failed .\n");
return LOS_NOK;
}
return LOS_OK;
}
编译运行得到的结果为:
Example_SemTask2 try get sem g_semId wait forever.
Example_SemTask1 try get sem g_semId ,timeout 10 ticks.
Example_SemTask2 get sem g_semId and then delay 20ticks .
Example_SemTask1 timeout and try get sem g_semId wait forever.
Example_SemTask2 post sem g_semId .
Example_SemTask1 wait_forever and get sem g_semId .
互斥锁又称互斥型信号量,是一种特殊的二值性信号量,用于实现对临界资源的独占式处理。另外,互斥锁可以解决信号量存在的优先级翻转问题。
任意时刻互斥锁只有两种状态,开锁或闭锁。当任务持有时,这个任务获得该互斥锁的所有权,互斥锁处于闭锁状态。当该任务释放锁后,任务失去该互斥锁的所有权,互斥锁处于开锁状态。当一个任务持有互斥锁时,其他任务不能再对该互斥锁进行开锁或持有。
Huawei LiteOS提供的互斥锁具有如下特点:
- 通过优先级继承算法,解决优先级翻转问题。
- 多任务阻塞等待同一个锁的场景,支持基于任务优先级等待和FIFO两种模式。
多任务环境下会存在多个任务访问同一公共资源的场景,而有些公共资源是非共享的临界资源,只能被独占使用。互斥锁怎样来避免这种冲突呢?
用互斥锁处理临界资源的同步访问时,如果有任务访问该资源,则互斥锁为加锁状态。此时其他任务如果想访问这个临界资源则会被阻塞,直到互斥锁被持有该锁的任务释放后,其他任务才能重新访问该公共资源,此时互斥锁再次上锁,如此确保同一时刻只有一个任务正在访问这个临界资源,保证了临界资源操作的完整性。
多任务环境下往往存在多个任务竞争同一临界资源的应用场景,互斥锁可以提供任务间的互斥机制,防止两个任务在同一时刻访问相同的临界资源,从而实现独占式访问。
Huawei LiteOS 的互斥锁模块为用户提供下面几种功能,接口详细信息可以查看API参考。
说明: 申请互斥锁有三种模式:无阻塞模式、永久阻塞模式、定时阻塞模式。
- 无阻塞模式:即任务申请互斥锁时,入参timeout等于0。若当前没有任务持有该互斥锁,或者持有该互斥锁的任务和申请该互斥锁的任务为同一个任务,则申请成功,否则立即返回申请失败。
- 永久阻塞模式:即任务申请互斥锁时,入参timeout等于0xFFFFFFFF。若当前没有任务持有该互斥锁,则申请成功。否则,任务进入阻塞态,系统切换到就绪任务中优先级最高者继续执行。任务进入阻塞态后,直到有其他任务释放该互斥锁,阻塞任务才会重新得以执行。
- 定时阻塞模式:即任务申请互斥锁时,0<timeout<0xFFFFFFFF。若当前没有任务持有该互斥锁,则申请成功。否则该任务进入阻塞态,系统切换到就绪任务中优先级最高者继续执行。任务进入阻塞态后,超时前如果有其他任务释放该互斥锁,则该任务可成功获取互斥锁继续执行,若超时前未获取到该互斥锁,接口将返回超时错误码。 释放互斥锁:
- 如果有任务阻塞于该互斥锁,则唤醒被阻塞任务中优先级最高的,该任务进入就绪态,并进行任务调度。
- 如果没有任务阻塞于该互斥锁,则互斥锁释放成功。
对存在失败可能性的操作返回对应的错误码,以便快速定位错误原因。
须知: 错误码定义见错误码简介。8~15位的所属模块为互斥锁模块,值为0x1d。
互斥锁典型场景的开发流程:
-
通过make menuconfig配置互斥锁模块。
-
创建互斥锁LOS_MuxCreate。
-
申请互斥锁LOS_MuxPend。
-
释放互斥锁LOS_MuxPost。
-
删除互斥锁LOS_MuxDelete。
无。
- 互斥锁不能在中断服务程序中使用。
- Huawei LiteOS作为实时操作系统需要保证任务调度的实时性,尽量避免任务的长时间阻塞,因此在获得互斥锁之后,应该尽快释放互斥锁。
- 持有互斥锁的过程中,不得再调用LOS_TaskPriSet等接口更改持有互斥锁任务的优先级。
- 互斥锁不支持多个相同优先级任务翻转的场景。
本实例实现如下流程。
- 任务Example_TaskEntry创建一个互斥锁,锁任务调度,创建两个任务Example_MutexTask1、Example_MutexTask2。Example_MutexTask2优先级高于Example_MutexTask1,解锁任务调度,然后Example_TaskEntry任务休眠300Tick。
- Example_MutexTask2被调度,以永久阻塞模式申请互斥锁,并成功获取到该互斥锁,然后任务休眠100Tick,Example_MutexTask2挂起,Example_MutexTask1被唤醒。
- Example_MutexTask1以定时阻塞模式申请互斥锁,等待时间为10Tick,因互斥锁仍被Example_MutexTask2持有,Example_MutexTask1挂起。10Tick超时时间到达后,Example_MutexTask1被唤醒,以永久阻塞模式申请互斥锁,因互斥锁仍被Example_MutexTask2持有,Example_MutexTask1挂起。
- 100Tick休眠时间到达后,Example_MutexTask2被唤醒, 释放互斥锁,唤醒Example_MutexTask1。Example_MutexTask1成功获取到互斥锁后,释放锁。
- 300Tick休眠时间到达后,任务Example_TaskEntry被调度运行,删除互斥锁,删除两个任务。
前提条件:通过make menuconfig完成互斥锁的配置。
代码实现如下:
/* 互斥锁句柄id */
UINT32 g_testMux;
/* 任务ID */
UINT32 g_testTaskId01;
UINT32 g_testTaskId02;
VOID Example_MutexTask1(VOID)
{
UINT32 ret;
printf("task1 try to get mutex, wait 10 ticks.\n");
/* 申请互斥锁 */
ret = LOS_MuxPend(g_testMux, 10);
if (ret == LOS_OK) {
printf("task1 get mutex g_testMux.\n");
/* 释放互斥锁 */
LOS_MuxPost(g_testMux);
return;
} else if (ret == LOS_ERRNO_MUX_TIMEOUT ) {
printf("task1 timeout and try to get mutex, wait forever.\n");
/* 申请互斥锁 */
ret = LOS_MuxPend(g_testMux, LOS_WAIT_FOREVER);
if (ret == LOS_OK) {
printf("task1 wait forever, get mutex g_testMux.\n");
/* 释放互斥锁 */
LOS_MuxPost(g_testMux);
return;
}
}
return;
}
VOID Example_MutexTask2(VOID)
{
printf("task2 try to get mutex, wait forever.\n");
/* 申请互斥锁 */
(VOID)LOS_MuxPend(g_testMux, LOS_WAIT_FOREVER);
printf("task2 get mutex g_testMux and suspend 100 ticks.\n");
/* 任务休眠100Ticks */
LOS_TaskDelay(100);
printf("task2 resumed and post the g_testMux\n");
/* 释放互斥锁 */
LOS_MuxPost(g_testMux);
return;
}
UINT32 Example_TaskEntry(VOID)
{
UINT32 ret;
TSK_INIT_PARAM_S task1;
TSK_INIT_PARAM_S task2;
/* 创建互斥锁 */
LOS_MuxCreate(&g_testMux);
/* 锁任务调度 */
LOS_TaskLock();
/* 创建任务1 */
memset(&task1, 0, sizeof(TSK_INIT_PARAM_S));
task1.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_MutexTask1;
task1.pcName = "MutexTsk1";
task1.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
task1.usTaskPrio = 5;
ret = LOS_TaskCreate(&g_testTaskId01, &task1);
if (ret != LOS_OK) {
printf("task1 create failed.\n");
return LOS_NOK;
}
/* 创建任务2 */
memset(&task2, 0, sizeof(TSK_INIT_PARAM_S));
task2.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_MutexTask2;
task2.pcName = "MutexTsk2";
task2.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
task2.usTaskPrio = 4;
ret = LOS_TaskCreate(&g_testTaskId02, &task2);
if (ret != LOS_OK) {
printf("task2 create failed.\n");
return LOS_NOK;
}
/* 解锁任务调度 */
LOS_TaskUnlock();
/* 休眠300Ticks */
LOS_TaskDelay(300);
/* 删除互斥锁 */
LOS_MuxDelete(g_testMux);
/* 删除任务1 */
ret = LOS_TaskDelete(g_testTaskId01);
if (ret != LOS_OK) {
printf("task1 delete failed .\n");
return LOS_NOK;
}
/* 删除任务2 */
ret = LOS_TaskDelete(g_testTaskId02);
if (ret != LOS_OK) {
printf("task2 delete failed .\n");
return LOS_NOK;
}
return LOS_OK;
}
编译运行得到的结果为:
task2 try to get mutex, wait forever.
task2 get mutex g_testMux and suspend 100 ticks.
task1 try to get mutex, wait 10 ticks.
task1 timeout and try to get mutex, wait forever.
task2 resumed and post the g_testMux
task1 wait forever,get mutex g_testMux.
软件定时器,是基于系统Tick时钟中断且由软件来模拟的定时器。当经过设定的Tick数后,会触发用户自定义的回调函数。
硬件定时器受硬件的限制,数量上不足以满足用户的实际需求。因此为了满足用户需求,提供更多的定时器,Huawei LiteOS提供了软件定时器功能,支持如下特性:
- 创建软件定时器。
- 启动软件定时器。
- 停止软件定时器。
- 删除软件定时器。
- 获取软件定时器剩余Tick数。
- 可配置支持的软件定时器个数。
软件定时器是系统资源,在模块初始化的时候已经分配了一块连续内存。
软件定时器使用了系统的一个队列和一个任务资源,软件定时器的触发遵循队列规则,先进先出。定时时间短的定时器总是比定时时间长的靠近队列头,满足优先触发的准则。
软件定时器以Tick为基本计时单位,当创建并启动一个软件定时器时,Huawei LiteOS会根据当前系统Tick时间及设置的定时时长确定该定时器的到期Tick时间,并将该定时器控制结构挂入计时全局链表。
当Tick中断到来时,在Tick中断处理函数中扫描软件定时器的计时全局链表,检查是否有定时器超时,若有则将超时的定时器记录下来。Tick中断处理函数结束后,软件定时器任务(优先级为最高)被唤醒,在该任务中调用已经记录下来的定时器的回调函数。
定时器状态
-
OS_SWTMR_STATUS_UNUSED(定时器未使用)
系统在定时器模块初始化时,会将系统中所有定时器资源初始化成该状态。
-
OS_SWTMR_STATUS_TICKING(定时器处于计数状态)
在定时器创建后调用LOS_SwtmrStart接口启动,定时器将变成该状态,是定时器运行时的状态。
-
OS_SWTMR_STATUS_CREATED(定时器创建后未启动,或已停止)
定时器创建后,不处于计数状态时,定时器将变成该状态。
定时器模式
软件定时器提供了三类模式:
- 单次触发定时器,这类定时器在启动后只会触发一次定时器事件,然后定时器自动删除。
- 周期触发定时器,这类定时器会周期性的触发定时器事件,直到用户手动停止定时器,否则将永远持续执行下去。
- 单次触发定时器,但这类定时器超时触发后不会自动删除,需要调用定时器删除接口删除定时器。
- 创建一个单次触发的定时器,超时后执行用户自定义的回调函数。
- 创建一个周期性触发的定时器,超时后执行用户自定义的回调函数。
Huawei LiteOS的软件定时器模块为用户提供下面几种功能,接口详细信息可以查看API参考。
表 1
对存在失败可能性的操作返回对应的错误码,以便快速定位错误原因。
须知: 错误码定义见错误码简介。8~15位的所属模块为软件定时器模块,值为0x03。
软件定时器的典型开发流程:
-
通过make menuconfig配置软件定时器。
-
创建定时器LOS_SwtmrCreate,设置定时器的定时时长、定时器模式、超时后的回调函数。
-
启动定时器LOS_SwtmrStart。
-
获得软件定时器剩余Tick数LOS_SwtmrTimeGet。
-
停止定时器LOS_SwtmrStop。
-
删除定时器LOS_SwtmrDelete。
- 软件定时器的回调函数中不应执行过多操作,不建议使用可能引起任务挂起或者阻塞的接口或操作,如果使用会导致软件定时器响应不及时,造成的影响无法确定。
- 软件定时器使用了系统的一个队列和一个任务资源。软件定时器任务的优先级设定为0,且不允许修改 。
- 系统可配置的软件定时器个数是指:整个系统可使用的软件定时器总个数,并非用户可使用的软件定时器个数。例如:系统多占用一个软件定时器,那么用户能使用的软件定时器资源就会减少一个。
- 创建单次不自删除属性的定时器,用户需要自行调用定时器删除接口删除定时器,回收定时器资源,避免资源泄露。
- 软件定时器的定时精度与系统Tick时钟的周期有关。
在下面的例子中,演示如下功能:
- 软件定时器创建、启动、停止、删除操作。
- 单次软件定时器,周期软件定时器使用方法。
前提条件:在menuconfig菜单中完成软件定时器的配置。
代码实现如下:
UINT32 g_timerCount1 = 0;
UINT32 g_timerCount2 = 0;
VOID Timer1_CallBack(UINT32 arg)
{
UINT64 lastTick;
g_timerCount1++;
lastTick=(UINT32)LOS_TickCountGet();
dprintf("g_timerCount1=%d\n", g_timerCount1);
dprintf("tick_last1=%d\n", lastTick);
}
VOID Timer2_CallBack(UINT32 arg)
{
UINT64 lastTick;
lastTick=(UINT32)LOS_TickCountGet();
g_timerCount2++;
dprintf("g_timerCount2=%d\n", g_timerCount2);
dprintf("tick_last2=%d\n", lastTick);
}
VOID Timer_example(VOID)
{
UINT16 id1; // Timer1 id
UINT16 id2; // Timer2 id
UINT32 tick;
LOS_SwtmrCreate(1000, LOS_SWTMR_MODE_ONCE, Timer1_CallBack, &id1, 1);
LOS_SwtmrCreate(100, LOS_SWTMR_MODE_PERIOD, Timer2_CallBack, &id2, 1);
dprintf("create Timer1 success\n");
LOS_SwtmrStart(id1);
dprintf("start Timer1 sucess\n");
LOS_TaskDelay(200);
LOS_SwtmrTimeGet(id1, &tick);
dprintf("tick =%d\n", tick);
LOS_SwtmrStop(id1);
dprintf("stop Timer1 sucess\n");
LOS_SwtmrStart(id1);
LOS_TaskDelay(1000);
LOS_SwtmrDelete(id1);
dprintf("delete Timer1 sucess\n");
LOS_SwtmrStart(id2);
dprintf("start Timer2\n");
LOS_TaskDelay(1000);
LOS_SwtmrStop(id2);
LOS_SwtmrDelete(id2);
}
得到的结果为:
Create Timer1 success
start Timer1 sucess
tick =800
Stop Timer1 sucess
g_timerCount1=1201
tick_last1=1201
delete Timer1 success
Start Timer2
g_timerCount2=1
tick_last2=1301
g_timerCount2=2
tick_last2=1401
g_timerCount2=3
tick_last2=1501
g_timerCount2=4
tick_last2=1601
g_timerCount2=5
tick_last2=1701
g_timerCount2=6
tick_last2=1801
g_timerCount2=7
tick_last2=1901
g_timerCount2=8
tick_last2=2001
g_timerCount2=9
tick_last2=2101
g_timerCount2=10
tick_last2=2201
在多核环境中,由于使用相同的内存空间,存在对同一资源进行访问的情况,所以需要互斥访问机制来保证同一时刻只有一个核进行操作。自旋锁就是这样的一种机制。
自旋锁是指当一个线程在获取锁时,如果锁已经被其它线程获取,那么该线程将循环等待,并不断判断是否能够成功获取锁,直到获取到锁才会退出循环。因此建议保护耗时较短的操作,防止对系统整体性能有明显的影响。
自旋锁与互斥锁比较类似,它们都是为了解决对共享资源的互斥使用问题。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个持有者。但是两者在调度机制上略有不同,对于互斥锁,如果锁已经被占用,锁申请者会被阻塞;但是自旋锁不会引起调用者阻塞,会一直循环检测自旋锁是否已经被释放。
自旋锁可以提供任务之间的互斥访问机制,用来防止两个任务在同一时刻访问相同的共享资源。
Huawei LiteOS 的自旋锁模块为用户提供下面几种功能,接口详细信息可以查看API参考。
自旋锁的开发典型流程:
-
自旋锁依赖于SMP,可以通过make menuconfig配置。
-
创建自旋锁:使用LOS_SpinInit初始化自旋锁,或者使用SPIN_LOCK_INIT初始化静态内存的自旋锁。
-
申请自旋锁:使用接口LOS_SpinLock/LOS_SpinTrylock/LOS_SpinLockSave申请指定的自旋锁,申请成功就继续往后执行锁保护的代码;申请失败在自旋锁申请中忙等,直到申请到自旋锁为止。
-
释放自旋锁:使用LOS_SpinUnlock/LOS_SpinUnlockRestore接口释放自旋锁。锁保护代码执行完毕后,释放对应的自旋锁,以便其他核申请自旋锁。
- 同一个任务不能对同一把自旋锁进行多次加锁,否则会导致死锁。
- 自旋锁中会执行本核的锁任务操作,因此需要等到最外层完成解锁后本核才会进行任务调度。
- LOS_SpinLock与LOS_SpinUnlock允许单独使用,即可以不进行关中断,但是用户需要保证使用的接口只会在任务或中断中使用。如果接口同时会在任务和中断中被调用,请使用LOS_SpinLockSave与LOS_SpinUnlockRestore,因为在未关中断的情况下使用LOS_SpinLock可能会导致死锁。
- 耗时的操作谨慎选用自旋锁,可使用互斥锁进行保护。
- 未开启SMP的单核场景下,自旋锁功能无效,只有LOS_SpinLockSave与LOS_SpinUnlockRestore接口有关闭恢复中断功能。
- 建议LOS_SpinLock和LOS_SpinUnlock,LOS_SpinLockSave和LOS_SpinUnlockRestore配对使用,避免出错。
本实例实现如下流程。
- 任务Example_TaskEntry初始化自旋锁,创建两个任务Example_SpinTask1、Example_SpinTask2,分别运行于两个核。
- Example_SpinTask1、Example_SpinTask2中均执行申请自旋锁的操作,同时为了模拟实际操作,在持有自旋锁后进行延迟操作,最后释放自旋锁。
- 300Tick后任务Example_TaskEntry被调度运行,删除任务Example_SpinTask1和Example_SpinTask2。
前提条件:在menuconfig中,将LOSCFG_KERNEL_SMP配置项打开,并设置多核core数量。
代码实现如下:
#include "los_spinlock.h"
#include "los_task.h"
/* 自旋锁句柄id */
SPIN_LOCK_S g_testSpinlock;
/* 任务ID */
UINT32 g_testTaskId01;
UINT32 g_testTaskId02;
VOID Example_SpinTask1(VOID)
{
UINT32 i;
UINTPTR intSave;
/* 申请自旋锁 */
dprintf("task1 try to get spinlock\n");
LOS_SpinLockSave(&g_testSpinlock, &intSave);
dprintf("task1 got spinlock\n");
for(i = 0; i < 5000; i++) {
asm volatile("nop");
}
/* 释放自旋锁 */
dprintf("task1 release spinlock\n");
LOS_SpinUnlockRestore(&g_testSpinlock, intSave);
return;
}
VOID Example_SpinTask2(VOID)
{
UINT32 i;
UINTPTR intSave;
/* 申请自旋锁 */
dprintf("task2 try to get spinlock\n");
LOS_SpinLockSave(&g_testSpinlock, &intSave);
dprintf("task2 got spinlock\n");
for(i = 0; i < 5000; i++) {
asm volatile("nop");
}
/* 释放自旋锁 */
dprintf("task2 release spinlock\n");
LOS_SpinUnlockRestore(&g_testSpinlock, intSave);
return;
}
UINT32 Example_TaskEntry(VOID)
{
UINT32 ret;
TSK_INIT_PARAM_S stTask1;
TSK_INIT_PARAM_S stTask2;
/* 初始化自旋锁 */
LOS_SpinInit(&g_testSpinlock);
/* 创建任务1 */
memset(&stTask1, 0, sizeof(TSK_INIT_PARAM_S));
stTask1.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_SpinTask1;
stTask1.pcName = "SpinTsk1";
stTask1.uwStackSize = LOSCFG_TASK_MIN_STACK_SIZE;
stTask1.usTaskPrio = 5;
#ifdef LOSCFG_KERNEL_SMP
/* 绑定任务到CPU0运行 */
stTask1.usCpuAffiMask = CPUID_TO_AFFI_MASK(0);
#endif
ret = LOS_TaskCreate(&g_testTaskId01, &stTask1);
if(ret != LOS_OK) {
dprintf("task1 create failed .\n");
return LOS_NOK;
}
/* 创建任务2 */
memset(&stTask2, 0, sizeof(TSK_INIT_PARAM_S));
stTask2.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_SpinTask2;
stTask2.pcName = "SpinTsk2";
stTask2.uwStackSize = LOSCFG_TASK_MIN_STACK_SIZE;
stTask2.usTaskPrio = 5;
#ifdef LOSCFG_KERNEL_SMP
/* 绑定任务到CPU1运行 */
stTask1.usCpuAffiMask = CPUID_TO_AFFI_MASK(1);
#endif
ret = LOS_TaskCreate(&g_testTaskId02, &stTask2);
if(ret != LOS_OK) {
dprintf("task2 create failed .\n");
return LOS_NOK;
}
/* 任务休眠300Ticks */
LOS_TaskDelay(300);
/* 删除任务1 */
ret = LOS_TaskDelete(g_testTaskId01);
if(ret != LOS_OK) {
dprintf("task1 delete failed .\n");
return LOS_NOK;
}
/* 删除任务2 */
ret = LOS_TaskDelete(g_testTaskId02);
if(ret != LOS_OK) {
dprintf("task2 delete failed .\n");
return LOS_NOK;
}
return LOS_OK;
}
编译运行得到的结果为:
task2 try to get spinlock
task2 got spinlock
task1 try to get spinlock
task2 release spinlock
task1 got spinlock
task1 release spinlock
C++作为目前使用最广泛的编程语言之一,支持类、封装、重载等特性,是在C语言基础上开发的一种面向对象的编程语言。
STL(Standard Template Library)标准模板库,是一些“容器”的集合,也是算法和其他一些组件的集合。其目的是标准化组件,使用标准化组件后可以不用重新开发,直接使用现成的组件。
- 第一个参数:init_array段的起始地址。
- 第二个参数:init_array段的结束地址。
- 第三个参数:标记调用C++特性时的场景。
-
通过make menuconfig使能C++支持。
-
使用C++特性之前,需要调用函数LOS_CppSystemInit,初始化C++构造函数。
-
C函数与C++函数混合调用。
在C++中调用C程序的函数,代码需加入C++包含的宏:
#ifdef __cplusplus #if __cplusplus extern "C" { #endif /* __cplusplus */ #endif /* __cplusplus */ /* code */ ... #ifdef __cplusplus #if __cplusplus } #endif /* __cplusplus */ #endif /* __cplusplus */
Huawei LiteOS的C++功能需要编译器适配才能支持,编译器编译链接C++代码时需要使用Huawei LiteOS提供的C库。
本实例实现了在Huawei LiteOS上运行C++的代码。
- 编写C++代码。
- 在运行C++代码之前,先在app_init函数里以NO_SCATTER参数调用LOS_CppSystemInit初始化C++构造函数。
- 在LOS_CppSystemInit之后调用编写好的C++代码。
前提条件:通过make menuconfig使能C++支持。
C++代码实现如下:
#ifdef __cplusplus
#if __cplusplus
extern "C" {
#endif /* __cplusplus */
#endif /* __cplusplus */
using namespace std;
class TestClass {
public:
TestClass(int arg);
~TestClass(void);
void PrintTest(void);
void StringTest(void);
void MapTest(void);
private:
int intTest;
string stringTest;
map<string, int> mapTest;
};
TestClass::TestClass(int arg)
{
cout << "TestClass is constructed here, arg = " << arg << endl;
intTest = arg;
}
TestClass::~TestClass(void)
{
cout << "TestClass is destructed" << endl;
}
void TestClass::PrintTest(void)
{
cout << __FUNCTION__ << " enter" << endl;
cout << " intTest = " << this->intTest << endl;
}
void TestClass::StringTest(void)
{
cout << __FUNCTION__ << " enter" << endl;
string a("Lite");
string b("OS");
string c("LiteOS");
if (a != b) {
cout << " " << a << " != " << b << endl;
}
a += b;
if (a == c) {
cout << " " << a << " == " << c << endl;
}
}
void TestClass::MapTest(void)
{
cout << __FUNCTION__ << " enter" << endl;
mapTest.insert(pair<string, int>("Huawei", 1));
mapTest.insert(pair<string, int>("LiteOS", 2));
mapTest.insert(pair<string, int>("Open", 3));
mapTest.insert(pair<string, int>("Source", 4));
cout << " show map key&value" << endl;
for (auto &it : mapTest) {
cout << " " << it.first << " " << it.second << endl;
}
mapTest["LiteOS"] = 8; /* 8: new value */
cout << " change value of \"LiteOS\" key" << endl;
for (auto &it : mapTest) {
cout << " " << it.first << " " << it.second << endl;
}
}
void CppTestEntry(void)
{
cout << "LiteOS cpp sample start" << endl;
TestClass test(123);
test.PrintTest();
test.StringTest();
test.MapTest();
cout << "LiteOS cpp sample stop" << endl;
}
#ifdef __cplusplus
#if __cplusplus
}
#endif /* __cplusplus */
#endif /* __cplusplus */
app_init中初始化C++构造函数后,调用C++代码,实现如下:
void app_init(void)
{
......
/* 初始化C++构造函数 */
LOS_CppSystemInit((UINT32)&__init_array_start__, (UINT32)&__init_array_end__, NO_SCATTER);
/* 调用C++代码 */
CppTestEntry();
......
}
运行CppTestEntry()函数,结果如下:
LiteOS cpp sample start
TestClass is constructed here, arg = 123
PrintTest enter
intTest = 123
StringTest enter
Lite != OS
LiteOS == LiteOS
MapTest enter
show map key&value
Huawei 1
LiteOS 2
Open 3
Source 4
change value of "LiteOS" key
Huawei 1
LiteOS 8
Open 3
Source 4
LiteOS cpp sample stop
TestClass is destructed
时间管理以系统时钟为基础,给应用程序提供所有和时间有关的服务。
系统时钟是由定时器/计数器产生的输出脉冲触发中断产生的,一般定义为整数或长整数。输出脉冲的周期叫做一个“时钟滴答”。系统时钟也称为时标或者Tick。
用户以秒、毫秒为单位计时,而操作系统以Tick为单位计时,当用户需要对系统进行操作时,例如任务挂起、延时等,此时需要时间管理模块对Tick和秒/毫秒进行转换。
Huawei LiteOS的时间管理模块提供时间转换、统计、延迟功能。
-
Cycle
系统最小的计时单位。Cycle的时长由系统主时钟频率决定,系统主时钟频率就是每秒钟的Cycle数。
-
Tick
Tick是操作系统的基本时间单位,由用户配置的每秒Tick数决定。
用户需要了解当前系统运行的时间以及Tick与秒、毫秒之间的转换关系等。
Huawei LiteOS的时间管理提供下面几种功能,接口详细信息可以查看API参考。
时间转换存在出错的可能性,需要返回对应的错误码,以便快速定位错误原因。
在板级配置适配时配置有效的系统主时钟频率OS_SYS_CLOCK,通过make menuconfig配置有效的LOSCFG_BASE_CORE_TICK_PER_SECOND |
||||
时间管理的典型开发流程:
-
根据实际需求,在板级配置适配时确认是否使能LOSCFG_BASE_CORE_TICK_HW_TIME宏选择外部定时器,并配置系统主时钟频率OS_SYS_CLOCK(单位Hz)。OS_SYS_CLOCK的默认值基于硬件平台配置。
-
通过make menuconfig配置LOSCFG_BASE_CORE_TICK_PER_SECOND。
-
调用时钟转换/统计接口。
- 时间管理不是单独的功能模块,依赖于OS_SYS_CLOCK和LOSCFG_BASE_CORE_TICK_PER_SECOND两个配置选项。
- 系统的Tick数在关中断的情况下不进行计数,故系统Tick数不能作为准确时间使用。
在下面的例子中,介绍了时间管理的基本方法,包括:
- 时间转换:将毫秒数转换为Tick数,或将Tick数转换为毫秒数。
- 时间统计:每Tick的Cycle数、自系统启动以来的Tick数和延迟后的Tick数。
前提条件:
- 使用每秒的Tick数LOSCFG_BASE_CORE_TICK_PER_SECOND的默认值100。
- 配好OS_SYS_CLOCK系统主时钟频率。
时间转换:
VOID Example_TransformTime(VOID)
{
UINT32 ms;
UINT32 tick;
tick = LOS_MS2Tick(10000); // 10000ms转换为tick
dprintf("tick = %d \n",tick);
ms = LOS_Tick2MS(100); // 100tick转换为ms
dprintf("ms = %d \n",ms);
}
时间统计和时间延迟:
VOID Example_GetTime(VOID)
{
UINT32 cyclePerTick;
UINT64 tickCount;
cyclePerTick = LOS_CyclePerTickGet();
if(0 != cyclePerTick) {
dprintf("LOS_CyclePerTickGet = %d \n", cyclePerTick);
}
tickCount = LOS_TickCountGet();
if(0 != tickCount) {
dprintf("LOS_TickCountGet = %d \n", (UINT32)tickCount);
}
LOS_TaskDelay(200);
tickCount = LOS_TickCountGet();
if(0 != tickCount) {
dprintf("LOS_TickCountGet after delay = %d \n", (UINT32)tickCount);
}
}
编译运行得到的结果为:
时间转换:
tick = 1000
ms = 1000
时间统计和时间延迟:
LOS_CyclePerTickGet = 495000
LOS_TickCountGet = 1
LOS_TickCountGet after delay = 201
双向链表是指含有往前和往后两个方向的链表,即每个结点中除存放下一个节点指针外,还增加一个指向前一个节点的指针。其头指针head是唯一确定的。
从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点,这种数据结构形式使得双向链表在查找时更加方便,特别是大量数据的遍历。由于双向链表具有对称性,能方便地完成各种插入、删除等操作,但需要注意前后方向的操作。
Huawei LiteOS的双向链表模块为用户提供下面几种功能,接口详细信息可以查看API参考。
获取包含链表的结构体地址,接口的第一个入参表示的是链表中的某个节点,第二个入参是要获取的结构体名称,第三个入参是链表在该结构体中的名称 |
||
双向链表的典型开发流程:
- 调用LOS_ListInit/LOS_DL_LIST_HEAD初始双向链表。
- 调用LOS_ListAdd/LOS_ListHeadInsert向链表头部插入节点。
- 调用LOS_ListTailInsert向链表尾部插入节点。
- 调用LOS_ListDelete删除指定节点。
- 调用LOS_ListEmpty判断链表是否为空。
- 调用LOS_ListDelInit删除指定节点并以此节点初始化链表。
- 需要注意节点指针前后方向的操作。
- 链表操作接口,为底层接口,不对入参进行判空,需要使用者确保传参合法。
- 如果链表节点的内存是动态申请的,删除节点时,要注意释放内存。
本实例实现如下功能:
- 初始化双向链表。
- 增加节点。
- 删除节点。
- 测试操作是否成功。
代码实现如下:
#include "stdio.h"
#include "los_list.h"
#ifdef __cplusplus
#if __cplusplus
extern "C" {
#endif /* __cpluscplus */
#endif /* __cpluscplus */
static UINT32 DLlist_sample(VOID)
{
LOS_DL_LIST DLlist = {NULL,NULL};
LOS_DL_LIST DLlistNode01 = {NULL,NULL};
LOS_DL_LIST DLlistNode02 = {NULL,NULL};
LOS_DL_LIST DLlistNode03 = {NULL,NULL};
dprintf("Initial head\n");
LOS_ListInit(&DLlist);
LOS_ListAdd(&DLlist, &DLlistNode01);
if (DLlistNode01.pstNext == &DLlist && DLlistNode01.pstPrev == &DLlist) {
dprintf("Add DLlistNode01 success \n");
}
LOS_ListTailInsert(&DLlist, &DLlistNode02);
if (DLlistNode02.pstNext == &DLlist && DLlistNode02.pstPrev == &DLlistNode01) {
dprintf("Tail insert DLlistNode02 success \n");
}
LOS_ListHeadInsert(&DLlistNode02, &DLlistNode03);
if (DLlistNode03.pstNext == &DLlist && DLlistNode03.pstPrev == &DLlistNode02) {
dprintf("Head insert DLlistNode03 success \n");
}
LOS_ListDelInit(&DLlistNode03);
LOS_ListDelete(&DLlistNode01);
LOS_ListDelete(&DLlistNode02);
if (LOS_ListEmpty(&DLlist)) {
dprintf("Delete success \n");
}
return LOS_OK;
}
#ifdef __cplusplus
#if __cplusplus
}
#endif /* __cpluscplus */
#endif /* __cpluscplus */
编译运行得到的结果为:
Initial head
Add DLlistNode01 success
Tail insert DLlistNode02 success
Head insert DLlistNode03 success
Delete success
在支持多任务的操作系统中,修改一块内存区域的数据需要“读取-修改-写入”三个步骤。然而同一内存区域的数据可能同时被多个任务访问,如果在修改数据的过程中被其他任务打断,就会造成该操作的执行结果无法预知。
使用开关中断的方法固然可以保证多任务执行结果符合预期,但这种方法显然会影响系统性能。
ARMv6架构引入了LDREX和STREX指令,以支持对共享存储器更缜密的非阻塞同步。由此实现的原子操作能确保对同一数据的“读取-修改-写入”操作在它的执行期间不会被打断,即操作的原子性。
Huawei LiteOS通过对ARMv6架构中的LDREX和STREX进行封装,向用户提供了一套原子操作接口。
-
LDREX Rx, [Ry]
读取内存中的值,并标记对该段内存为独占访问:
- 读取寄存器Ry指向的4字节内存数据,保存到Rx寄存器中。
- 对Ry指向的内存区域添加独占访问标记。
-
STREX Rf, Rx, [Ry]
检查内存是否有独占访问标记,如果有则更新内存值并清空标记,否则不更新内存:
-
有独占访问标记
- 将寄存器Rx中的值更新到寄存器Ry指向的内存。
- 标志寄存器Rf置为0。
-
没有独占访问标记
- 不更新内存。
- 标志寄存器Rf置为1。
-
-
判断标志寄存器
- 标志寄存器为0时,退出循环,原子操作结束。
- 标志寄存器为1时,继续循环,重新进行原子操作。
有多个任务对同一个内存数据进行加减或交换操作时,使用原子操作保证结果的可预知性。
Huawei LiteOS的原子数据包含两种类型Atomic(有符号32位数)与 Atomic64(有符号64位数)。原子操作模块为用户提供下面几种功能,接口详细信息可以查看API参考。
表 1 Atomic原子操作功能列表
表 2 Atomic64原子操作功能列表
无。
目前原子操作接口只支持整型数据。
调用原子操作相关接口,观察结果:
-
创建两个任务
- 任务一用LOS_AtomicInc对全局变量加100次。
- 任务二用LOS_AtomicDec对全局变量减100次。
-
子任务结束后在主任务中打印全局变量的值。
#include "los_hwi.h"
#include "los_atomic.h"
#include "los_task.h"
UINT32 g_testTaskId01;
UINT32 g_testTaskId02;
Atomic g_sum;
Atomic g_count;
UINT32 Example_Atomic01(VOID)
{
int i = 0;
for(i = 0; i < 100; ++i) {
LOS_AtomicInc(&g_sum);
}
LOS_AtomicInc(&g_count);
return LOS_OK;
}
UINT32 Example_Atomic02(VOID)
{
int i = 0;
for(i = 0; i < 100; ++i) {
LOS_AtomicDec(&g_sum);
}
LOS_AtomicInc(&g_count);
return LOS_OK;
}
UINT32 Example_TaskEntry(VOID)
{
TSK_INIT_PARAM_S stTask1={0};
stTask1.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_Atomic01;
stTask1.pcName = "TestAtomicTsk1";
stTask1.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
stTask1.usTaskPrio = 4;
stTask1.uwResved = LOS_TASK_STATUS_DETACHED;
TSK_INIT_PARAM_S stTask2={0};
stTask2.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_Atomic02;
stTask2.pcName = "TestAtomicTsk2";
stTask2.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
stTask2.usTaskPrio = 4;
stTask2.uwResved = LOS_TASK_STATUS_DETACHED;
LOS_TaskLock();
LOS_TaskCreate(&g_testTaskId01, &stTask1);
LOS_TaskCreate(&g_testTaskId02, &stTask2);
LOS_TaskUnlock();
while(LOS_AtomicRead(&g_count) != 2);
dprintf("g_sum = %d\n", g_sum);
return LOS_OK;
}
g_sum = 0
位操作是指对二进制数的bit位进行操作。程序可以设置某一变量为状态字,状态字中的每一bit位(标志位)可以具有自定义的含义。
系统提供标志位的置1和清0操作,可以改变标志位的内容,同时还提供获取状态字中标志位为1的最高位和最低位的功能。用户也可以对系统的寄存器进行位操作。
Huawei LiteOS的位操作模块为用户提供下面几种功能,接口详细信息可以查看API参考。
无。
对数据实现位操作,本实例实现如下功能:
- 某一标志位置1。
- 获取标志位为1的最高bit位。
- 某一标志位清0。
- 获取标志位为1的最低bit位。
#include "los_bitmap.h"
#include "los_printf.h"
static UINT32 Bit_Sample(VOID)
{
UINT32 flag = 0x10101010;
UINT16 pos;
dprintf("\nBitmap Sample!\n");
dprintf("The flag is 0x%8x\n", flag);
pos = 8;
LOS_BitmapSet(&flag, pos);
dprintf("LOS_BitmapSet:\t pos : %d, the flag is 0x%0+8x\n", pos, flag);
pos = LOS_HighBitGet(flag);
dprintf("LOS_HighBitGet:\t The highest one bit is %d, the flag is 0x%0+8x\n", pos, flag);
LOS_BitmapClr(&flag, pos);
dprintf("LOS_BitmapClr:\t pos : %d, the flag is 0x%0+8x\n", pos, flag);
pos = LOS_LowBitGet(flag);
dprintf("LOS_LowBitGet:\t The lowest one bit is %d, the flag is 0x%0+8x\n\n", pos, flag);
return LOS_OK;
}
编译运行得到结果:
Bitmap Sample!
The flag is 0x10101010
LOS_BitmapSet: pos : 8, the flag is 0x10101110
LOS_HighBitGet:The highest one bit is 28, the flag is 0x10101110
LOS_BitmapClr: pos : 28, the flag is 0x00101110
LOS_LowBitGet: The lowest one bit is 4, the flag is 0x00101110