
OpenHarmony设备开发 轻量系统内核(LiteOS-M)内核调测(上)
内存调测
内存调测方法旨在辅助定位动态内存相关问题,提供了基础的动态内存池信息统计手段,向用户呈现内存池水线、碎片率等信息;提供了内存泄漏检测手段,方便用户准确定位存在内存泄漏的代码行,也可以辅助分析系统各个模块内存的使用情况;提供了踩内存检测手段,可以辅助定位越界踩内存的场景。
内存信息统计
基础概念
内存信息包括内存池大小、内存使用量、剩余内存大小、最大空闲内存、内存水线、内存节点数统计、碎片率等。
- 内存水线:即内存池的最大使用量,每次申请和释放时,都会更新水线值,实际业务可根据该值,优化内存池大小;
- 碎片率:衡量内存池的碎片化程度,碎片率高表现为内存池剩余内存很多,但是最大空闲内存块很小,可以用公式(fragment=100-100*最大空闲内存块大小/剩余内存大小)来度量;
- 其他参数:通过调用接口(详见内存管理章节接口说明),扫描内存池的节点信息,统计出相关信息。
功能配置
LOSCFG_MEM_WATERLINE:开关宏,默认打开;若关闭这个功能,在target_config.h中将这个宏定义为0。如需获取内存水线,需要打开该配置。
开发指导
开发流程
关键结构体介绍:
- 内存水线获取:调用LOS_MemInfoGet接口,第1个参数是内存池首地址,第2个参数是LOS_MEM_POOL_STATUS类型的句柄,其中字段usageWaterLine即水线值。
- 内存碎片率计算:同样调用LOS_MemInfoGet接口,可以获取内存池的剩余内存大小和最大空闲内存块大小,然后根据公式(fragment=100-100*最大空闲内存块大小/剩余内存大小)得出此时的动态内存池碎片率。
编程实例
本实例实现如下功能:
1.创建一个监控任务,用于获取内存池的信息;
2.调用LOS_MemInfoGet接口,获取内存池的基础信息;
3.利用公式算出使用率及碎片率。
示例代码
代码实现如下:
本演示代码在 ./kernel/liteos_m/testsuites/src/osTest.c 中编译验证,在TestTaskEntry中调用验证入口函数MemTest。
结果验证
编译运行输出的结果如下:
内存泄漏检测
基础概念
内存泄漏检测机制作为内核的可选功能,用于辅助定位动态内存泄漏问题。开启该功能,动态内存机制会自动记录申请内存时的函数调用关系(下文简称LR)。如果出现泄漏,就可以利用这些记录的信息,找到内存申请的地方,方便进一步确认。
功能配置
- LOSCFG_MEM_LEAKCHECK:开关宏,默认关闭;若打开这个功能,在target_config.h中将这个宏定义为1。
- LOSCFG_MEM_RECORD_LR_CNT:记录的LR层数,默认3层;每层LR消耗sizeof(void *)字节数的内存。
- LOSCFG_MEM_OMIT_LR_CNT:忽略的LR层数,默认4层,即从调用LOS_MemAlloc的函数开始记录,可根据实际情况调整。为啥需要这个配置?有3点原因如下:
- LOS_MemAlloc接口内部也有函数调用;
- 外部可能对LOS_MemAlloc接口有封装;
- LOSCFG_MEM_RECORD_LR_CNT 配置的LR层数有限;
正确配置这个宏,将无效的LR层数忽略,就可以记录有效的LR层数,节省内存消耗。
开发指导
开发流程
该调测功能可以分析关键的代码逻辑中是否存在内存泄漏。开启这个功能,每次申请内存时,会记录LR信息。在需要检测的代码段前后,调用LOS_MemUsedNodeShow接口,每次都会打印指定内存池已使用的全部节点信息,对比前后两次的节点信息,新增的节点信息就是疑似泄漏的内存节点。通过LR,可以找到具体申请的代码位置,进一步确认是否泄漏。
调用LOS_MemUsedNodeShow接口输出的节点信息格式如下:每1行为一个节点信息;第1列为节点地址,可以根据这个地址,使用GDB等手段查看节点完整信息;第2列为节点的大小,等于节点头大小+数据域大小;第3~5列为函数调用关系LR地址,可以根据这个值,结合汇编文件,查看该节点具体申请的位置。
注意:
开启内存检测会影响内存申请的性能,且每个内存节点都会记录LR地址,内存开销也加大。
编程实例
本实例实现如下功能:构建内存泄漏代码段。
- 调用LOS_MemUsedNodeShow接口,输出全部节点信息打印;
- 申请内存,但没有释放,模拟内存泄漏;
- 再次调用LOS_MemUsedNodeShow接口,输出全部节点信息打印;
- 将两次log进行对比,得出泄漏的节点信息;
- 通过LR地址,找出泄漏的代码位置;
示例代码
代码实现如下:
本演示代码在 ./kernel/liteos_m/testsuites/src/osTest.c 中编译验证,在TestTaskEntry中调用验证入口函数MemLeakTest。
qemu平台运行时需确保target_config.h 中对应的LOSCFG_MEM_FREE_BY_TASKID为0。
由于打开内存检测后,部分平台有其他任务运行,会频繁调用内存相关打印如:psp, start = xxxxx, end = xxxxxxx,请忽略打印或删除OsStackAddrGet函数中调用的打印即可。
结果验证
编译运行输出示例log如下:
对比两次log,差异如下,这些内存节点就是疑似泄漏的内存块:
部分汇编文件如下:
其中,通过查找0x080041ee,就可以发现该内存节点是在MemLeakTest接口里申请的且是没有释放的。
踩内存检测
基础概念
踩内存检测机制作为内核的可选功能,用于检测动态内存池的完整性。通过该机制,可以及时发现内存池是否发生了踩内存问题,并给出错误信息,便于及时发现系统问题,提高问题解决效率,降低问题定位成本。
功能配置
LOSCFG_BASE_MEM_NODE_INTEGRITY_CHECK:开关宏,默认关闭;若打开这个功能,在target_config.h中将这个宏定义为1。
- 开启这个功能,每次申请内存,会实时检测内存池的完整性。
- 如果不开启该功能,也可以调用LOS_MemIntegrityCheck接口检测,但是每次申请内存时,不会实时检测内存完整性,而且由于节点头没有魔鬼数字(开启时才有,省内存),检测的准确性也会相应降低,但对于系统的性能没有影响,故根据实际情况开关该功能。
由于该功能只会检测出哪个内存节点被破坏了,并给出前节点信息(因为内存分布是连续的,当前节点最有可能被前节点破坏)。如果要进一步确认前节点在哪里申请的,需开启内存泄漏检测功能,通过LR记录,辅助定位。
注意:
开启该功能,节点头多了魔鬼数字字段,会增大节点头大小。由于实时检测完整性,故性能影响较大;若性能敏感的场景,可以不开启该功能,使用LOS_MemIntegrityCheck接口检测。
开发指导
开发流程
通过调用LOS_MemIntegrityCheck接口检测内存池是否发生了踩内存,如果没有踩内存问题,那么接口返回0且没有log输出;如果存在踩内存问题,那么会输出相关log,详见下文编程实例的结果输出。
编程实例
本实例实现如下功能:
- 申请两个物理上连续的内存块;
- 通过memset构造越界访问,踩到下个节点的头4个字节;
- 调用LOS_MemIntegrityCheck检测是否发生踩内存。
示例代码
代码实现如下:
本演示代码在 ./kernel/liteos_m/testsuites/src/osTest.c 中编译验证,在TestTaskEntry中调用验证入口函数MemIntegrityTest。
qemu平台运行时需确保target_config.h 中对应的LOSCFG_MEM_FREE_BY_TASKID为0。
由于执行时主动触发异常,执行结束后需要重启qemu(例如打开一个新的终端界面输入killall qemu-system-arm)
结果验证
编译运行输出log如下:
异常调测
基本概念
OpenHarmony LiteOS-M提供异常接管调测手段,帮助开发者定位分析问题。异常接管是操作系统对运行期间发生的异常情况进行处理的一系列动作,例如打印异常发生时异常类型、发生异常时的系统状态、当前函数的调用栈信息、CPU现场信息、任务调用堆栈等信息。
运行机制
栈帧用于保存函数调用过程中的函数参数、变量、返回值等信息。调用函数时,会创建子函数的栈帧,同时将函数入参、局部变量、寄存器入栈。栈帧从高地址向低地址生长。以ARM32 CPU架构为例,每个栈帧中都会保存PC、LR、SP和FP寄存器的历史值。LR链接寄存器(Link Register)指向函数的返回地址,FP帧指针寄存器(Frame Point)指向当前函数的父函数的栈帧起始地址。利用FP寄存器可以得到父函数的栈帧,从栈帧中获取父函数的FP,就可以得到祖父函数的栈帧,以此类推,可以追溯程序调用栈,得到函数间的调用关系。
当系统发生异常时,系统打印异常函数的栈帧中保存的寄存器内容,以及父函数、祖父函数的栈帧中的LR链接寄存器、FP帧指针寄存器内容,用户就可以据此追溯函数间的调用关系,定位异常原因。
堆栈分析原理如下图所示,实际堆栈信息根据不同CPU架构有所差异,此处仅做示意。
图1 堆栈分析原理示意图
图中不同颜色的寄存器表示不同的函数。可以看到函数调用过程中,寄存器的保存。通过FP寄存器,栈回溯到异常函数的父函数,继续按照规律对栈进行解析,推出函数调用关系,方便用户定位问题。
接口说明
OpenHarmony LiteOS-M内核的回溯栈模块提供下面几种功能,接口详细信息可以查看API参考。
表1 回溯栈模块接口
功能分类 | 接口名 |
回溯栈接口 | LOS_BackTrace:打印调用处的函数调用栈关系。 LOS_RecordLR:在无法打印的场景,用该接口获取调用处的函数调用栈关系。 |
使用指导
开发流程
开启异常调测的典型流程如下:
- 配置异常接管相关宏。 需要在target_config.h头文件中修改配置:
配置项 | 含义 | 设置值 |
LOSCFG_BACKTRACE_DEPTH | 函数调用栈深度,默认15层 | 15 |
LOSCFG_BACKTRACE_TYPE | 回溯栈类型: 0:表示关闭该功能; 1:表示支持Cortex-m系列硬件的函数调用栈解析; 2:表示用于Risc-v系列硬件的函数调用栈解析; | 根据工具链类型设置1或2 |
2.使用示例中有问题的代码,编译、运行工程,在串口终端中查看异常信息输出。示例代码模拟异常代码,实际产品开发时使用异常调测机制定位异常问题。 本示例演示异常输出,包含1个任务,该任务入口函数模拟若干函数调用,最终调用一个模拟异常的函数。代码实现如下:
本演示代码在 ./kernel/liteos_m/testsuites/src/osTest.c 中编译验证,在TestTaskEntry中调用验证入口函数ExampleExcEntry。
述代码串口终端输出异常信息如下:
定位流程
异常接管一般的定位步骤如下:
- 确认编译时关掉优化选项,否则下述的描述内容可能被优化掉。
- 打开编译后生成的镜像反汇编(asm)文件。如果默认没有生成,可以使用objdump工具生成,命令为:
- 搜索PC指针(指向当前正在执行的指令)在asm中的位置,找到发生异常的函数。 PC地址指向发生异常时程序正在执行的指令。在当前执行的二进制文件对应的asm文件中,查找PC值0x2101c61a,找到当前CPU正在执行的指令行,反汇编如下所示:
3.可以看到:
- 异常时CPU正在执行的指令是ldr r3, [r3, #0],其中r3取值为0xffffffff,导致发生非法地址异常。
- 异常发生在函数GetResultException0中。
4.根据LR值查找异常函数的父函数。 包含LR值0x2101c64d的反汇编如下所示:
5.LR值2101c648上一行是bl 2101c60c <GetResultException0>,此处调用了异常函数,调用异常函数的父函数为GetResultException1。
6.重复步骤3,解析异常信息中backtrace start至backtrace end之间的LR值,得到调用产生异常的函数调用栈关系,找到异常原因。
Trace调测
基本概念
Trace调测旨在帮助开发者获取内核的运行流程,各个模块、任务的执行顺序,从而可以辅助开发者定位一些时序问题或者了解内核的代码运行过程。
运行机制
内核提供一套Hook框架,将Hook点预埋在各个模块的主要流程中, 在内核启动初期完成Trace功能的初始化,并注册Trace的处理函数到Hook中。
当系统触发到一个Hook点时,Trace模块会对输入信息进行封装,添加Trace帧头信息,包含事件类型、运行的cpuid、运行的任务id、运行的相对时间戳等信息;
Trace提供2种工作模式,离线模式和在线模式。
离线模式会将trace frame记录到预先申请好的循环buffer中。如果循环buffer记录的frame过多则可能出现翻转,会覆盖之前的记录,故保持记录的信息始终是最新的信息。Trace循环buffer的数据可以通过shell命令导出进行详细分析,导出信息已按照时间戳信息完成排序。
在线模式需要配合IDE使用,实时将trace frame记录发送给IDE,IDE端进行解析并可视化展示。
接口说明
OpenHarmony LiteOS-M内核的Trace模块提供下面几种功能,接口详细信息可以查看API参考。
表1 Trace模块接口说明
功能分类 | 接口名 |
启停控制 | - LOS_TraceStart:启动Trace - LOS_TraceStop:停止Trace |
操作Trace记录的数据 | - LOS_TraceRecordDump:输出Trace缓冲区数据 - LOS_TraceRecordGet:获取Trace缓冲区的首地址 - LOS_TraceReset:清除Trace缓冲区中的事件 |
过滤Trace记录的数据 | LOS_TraceEventMaskSet:设置事件掩码,仅记录某些模块的事件 |
屏蔽某些中断号事件 | LOS_TraceHwiFilterHookReg:注册过滤特定中断号事件的钩子函数 |
插桩函数 | LOS_TRACE_EASY:简易插桩 LOS_TRACE:标准插桩 |
- 当用户需要针对自定义事件进行追踪时,可按规则在目标源代码中进行插桩,系统提供如下2种插桩接口:
- LOS_TRACE_EASY(TYPE, IDENTITY, params…) 简易插桩。
- 一句话插桩,用户在目标源代码中插入该接口即可。
- TYPE有效取值范围为[0, 0xF],表示不同的事件类型,不同取值表示的含义由用户自定义。
- IDENTITY类型UINTPTR,表示事件操作的主体对象。
- Params类型UINTPTR,表示事件的参数。
- 对文件fd读写操作的简易插桩示例:
- LOS_TRACE(TYPE, IDENTITY, params…) 标准插桩。
- 相比简易插桩,支持动态过滤事件和参数裁剪,但使用上需要用户按规则来扩展。
- TYPE用于设置具体的事件类型,可以在头文件los_trace.h中的enum LOS_TRACE_TYPE中自定义事件类型。定义方法和规则可以参考其他事件类型。
- IDENTITY和Params的类型及含义同简易插桩。
- 示例:
1.定义FS模块的类型,即FS模块的事件掩码
2.定义FS模块的具体事件类型
3.定义事件参数
4.在目标代码中插桩
说明:
预置的Trace事件及参数均可以通过上述方式进行裁剪,参数详见kernel\include\los_trace.h。
- Trace Mask事件过滤接口LOS_TraceEventMaskSet(UINT32 mask),其入参mask仅高28位生效(对应LOS_TRACE_MASK中某模块的使能位),仅用于模块的过滤,暂不支持针对某个特定事件的细粒度过滤。例如:LOS_TraceEventMaskSet(0x202),则实际设置生效的mask为0x200(TRACE_QUE_FLAG),QUE模块的所有事件均会被采集。一般建议使用的方法为: LOS_TraceEventMaskSet(TRACE_EVENT_FLAG | TRACE_MUX_FLAG | TRACE_SEM_FLAG | TRACE_QUE_FLAG);
- 如果仅需要简易插桩事件,通过设置Trace Mask为TRACE_MAX_FLAG即可。
- Trace缓冲区有限,事件写满之后会覆盖写,用户可通过LOS_TraceRecordDump中打印的CurEvtIndex识别最新记录。
- Trace的典型操作流程为:LOS_TraceStart、 LOS_TraceStop、 LOS_TraceRecordDump.
- 针对中断事件的Trace, 提供中断号过滤,用于解决某些场景下特定中断号频繁触发导致其他事件被覆盖的情况,用户可自定义中断过滤的规则, 示例如下:
则当中断号为TIMER_INT 或者DMA_INT时,不记录中断事件。
开发指导
开发流程
开启Trace调测的典型流程如下:
- 配置Trace模块相关宏。 需要在target_config.h头文件中修改配置:
配置项 | 含义 | 设置值 |
LOSCFG_KERNEL_TRACE | Trace模块的裁剪开关 | YES/NO |
LOSCFG_RECORDER_MODE_OFFLINE | Trace工作模式为离线模式 | YES/NO |
LOSCFG_RECORDER_MODE_ONLINE | Trace工作模式为在线模式 | YES/NO |
LOSCFG_TRACE_CLIENT_INTERACT | 使能与Trace IDE (dev tools)的交互,包括数据可视化和流程控制 | YES/NO |
LOSCFG_TRACE_FRAME_CORE_MSG | 记录CPUID、中断状态、锁任务状态 | YES/NO |
LOSCFG_TRACE_FRAME_EVENT_COUNT | 记录事件的次序编号 | YES/NO |
LOSCFG_TRACE_FRAME_MAX_PARAMS | 配置记录事件的最大参数个数 | INT |
LOSCFG_TRACE_BUFFER_SIZE | 配置Trace的缓冲区大小 | INT |
2.(可选)预置事件参数和事件桩(或使用系统默认的事件参数配置和事件桩)。
3.(可选)调用LOS_TraceStop停止Trace后,清除缓冲区LOS_TraceReset(系统默认已启动trace)。
4.(可选)调用LOS_TraceEventMaskSet设置需要追踪的事件掩码(系统默认的事件掩码仅使能中断与任务事件),事件掩码参见los_trace.h 中的LOS_TRACE_MASK定义。
5.在需要记录事件的代码起始点调用LOS_TraceStart。
6.在需要记录事件的代码结束点调用LOS_TraceStop。
7.调用LOS_TraceRecordDump输出缓冲区数据(函数的入参为布尔型,FALSE表示格式化输出,TRUE表示输出到windows客户端)。
上述第3-7步中的接口,均封装有对应的shell命令,对应关系如下
- LOS_TraceReset —— trace_reset
- LOS_TraceEventMaskSet —— trace_mask
- LOS_TraceStart —— trace_start
- LOS_TraceStop —— trace_stop
- LOS_TraceRecordDump —— trace_dump
编程实例
本实例实现如下功能:
- 创建一个用于Trace测试的任务。
- 设置事件掩码。
- 启动trace。
- 停止trace。
- 格式化输出trace数据。
示例代码
示例代码如下:
本演示代码在 ./kernel/liteos_m/testsuites/src/osTest.c 中编译验证,在TestTaskEntry中调用验证入口函数ExampleTraceTest。
结果验证
输出结果如下:
输出的事件信息包括:发生时间、事件类型、事件发生在哪个任务中、事件操作的主体对象、事件的其他参数。
- EventType:表示的具体事件可查阅头文件los_trace.h中的enum LOS_TRACE_TYPE。
- CurrentTask:表示当前运行在哪个任务中,值为taskid。
- Identity:表示事件操作的主体对象,可查阅头文件los_trace.h中的#TYPE#_PARAMS。
- params:表示的事件参数可查阅头文件los_trace.h中的#TYPE#_PARAMS。
下面以序号为0的输出项为例,进行说明。
- Time cycles可换算成时间,换算公式为cycles/clockFreq,单位为s。
- 0x45为TASK_SWITCH即任务切换事件,当前运行的任务taskid为0x1。
- Identity和params的含义需要查看TASK_SWITCH_PARAMS宏定义:
因为#TYPE#_PARAMS(IDENTITY, parma1…) IDENTITY, …,所以Identity为taskId(0x0),第一个参数为oldPriority(0x1f)
说明:
params的个数由LOSCFG_TRACE_FRAME_MAX_PARAMS配置,默认为3,超出的参数不会被记录,用户应自行合理配置该值。
综上所述,任务由0x1切换到0x0,0x1任务的优先级为0x1f,状态为0x4,0x0任务的优先级为0x0。
文章转载自:https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/device-dev/kernel/kernel-mini-memory-debug.md/
