OpenHarmony - 小型系统内核(LiteOS-A)(二)
三、基础内核
3.1、中断及异常处理
基本概念
中断是指出现需要时,CPU暂停执行当前程序,转而执行新程序的过程。即在程序运行过程中,出现了一个必须由CPU立即处理的事务,此时CPU暂时中止当前程序的执行转而处理这个事务,这个过程就叫做中断。通过中断机制,可以使CPU避免把大量时间耗费在等待、查询外设状态的操作上,大大提高系统实时性以及执行效率。
目前的中断支持有:
- 中断初始化
- 中断创建
- 开/关中断
- 恢复中断
- 删除中断
异常处理是操作系统对运行期间发生的异常情况(芯片硬件异常)进行处理的一系列动作,例如虚拟内存缺页异常、打印异常发生时函数的调用栈信息、CPU现场信息、任务的堆栈情况等。
运行机制
外设可以在没有CPU介入的情况下完成一定的工作,但某些情况下也需要CPU为其执行一定的工作。通过中断机制,在外设不需要CPU介入时,CPU可以执行其它任务,而当外设需要CPU时,产生一个中断信号,该信号连接至中断控制器。
中断控制器一方面接收其它外设中断引脚的输入,另一方面会发出中断信号给CPU。可以通过对中断控制器编程来打开和关闭中断源、设置中断源的优先级和触发方式。常用的中断控制器有VIC(Vector Interrupt Controller)和GIC(General Interrupt Controller)。在ARM Cortex-A7中使用的中断控制器是GIC。
CPU收到中断控制器发送的中断信号后,中断当前任务来响应中断请求。
异常指可以打断CPU正常运行流程的一些事情,如未定义指令异常、试图修改只读的数据异常、不对齐的地址访问异常等。当异常发生时,CPU暂停当前的程序,先处理异常事件,然后再继续执行被异常打断的程序。
以ARMv7-a架构为例,中断和异常处理的入口为中断向量表,中断向量表包含各个中断和异常处理的入口函数。
图1 中断向量表
开发指导
接口说明
异常处理为内部机制,不对外提供接口,中断模块提供对外接口如下:
创建删除中断
接口名 | 接口描述 |
---|---|
LOS_HwiCreate | 中断创建,注册中断号、中断触发模式、中断优先级、中断处理程序。中断被触发时,会调用该中断处理程序 |
LOS_HwiDelete | 根据所提供的中断号删除中断 |
开/关中断
接口名 | 接口描述 |
---|---|
LOS_IntUnlock | 打开当前处理器所有中断响应 |
LOS_IntLock | 关闭当前处理器所有中断响应 |
LOS_IntRestore | 与LOS_IntLock配套使用,恢复到使用LOS_IntLock关闭所有中断之前的状态 |
获取系统中断信息
接口名 | 接口描述 |
---|---|
LOS_GetSystemHwiMaximum | 获取系统支持的最大中断数 |
开发流程
-
调用中断创建接口LOS_HwiCreate创建中断。
-
调用LOS_HwiDelete接口删除指定中断,此接口根据实际情况使用,判断是否需要删除中断。
编程实例
本实例实现如下功能:
-
创建中断。
-
删除中断。
代码实现如下,演示如何创建中断和删除中断,当指定的中断号HWI_NUM_TEST产生中断时,会调用中断处理函数(该示例代码的测试函数可以加在kernel/liteos_a/testsuites/kernel/src/osTest.c中的TestTaskEntry中进行测试):
#include "los_hwi.h"
/*中断处理函数*/
STATIC VOID HwiUsrIrq(VOID)
{
PRINK("in the func HwiUsrIrq \n");
}
static UINT32 Example_Interrupt(VOID)
{
UINT32 ret;
HWI_HANDLE_T hwiNum = 7; // 7: 使用的中断号
HWI_PRIOR_T hwiPrio = 3; // 3: 中断优先级
HWI_MODE_T mode = 0;
HWI_ARG_T arg = 0;
/*创建中断*/
ret = LOS_HwiCreate(hwiNum, hwiPrio, mode, (HWI_PROC_FUNC)HwiUsrIrq, (HwiIrqParam *)arg);
if (ret == LOS_OK) {
PRINK("Hwi create success!\n");
} else {
PRINK("Hwi create failed!\n");
return LOS_NOK;
}
/* 延时50个Ticks, 当有硬件中断发生时,会调用函数HwiUsrIrq*/
LOS_TaskDelay(50);
/*删除中断*/
ret = LOS_HwiDelete(hwiNum, (HwiIrqParam *)arg);
if (ret == LOS_OK) {
PRINK("Hwi delete success!\n");
} else {
PRINK("Hwi delete failed!\n");
return LOS_NOK;
}
return LOS_OK;
}
结果验证
编译运行得到的结果为:
Hwi create success!
Hwi delete success!
3.2、进程管理
3.2.1、进程基本概念
进程是系统资源管理的最小单元。OpenHarmony LiteOS-A 内核提供的进程模块主要用于实现用户态进程的隔离,内核态被视为一个进程空间,不存在其它进程(KIdle除外,KIdle进程是系统提供的空闲进程,和KProcess共享一个进程空间。KProcess 是内核态进程的根进程,KIdle 是其子进程)。
-
进程模块主要为用户提供多个进程,实现了进程之间的切换和通信,帮助用户管理业务程序流程。
-
进程采用抢占式调度机制,采用高优先级优先+同优先级时间片轮转的调度算法。
-
进程一共有32个优先级(0-31),用户进程可配置的优先级有22个(10-31),最高优先级为10,最低优先级为31。
-
高优先级的进程可抢占低优先级进程,低优先级进程必须在高优先级进程阻塞或结束后才能得到调度。
-
每一个用户态进程均拥有自己独立的进程空间,相互之间不可见,实现进程间隔离。
-
用户态根进程init由内核态创建,其它用户态子进程均由init进程fork而来。
进程状态说明:
-
初始化(Init):进程正在被创建。
-
就绪(Ready):进程在就绪列表中,等待CPU调度。
-
运行(Running):进程正在运行。
-
阻塞(Pending):进程被阻塞挂起。本进程内所有的线程均被阻塞时,进程被阻塞挂起。
-
僵尸(Zombies):进程运行结束,等待父进程回收其控制块资源。
图1 进程状态迁移示意图
进程状态迁移说明:
-
Init→Ready: 进程创建或 fork 时,拿到对应进程控制块后进入 Init 状态,即进程初始化阶段,当该阶段完成后进程将被插入调度队列,此时进程进入就绪状态。
-
Ready→Running: 进程创建后进入就绪态,发生进程切换时,就绪列表中优先级最高且获得时间片的进程被执行,从而进入运行态。若此时该进程中已无其它线程处于就绪态,则进程从就绪列表删除,只处于运行态;若此时该进程中还有其它线程处于就绪态,则该进程依旧在就绪队列,此时进程的就绪态和运行态共存,但对外呈现的进程状态为运行态。
-
Running→Pending: 进程在最后一个线程转为阻塞态时, 进程内所有的线程均处于阻塞态,此时进程同步进入阻塞态,然后发生进程切换。
-
Pending→Ready: 阻塞进程内的任意线程恢复就绪态时,进程被加入到就绪队列,同步转为就绪态。
-
Ready→Pending: 进程内的最后一个就绪态线程转为阻塞态时,进程从就绪列表中删除,进程由就绪态转为阻塞态。
-
Running→Ready: 进程由运行态转为就绪态的情况有以下两种:
- 有更高优先级的进程创建或者恢复后,会发生进程调度,此刻就绪列表中最高优先级进程变为运行态,那么原先运行的进程由运行态变为就绪态。
- 若进程的调度策略为 LOS_SCHED_RR(时间片轮转),且存在同一优先级的另一个进程处于就绪态,则该进程的时间片消耗光之后,该进程由运行态转为就绪态,另一个同优先级的进程由就绪态转为运行态。
-
Running→Zombies: 当进程的主线程或所有线程运行结束后,进程由运行态转为僵尸态,等待父进程回收资源。
运行机制
OpenHarmony 提供的进程模块主要用于实现用户态进程的隔离,支持用户态进程的创建、退出、资源回收、设置/获取调度参数、获取进程ID、设置/获取进程组ID等功能。
用户态进程通过fork父进程而来,fork进程时会将父进程的进程虚拟内存空间clone到子进程,子进程实际运行时通过写时复制机制将父进程的内容按需复制到子进程的虚拟内存空间。
进程只是资源管理单元,实际运行是由进程内的各个线程完成的,不同进程内的线程相互切换时会进行进程空间的切换。
图2 进程管理示意图
开发指导
接口说明
表1 进程及进程组
接口名 | 接口描述 |
---|---|
LOS_GetCurrProcessID | 获取当前进程的进程ID |
LOS_GetProcessGroupID | 获取指定进程的进程组ID |
LOS_GetCurrProcessGroupID | 获取当前进程的进程组ID |
表2 用户及用户组
接口名 | 接口描述 |
---|---|
LOS_GetUserID | 获取当前进程的用户ID |
LOS_GetGroupID | 获取当前进程的用户组ID |
LOS_CheckInGroups | 检查指定用户组ID是否在当前进程的用户组内 |
表3 进程调度控制
接口名 | 接口 |
---|---|
LOS_GetProcessScheduler | 获取指定进程的调度策略 |
LOS_SetProcessScheduler | 设置指定进程的调度参数,包括优先级和调度策略 |
LOS_SetProcessPriority | 设置进程优先级 |
LOS_GetProcessPriority | 获取进程优先级 |
表4 系统进程信息获取
接口名 | 接口描述 |
---|---|
LOS_GetSystemProcessMaximum | 获取系统支持的最大进程数目 |
LOS_GetUsedPIDList | 获得已使用的进程ID列表 |
表5 进程创建与结束
接口名 | 接口描述 |
---|---|
LOS_Fork | 创建子进程 |
LOS_Wait | 等待子进程结束并回收子进程 |
LOS_Waitid | 等待相应ID的进程结束 |
LOS_Exit | 退出进程 |
开发流程
不支持内核态进程创建,内核态不涉及进程相关开发。
说明:
idle线程的数量跟随CPU核心数,每个CPU均有一个相应的idle线程。
不支持创建除KProcess和KIdle进程之外的其它内核态进程。
用户态进程通过系统调用进入内核态后创建的线程属于KProcess, 不属于当前用户态进程。
3.2.2、任务
基本概念
从系统的角度看,任务Task是竞争系统资源的最小运行单元。任务可以使用或等待CPU、使用内存空间等系统资源,并独立于其它任务运行。
OpenHarmony 内核中使用一个任务表示一个线程。
OpenHarmony 内核中同优先级进程内的任务统一调度、运行。
OpenHarmony 内核中的任务采用抢占式调度机制,同时支持时间片轮转调度和FIFO调度方式。
OpenHarmony 内核的任务一共有32个优先级(0-31),最高优先级为0,最低优先级为31。
当前进程内, 高优先级的任务可抢占低优先级任务,低优先级任务必须在高优先级任务阻塞或结束后才能得到调度。
任务状态说明:
-
初始化(Init):任务正在被创建。
-
就绪(Ready):任务在就绪列表中,等待CPU调度。
-
运行(Running):任务正在运行。
-
阻塞(Blocked):任务被阻塞挂起。Blocked状态包括:pending(因为锁、事件、信号量等阻塞)、suspended(主动pend)、delay(延时阻塞)、pendtime(因为锁、事件、信号量时间等超时等待)。
-
退出(Exit):任务运行结束,等待父任务回收其控制块资源。
图1 任务状态迁移示意图
任务状态迁移说明:
-
Init→Ready: 任务创建拿到控制块后为初始化阶段(Init状态),当任务初始化完成将任务插入调度队列,此时任务进入就绪状态。
-
Ready→Running: 任务创建后进入就绪态,发生任务切换时,就绪列表中最高优先级的任务被执行,从而进入运行态,此刻该任务从就绪列表中删除。
-
Running→Blocked: 正在运行的任务发生阻塞(挂起、延时、读信号量等)时,任务状态由运行态变成阻塞态,然后发生任务切换,运行就绪列表中剩余最高优先级任务。
-
Blocked→Ready : 阻塞的任务被恢复后(任务恢复、延时时间超时、读信号量超时或读到信号量等),此时被恢复的任务会被加入就绪列表,从而由阻塞态变成就绪态。
-
Ready→Blocked: 任务也有可能在就绪态时被阻塞(挂起),此时任务状态会由就绪态转变为阻塞态,该任务从就绪列表中删除,不会参与任务调度,直到该任务被恢复。
-
Running→Ready: 有更高优先级任务创建或者恢复后,会发生任务调度,此刻就绪列表中最高优先级任务变为运行态,那么原先运行的任务由运行态变为就绪态,并加入就绪列表中。
-
Running→Exit: 运行中的任务运行结束,任务状态由运行态变为退出态。若为设置了分离属性( 由头文件 los_task.h 中的宏定义 LOS_TASK_STATUS_DETACHED 设置)的任务,运行结束后将直接销毁。
运行机制
OpenHarmony 任务管理模块提供任务创建、任务延时、任务挂起和任务恢复、锁任务调度和解锁任务调度、根据ID查询任务控制块信息功能。
用户创建任务时,系统会将任务栈进行初始化,预置上下文。此外,系统还会将“任务入口函数”地址放在相应位置。这样在任务第一次启动进入运行态时,将会执行任务入口函数。
开发指导
接口说明
表1 任务的创建和删除
接口名 | 接口描述 |
---|---|
LOS_TaskCreate | 创建任务,若所创建任务的优先级比当前的运行的任务优先级高且任务调度没有锁定, 则该任务将被调度进入运行态 |
LOS_TaskCreateOnly | 创建任务并阻塞,任务恢复前不会将其加入就绪队列中 |
LOS_TaskDelete | 删除指定的任务,回收其任务控制块和任务栈所消耗的资源 |
表2 任务的状态控制
接口名 | 接口描述 |
---|---|
LOS_TaskResume | 恢复挂起的任务 |
LOS_TaskSuspend | 挂起指定的任务,该任务将从就绪任务队列中移除 |
LOS_TaskJoin | 阻塞当前任务,等待指定任务运行结束并回收其资源 |
LOS_TaskDetach | 修改任务的 joinable 属性为 detach 属性,detach 属性的任务运行结束会自动回收任务控制块资源 |
LOS_TaskDelay | 延迟当前任务的执行,在延后指定的时间(tick数)后可以被调度 |
LOS_TaskYield | 将当前任务从具有相同优先级的任务队列,移动到就绪任务队列的末尾 |
表3 任务调度
接口名 | 接口描述 |
---|---|
LOS_TaskLock | 锁定任务调度,阻止任务切换 |
LOS_TaskUnlock | 解锁任务调度。通过该接口可以使任务锁数量减1,若任务多次加锁,那么 任务调度在锁数量减为0时才会完全解锁 |
LOS_GetTaskScheduler | 获取指定任务的调度策略 |
LOS_SetTaskScheduler | 设置指定任务的调度参数,包括优先级和调度策略 |
LOS_Schedule | 触发主动的任务调度 |
表4 任务相关信息获取
接口名 | 接口描述 |
---|---|
LOS_CurTaskIDGet | 获取当前任务的ID |
LOS_TaskInfoGet | 获取指定任务的信息 |
LOS_GetSystemTaskMaximum | 获取系统支持的最大任务数 |
表5 任务优先级
接口名 | 接口描述 |
---|---|
LOS_CurTaskPriSet | 设置当前正在运行的任务的优先级 |
LOS_TaskPriSet | 设置指定任务的优先级 |
LOS_TaskPriGet | 获取指定任务的优先级 |
表6 任务绑核操作
接口名 | 接口描述 |
---|---|
LOS_TaskCpuAffiSet | 绑定指定任务到指定CPU上运行,仅在多核下使用 |
LOS_TaskCpuAffiGet | 获取指定任务的绑核信息,仅在多核下使用 |
开发流程
任务的典型开发流程:
-
通过LOS_TaskCreate创建一个任务。
- 指定任务的执行入口函数
- 指定任务名
- 指定任务的栈大小
- 指定任务的优先级
- 指定任务的属性,LOS_TASK_ATTR_JOINABLE和LOS_TASK_STATUS_DETACHED属性
- 多核运行时,可以选择设置任务的绑核属性
-
任务参与调度运行,执行用户指定的业务代码。
-
任务执行结束,如果设置了 LOS_TASK_STATUS_DETACHED 属性,则自动回收任务资源,如果任务设置了 LOS_TASK_ATTR_JOINABLE 属性,则需要调用LOS_TaskJoin 回收任务资源,默认为 LOS_TASK_STATUS_DETACHED 属性。
说明:
内核态具有最高权限,可以操作任意进程内的任务。
用户态进程通过系统调用进入内核态后创建的任务属于KProcess, 不属于当前用户态进程。
编程实例
代码实现如下(该示例代码的测试函数可以加在 kernel /liteos_a/testsuites /kernel /src /osTest.c 中的 TestTaskEntry 中进行测试。):
UINT32 g_taskLoID;
UINT32 g_taskHiID;
#define TSK_PRIOR_HI 4
#define TSK_PRIOR_LO 5
UINT32 ExampleTaskHi(VOID)
{
UINT32 ret;
PRINTK("Enter TaskHi Handler.\n");
/* 延时2个Tick,延时后该任务会挂起,执行剩余任务中最高优先级的任务(g_taskLoID任务) */
ret = LOS_TaskDelay(2);
if (ret != LOS_OK) {
PRINTK("Delay Task Failed.\n");
return LOS_NOK;
}
/* 2个Tick时间到了后,该任务恢复,继续执行 */
PRINTK("TaskHi LOS_TaskDelay Done.\n");
/* 挂起自身任务 */
ret = LOS_TaskSuspend(g_taskHiID);
if (ret != LOS_OK) {
PRINTK("Suspend TaskHi Failed.\n");
return LOS_NOK;
}
PRINTK("TaskHi LOS_TaskResume Success.\n");
return LOS_OK;
}
/* 低优先级任务入口函数 */
UINT32 ExampleTaskLo(VOID)
{
UINT32 ret;
PRINTK("Enter TaskLo Handler.\n");
/* 延时2个Tick,延时后该任务会挂起,执行剩余任务中就高优先级的任务(背景任务) */
ret = LOS_TaskDelay(2);
if (ret != LOS_OK) {
PRINTK("Delay TaskLo Failed.\n");
return LOS_NOK;
}
PRINTK("TaskHi LOS_TaskSuspend Success.\n");
/* 恢复被挂起的任务g_taskHiID */
ret = LOS_TaskResume(g_taskHiID);
if (ret != LOS_OK) {
PRINTK("Resume TaskHi Failed.\n");
return LOS_NOK;
}
PRINTK("TaskHi LOS_TaskDelete Success.\n");
return LOS_OK;
}
/* 任务测试入口函数,在里面创建优先级不一样的两个任务 */
UINT32 ExampleTaskCaseEntry(VOID)
{
UINT32 ret;
TSK_INIT_PARAM_S initParam = {0};
/* 锁任务调度 */
LOS_TaskLock();
PRINTK("LOS_TaskLock() Success!\n");
/* 高优先级任务的初始化参数,其资源回收需要其他任务调用 LOS_TaskJoin */
initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)ExampleTaskHi;
initParam.usTaskPrio = TSK_PRIOR_HI;
initParam.pcName = "HIGH_NAME";
initParam.uwStackSize = LOS_TASK_MIN_STACK_SIZE;
initParam.uwResved = LOS_TASK_ATTR_JOINABLE;
/* 创建高优先级任务,由于锁任务调度,任务创建成功后不会马上执行 */
ret = LOS_TaskCreate(&g_taskHiID, &initParam);
if (ret != LOS_OK) {
LOS_TaskUnlock();
PRINTK("ExampleTaskHi create Failed! ret=%d\n", ret);
return LOS_NOK;
}
PRINTK("ExampleTaskHi create Success!\n");
/* 低优先级任务的初始化参数,任务结束后会自行结束销毁 */
initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)ExampleTaskLo;
initParam.usTaskPrio = TSK_PRIOR_LO;
initParam.pcName = "LOW_NAME";
initParam.uwStackSize = LOS_TASK_MIN_STACK_SIZE;
initParam.uwResved = LOS_TASK_STATUS_DETACHED;
/* 创建低优先级任务,由于锁任务调度,任务创建成功后不会马上执行 */
ret = LOS_TaskCreate(&g_taskLoID, &initParam);
if (ret!= LOS_OK) {
LOS_TaskUnlock();
PRINTK("ExampleTaskLo create Failed!\n");
return LOS_NOK;
}
PRINTK("ExampleTaskLo create Success!\n");
/* 解锁任务调度,此时会发生任务调度,执行就绪列表中最高优先级任务 */
LOS_TaskUnlock();
ret = LOS_TaskJoin(g_taskHiID, NULL);
if (ret != LOS_OK) {
PRINTK("Join ExampleTaskHi Failed!\n");
} else {
PRINTK("Join ExampleTaskHi Success!\n");
}
while(1){};
return LOS_OK;
}
编译运行得到的结果为:
LOS_TaskLock() Success!
ExampleTaskHi create Success!
ExampleTaskLo create Success!
Enter TaskHi Handler.
Enter TaskLo Handler.
TaskHi LOS_TaskDelay Done.
TaskHi LOS_TaskSuspend Success.
TaskHi LOS_TaskResume Success.
TaskHi LOS_TaskDelete Success.
Join ExampleTaskHi Success!
3.2.3、调度器
基本概念
OpenHarmony LiteOS-A内核采用了高优先级优先 + 同优先级时间片轮转的抢占式调度机制,系统从启动开始基于real time的时间轴向前运行,使得该调度算法具有很好的实时性。
OpenHarmony 的调度算法将 tickless 机制天然嵌入到调度算法中,一方面使得系统具有更低的功耗,另一方面也使得 tick 中断按需响应,减少无用的 tick 中断响应,进一步提高系统的实时性。
OpenHarmony 的进程调度策略支持 SCHED_RR(时间片轮转),线程调度策略支持 SCHED_RR 和 SCHED_FIFO(先进先出)。
OpenHarmony 调度的最小单元为线程。
运行机制
OpenHarmony 采用进程优先级队列 + 线程优先级队列的方式,进程优先级范围为0-31,共有32个进程优先级桶队列,每个桶队列对应一个线程优先级桶队列;线程优先级范围也为0-31,一个线程优先级桶队列也有32个优先级队列。
图1 调度优先级桶队列示意图
OpenHarmony 在系统启动内核初始化之后开始调度,运行过程中创建的进程或线程会被加入到调度队列,系统根据进程和线程的优先级及线程的时间片消耗情况选择最优的线程进行调度运行,线程一旦调度到就会从调度队列上删除,线程在运行过程中发生阻塞,会被加入到对应的阻塞队列中并触发一次调度,调度其它线程运行。如果调度队列上没有可以调度的线程,则系统就会选择KIdle进程的线程进行调度运行。
图2 调度流程示意图
开发指导
接口说明
接口名称 | 描述 |
---|---|
LOS_Schedule | 触发系统调度 |
LOS_GetTaskScheduler | 获取指定任务的调度策略 |
LOS_SetTaskScheduler | 设置指定任务的调度策略 |
LOS_GetProcessScheduler | 获取指定进程的调度策略 |
LOS_SetProcessScheduler | 设置指定进程的调度参数,包括优先级和调度策略 |
开发流程
说明: 系统启动初始化阶段,不允许触发调度。
3.3、内存管理
3.3.1、堆内存管理
基本概念
内存管理模块管理系统的内存资源,它是操作系统的核心模块之一,主要包括内存的初始化、分配以及释放。OpenHarmony LiteOS-A的堆内存管理提供内存初始化、分配、释放等功能。在系统运行过程中,堆内存管理模块通过对内存的申请/释放来管理用户和OS对内存的使用,使内存的利用率和使用效率达到最优,同时最大限度地解决系统的内存碎片问题。
运行机制
堆内存管理,即在内存资源充足的情况下,根据用户需求,从系统配置的一块比较大的连续内存(内存池,也是堆内存)中分配任意大小的内存块。当用户不需要该内存块时,又可以释放回系统供下一次使用。与静态内存相比,动态内存管理的优点是按需分配,缺点是内存池中容易出现碎片。OpenHarmony LiteOS-A堆内存在TLSF算法的基础上,对区间的划分进行了优化,获得更优的性能,降低了碎片率。动态内存核心算法框图如下:
图1 小型系统动态内存核心算法
根据空闲内存块的大小,使用多个空闲链表来管理。根据内存空闲块大小分为两个部分:[4, 127]和[27, 231],如上图size class所示:
-
对[4,127]区间的内存进行等分,如上图下半部分所示,分为31个小区间,每个小区间对应内存块大小为4字节的倍数。每个小区间对应一个空闲内存链表和用于标记对应空闲内存链表是否为空的一个比特位,值为1时,空闲链表非空。[4,127]区间的31个小区间内存对应31个比特位进行标记链表是否为空。
-
大于127字节的空闲内存块,按照2的次幂区间大小进行空闲链表管理。总共分为24个小区间,每个小区间又等分为8个二级小区间,见上图上半部分的Size Class和Size SubClass部分。每个二级小区间对应一个空闲链表和用于标记对应空闲内存链表是否为空的一个比特位。总共24*8=192个二级小区间,对应192个空闲链表和192个比特位进行标记链表是否为空。
例如,当有40字节的空闲内存需要插入空闲链表时,对应小区间[40,43],第10个空闲链表,位图标记的第10比特位。把40字节的空闲内存挂载第10个空闲链表上,并判断是否需要更新位图标记。当需要申请40字节的内存时,根据位图标记获取存在满足申请大小的内存块的空闲链表,从空闲链表上获取空闲内存节点。如果分配的节点大于需要申请的内存大小,进行分割节点操作,剩余的节点重新挂载到相应的空闲链表上。当有580字节的空闲内存需要插入空闲链表时,对应二级小区间[2^9,2^9+2^6],第31+2*8=47个空闲链表,并使用位图的第47个比特位来标记链表是否为空。把580字节的空闲内存挂载第47个空闲链表上,并判断是否需要更新位图标记。当需要申请580字节的内存时,根据位图标记获取存在满足申请大小的内存块的空闲链表,从空闲链表上获取空闲内存节点。如果分配的节点大于需要申请的内存大小,进行分割节点操作,剩余的节点重新挂载到相应的空闲链表上。如果对应的空闲链表为空,则向更大的内存区间去查询是否有满足条件的空闲链表,实际计算时,会一次性查找到满足申请大小的空闲链表。
内存管理结构如下图所示:
图2 小型系统动态内存管理结构图
-
内存池池头部分 内存池池头部分包含内存池信息、位图标记数组和空闲链表数组。内存池信息包含内存池起始地址及堆区域总大小,内存池属性。位图标记数组有7个32位无符号整数组成,每个比特位标记对应的空闲链表是否挂载空闲内存块节点。空闲内存链表包含223个空闲内存头节点信息,每个空闲内存头节点信息维护内存节点头和空闲链表中的前驱、后继空闲内存节点。
-
内存池节点部分 包含3种类型节点:未使用空闲内存节点,已使用内存节点和尾节点。每个内存节点维护一个前序指针,指向内存池中上一个内存节点,还维护内存节点的大小和使用标记。空闲内存节点和已使用内存节点后面的内存区域是数据域,尾节点没有数据域。
开发指导
使用场景
堆内存管理的主要工作是动态分配并管理用户申请到的内存区间,主要用于用户需要使用大小不等的内存块的场景,当用户需要使用内存时,可以通过操作系统的动态内存申请函数索取指定大小的内存块。一旦使用完毕,通过内存释放函数释放所占用内存,使之可以重复使用。
接口说明
OpenHarmony LiteOS-A的堆内存管理主要为用户提供以下功能,接口详细信息可以查看API参考。
表1 堆内存管理接口
功能分类 | 接口描述 |
---|---|
初始化和删除内存池 | - LOS_MemInit:初始化一块指定的动态内存池,大小为size - LOS_MemDeInit:删除指定内存池,仅打开LOSCFG_MEM_MUL_POOL时有效 |
申请、释放动态内存 | - LOS_MemAlloc:从指定动态内存池中申请size长度的内存 - LOS_MemFree:释放从指定动态内存中申请的内存 - LOS_MemRealloc: - 按size大小重新分配内存块,并将原内存块内容拷贝到新内存块。如果新内存块申请成功,则释放原内存块 - LOS_MemAllocAlign:从指定动态内存池中申请长度为size且地址按boundary字节对齐的内存 |
获取内存池信息 | - LOS_MemPoolSizeGet:获取指定动态内存池的总大小 - LOS_MemTotalUsedGet:获取指定动态内存池的总使用量大小 - LOS_MemInfoGet:获取指定内存池的内存结构信息,包括空闲内存大小、已使用内存大小、空闲内存块数量、已使用的内存块数量、最大的空闲内存块大小 - LOS_MemPoolList:打印系统中已初始化的所有内存池,包括内存池的起始地址、内存池大小、空闲内存总大小、已使用内存总大小、最大的空闲内存块大小、空闲内存块数量、已使用的内存块数量。仅打开LOSCFG_MEM_MUL_POOL时有效 |
获取内存块信息 | LOS_MemFreeNodeShow:打印指定内存池的空闲内存块的大小及数量 |
检查指定内存池的完整性 | LOS_MemIntegrityCheck:对指定内存池做完整性检查,仅打开LOSCFG_BASE_MEM_NODE_INTEGRITY_CHECK时有效 |
说明:
由于动态内存管理需要管理控制块数据结构来管理内存,这些数据结构会额外消耗内存,故实际用户可使用内存总量小于配置项OS_SYS_MEM_SIZE的大小。
对齐分配内存接口LOS_MemAllocAlign/LOS_MemMallocAlign因为要进行地址对齐,可能会额外消耗部分内存,故存在一些遗失内存,当系统释放该对齐内存时,同时回收由于对齐导致的遗失内存。
开发流程
本节介绍使用动态内存的典型场景开发流程。
-
初始化LOS_MemInit。 初始一个内存池后生成一个内存池控制头、尾节点EndNode,剩余的内存被标记为FreeNode内存节点。注:EndNode作为内存池末尾的节点,size为0。
-
申请任意大小的动态内存LOS_MemAlloc。 判断动态内存池中是否存在大于申请量大小的空闲内存块空间,若存在,则划出一块内存块,以指针形式返回,若不存在,返回NULL。如果空闲内存块大于申请量,需要对内存块进行分割,剩余的部分作为空闲内存块挂载到空闲内存链表上。
-
释放动态内存LOS_MemFree。 回收内存块,供下一次使用。调用LOS_MemFree释放内存块,则会回收内存块,并且将其标记为FreeNode。在回收内存块时,相邻的FreeNode会自动合并。
编程实例
本实例执行以下步骤:
-
初始化一个动态内存池。
-
从动态内存池中申请一个内存块。
-
在内存块中存放一个数据。
-
打印出内存块中的数据。
-
释放该内存块。
示例代码如下:
#include "los_memory.h"
#define TEST_POOL_SIZE (2*1024*1024)
__attribute__((aligned(4))) 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) {
printf("Mem init success!\n");
} else {
printf("Mem init failed!\n");
return;
}
/*分配内存*/
mem = (UINT32 *)LOS_MemAlloc(g_testPool, 4);
if (NULL == mem) {
printf("Mem alloc failed!\n");
return;
}
printf("Mem alloc success!\n");
/*赋值*/
*mem = 828;
printf("*mem = %d\n", *mem);
/*释放内存*/
ret = LOS_MemFree(g_testPool, mem);
if (LOS_OK == ret) {
printf("Mem free success!\n");
} else {
printf("Mem free failed!\n");
}
return;
}
UINT32 ExampleDynMemEntry(VOID)
{
UINT32 ret;
TSK_INIT_PARAM_S initParam = {0};
initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_DynMem;
initParam.usTaskPrio = 10;
initParam.pcName = "Example_DynMem";
initParam.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
initParam.uwResved = LOS_TASK_STATUS_DETACHED;
/* 创建高优先级任务,由于锁任务调度,任务创建成功后不会马上执行 */
ret = LOS_TaskCreate(&g_taskHiID, &initParam);
if (ret != LOS_OK) {
LOS_TaskUnlock();
PRINTK("Example_DynMem create Failed! ret=%d\n", ret);
return LOS_NOK;
}
PRINTK("Example_DynMem create Success!\n");
while(1){};
return LOS_OK;
}
结果验证
输出结果如下:
Mem init success!
Mem alloc success!
*mem = 828
Mem free success!
3.3.2、物理内存管理
基本概念
物理内存是计算机上最重要的资源之一,指的是实际的内存设备提供的、可以通过CPU总线直接进行寻址的内存空间,其主要作用是为操作系统及程序提供临时存储空间。LiteOS-A内核管理物理内存是通过分页实现的,除了内核堆占用的一部分内存外,其余可用内存均以4KiB为单位划分成页帧,内存分配和内存回收便是以页帧为单位进行操作。内核采用伙伴算法管理空闲页面,可以降低一定的内存碎片率,提高内存分配和释放的效率,但是一个很小的块往往也会阻塞一个大块的合并,导致不能分配较大的内存块。
运行机制
如下图所示,LiteOS-A内核的物理内存使用分布视图,主要由内核镜像、内核堆及物理页组成。内核堆部分见堆内存管理一节。
图1 物理内存使用分布图
伙伴算法把所有空闲页帧分成9个内存块组,每组中内存块包含2的幂次方个页帧,例如:第0组的内存块包含2的0次方个页帧,即1个页帧;第8组的内存块包含2的8次方个页帧,即256个页帧。相同大小的内存块挂在同一个链表上进行管理。
-
申请内存 系统申请12KiB内存,即3个页帧时,9个内存块组中索引为3的链表挂着一块大小为8个页帧的内存块满足要求,分配出12KiB内存后还剩余20KiB内存,即5个页帧,将5个页帧分成2的幂次方之和,即4跟1,尝试查找伙伴进行合并。4个页帧的内存块没有伙伴则直接插到索引为2的链表上,继续查找1个页帧的内存块是否有伙伴,索引为0的链表上此时有1个,如果两个内存块地址连续则进行合并,并将内存块挂到索引为1的链表上,否则不做处理。
图2 内存申请示意图
-
释放内存 系统释放12KiB内存,即3个页帧,将3个页帧分成2的幂次方之和,即2跟1,尝试查找伙伴进行合并,索引为1的链表上有1个内存块,若地址连续则合并,并将合并后的内存块挂到索引为2的链表上,索引为0的链表上此时也有1个,如果地址连续则进行合并,并将合并后的内存块挂到索引为1的链表上,此时继续判断是否有伙伴,重复上述操作。
图3 内存释放示意图
开发指导
接口说明
表1 物理内存管理模块接口
功能分类 | 接口描述 |
---|---|
申请物理内存 | - LOS_PhysPageAlloc:申请一个物理页 - LOS_PhysPagesAlloc:申请物理页并挂在对应的链表上 - LOS_PhysPagesAllocContiguous:申请多页地址连续的物理内存 |
释放物理内存 | - LOS_PhysPageFree:释放一个物理页 - LOS_PhysPagesFree:释放挂在链表上的物理页 - LOS_PhysPagesFreeContiguous:释放多页地址连续的物理内存 |
查询地址 | - LOS_VmPageGet:根据物理地址获取其对应的物理页结构体指针 - LOS_PaddrToKVaddr:根据物理地址获取其对应的内核虚拟地址 |
开发流程
内存申请时根据需要调用相关接口,小内存申请建议使用堆内存申请相关接口,4KiB及以上内存申请可以使用上述物理内存相关接口。
说明:
物理内存申请相关接口需要在OsSysMemInit接口完成初始化之后再使用;
内存申请的基本单位是页帧,即4KiB;
物理内存申请时,有地址连续要求的使用LOS_PhysPagesAllocContiguous接口,无地址连续的要求尽量使用LOS_PhysPagesAlloc接口,将连续的大块内存留给有需要的模块使用。
编程实例
编程示例主要是调用申请、释放接口对内存进行操作,包括申请一个页以及多个页的示例。
#include "los_vm_phys.h"
#define PHYS_PAGE_SIZE 0x4000
// 申请一个页
VOID OsPhysPagesAllocTest3(VOID)
{
PADDR_T newPaddr;
VOID *kvaddr = NULL;
LosVmPage *newPage = NULL;
newPage = LOS_PhysPageAlloc();
if (newPage == NULL) {
printf("LOS_PhysPageAlloc fail\n");
return;
}
printf("LOS_PhysPageAlloc success\n");
newPaddr = VM_PAGE_TO_PHYS(newPage);
kvaddr = OsVmPageToVaddr(newPage);
// Handle the physical memory
// Free the physical memory
LOS_PhysPageFree(newPage);
}
// 申请多个页,不要求连续
VOID OsPhysPagesAllocTest2(VOID)
{
UINT32 sizeCount;
UINT32 count;
UINT32 size = PHYS_PAGE_SIZE;
LosVmPage *vmPageArray[PHYS_PAGE_SIZE >> PAGE_SHIFT] = { NULL };
UINT32 i = 0;
LosVmPage *vmPage = NULL;
PADDR_T pa;
size = LOS_Align(size, PAGE_SIZE);
if (size == 0) {
return;
}
sizeCount = size >> PAGE_SHIFT;
LOS_DL_LIST_HEAD(pageList);
count = LOS_PhysPagesAlloc(sizeCount, &pageList);
if (count < sizeCount) {
printf("failed to allocate enough pages (ask %zu, got %zu)\n", sizeCount, count);
goto ERROR;
}
printf("LOS_PhysPagesAlloc success\n");
while ((vmPage = LOS_ListRemoveHeadType(&pageList, LosVmPage, node))) {
pa = vmPage->physAddr;
vmPageArray[i++] = vmPage;
// Handle the physical memory
}
// Free the physical memory
for (i = 0; i < sizeCount; ++i) {
LOS_PhysPageFree(vmPageArray[i]);
}
return;
ERROR:
(VOID)LOS_PhysPagesFree(&pageList);
}
// 申请多个连续页
VOID OsPhysPagesAllocTest1(VOID)
{
VOID *ptr = NULL;
LosVmPage *page = NULL;
UINT32 size = PHYS_PAGE_SIZE;
ptr = LOS_PhysPagesAllocContiguous(ROUNDUP(size, PAGE_SIZE) >> PAGE_SHIFT);
if (ptr == NULL) {
printf("LOS_PhysPagesAllocContiguous fail\n");
return;
}
printf("LOS_PhysPagesAllocContiguous success\n");
// Handle the physical memory
// Free the physical memory
page = OsVmVaddrToPage((VOID *)ptr);
LOS_PhysPagesFreeContiguous((VOID *)ptr, size >> PAGE_SHIFT);
}
UINT32 ExamplePhyMemCaseEntry(VOID)
{
OsPhysPagesAllocTest1();
OsPhysPagesAllocTest2();
OsPhysPagesAllocTest3();
return LOS_OK;
}
结果验证
编译运行得到的结果为:
LOS_PhysPagesAllocContiguous success
LOS_PhysPagesAlloc success
LOS_PhysPageAlloc success
3.3.3、虚拟内存管理
基本概念
虚拟内存管理是计算机系统管理内存的一种技术。每个进程都有连续的虚拟地址空间,虚拟地址空间的大小由CPU的位数决定,32位的硬件平台可以提供的最大的寻址空间为0-4GiB。整个4GiB空间分成两部分,LiteOS-A内核占据3GiB的高地址空间,1GiB的低地址空间留给用户态进程使用。各个进程空间的虚拟地址空间是独立的,代码、数据互不影响。
系统将虚拟内存分割为称为虚拟页的内存块,大小一般为4KiB或64KiB,LiteOS-A内核默认的页的大小是4KiB,根据需要可以对MMU(Memory Management Units)进行配置。虚拟内存管理操作的最小单位就是一个页,LiteOS-A内核中一个虚拟地址区间region包含地址连续的多个虚拟页,也可只有一个页。同样,物理内存也会按照页大小进行分割,分割后的每个内存块称为页帧。虚拟地址空间划分:内核态占高地址3GiB(0x40000000 ~ 0xFFFFFFFF),用户态占低地址1GiB(0x01000000 ~ 0x3F000000),具体见下表,详细可以查看或配置los_vm_zone.h。
表1 内核态地址规划
Zone名称 | 描述 | 属性 |
---|---|---|
DMA zone | 供IO设备的DMA使用。 | Uncache |
Normal zone | 加载内核代码段、数据段、堆和栈的地址区间。 | Cache |
high mem zone | 可以分配连续的虚拟内存,但其所映射的物理内存不一定连续。 | Cache |
表2 用户态虚地址规划
Zone名称 | 描述 | 属性 |
---|---|---|
代码段 | 用户态代码段地址区间。 | Cache |
堆 | 用户态堆地址区间。 | Cache |
栈 | 用户态栈地址区间。 | Cache |
共享库 | 用于加载用户态共享库的地址区间,包括mmap所映射的区间。 | Cache |
运行机制
虚拟内存管理中,虚拟地址空间是连续的,但是其映射的物理内存并不一定是连续的,如下图所示。可执行程序加载运行,CPU访问虚拟地址空间的代码或数据时存在两种情况:
-
CPU访问的虚拟地址所在的页,如V0,已经与具体的物理页P0做映射,CPU通过找到进程对应的页表条目(详见虚实映射),根据页表条目中的物理地址信息访问物理内存中的内容并返回。
-
CPU访问的虚拟地址所在的页,如V2,没有与具体的物理页做映射,系统会触发缺页异常,系统申请一个物理页,并把相应的信息拷贝到物理页中,并且把物理页的起始地址更新到页表条目中。此时CPU重新执行访问虚拟内存的指令便能够访问到具体的代码或数据。
图1 内存映射示意图
开发指导
接口说明
表3 获取进程空间系列接口
接口名称 | 描述 |
---|---|
LOS_CurrSpaceGet | 获取当前进程空间结构体指针 |
LOS_SpaceGet | 获取虚拟地址对应的进程空间结构体指针 |
LOS_GetKVmSpace | 获取内核进程空间结构体指针 |
LOS_GetVmallocSpace | 获取vmalloc空间结构体指针 |
LOS_GetVmSpaceList | 获取进程空间链表指针 |
表4 虚拟地址区间region相关的操作
接口名称 | 描述 |
---|---|
LOS_RegionFind | 在进程空间内查找并返回指定地址对应的虚拟地址区间 |
LOS_RegionRangeFind | 在进程空间内查找并返回指定地址范围对应的虚拟地址区间 |
LOS_IsRegionFileValid | 判断虚拟地址区间region是否与文件关联映射 |
LOS_RegionAlloc | 申请空闲的虚拟地址区间 |
LOS_RegionFree | 释放进程空间内特定的region |
LOS_RegionEndAddr | 获取指定地址区间region的结束地址 |
LOS_RegionSize | 获取region的大小 |
LOS_IsRegionTypeFile | 判断是否为文件内存映射 |
LOS_IsRegionPermUserReadOnly | 判断地址区间是否是用户空间只读属性 |
LOS_IsRegionFlagPrivateOnly | 判断地址区间是否是具有私有属性 |
LOS_SetRegionTypeFile | 设置文件内存映射属性 |
LOS_IsRegionTypeDev | 判断是否为设备内存映射 |
LOS_SetRegionTypeDev | 设置设备内存映射属性 |
LOS_IsRegionTypeAnon | 判断是否为匿名映射 |
LOS_SetRegionTypeAnon | 设置匿名映射属性 |
表5 地址校验
接口名称 | 描述 |
---|---|
LOS_IsUserAddress | 判断地址是否在用户态空间 |
LOS_IsUserAddressRange | 判断地址区间是否在用户态空间 |
LOS_IsKernelAddress | 判断地址是否在内核空间 |
LOS_IsKernelAddressRange | 判断地址区间是否在内核空间 |
LOS_IsRangeInSpace | 判断地址区间是否在进程空间内 |
表6 vmalloc操作
接口名称 | 描述 |
---|---|
LOS_VMalloc | vmalloc申请内存 |
LOS_VFree | vmalloc释放内存 |
LOS_IsVmallocAddress | 判断地址是否是通过vmalloc申请的 |
表7 内存申请系列接口
接口名称 | 描述 |
---|---|
LOS_KernelMalloc | 当申请的内存小于16KiB时,系统从堆内存池分配内存;当申请的内存超过16KiB时,系统分配多个连续物理页用于内存分配 |
LOS_KernelMallocAlign | 申请具有对齐属性的内存,申请规则同LOS_KernelMalloc接口 |
LOS_KernelFree | 释放由LOS_KernelMalloc和LOS_KernelMallocAlign接口申请的内存 |
LOS_KernelRealloc | 重新分配由LOS_KernelMalloc和LOS_KernelMallocAlign接口申请的内存 |
表8 其他
接口名称 | 描述 |
LOS_PaddrQuery | 根据虚拟地址获取对应的物理地址 |
LOS_VmSpaceFree | 释放进程空间,包括虚拟内存区间、页表等信息 |
LOS_VmSpaceReserve | 在进程空间中预留一块内存空间 |
LOS_VaddrToPaddrMmap | 将指定长度的物理地址区间与虚拟地址区间做映射,需提前申请物理地址区间 |
开发流程
虚拟内存相关接口的使用:
-
根据进程空间获取的系列接口可以得到进程空间结构体,进而可以读取结构体相应信息。
-
对虚拟地址区间做相关操作:
-
通过LOS_RegionAlloc申请虚拟地址区间;
-
通过LOS_RegionFind、LOS_RegionRangeFind可以查询是否存在相应的地址区间;
-
通过LOS_RegionFree释放虚拟地址区间。
-
-
vmalloc接口及内存申请系列接口可以在内核中根据需要申请内存。
说明: 内存申请系列接口申请的内存要求物理内存是连续的,当系统内存无法满足大块连续内存的申请条件时会申请失败,一般适用于小块内存的申请;vmalloc相关接口申请的内存可以获得不连续的物理内存,但其是以页(当前系统一个页为4096字节)为单位的,当需要申请以页为整数倍的内存时可以通过vmalloc申请,例如文件系统中文件读取需要较大的缓存,便可以通过vmalloc相关接口申请内存。
3.3.4、虚实映射
基本概念
虚实映射是指系统通过内存管理单元(MMU,Memory Management Unit)将进程空间的虚拟地址与实际的物理地址做映射,并指定相应的访问权限、缓存属性等。程序执行时,CPU访问的是虚拟内存,通过MMU页表条目找到对应的物理内存,并做相应的代码执行或数据读写操作。MMU的映射由页表(Page Table)来描述,其中保存虚拟地址和物理地址的映射关系以及访问权限等。每个进程在创建的时候都会创建一个页表,页表由一个个页表条目(Page Table Entry, PTE)构成,每个页表条目描述虚拟地址区间与物理地址区间的映射关系。MMU中有一块页表缓存,称为快表(TLB, Translation Lookaside Buffers),做地址转换时,MMU首先在TLB中查找,如果找到对应的页表条目可直接进行转换,提高了查询效率。CPU访问内存或外设的示意图如下:
图1 CPU访问内存或外设的示意图
运行机制
虚实映射其实就是一个建立页表的过程。MMU支持多级页表,LiteOS-A内核采用二级页表描述进程空间。每个一级页表条目描述符占用4个字节,可表示1MiB的内存空间的映射关系,即1GiB用户空间(LiteOS-A内核中用户空间占用1GiB)的虚拟内存空间需要1024个。系统创建用户进程时,在内存中申请一块4KiB大小的内存块作为一级页表的存储区域,系统根据当前进程的需要会动态申请内存作为二级页表的存储区域。
-
用户程序加载启动时,会将代码段、数据段映射进虚拟内存空间(详细可参考动态加载与链接),此时并没有物理页做实际的映射;
-
程序执行时,如下图粗箭头所示,CPU访问虚拟地址,通过MMU查找是否有对应的物理内存,若该虚拟地址无对应的物理地址则触发缺页异常,内核申请物理内存并将虚实映射关系及对应的属性配置信息写进页表,并把页表条目缓存至TLB,接着CPU可直接通过转换关系访问实际的物理内存;
-
若CPU访问已缓存至TLB的页表条目,无需再访问保存在内存中的页表,可加快查找速度。
图2 CPU访问内存示意图
开发指导
接口说明
表1 MMU相关操作
接口名称 | 描述 |
---|---|
LOS_ArchMmuQuery | 获取进程空间虚拟地址对应的物理地址以及映射属性。 |
LOS_ArchMmuMap | 映射进程空间虚拟地址区间与物理地址区间。 |
LOS_ArchMmuUnmap | 解除进程空间虚拟地址区间与物理地址区间的映射关系。 |
LOS_ArchMmuChangeProt | 修改进程空间虚拟地址区间的映射属性。 |
LOS_ArchMmuMove | 将进程空间一个虚拟地址区间的映射关系转移至另一块未使用的虚拟地址区间重新做映射。 |
开发流程
虚实映射相关接口的使用:
-
通过LOS_ArchMmuMap映射一块物理内存。
-
对映射的地址区间做相关操作:
-
通过LOS_ArchMmuQuery可以查询相应虚拟地址区间映射的物理地址区间及映射属性;
-
通过LOS_ArchMmuChangeProt修改映射属性;
-
通过LOS_ArchMmuMove做虚拟地址区间的重映射。
-
-
通过LOS_ArchMmuUnmap解除映射关系。
说明: 上述接口的使用都是基于MMU初始化完成以及相关进程页表的建立,MMU在系统启动阶段已完成初始化,进程创建的时候会建立页表,开发者无需介入操作。
3.4、内核通信机制
3.4.1、事件
基本概念
事件(Event)是一种任务间通信的机制,可用于任务间的同步。
多任务环境下,任务之间往往需要同步操作,一个等待即是一个同步。事件可以提供一对多、多对多的同步操作。
-
一对多同步模型:一个任务等待多个事件的触发。可以是任意一个事件发生时唤醒任务处理事件,也可以是几个事件都发生后才唤醒任务处理事件。
-
多对多同步模型:多个任务等待多个事件的触发。
OpenHarmony LiteOS-A的事件模块提供的事件,具有如下特点:
-
任务通过创建事件控制块来触发事件或等待事件。
-
事件间相互独立,内部实现为一个32位无符号整型,每一位标识一种事件类型。(0表示该时间类型未发生,1表示该事件类型已经发生,一共31种事件类型,第25bit位(
0x02U << 24
)系统保留) -
事件仅用于任务间的同步,不提供数据传输功能。
-
多次向事件控制块写入同一事件类型,在被清零前等效于只写入一次。
-
多个任务可以对同一事件进行读写操作。
-
支持事件读写超时机制。
运行机制
事件控制块
/**
* 事件控制块数据结构
*/
typedef struct tagEvent {
UINT32 uwEventID; /* 事件集合,表示已经处理(写入和清零)的事件集合 */
LOS_DL_LIST stEventList; /* 等待特定事件的任务链表 */
} EVENT_CB_S, *PEVENT_CB_S;
事件运作原理
事件初始化:会创建一个事件控制块,该控制块维护一个已处理的事件集合,以及等待特定事件的任务链表。
写事件:会向事件控制块写入指定的事件,事件控制块更新事件集合,并遍历任务链表,根据任务等待具体条件满足情况决定是否唤醒相关任务。
读事件:如果读取的事件已存在时,会直接同步返回。其他情况会根据超时时间以及事件触发情况,来决定返回时机:等待的事件条件在超时时间耗尽之前到达,阻塞任务会被直接唤醒,否则超时时间耗尽该任务才会被唤醒。
读事件条件满足与否取决于入参eventMask和mode,eventMask即需要关注的事件类型掩码。mode是具体处理方式,分以下三种情况:
-
LOS_WAITMODE_AND:逻辑与,基于接口传入的事件类型掩码eventMask,只有这些事件都已经发生才能读取成功,否则该任务将阻塞等待或者返回错误码。
-
LOS_WAITMODE_OR:逻辑或,基于接口传入的事件类型掩码eventMask,只要这些事件中有任一种事件发生就可以读取成功,否则该任务将阻塞等待或者返回错误码。
-
LOS_WAITMODE_CLR:这是一种附加读取模式,需要与所有事件模式或任一事件模式结合使用(LOS_WAITMODE_AND | LOS_WAITMODE_CLR或 LOS_WAITMODE_OR | LOS_WAITMODE_CLR)。在这种模式下,当设置的所有事件模式或任一事件模式读取成功后,会自动清除事件控制块中对应的事件类型位。
事件清零:根据指定掩码,去对事件控制块的事件集合进行清零操作。当掩码为0时,表示将事件集合全部清零。当掩码为0xffff时,表示不清除任何事件,保持事件集合原状。
事件销毁:销毁指定的事件控制块。
图1 小型系统事件运作原理图
开发指导
接口说明
OpenHarmony LiteOS-A内核的事件模块提供下面几种功能。
表1 事件模块接口
功能分类 | 接口描述 |
---|---|
初始化事件 | LOS_EventInit:初始化一个事件控制块 |
读/写事件 | - LOS_EventRead:读取指定事件类型,超时时间为相对时间:单位为Tick - LOS_EventWrite:写指定的事件类型 |
清除事件 | LOS_EventClear:清除指定的事件类型 |
校验事件掩码 | - LOS_EventPoll:根据用户传入的事件ID、事件掩码及读取模式,返回用户传入的事件是否符合预期 - LOS_EventDestroy:销毁指定的事件控制块 |
销毁事件 | LOS_EventDestroy:销毁指定的事件控制块 |
开发流程
事件的典型开发流程:
-
初始化事件控制块
-
阻塞读事件控制块
-
写入相关事件
-
阻塞任务被唤醒,读取事件并检查是否满足要求
-
处理事件控制块
-
事件控制块销毁
说明:
进行事件读写操作时,事件的第25bit(
0x02U << 24
)为保留bit位,不可以进行位设置。对同一事件反复写入,算作一次写入。
编程实例
实例描述
示例中,任务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得以执行,直到任务结束。
编程示例
本演示代码在./kernel/liteos_a/testsuites/kernel/src/osTest.c中编译验证,在TestTaskEntry中调用验证入口函数Example_EventEntry。
示例代码如下:
#include "los_event.h"
#include "los_task.h"
#include "securec.h"
/* 任务ID */
UINT32 g_testTaskId;
/* 事件控制结构体 */
EVENT_CB_S g_exampleEvent;
/* 等待的事件类型 */
#define EVENT_WAIT 0x00000001
#define EVENT_TIMEOUT 500
/* 用例任务入口函数 */
VOID Example_Event(VOID)
{
UINT32 event;
/* 超时等待方式读事件,超时时间为100 ticks, 若100 ticks后未读取到指定事件,读事件超时,任务直接唤醒 */
dprintf("Example_Event wait event 0x%x \n", EVENT_WAIT);
event = LOS_EventRead(&g_exampleEvent, EVENT_WAIT, LOS_WAITMODE_AND, EVENT_TIMEOUT);
if (event == EVENT_WAIT) {
dprintf("Example_Event,read event :0x%x\n", event);
} else {
dprintf("Example_Event,read event timeout\n");
}
}
UINT32 Example_EventEntry(VOID)
{
UINT32 ret;
TSK_INIT_PARAM_S task1;
/* 事件初始化 */
ret = LOS_EventInit(&g_exampleEvent);
if (ret != LOS_OK) {
dprintf("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 = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
task1.usTaskPrio = 5;
ret = LOS_TaskCreate(&g_testTaskId, &task1);
if (ret != LOS_OK) {
dprintf("task create failed.\n");
return LOS_NOK;
}
/* 写g_testTaskId 等待事件 */
dprintf("Example_TaskEntry write event.\n");
ret = LOS_EventWrite(&g_exampleEvent, EVENT_WAIT);
if (ret != LOS_OK) {
dprintf("event write failed.\n");
return LOS_NOK;
}
/* 清标志位 */
dprintf("EventMask:%d\n", g_exampleEvent.uwEventID);
LOS_EventClear(&g_exampleEvent, ~g_exampleEvent.uwEventID);
dprintf("EventMask:%d\n", g_exampleEvent.uwEventID);
return LOS_OK;
}
结果验证
编译运行得到的结果为:
Example_Event wait event 0x1
Example_TaskEntry write event.
Example_Event,read event :0x1
EventMask:1
EventMask:0
3.4.2、信号量
基本概念
信号量(Semaphore)是一种实现任务间通信的机制,可以实现任务间同步或共享资源的互斥访问。
一个信号量的数据结构中,通常有一个计数值,用于对有效资源数的计数,表示剩下的可被使用的共享资源数,其值的含义分两种情况:
-
0,表示该信号量当前不可获取,因此可能存在正在等待该信号量的任务。
-
正值,表示该信号量当前可被获取。
以同步为目的的信号量和以互斥为目的的信号量在使用上有如下不同:
-
用作互斥时,初始信号量计数值不为0,表示可用的共享资源个数。在需要使用共享资源前,先获取信号量,然后使用一个共享资源,使用完毕后释放信号量。这样在共享资源被取完,即信号量计数减至0时,其他需要获取信号量的任务将被阻塞,从而保证了共享资源的互斥访问。另外,当共享资源数为1时,建议使用二值信号量,一种类似于互斥锁的机制。
-
用作同步时,初始信号量计数值为0。任务1因获取不到信号量而阻塞,直到任务2或者某中断释放信号量,任务1才得以进入Ready或Running态,从而达到了任务间的同步。
运行机制
信号量控制块
/**
* 信号量控制块数据结构
*/
typedef struct {
UINT16 semStat; /* 信号量状态 */
UINT16 semType; /* 信号量类型 */
UINT16 semCount; /* 信号量计数 */
UINT16 semId; /* 信号量索引号 */
LOS_DL_LIST semList; /* 用于插入阻塞于信号量的任务 */
} LosSemCB;
信号量运作原理
信号量允许多个任务在同一时刻访问共享资源,但会限制同一时刻访问此资源的最大任务数目。当访问资源的任务数达到该资源允许的最大数量时,会阻塞其他试图获取该资源的任务,直到有任务释放该信号量。
-
信号量初始化 初始化时为配置的N个信号量申请内存(N值可以由用户自行配置,通过LOSCFG_BASE_IPC_SEM_LIMIT宏实现),并把所有信号量初始化成未使用,加入到未使用链表中供系统使用。
-
信号量创建 从未使用的信号量链表中获取一个信号量,并设定初值。
-
信号量申请 若其计数器值大于0,则直接减1返回成功。否则任务阻塞,等待其它任务释放该信号量,等待的超时时间可设定。当任务被一个信号量阻塞时,将该任务挂到信号量等待任务队列的队尾。
-
信号量释放 若没有任务等待该信号量,则直接将计数器加1返回。否则唤醒该信号量等待任务队列上的第一个任务。
-
信号量删除 将正在使用的信号量置为未使用信号量,并挂回到未使用链表。
运行示意图如下图所示:
图1 小型系统信号量运作示意图
开发指导
接口说明
表1 创建/删除信号量
接口名称 | 描述 |
---|---|
LOS_SemCreate | 创建信号量,返回信号量ID。 |
LOS_BinarySemCreate | 创建二值信号量,其计数值最大为1。 |
LOS_SemDelete | 删除指定的信号量。 |
表2 申请/释放信号量
接口名称 | 描述 |
---|---|
LOS_SemPend | 申请指定的信号量,并设置超时时间。 |
LOS_SemPost | 释放指定的信号量。 |
开发流程
-
创建信号量LOS_SemCreate,若要创建二值信号量则调用LOS_BinarySemCreate。
-
申请信号量LOS_SemPend。
-
释放信号量LOS_SemPost。
-
删除信号量LOS_SemDelete。
说明: 由于中断不能被阻塞,因此不能在中断中使用阻塞模式申请信号量。
编程实例
实例描述
本实例实现如下功能:
-
测试任务ExampleSem创建一个信号量,锁任务调度,创建两个任务ExampleSemTask1、ExampleSemTask2, ExampleSemTask2优先级高于ExampleSemTask1,两个任务中申请同一信号量,解锁任务调度后两任务阻塞,测试任务ExampleSem释放信号量。
-
ExampleSemTask2得到信号量,被调度,然后任务休眠20Ticks,ExampleSemTask2延迟,ExampleSemTask1被唤醒。
-
ExampleSemTask1定时阻塞模式申请信号量,等待时间为10Ticks,因信号量仍被ExampleSemTask2持有,ExampleSemTask1挂起,10Ticks后仍未得到信号量,ExampleSemTask1被唤醒,试图以永久阻塞模式申请信号量,ExampleSemTask1挂起。
-
20Tick后ExampleSemTask2唤醒, 释放信号量后,ExampleSemTask1得到信号量被调度运行,最后释放信号量。
-
ExampleSemTask1执行完,400Ticks后任务ExampleSem被唤醒,执行删除信号量。
编程示例
本演示代码在./kernel/liteos_a/testsuites/kernel/src/osTest.c中编译验证,在TestTaskEntry中调用验证入口函数ExampleSem。
示例代码如下:
#include "los_sem.h"
#include "securec.h"
/* 任务ID */
static UINT32 g_testTaskId01;
static UINT32 g_testTaskId02;
/* 测试任务优先级 */
#define TASK_PRIO_LOW 5
#define TASK_PRIO_HI 4
/* 信号量结构体id */
static UINT32 g_semId;
VOID ExampleSemTask1(VOID)
{
UINT32 ret;
dprintf("ExampleSemTask1 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) {
dprintf("ExampleSemTask1 timeout and try get sem g_semId wait forever.\n");
/*永久阻塞模式申请信号量*/
ret = LOS_SemPend(g_semId, LOS_WAIT_FOREVER);
dprintf("ExampleSemTask1 wait_forever and get sem g_semId.\n");
if (ret == LOS_OK) {
dprintf("ExampleSemTask1 post sem g_semId.\n");
LOS_SemPost(g_semId);
return;
}
}
}
VOID ExampleSemTask2(VOID)
{
UINT32 ret;
dprintf("ExampleSemTask2 try get sem g_semId wait forever.\n");
/* 永久阻塞模式申请信号量 */
ret = LOS_SemPend(g_semId, LOS_WAIT_FOREVER);
if (ret == LOS_OK) {
dprintf("ExampleSemTask2 get sem g_semId and then delay 20 ticks.\n");
}
/* 任务休眠20 ticks */
LOS_TaskDelay(20);
dprintf("ExampleSemTask2 post sem g_semId.\n");
/* 释放信号量 */
LOS_SemPost(g_semId);
return;
}
UINT32 ExampleSem(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)ExampleSemTask1;
task1.pcName = "TestTask1";
task1.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
task1.usTaskPrio = TASK_PRIO_LOW;
ret = LOS_TaskCreate(&g_testTaskId01, &task1);
if (ret != LOS_OK) {
dprintf("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)ExampleSemTask2;
task2.pcName = "TestTask2";
task2.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
task2.usTaskPrio = TASK_PRIO_HI;
ret = LOS_TaskCreate(&g_testTaskId02, &task2);
if (ret != LOS_OK) {
dprintf("task2 create failed.\n");
return LOS_NOK;
}
/* 解锁任务调度 */
LOS_TaskUnlock();
/* 任务休眠400 ticks */
LOS_TaskDelay(400);
ret = LOS_SemPost(g_semId);
/* 任务休眠400 ticks */
LOS_TaskDelay(400);
/* 删除信号量 */
LOS_SemDelete(g_semId);
return LOS_OK;
}
结果验证
编译运行得到的结果为:
ExampleSemTask2 try get sem g_semId wait forever.
ExampleSemTask1 try get sem g_semId, timeout 10 ticks.
ExampleSemTask1 timeout and try get sem g_semId wait forever.
ExampleSemTask2 get sem g_semId and then delay 20 ticks.
ExampleSemTask2 post sem g_semId.
ExampleSemTask1 wait_forever and get sem g_semId.
ExampleSemTask1 post sem g_semId.
3.4.3、互斥锁
基本概念
互斥锁又称互斥型信号量,用于实现对共享资源的独占式处理。当有任务持有时,这个任务获得该互斥锁的所有权。当该任务释放它时,任务失去该互斥锁的所有权。当一个任务持有互斥锁时,其他任务将不能再持有该互斥锁。多任务环境下往往存在多个任务竞争同一共享资源的应用场景,互斥锁可被用于对共享资源的保护从而实现独占式访问。
互斥锁属性包含3个属性:协议属性、优先级上限属性和类型属性。协议属性用于处理不同优先级的任务申请互斥锁,协议属性包含如下三种:
-
LOS_MUX_PRIO_NONE 不对申请互斥锁的任务的优先级进行继承或保护操作。
-
LOS_MUX_PRIO_INHERIT 优先级继承属性,默认设置为该属性,对申请互斥锁的任务的优先级进行继承。在互斥锁设置为本协议属性情况下,申请互斥锁时,如果高优先级任务阻塞于互斥锁,则把持有互斥锁任务的优先级备份到任务控制块的优先级位图中,然后把任务优先级设置为和高优先级任务相同的优先级;持有互斥锁的任务释放互斥锁时,从任务控制块的优先级位图恢复任务优先级。
-
LOS_MUX_PRIO_PROTECT 优先级保护属性,对申请互斥锁的任务的优先级进行保护。在互斥锁设置为本协议属性情况下,申请互斥锁时,如果任务优先级小于互斥锁优先级上限,则把任务优先级备份到任务控制块的优先级位图中,然后把任务优先级设置为互斥锁优先级上限属性值;释放互斥锁时,从任务控制块的优先级位图恢复任务优先级。
互斥锁的类型属性用于标记是否检测死锁,是否支持递归持有,类型属性包含如下三种:
-
LOS_MUX_NORMAL 普通互斥锁,不会检测死锁。如果任务试图对一个互斥锁重复持有,将会引起这个线程的死锁。如果试图释放一个由别的任务持有的互斥锁,或者如果一个任务试图重复释放互斥锁都会引发不可预料的结果。
-
LOS_MUX_RECURSIVE 递归互斥锁,默认设置为该属性。在互斥锁设置为本类型属性情况下,允许同一个任务对互斥锁进行多次持有锁,持有锁次数和释放锁次数相同,其他任务才能持有该互斥锁。如果试图持有已经被其他任务持有的互斥锁,或者如果试图释放已经被释放的互斥锁,会返回错误码。
-
LOS_MUX_ERRORCHECK 错误检测互斥锁,会自动检测死锁。在互斥锁设置为本类型属性情况下,如果任务试图对一个互斥锁重复持有,或者试图释放一个由别的任务持有的互斥锁,或者如果一个任务试图释放已经被释放的互斥锁,都会返回错误码。
运行机制
多任务环境下会存在多个任务访问同一公共资源的场景,而有些公共资源是非共享的,需要任务进行独占式处理。互斥锁怎样来避免这种冲突呢?
用互斥锁处理非共享资源的同步访问时,如果有任务访问该资源,则互斥锁为加锁状态。此时其他任务如果想访问这个公共资源则会被阻塞,直到互斥锁被持有该锁的任务释放后,其他任务才能重新访问该公共资源,此时互斥锁再次上锁,如此确保同一时刻只有一个任务正在访问这个公共资源,保证了公共资源操作的完整性。
图1 小型系统互斥锁运作示意图
开发指导
接口说明
表1 互斥锁模块接口
功能分类 | 接口描述 |
---|---|
初始化和销毁互斥锁 | - LOS_MuxInit:互斥锁初始化 - LOS_MuxDestroy:销毁指定的互斥锁 |
互斥锁的申请和释放 | - LOS_MuxLock:申请指定的互斥锁 - LOS_MuxTrylock:尝试申请指定的互斥锁,不阻塞 - LOS_MuxUnlock:释放指定的互斥锁 |
校验互斥锁 | - LOS_MuxIsValid:判断互斥锁释放有效 - LOS_MuxAttrDestroy:销毁指定的互斥锁属性 |
设置和获取互斥锁属性 | - LOS_MuxAttrGetType:获取指定互斥锁属性的类型属性 - LOS_MuxAttrSetType:设置指定互斥锁属性的类型属性 - LOS_MuxAttrGetProtocol:获取指定互斥锁属性的协议属性 - LOS_MuxAttrSetProtocol:设置指定互斥锁属性的协议属性 - LOS_MuxAttrGetPrioceiling:获取指定互斥锁属性的优先级上限属性 - LOS_MuxAttrSetPrioceiling:设置指定互斥锁属性的优先级上限属性 - LOS_MuxGetPrioceiling:获取互斥锁优先级上限属性 - LOS_MuxSetPrioceiling:设置互斥锁优先级上限属性 |
开发流程
互斥锁典型场景的开发流程:
-
初始化互斥锁LOS_MuxInit。
-
申请互斥锁LOS_MuxLock。
申请模式有三种:无阻塞模式、永久阻塞模式、定时阻塞模式。
-
无阻塞模式:任务需要申请互斥锁,若该互斥锁当前没有任务持有,或者持有该互斥锁的任务和申请该互斥锁的任务为同一个任务,则申请成功;
-
永久阻塞模式:任务需要申请互斥锁,若该互斥锁当前没有被占用,则申请成功。否则,该任务进入阻塞态,系统切换到就绪任务中优先级高者继续执行。任务进入阻塞态后,直到有其他任务释放该互斥锁,阻塞任务才会重新得以执行;
-
定时阻塞模式:任务需要申请互斥锁,若该互斥锁当前没有被占用,则申请成功。否则该任务进入阻塞态,系统切换到就绪任务中优先级高者继续执行。任务进入阻塞态后,指定时间超时前有其他任务释放该互斥锁,或者用 户指定时间超时后,阻塞任务才会重新得以执行。
- 释放互斥锁LOS_MuxUnlock。
-
如果有任务阻塞于指定互斥锁,则唤醒被阻塞任务中优先级高的,该任务进入就绪态,并进行任务调度;
-
如果没有任务阻塞于指定互斥锁,则互斥锁释放成功。
- 销毁互斥锁LOS_MuxDestroy。
说明:
两个任务不能对同一把互斥锁加锁。如果某任务对已被持有的互斥锁加锁,则该任务会被挂起,直到持有该锁的任务对互斥锁解锁,才能执行对这把互斥锁的加锁操作。
互斥锁不能在中断服务程序中使用。
LiteOS-A内核作为实时操作系统需要保证任务调度的实时性,尽量避免任务的长时间阻塞,因此在获得互斥锁之后,应该尽快释放互斥锁。
编程实例
实例描述
本实例实现如下流程:
-
任务Example_TaskEntry创建一个互斥锁,锁任务调度,创建两个任务Example_MutexTask1、Example_MutexTask2。Example_MutexTask2优先级高于Example_MutexTask1,解锁任务调度。
-
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成功获取到互斥锁后,释放,删除互斥锁。
编程示例
本演示代码在./kernel/liteos_a/testsuites/kernel/src/osTest.c中编译验证,在TestTaskEntry中调用验证入口函数Example_MutexEntry。
示例代码如下:
#include <string.h>
#include "los_mux.h"
/* 互斥锁 */
LosMux g_testMutex;
/* 任务ID */
UINT32 g_testTaskId01;
UINT32 g_testTaskId02;
VOID Example_MutexTask1(VOID)
{
UINT32 ret;
LOS_TaskDelay(50);
dprintf("task1 try to get mutex, wait 10 ticks.\n");
/* 申请互斥锁 */
ret = LOS_MuxLock(&g_testMutex, 10);
if (ret == LOS_OK) {
dprintf("task1 get mutex g_testMux.\n");
/* 释放互斥锁 */
LOS_MuxUnlock(&g_testMutex);
return;
}
if (ret == LOS_ETIMEDOUT) {
dprintf("task1 timeout and try to get mutex, wait forever.\n");
/* 申请互斥锁 */
ret = LOS_MuxLock(&g_testMutex, LOS_WAIT_FOREVER);
if (ret == LOS_OK) {
dprintf("task1 wait forever, get mutex g_testMux.\n");
/* 释放互斥锁 */
LOS_MuxUnlock(&g_testMutex);
/* 删除互斥锁 */
LOS_MuxDestroy(&g_testMutex);
dprintf("task1 post and delete mutex g_testMux.\n");
return;
}
}
return;
}
VOID Example_MutexTask2(VOID)
{
dprintf("task2 try to get mutex, wait forever.\n");
/* 申请互斥锁 */
(VOID)LOS_MuxLock(&g_testMutex, LOS_WAIT_FOREVER);
dprintf("task2 get mutex g_testMux and suspend 100 ticks.\n");
/* 任务休眠100Ticks */
LOS_TaskDelay(100);
dprintf("task2 resumed and post the g_testMux\n");
/* 释放互斥锁 */
LOS_MuxUnlock(&g_testMutex);
return;
}
UINT32 Example_MutexEntry(VOID)
{
UINT32 ret;
TSK_INIT_PARAM_S task1;
TSK_INIT_PARAM_S task2;
/* 初始化互斥锁 */
LOS_MuxInit(&g_testMutex, NULL);
/* 锁任务调度 */
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) {
dprintf("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) {
dprintf("task2 create failed.\n");
return LOS_NOK;
}
/* 解锁任务调度 */
LOS_TaskUnlock();
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.
task1 post and delete mutex g_testMux.
3.4.4、消息队列
基本概念
队列又称消息队列,是一种常用于任务间通信的数据结构。队列接收来自任务或中断的不固定长度消息,并根据不同的接口确定传递的消息是否存放在队列空间中。
任务能够从队列里面读取消息,当队列中的消息为空时,挂起读取任务;当队列中有新消息时,挂起的读取任务被唤醒并处理新消息。任务也能够往队列里写入消息,当队列已经写满消息时,挂起写入任务;当队列中有空闲消息节点时,挂起的写入任务被唤醒并写入消息。
可以通过调整读队列和写队列的超时时间来调整读写接口的阻塞模式,如果将读队列和写队列的超时时间设置为0,就不会挂起任务,接口会直接返回,这就是非阻塞模式。反之,如果将读队列和写队列的超时时间设置为大于0的时间,就会以阻塞模式运行。
消息队列提供了异步处理机制,允许将一个消息放入队列,但不立即处理。同时队列还有缓冲消息的作用,可以使用队列实现任务异步通信,队列具有如下特性:
-
消息以先进先出的方式排队,支持异步读写。
-
读队列和写队列都支持超时机制。
-
每读取一条消息,就会将该消息节点设置为空闲。
-
发送消息类型由通信双方约定,可以允许不同长度(不超过队列的消息节点大小)的消息。
-
一个任务能够从任意一个消息队列接收和发送消息。
-
多个任务能够从同一个消息队列接收和发送消息。
-
创建队列时所需的队列空间,接口内系统自行动态申请内存。
运行机制
队列控制块
/**
* 队列控制块数据结构
*/
typedef struct {
UINT8 *queueHandle; /**< Pointer to a queue handle */
UINT16 queueState; /**< Queue state */
UINT16 queueLen; /**< Queue length */
UINT16 queueSize; /**< Node size */
UINT32 queueID; /**< queueID */
UINT16 queueHead; /**< Node head */
UINT16 queueTail; /**< Node tail */
UINT16 readWriteableCnt[OS_QUEUE_N_RW]; /**< Count of readable or writable resources, 0:readable, 1:writable */
LOS_DL_LIST readWriteList[OS_QUEUE_N_RW]; /**< the linked list to be read or written, 0:readlist, 1:writelist */
LOS_DL_LIST memList; /**< Pointer to the memory linked list */
} LosQueueCB;
每个队列控制块中都含有队列状态,表示该队列的使用情况:
-
OS_QUEUE_UNUSED:队列未被使用。
-
OS_QUEUE_INUSED:队列被使用中。
队列运作原理
-
创建队列时,创建队列成功会返回队列ID。
-
在队列控制块中维护着一个消息头节点位置Head和一个消息尾节点位置Tail,用于表示当前队列中消息的存储情况。Head表示队列中被占用的消息节点的起始位置。Tail表示被占用的消息节点的结束位置,也是空闲消息节点的起始位置。队列刚创建时,Head和Tail均指向队列起始位置。
-
写队列时,根据readWriteableCnt[1]判断队列是否可以写入,不能对已满(readWriteableCnt[1]为0)队列进行写操作。写队列支持两种写入方式:向队列尾节点写入,也可以向队列头节点写入。尾节点写入时,根据Tail找到起始空闲消息节点作为数据写入对象,如果Tail已经指向队列尾部则采用回卷方式。头节点写入时,将Head的前一个节点作为数据写入对象,如果Head指向队列起始位置则采用回卷方式。
-
读队列时,根据readWriteableCnt[0]判断队列是否有消息需要读取,对全部空闲(readWriteableCnt[0]为0)队列进行读操作会引起任务挂起。如果队列可以读取消息,则根据Head找到最先写入队列的消息节点进行读取。如果Head已经指向队列尾部则采用回卷方式。
-
删除队列时,根据队列ID找到对应队列,把队列状态置为未使用,把队列控制块置为初始状态,并释放队列所占内存。
图1 队列读写数据操作示意图
上图对读写队列做了示意,图中只画了尾节点写入方式,没有画头节点写入,但是两者是类似的。
开发指导
接口说明
功能分类 | 接口描述 |
---|---|
创建/删除消息队列 | - LOS_QueueCreate:创建一个消息队列,由系统动态申请队列空间 - LOS_QueueDelete:根据队列ID删除一个指定队列 |
读/写队列(不带拷贝) | - LOS_QueueRead:读取指定队列头节点中的数据(队列节点中的数据实际上是一个地址) - LOS_QueueWrite:向指定队列尾节点中写入入参bufferAddr的值(即buffer的地址) - LOS_QueueWriteHead:向指定队列头节点中写入入参bufferAddr的值(即buffer的地址) |
读/写队列(带拷贝) | - LOS_QueueReadCopy:读取指定队列头节点中的数据 - LOS_QueueWriteCopy:向指定队列尾节点中写入入参bufferAddr中保存的数据 - LOS_QueueWriteHeadCopy:向指定队列头节点中写入入参bufferAddr中保存的数据 |
获取队列信息 | LOS_QueueInfoGet:获取指定队列的信息,包括队列ID、队列长度、消息节点大小、头节点、尾节点、可读节点数量、可写节点数量、等待读操作的任务、等待写操作的任务 |
开发流程
-
用LOS_QueueCreate创建队列。创建成功后,可以得到队列ID。
-
通过LOS_QueueWrite或者LOS_QueueWriteCopy写队列。
-
通过LOS_QueueRead或者LOS_QueueReadCopy读队列。
-
通过LOS_QueueInfoGet获取队列信息。
-
通过LOS_QueueDelete删除队列。
说明:
系统支持的最大队列数是指:整个系统的队列资源总个数,而非用户能使用的个数。例如:系统软件定时器多占用一个队列资源,那么用户能使用的队列资源就会减少一个。
创建队列时传入的队列名和flags暂时未使用,作为以后的预留参数。
队列接口函数中的入参timeOut是相对时间。
LOS_QueueReadCopy和LOS_QueueWriteCopy及LOS_QueueWriteHeadCopy是一组接口,LOS_QueueRead和LOS_QueueWrite及LOS_QueueWriteHead是一组接口,每组接口需要配套使用。
鉴于LOS_QueueWrite和LOS_QueueWriteHead和LOS_QueueRead这组接口实际操作的是数据地址,用户必须保证调用LOS_QueueRead获取到的指针所指向的内存区域在读队列期间没有被异常修改或释放,否则可能导致不可预知的后果。
LOS_QueueRead和LOS_QueueReadCopy接口的读取长度如果小于消息实际长度,消息将被截断。
鉴于LOS_QueueWrite和LOS_QueueWriteHead和LOS_QueueRead这组接口实际操作的是数据地址,也就意味着实际写和读的消息长度仅仅是一个指针数据,因此用户使用这组接口之前,需确保创建队列时的消息节点大小,为一个指针的长度,避免不必要的浪费和读取失败。
编程实例
实例描述
创建一个队列,两个任务。任务1调用写队列接口发送消息,任务2通过读队列接口接收消息。
-
通过LOS_TaskCreate创建任务1和任务2。
-
通过LOS_QueueCreate创建一个消息队列。
-
在任务1 SendEntry中发送消息。
-
在任务2 RecvEntry中接收消息。
-
通过LOS_QueueDelete删除队列。
编程示例
本演示代码在./kernel/liteos_a/testsuites/kernel/src/osTest.c中编译验证,在TestTaskEntry中调用验证入口函数ExampleQueue,
为方便用户观察,建议调用ExampleQueue前先调用 LOS_Msleep(5000) 进行短时间延时,避免其他打印过多。
示例代码如下:
#include "los_task.h"
#include "los_queue.h"
static UINT32 g_queue;
#define BUFFER_LEN 50
VOID SendEntry(VOID)
{
UINT32 ret = 0;
CHAR abuf[] = "test message";
UINT32 len = sizeof(abuf);
ret = LOS_QueueWriteCopy(g_queue, abuf, len, 0);
if(ret != LOS_OK) {
dprintf("send message failure, error: %x\n", ret);
}
}
VOID RecvEntry(VOID)
{
UINT32 ret = 0;
CHAR readBuf[BUFFER_LEN] = {0};
UINT32 readLen = BUFFER_LEN;
LOS_Msleep(1000);
ret = LOS_QueueReadCopy(g_queue, readBuf, &readLen, 0);
if(ret != LOS_OK) {
dprintf("recv message failure, error: %x\n", ret);
}
dprintf("recv message: %s\n", readBuf);
ret = LOS_QueueDelete(g_queue);
if(ret != LOS_OK) {
dprintf("delete the queue failure, error: %x\n", ret);
}
dprintf("delete the queue success!\n");
}
UINT32 ExampleQueue(VOID)
{
dprintf("start queue example\n");
UINT32 ret = 0;
UINT32 task1, task2;
TSK_INIT_PARAM_S initParam = {0};
ret = LOS_QueueCreate("queue", 5, &g_queue, 0, 50);
if(ret != LOS_OK) {
dprintf("create queue failure, error: %x\n", ret);
}
dprintf("create the queue success!\n");
initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)SendEntry;
initParam.usTaskPrio = 9;
initParam.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
initParam.pcName = "SendQueue";
LOS_TaskLock();
ret = LOS_TaskCreate(&task1, &initParam);
if(ret != LOS_OK) {
dprintf("create task1 failed, error: %x\n", ret);
LOS_QueueDelete(g_queue);
return ret;
}
initParam.pcName = "RecvQueue";
initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)RecvEntry;
ret = LOS_TaskCreate(&task2, &initParam);
if(ret != LOS_OK) {
dprintf("create task2 failed, error: %x\n", ret);
LOS_QueueDelete(g_queue);
return ret;
}
LOS_TaskUnlock();
LOS_Msleep(5000);
return ret;
}
结果验证
编译运行得到的结果为:
start queue example
create the queue success!
recv message: test message
delete the queue success!
3.4.5、读写锁
基本概念
读写锁与互斥锁类似,可用来同步同一进程中的各个任务,但与互斥锁不同的是,其允许多个读操作并发重入,而写操作互斥。
相对于互斥锁的开锁或闭锁状态,读写锁有三种状态:读模式下的锁,写模式下的锁,无锁。
读写锁的使用规则:
-
保护区无写模式下的锁,任何任务均可以为其增加读模式下的锁。
-
保护区处于无锁状态下,才可增加写模式下的锁。
多任务环境下往往存在多个任务访问同一共享资源的应用场景,读模式下的锁以共享状态对保护区访问,而写模式下的锁可被用于对共享资源的保护从而实现独占式访问。
这种共享-独占的方式非常适合多任务中读数据频率远大于写数据频率的应用中,提高应用多任务并发度。
运行机制
相较于互斥锁,读写锁如何实现读模式下的锁及写模式下的锁来控制多任务的读写访问呢?
-
若A任务首次获取了写模式下的锁,有其他任务来获取或尝试获取读模式下的锁,均无法再上锁。
-
若A任务获取了读模式下的锁,当有任务来获取或尝试获取读模式下的锁时,读写锁计数均加一。
开发指导
接口说明
表1 读写锁模块接口
功能分类 | 接口描述 |
---|---|
读写锁的创建和删除 | - LOS_RwlockInit:创建读写锁 - LOS_RwlockDestroy:删除指定的读写锁 |
读模式下的锁的申请 | - LOS_RwlockRdLock:申请指定的读模式下的锁 - LOS_RwlockTryRdLock:尝试申请指定的读模式下的锁 |
写模式下的锁的申请 | - LOS_RwlockWrLock:申请指定的写模式下的锁 - LOS_RwlockTryWrLock:尝试申请指定的写模式下的锁 |
读写锁的释放 | LOS_RwlockUnLock:释放指定读写锁 |
读写锁有效性判断 | LOS_RwlockIsValid:判断读写锁有效性 |
开发流程
读写锁典型场景的开发流程:
-
创建读写锁LOS_RwlockInit。
-
申请读模式下的锁LOS_RwlockRdLock或写模式下的锁LOS_RwlockWrLock。
申请读模式下的锁:
-
若无人持有锁,读任务可获得锁。
-
若有人持有锁,读任务可获得锁,读取顺序按照任务优先级。
-
若有人(非自己)持有写模式下的锁,则当前任务无法获得锁,直到写模式下的锁释放。
申请写模式下的锁:
-
若该锁当前没有任务持有,或者持有该读模式下的锁的任务和申请该锁的任务为同一个任务,则申请成功,可立即获得写模式下的锁。
-
若该锁当前已经存在读模式下的锁,且读取任务优先级较高,则当前任务挂起,直到读模式下的锁释放。
-
3.申请读模式下的锁和写模式下的锁均有三种:无阻塞模式、永久阻塞模式、定时阻塞模式,区别在于挂起任务的时间。
4.释放读写锁LOS_RwlockUnLock。
-
如果有任务阻塞于指定读写锁,则唤醒被阻塞任务中优先级高的,该任务进入就绪态,并进行任务调度;
-
如果没有任务阻塞于指定读写锁,则读写锁释放成功。
-
删除读写锁LOS_RwlockDestroy。
说明:
-
读写锁不能在中断服务程序中使用。
-
LiteOS-A内核作为实时操作系统需要保证任务调度的实时性,尽量避免任务的长时间阻塞,因此在获得读写锁之后,应该尽快释放该锁。
-
持有读写锁的过程中,不得再调用LOS_TaskPriSet等接口更改持有读写锁任务的优先级。
-
3.4.6、用户态快速互斥锁
基本概念
Futex(Fast userspace mutex,用户态快速互斥锁)是内核提供的一种系统调用能力,通常作为基础组件与用户态的相关锁逻辑结合组成用户态锁,是一种用户态与内核态共同作用的锁,例如用户态mutex锁、barrier与cond同步锁、读写锁。其用户态部分负责锁逻辑,内核态部分负责锁调度。
当用户态线程请求锁时,先在用户态进行锁状态的判断维护,若此时不产生锁的竞争,则直接在用户态进行上锁返回;反之,则需要进行线程的挂起操作,通过Futex系统调用请求内核介入来挂起线程,并维护阻塞队列。
当用户态线程释放锁时,先在用户态进行锁状态的判断维护,若此时没有其他线程被该锁阻塞,则直接在用户态进行解锁返回;反之,则需要进行阻塞线程的唤醒操作,通过Futex系统调用请求内核介入来唤醒阻塞队列中的线程。
运行机制
当用户态产生锁的竞争或释放需要进行相关线程的调度操作时,会触发Futex系统调用进入内核,此时会将用户态锁的地址传入内核,并在内核的Futex中以锁地址来区分用户态的每一把锁,因为用户态可用虚拟地址空间为1GiB,为了便于查找、管理,内核Futex采用哈希桶来存放用户态传入的锁。
当前哈希桶共有80个,0-63号桶用于存放私有锁(以虚拟地址进行哈希),64-79号桶用于存放共享锁(以物理地址进行哈希),私有/共享属性通过用户态锁的初始化以及Futex系统调用入参确定。
如下图所示,每个futex哈希桶中存放被futex_list串联起来的哈希值相同的futex node,每个futex node对应一个被挂起的task,node中key值唯一标识一把用户态锁,具有相同key值的node被queue_list串联起来表示被同一把锁阻塞的task队列。
图1 Futex设计图
Futex操作
Futex模块接口
Futex模块支持以下三种操作:
功能分类 | 接口名称 | 描述 |
---|---|---|
设置线程等待 | OsFutexWait | 向Futex表中插入代表被阻塞的线程的node |
唤醒被阻塞线程 | OsFutexWake | 唤醒一个被指定锁阻塞的线程 |
调整锁的地址 | OsFutexRequeue | 调整指定锁在Futex表中的位置 |
说明: Futex系统调用通常与用户态逻辑共同组成用户态锁,故推荐使用用户态POSIX接口的锁。
3.4.7、信号
基本概念
信号(signal)是一种常用的进程间异步通信机制,用软件的方式模拟中断信号,当一个进程需要传递信息给另一个进程时,则会发送一个信号给内核,再由内核将信号传递至指定进程,而指定进程不必进行等待信号的动作。
运行机制
信号的运作流程分为三个部分,如表1:
表1 信号的运作流程及相关接口(用户态接口)
功能分类 | 接口名称 | 描述 |
---|---|---|
注册信号回调函数 | signal | 注册信号总入口及注册和去注册某信号的回调函数。 |
注册信号回调函数 | sigaction | 功能同signal,但增加了信号发送相关的配置选项,目前仅支持SIGINFO结构体中的部分参数。 |
发送信号 | kill pthread_kill raise alarm abort | 发送信号给某个进程或进程内发送消息给某线程,为某进程下的线程设置信号标志位。 |
触发回调 | 无 | 由系统调用与中断触发,内核态与用户态切换前会先进入用户态指定函数并处理完相应回调函数,再回到原用户态程序继续运行。 |
说明: 信号机制为提供给用户态程序进程间通信的能力,故推荐使用上表1列出的用户态POSIX相关接口。
注册回调函数:
void *signal(int sig, void (*func)(int))(int);
31 号信号,该信号用来注册该进程的回调函数处理入口,不可重复注册。
0-30 号信号,该信号段用来注册与去注册回调函数。
注册回调函数:
int sigaction(int, const struct sigaction *__restrict, struct sigaction *__restrict);
支持信号注册的配置修改和配置获取,目前仅支持SIGINFO的选项,SIGINFO内容见sigtimedwait接口内描述。
发送信号:
进程接收信号存在默认行为,单不支持POSIX标准所给出的STOP及CONTINUE、COREDUMP功能。
进程无法屏蔽SIGSTOP、SIGKILL、SIGCONT信号。
某进程后被杀死后,若其父进程不回收该进程,其转为僵尸进程。
进程接收到某信号后,直到该进程被调度后才会执行信号回调。
进程结束后会发送SIGCHLD信号给父进程,该发送动作无法取消。
无法通过信号唤醒处于DELAY状态的进程。
3.5、时间管理
基本概念
时间管理以系统时钟为基础。时间管理提供给应用程序所有和时间有关的服务。系统时钟是由定时/计数器产生的输出脉冲触发中断而产生的,一般定义为整数或长整数。输出脉冲的周期叫做一个“时钟滴答”。
系统时钟也称为时标或者Tick。一个Tick的时长可以静态配置。用户是以秒、毫秒为单位计时,而操作系统时钟计时是以Tick为单位的,当用户需要对系统操作时,例如任务挂起、延时等,输入秒为单位的数值,此时需要时间管理模块对二者进行转换。
Tick与秒之间的对应关系可以配置。
-
Cycle 系统最小的计时单位。Cycle的时长由系统主频决定,系统主频就是每秒钟的Cycle数。
-
Tick Tick是操作系统的基本时间单位,对应的时长由系统主频及每秒Tick数决定,由用户配置。
OpenHarmony系统的时间管理模块提供时间转换、统计、延迟功能以满足用户对时间相关需求的实现。
开发指导
用户需要了解当前系统运行的时间以及Tick与秒、毫秒之间的转换关系,以及需要使用到时间管理模块的接口。
接口说明
OpenHarmony LiteOS-A内核的时间管理提供以下几种功能,接口详细信息可查看API参考。
表1 时间管理相关接口说明:
功能分类 | 接口描述 |
---|---|
时间转换 | LOS_MS2Tick:毫秒转换成Tick LOS_Tick2MS:Tick转换成毫秒 |
时间统计 | LOS_TickCountGet:获取当前Tick数 LOS_CyclePerTickGet:每个Tick的cycle数 |
开发流程
-
调用时间转换接口;
-
获取系统Tick数完成时间统计等。
说明:
获取系统Tick数需要在系统时钟使能之后。
时间管理不是单独的功能模块,依赖于los_config.h中的OS_SYS_CLOCK和LOSCFG_BASE_CORE_TICK_PER_SECOND两个配置选项。
系统的Tick数在关中断的情况下不进行计数,故系统Tick数不能作为准确时间计算。
编程实例
前置条件:
-
配置好LOSCFG_BASE_CORE_TICK_PER_SECOND,即系统每秒的Tick数,范围(0, 1000)。
-
配置好OS_SYS_CLOCK 系统时钟频率,单位:Hz。
示例代码
时间转换:
VOID Example_TransformTime(VOID)
{
UINT32 uwMs;
UINT32 uwTick;
uwTick = LOS_MS2Tick(10000); //10000 ms数转换为Tick数
PRINTK("uwTick = %d \n",uwTick);
uwMs= LOS_Tick2MS(100); //100 Tick数转换为ms数
PRINTK("uwMs = %d \n",uwMs);
}
时间统计和时间延迟:
VOID Example_GetTime(VOID)
{
UINT32 uwcyclePerTick;
UINT64 uwTickCount;
uwcyclePerTick = LOS_CyclePerTickGet(); //每个Tick多少Cycle数
if(0 != uwcyclePerTick)
{
PRINTK("LOS_CyclePerTickGet = %d \n", uwcyclePerTick);
}
uwTickCount = LOS_TickCountGet(); //获取Tick数
if(0 != uwTickCount)
{
PRINTK("LOS_TickCountGet = %d \n", (UINT32)uwTickCount);
}
LOS_TaskDelay(200);//延迟200 Tick
uwTickCount = LOS_TickCountGet();
if(0 != uwTickCount)
{
PRINTK("LOS_TickCountGet after delay = %d \n", (UINT32)uwTickCount);
}
}
结果验证
编译运行的结果如下:
时间转换:
uwTick = 10000
uwMs = 100
时间统计和时间延迟:
LOS_CyclePerTickGet = 49500
LOS_TickCountGet = 347931
LOS_TickCountGet after delay = 348134
3.6、软件定时器
基本概念
软件定时器,是基于系统Tick时钟中断且由软件来模拟的定时器,当经过设定的Tick时钟计数值后会触发用户定义的回调函数。定时精度与系统Tick时钟的周期有关。
硬件定时器受硬件的限制,数量上不足以满足用户的实际需求,因此为了满足用户需求,提供更多的定时器,OpenHarmony LiteOS-A内核提供软件定时器功能。
软件定时器扩展了定时器的数量,允许创建更多的定时业务。
软件定时器支持以下功能:
-
静态裁剪:能通过宏关闭软件定时器功能。
-
软件定时器创建。
-
软件定时器启动。
-
软件定时器停止。
-
软件定时器删除。
-
软件定时器剩余Tick数获取。
运行机制
软件定时器是系统资源,在模块初始化的时候已经分配了一块连续的内存,系统支持的最大定时器个数由los_config.h中的LOSCFG_BASE_CORE_SWTMR_LIMIT宏配置。
软件定时器使用了系统的一个队列和一个任务资源,软件定时器的触发遵循队列规则,先进先出。同一时刻设置的定时时间短的定时器总是比定时时间长的靠近队列头,满足优先被触发的准则。
软件定时器以Tick为基本计时单位,当用户创建并启动一个软件定时器时,OpenHarmony系统会根据当前系统Tick时间及用户设置的定时间隔确定该定时器的到期Tick时间,并将该定时器控制结构挂入计时全局链表。
当Tick中断到来时,在Tick中断处理函数中扫描软件定时器的计时全局链表,看是否有定时器超时,若有则将超时的定时器记录下来。
Tick中断处理函数结束后,软件定时器任务(优先级为最高)被唤醒,在该任务中调用之前记录下来的定时器的超时回调函数。
定时器状态:
-
OS_SWTMR_STATUS_UNUSED(未使用) 系统在定时器模块初始化的时候将系统中所有定时器资源初始化成该状态。
-
OS_SWTMR_STATUS_CREATED(创建未启动/停止) 在未使用状态下调用LOS_SwtmrCreate接口或者启动后调用LOS_SwtmrStop接口后,定时器将变成该状态。
-
OS_SWTMR_STATUS_TICKING(计数) 在定时器创建后调用LOS_SwtmrStart接口,定时器将变成该状态,表示定时器运行时的状态。
定时器模式:
-
第一类是单次触发定时器,这类定时器在启动后只会触发一次定时器事件,然后定时器自动删除。
-
第二类是周期触发定时器,这类定时器会周期性的触发定时器事件,直到用户手动停止定时器,否则将永远持续执行下去。
-
第三类也是单次触发定时器,但与第一类不同之处在于这类定时器超时后不会自动删除,需要调用定时器删除接口删除定时器。
开发指导
接口说明
OpenHarmony LiteOS-A内核的软件定时器模块提供以下几种功能。
表1 软件定时器接口说明
功能分类 | 接口描述 |
---|---|
创建、删除定时器。 | LOS_SwtmrCreate:创建软件定时器。 LOS_SwtmrDelete:删除软件定时器。 |
启动、停止定时器。 | LOS_SwtmrStart:启动软件定时器。 LOS_SwtmrStop:停止软件定时器。 |
获得软件定时剩余Tick数。 | LOS_SwtmrTimeGet:获得软件定时器剩余Tick数。 |
开发流程
软件定时器的典型开发流程:
-
配置软件定时器。
- 确认配置项LOSCFG_BASE_CORE_SWTMR和LOSCFG_BASE_IPC_QUEUE为打开状态;
- 配置LOSCFG_BASE_CORE_SWTMR_LIMIT最大支持的软件定时器数;
- 配置OS_SWTMR_HANDLE_QUEUE_SIZE软件定时器队列最大长度;
-
创建定时器LOS_SwtmrCreate。
- 创建一个指定计时时长、指定超时处理函数、指定触发模式的软件定时器;
- 返回函数运行结果,成功或失败;
-
启动定时器LOS_SwtmrStart。
-
获得软件定时器剩余Tick数LOS_SwtmrTimeGet。
-
停止定时器LOS_SwtmrStop。
-
删除定时器LOS_SwtmrDelete。
说明:
软件定时器的回调函数中不要做过多操作,不要使用可能引起任务挂起或者阻塞的接口或操作。
软件定时器使用了系统的一个队列和一个任务资源,软件定时器任务的优先级设定为0,且不允许修改 。
系统可配置的软件定时器资源个数是指:整个系统可使用的软件定时器资源总个数,而并非是用户可使用的软件定时器资源个数。例如:系统软件定时器多占用一个软件定时器资源数,那么用户能使用的软件定时器资源就会减少一个。
创建单次软件定时器,该定时器超时执行完回调函数后,系统会自动删除该软件定时器,并回收资源。
创建单次不自删除属性的定时器,用户需要调用定时器删除接口删除定时器,回收定时器资源,避免资源泄露。
编程实例
前置条件
-
在los_config.h中,将LOSCFG_BASE_CORE_SWTMR配置项打开。
-
配置好LOSCFG_BASE_CORE_SWTMR_LIMIT最大支持的软件定时器数。
-
配置好OS_SWTMR_HANDLE_QUEUE_SIZE软件定时器队列最大长度。
编程示例
#include "los_swtmr.h"
void Timer1_Callback(uint32_t arg);
void Timer2_Callback(uint32_t arg);
UINT32 g_timercount1 = 0;
UINT32 g_timercount2 = 0;
void Timer1_Callback(uint32_t arg) // 回调函数1。
{
unsigned long tick_last1;
g_timercount1++;
tick_last1=(UINT32)LOS_TickCountGet(); // 获取当前Tick数。
PRINTK("g_timercount1=%d\n",g_timercount1);
PRINTK("tick_last1=%d\n",tick_last1);
}
void Timer2_Callback(uint32_t arg) // 回调函数2。
{
unsigned long tick_last2;
tick_last2=(UINT32)LOS_TickCountGet();
g_timercount2 ++;
PRINTK("g_timercount2=%d\n",g_timercount2);
PRINTK("tick_last2=%d\n",tick_last2);
}
void Timer_example(void)
{
UINT16 id1;
UINT16 id2; // timer id.
UINT32 uwTick;
/* 创建单次软件定时器,Tick数为1000,启动到1000Tick数时执行回调函数1 */
LOS_SwtmrCreate (1000, LOS_SWTMR_MODE_ONCE, Timer1_Callback, &id1, 1);
/* 创建周期性软件定时器,每100Tick数执行回调函数2 */
LOS_SwtmrCreate(100, LOS_SWTMR_MODE_PERIOD, Timer2_Callback, &id2, 1);
PRINTK("create Timer1 success\n");
LOS_SwtmrStart (id1); //启动单次软件定时器。
dprintf("start Timer1 success\n");
LOS_TaskDelay(200); // 延时200Tick数。
LOS_SwtmrTimeGet(id1, &uwTick); // 获得单次软件定时器剩余Tick数。
PRINTK("uwTick =%d\n", uwTick);
LOS_SwtmrStop(id1); // 停止软件定时器。
PRINTK("stop Timer1 success\n");
LOS_SwtmrStart(id1);
LOS_TaskDelay(1000);
LOS_SwtmrDelete(id1); // 删除软件定时器。
PRINTK("delete Timer1 success\n");
LOS_SwtmrStart(id2); // 启动周期性软件定时器。
PRINTK("start Timer2\n");
LOS_TaskDelay(1000);
LOS_SwtmrStop(id2);
LOS_SwtmrDelete(id2);
}
运行结果
create Timer1 success
start Timer1 success
uwTick =800
stop Timer1 success
g_timercount1=1
tick_last1=1201
delete Timer1 success
start Timer2
g_timercount2 =1
tick_last1=1301
g_timercount2 =2
tick_last1=1401
g_timercount2 =3
tick_last1=1501
g_timercount2 =4
tick_last1=1601
g_timercount2 =5
tick_last1=1701
g_timercount2 =6
tick_last1=1801
g_timercount2 =7
tick_last1=1901
g_timercount2 =8
tick_last1=2001
g_timercount2 =9
tick_last1=2101
g_timercount2 =10
tick_last1=2201
3.7、原子操作
基本概念
在支持多任务的操作系统中,修改一块内存区域的数据需要“读取-修改-写入”三个步骤。然而同一内存区域的数据可能同时被多个任务访问,如果在修改数据的过程中被其他任务打断,就会造成该操作的执行结果无法预知。
使用开关中断的方法固然可以保证多任务执行结果符合预期,但是显然这种方法会影响系统性能。
ARMv6架构引入了LDREX和STREX指令,以支持对共享存储器更缜密的非阻塞同步。由此实现的原子操作能确保对同一数据的“读取-修改-写入”操作在它的执行期间不会被打断,即操作的原子性。
运行机制
OpenHarmony系统通过对ARMv6架构中的LDREX和STREX进行封装,向用户提供了一套原子性的操作接口。
-
LDREX Rx, [Ry] 读取内存中的值,并标记对该段内存的独占访问:
- 读取寄存器Ry指向的4字节内存数据,保存到Rx寄存器中。
- 对Ry指向的内存区域添加独占访问标记。
-
STREX Rf, Rx, [Ry] 检查内存是否有独占访问标记,如果有则更新内存值并清空标记,否则不更新内存:
- 有独占访问标记
- 将寄存器Rx中的值更新到寄存器Ry指向的内存。
- 标志寄存器Rf置为0。
- 没有独占访问标记
- 不更新内存。
- 标志寄存器Rf置为1。
- 有独占访问标记
-
判断标志寄存器
- 标志寄存器为0时,退出循环,原子操作结束。
- 标志寄存器为1时,继续循环,重新进行原子操作。
开发指导
接口说明
OpenHarmony LiteOS-A内核的原子操作模块提供以下几种功能。
表1 原子操作接口说明
功能分类 | 接口名称 | 描述 |
---|---|---|
读 | LOS_AtomicRead | 读取32bit原子数据 |
读 | LOS_Atomic64Read | 读取64bit原子数据 |
写 | LOS_AtomicSet | 设置32bit原子数据 |
写 | LOS_Atomic64Set | 设置64bit原子数据 |
加 | LOS_AtomicAdd | 对32bit原子数据做加法 |
加 | LOS_Atomic64Add | 对64bit原子数据做加法 |
加 | LOS_AtomicInc | 对32bit原子数据做加1 |
加 | LOS_Atomic64Inc | 对64bit原子数据做加1 |
加 | LOS_AtomicIncRet | 对32bit原子数据做加1并返回 |
加 | LOS_Atomic64IncRet | 对64bit原子数据做加1并返回 |
减 | LOS_AtomicSub | 对32bit原子数据做减法 |
减 | LOS_Atomic64Sub | 对64bit原子数据做减法 |
减 | LOS_AtomicDec | 对32bit原子数据做减1 |
减 | LOS_Atomic64Dec | 对64bit原子数据做减1 |
减 | LOS_AtomicDecRet | 对32bit原子数据做减1并返回 |
减 | LOS_Atomic64DecRet | 对64bit原子数据做减1并返回 |
交换 | LOS_AtomicXchgByte | 交换8bit内存数据 |
交换 | LOS_AtomicXchg16bits | 交换16bit内存数据 |
交换 | LOS_AtomicXchg32bits | 交换32bit内存数据 |
交换 | LOS_AtomicXchg64bits | 交换64bit内存数据 |
先比较后交换 | LOS_AtomicCmpXchgByte | 比较相同后交换8bit内存数据 |
先比较后交换 | LOS_AtomicCmpXchg16bits | 比较相同后交换16bit内存数据 |
先比较后交换 | LOS_AtomicCmpXchg32bits | 比较相同后交换32bit内存数据 |
先比较后交换 | LOS_AtomicCmpXchg64bits | 比较相同后交换64bit内存数据 |
开发流程
有多个任务对同一个内存数据进行加减或交换等操作时,使用原子操作保证结果的可预知性。
说明: 原子操作接口仅支持整型数据。
编程实例
实例描述
调用原子操作相关接口,观察结果:
-
创建两个任务。
- 任务一用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_AtomicTaskEntry(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);
PRINTK("g_sum = %d\n", g_sum);
return LOS_OK;
}
结果验证
g_sum = 0