Skip to content

Latest commit

 

History

History
2084 lines (1635 loc) · 115 KB

LiteOS_Maintenance_Guide.md

File metadata and controls

2084 lines (1635 loc) · 115 KB

Huawei LiteOS 维测指南

CPU占用率

概述

基本概念

Huawei LiteOS的CPUP(Central Processing Unit Percent,CPU占用率)分为系统CPU占用率和任务CPU占用率。

系统CPU占用率是指周期时间内系统的CPU占用率,用于表示系统一段时间内的闲忙程度,也表示CPU的负载情况。

任务CPU占用率指单个任务的CPU占用率,用于表示单个任务在一段时间内的闲忙程度。

运作机制

Huawei LiteOS的CPUP采用任务级记录的方式,在任务切换时,记录任务启动时间,任务切出或者退出时间。每次任务退出时,系统会累加任务的占用时间。

CPU占用率的计算方法:

系统CPU占用率=系统中除idle任务外其他任务运行总时间/系统运行总时间。

任务CPU占用率=任务运行总时间/系统运行总时间。

开发指导

使用场景

通过系统CPU占用率,判断当前系统负载是否超出设计规格。

通过系统中各个任务的CPU占用率,判断各个任务的CPU占用率是否符合设计的预期。

功能

Huawei LiteOS的CPU占用率模块为用户提供下面几种功能,接口详细信息可以查看API参考。

表 1 功能列表

功能分类

接口名

描述

获取系统CPU占用率

LOS_HistorySysCpuUsage

获取系统CPU占用率,不包含idle任务

获取任务CPU占用率

LOS_HistoryTaskCpuUsage

使能LOSCFG_CPUP_INCLUDE_IRQ后,获取指定中断(传入中断号)的CPU占用率;未使能LOSCFG_CPUP_INCLUDE_IRQ,获取指定任务的CPU占用率

LOS_AllCpuUsage

使能LOSCFG_CPUP_INCLUDE_IRQ且设置入参flag为0时,获取所有中断的CPU占用率。设置入参flag为非0,或者关闭LOSCFG_CPUP_INCLUDE_IRQ后,获取所有任务的CPU占用率,这里的任务也包含了idel任务

重置CPU占用率

LOS_CpupReset

重置CPU占用率数据,包含系统和任务的CPU占用率

说明:

  1. 通过上述接口获取到的CPU占用率是千分比,所以CPU占用率的有效表示范围为0~1000。系统CPU占用率为1000,表示系统满负荷运转。任务CPU占用率为1000,表示在一段时间内系统一直在运行该任务。
  2. 获取任务CPU占用率有三种模式,通过入参mode设置:
  • CPUP_LAST_TEN_SECONDS(值为0):表示获取最近10s内的CPU占用率。
  • CPUP_LAST_ONE_SECONDS(值为1): 表示获取最近1s的CPU占用率。
  • CPUP_ALL_TIME(值为0xffff),或除0和1之外的其他值:表示获取自系统启动以来的CPU占用率。

CPUP错误码

序号

定义

实际数值

描述

参考解决方案

1

LOS_ERRNO_CPUP_NO_MEMORY

0x02001e00

初始化CPU占用率模块时,内存不足

调整OS_SYS_MEM_SIZE,以确保有足够的内存供CPU占用率模块使用

2

LOS_ERRNO_CPUP_TASK_PTR_NULL

0x02001e01

入参存在空指针

传入正确的指针

3

LOS_ERRNO_CPUP_NO_INIT

0x02001e02

CPU占用率模块没有初始化

初始化CPU占用率模块后,再使用该模块

4

LOS_ERRNO_CPUP_MAXNUM_INVALID

0x02001e03

传入LOS_AllCpuUsage接口的入参(最大线程数或中断数)是一个无效值

传入正确的最大线程数或中断数

5

LOS_ERRNO_CPUP_THREAD_NO_CREATED

0x02001e04

获取指定任务的CPU占用率时,发现该任务未创建

系统只能统计已经被创建的任务的CPU占用率,检查传入的任务ID对应的任务是否已经创建

6

LOS_ERRNO_CPUP_TSK_ID_INVALID

0x02001e05

调用获取指定任务的CPU占用率的接口时,传入了无效的任务ID

检查传入的任务ID的正确性

须知: 错误码定义见错误码简介。8~15位的所属模块为CPUP模块,值为0x1e。

开发流程

CPU占用率的典型开发流程:

  1. 通过make menuconfig的配置CPU占用率模块。

    配置项

    含义

    取值范围

    默认值

    依赖

    LOSCFG_KERNEL_CPUP

    CPUP模块的裁剪开关

    YES/NO

    YES

    LOSCFG_KERNEL_EXTKERNEL

    LOSCFG_CPUP_INCLUDE_IRQ

    使能该配置项后,可以在LOS_HistoryTaskCpuUsage和LOS_AllCpuUsage接口中获取中断的CPU占用率。关闭该配置项后,只能获取任务的CPU占用率。

    YES/NO

    YES

    LOSCFG_KERNEL_CPUP

  2. 获取系统CPU使用率LOS_HistorySysCpuUsage。

  3. 获取指定任务或中断的CPU使用率LOS_HistoryTaskCpuUsage。

  4. 获取所有任务或所有中断的CPU使用率LOS_AllCpuUsage。

平台差异性

无。

注意事项

  • CPU占用率对性能有一定影响,而一般只有在产品开发时需要了解各个任务的占用率,因此建议在发布产品时,关闭CPU占用率。
  • 关闭配置项LOSCFG_CPUP_INCLUDE_IRQ后,系统中的中断耗时会被统计到中断发生的任务中。

编程实例

实例描述

本实例实现如下功能:

  1. 创建一个测试CPUP的任务。
  2. 获取系统最近1s内所有任务或中断的CPUP。
  3. 获取系统(除idel任务外)最近10s内的总CPU占用率。
  4. 获取CPUP测试任务的CPUP。

编程示例

前提条件:通过make menuconfig配置好CPU占用率模块。

代码实现如下:

#include <unistd.h >
#include "los_task.h"
#include "los_cpup.h"

#define MAXTASKNUM  32

UINT32 cpupUse;
UINT32 g_cpuTestTaskId;

VOID Example_CPUP(VOID)
{
    printf("entry cpup test example\n");
    while(1) {
        usleep(100);
    }
}

UINT32 Example_CPUP_Test(VOID)
{
    UINT32 ret;
    TSK_INIT_PARAM_S cpupTestTask;
    CPUP_INFO_S cpupInfo;

    /* 创建测试CPUP的任务 */
    memset(&cpupTestTask, 0, sizeof(TSK_INIT_PARAM_S));
    cpupTestTask.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_CPUP;
    cpupTestTask.pcName       = "TestCpupTsk";                           /* 测试任务名称 */
    cpupTestTask.uwStackSize  = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
    cpupTestTask.usTaskPrio   = 5;
    cpupTestTask.uwResved   = LOS_TASK_STATUS_DETACHED;

    ret = LOS_TaskCreate(&g_cpuTestTaskId, &cpupTestTask);
    if(ret != LOS_OK) {
        printf("cpupTestTask create failed.\n");
        return LOS_NOK;
    }
    usleep(100);

    /* 系统中运行的任务或者中断数量 */
    UINT16 maxNum = MAXTASKNUM; 

    /* 获取系统所有任务或中断最近1s的CPU占用率 */
    cpupUse = LOS_AllCpuUsage(maxNum, &cpupInfo, CPUP_LAST_ONE_SECONDS, 0);
    printf("the system cpu usage in last 1s: %d\n", cpupUse);

    /* 获取最近10s内系统(除idel任务外)总CPU占用率 */
    cpupUse = LOS_HistorySysCpuUsage(CPUP_LAST_TEN_SECONDS);
    printf("the history system cpu usage in last 10s: %d\n", cpupUse);

    /* 获取指定任务在最近1s的CPU占用率,该测试例程中指定的任务为上面创建的CPUP测试任务 */
    cpupUse = LOS_HistoryTaskCpuUsage(g_cpuTestTaskId, CPUP_LAST_ONE_SECONDS);
    printf("cpu usage of the cpupTestTask in last 1s:\n TaskID: %d\n usage: %d\n", g_cpuTestTaskId, cpupUse);

    return LOS_OK;
}

结果验证

编译运行得到的结果为:

the system cpu usage in last 1s: 15
the history system cpu usage in last 10s: 3
cpu usage of the cpupTestTask in last 1s:
TaskID: 10
usage: 0

完整实例代码

sample_cpup.c

Trace

概述

基本概念

Trace即追踪,实时记录系统行为,类似系统“录像”功能。在系统发生异常后,能辅助用户查看历史事件,定位问题。

运作机制

Huawei LiteOS的Trace采用静态代码打桩和缓冲区记录方式,在桩被执行时,获取事件发生的上下文,并写入到缓冲区。

插桩函数的入参中需要提供追踪的事件类型,事件操作的主体对象,事件的参数,这些信息会被写入缓冲区,同时缓冲区中也会记录事件发生的时间、系统中的任务信息等事件上下文信息。

开发指导

使用场景

  • 通过Trace了解系统运转的轨迹,理解系统。

  • 通过Trace分析系统发生异常前的操作,定位死机问题。

功能

Huawei LiteOS的Trace模块为用户提供下面几种功能,接口详细信息可以查看API参考。

表 1 功能列表

功能分类

接口名

描述

配置Trace缓冲区

LOS_TraceInit

配置Trace缓冲区的地址和大小

开启/停止Trace事件记录

LOS_TraceStart

开启事件记录

LOS_TraceStop

停止事件记录

操作Trace记录的数据

LOS_TraceRecordDump

输出Trace缓冲区数据

LOS_TraceRecordGet

获取Trace缓冲区的首地址

LOS_TraceReset

清除Trace缓冲区中的事件

过滤Trace记录的模块

LOS_TraceEventMaskSet

设置事件掩码,仅记录某些模块的事件

屏蔽某些中断号事件

LOS_TraceHwiFilterHookReg

注册过滤特定中断号事件的钩子函数

插桩函数

LOS_TRACE_EASY

简易插桩

LOS_TRACE

标准插桩

说明:

  1. LOS_TRACE_EASY(TYPE, IDENTITY, params...) 简易插桩。
  • 一句话插桩,用户在目标源代码中插入该接口即可。
  • TYPE有效取值范围为[0, 0xF],表示不同的事件类型。
  • IDENTITY类型UINTPTR,表示事件操作的主体对象。
  • Params类型UINTPTR,表示事件的参数。
  • 示例:
    LOS_TRACE_EASY(1, userId0, userParam1, userParam2);
    LOS_TRACE_EASY(2, userId0);
    LOS_TRACE_EASY(1, userId1, userParam1, userParam2);
    LOS_TRACE_EASY(2, userId1);
  1. LOS_TRACE(TYPE, IDENTITY, params...) 标准插桩。
  • 相比简易插桩,支持动态过滤事件和参数裁剪,但使用上需要用户按规则来扩展。
  • TYPE用于设置具体的事件类型,可以在头文件los_trace.h中的enum LOS_TRACE_TYPE中自定义事件类型。定义方法和规则可以参考其他事件类型。
  • IDENTITY和Params的类型及含义同简易插桩。
  • 示例:
    1.在enum LOS_TRACE_MASK中定义事件掩码,即模块级别的事件类型。定义规范为TRACE_#MOD#_FLAG,#MOD#表示模块名,例如:
      TRACE_FS_FLAG = 0x2000
    2.在enum LOS_TRACE_TYPE中定义具体事件类型。定义规范为#TYPE# = TRACE_#MOD#_FLAG | NUMBER,例如:
      FS_READ  = TRACE_FS_FLAG | 0; // 读文件
      FS_WRITE = TRACE_FS_FLAG | 1; // 写文件
    3.定义事件参数。定义规范为#TYPE#_PARAMS(IDENTITY, parma1...) IDENTITY, ...
      其中的#TYPE#就是上面2中的#TYPE#,例如:
      #define FS_READ_PARAMS(fp, fd, flag, size)    fp, fd, flag, size
      宏定义的参数对应于Trace缓冲区中记录的事件参数,用户可对任意参数字段进行裁剪;
      当定义为空时,表示不追踪该类型事件:
      #define FS_READ_PARAMS(fp, fd, flag, size) // 不追踪文件读事件
    4.在适当位置插入代码桩。定义规范为LOS_TRACE(#TYPE#, #TYPE#_PARAMS(IDENTITY, parma1...))
      LOS_TRACE(FS_READ, fp, fd, flag, size); // 读文件的代码桩,#TYPE#之后的入参就是上面3中的FS_READ_PARAMS函数的入参
    
  1. Huawei Liteos预置的Trace事件及参数均可以通过上述方式进行裁剪,参数详见kernel\include\los_trace.h。

Trace错误码

对Trace存在失败可能性的操作,包括初始化Trace、启动Trace 均需要返回对应的错误码,以便快速定位错误原因。其他无返回值的接口如停止Trace、清除与dump Trace 数据均为Trace状态不合法,系统会直接打印错误原因。

序号

定义

实际数值

描述

参考解决方案

1

LOS_ERRNO_TRACE_ERROR_STATUS

0x02001400

初始化Trace或启动Trace时状态不正确

禁止重复初始化Trace,未初始化Trace前禁止启动Trace

2

LOS_ERRNO_TRACE_NO_MEMORY

0x02001401

初始化Trace时,缓冲区申请失败

解决方案有两个:

  • 减小Trace缓冲区的大小,可以修改los_config.h中的LOS_TRACE_BUFFER_SIZE
  • 配置LOSCFG_BASE_CORE_TSK_LIMIT减少最大任务数

3

LOS_ERRNO_TRACE_BUF_TOO_SMALL

0x02001402

初始化Trace时,缓冲区size设置过小

增大Trace缓冲区的大小,可以修改los_config.h中的LOS_TRACE_BUFFER_SIZE

须知: 错误码定义见错误码简介。8~15位的所属模块为Trace模块,值为0x14。

开发流程

Trace的典型开发流程:

  1. 通过make menuconfig配置Trace。

    配置项

    含义

    取值范围

    默认值

    依赖

    LOSCFG_KERNEL_TRACE

    Trace模块的裁剪开关

    YES/NO

    NO

    LOSCFG_KERNEL_EXTKERNEL

    LOSCFG_RECORDER_MODE_OFFLINE

    Trace工作模式为离线模式

    YES/NO

    YES

    LOSCFG_KERNEL_TRACE

    LOSCFG_RECORDER_MODE_ONLINE

    Trace工作模式为在线模式

    YES/NO

    NO

    LOSCFG_KERNEL_TRACE

    LOSCFG_TRACE_CLIENT_INTERACT

    使能与Trace IDE (Huawei LiteOS Studio)的交互,包括数据可视化和流程控制

    YES/NO

    NO

    LOSCFG_KERNEL_TRACE

    LOSCFG_TRACE_PIPELINE_SERIAL

    选择串口作为IDE数据可视化通道

    YES/NO

    YES

    LOSCFG_TRACE_CLIENT_INTERACT

    LOSCFG_TRACE_CONTROL_VIA_SHELL

    选择shell作为IDE流程控制的方式

    YES/NO

    YES

    LOSCFG_TRACE_CLIENT_INTERACT && LOSCFG_SHELL

    LOSCFG_TRACE_CONTROL_AGENT

    选择agent作为IDE流程控制的方式

    YES/NO

    NO

    LOSCFG_TRACE_CLIENT_INTERACT

    LOSCFG_TRACE_NO_CONTROL

    不启用IDE的流程控制

    YES/NO

    NO

    LOSCFG_TRACE_CLIENT_INTERACT

  2. (可选)预置事件参数和事件桩(或使用系统默认的事件参数配置和事件桩)。

  3. (可选)调用LOS_TraceStop停止Trace后,清除缓冲区LOS_TraceReset(系统默认已启动trace)。

  4. (可选)调用LOS_TraceEventMaskSet设置需要追踪的事件掩码(系统默认的事件掩码仅使能中断与任务切换)。

  5. 在需要记录事件的代码起始点调用LOS_TraceStart。

  6. 在需要记录事件的代码结束点调用LOS_TraceStop。

  7. 调用LOS_TraceRecordDump输出缓冲区数据(函数的入参为布尔型,FALSE表示格式化输出,TRUE表示输出到windows客户端)。

平台差异性

无。

注意事项

由于Trace会影响系统性能,同时考虑到一般只有在产品开发时才需要了解系统发生的事件,因此建议在产品发布时关闭Trace。

编程实例

实例描述

本实例实现如下功能:

  1. 创建一个用于Trace测试的任务。
  2. 设置事件掩码。
  3. 启动trace。
  4. 停止trace。
  5. 格式化输出trace数据。

编程实例

前提条件:在menuconfig菜单中完成Trace模块的配置。

代码实现如下:

#include "los_trace.h"

UINT32 g_traceTestTaskId;
VOID Example_Trace(VOID)
{
    UINT32 ret;

    LOS_TaskDelay(10);

    /* 开启trace */
    ret = LOS_TraceStart();
    if (ret != LOS_OK) {
        dprintf("trace start error\n");
        return;
    }

    /* 触发任务切换的事件 */
    LOS_TaskDelay(1);

    LOS_TaskDelay(1);

    LOS_TaskDelay(1);

    /* 停止trace */
    LOS_TraceStop();

    LOS_TraceRecordDump(FALSE);
}

UINT32 Example_Trace_test(VOID)
{
    UINT32 ret;
    TSK_INIT_PARAM_S traceTestTask;

    /* 创建用于trace测试的任务 */
    memset(&traceTestTask, 0, sizeof(TSK_INIT_PARAM_S));
    traceTestTask.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_Trace;
    traceTestTask.pcName       = "TestTraceTsk";    /* 测试任务名称 */
    traceTestTask.uwStackSize  = 0x800;
    traceTestTask.usTaskPrio   = 5;
    traceTestTask.uwResved   = LOS_TASK_STATUS_DETACHED;
    ret = LOS_TaskCreate(&g_traceTestTaskId, &traceTestTask);
    if(ret != LOS_OK){
        dprintf("TraceTestTask create failed .\n");
        return LOS_NOK;
    }

    /* 系统默认情况下已启动trace, 因此可先关闭trace后清除缓存区后,再重启trace */
    LOS_TraceStop();
    LOS_TraceReset();

    /* 开启任务模块事件记录 */
    LOS_TraceEventMaskSet(TRACE_TASK_FLAG);

    return LOS_OK;
}

结果验证

编译运行得到的结果为:

 
*******TraceInfo begin*******
clockFreq = 50000000
CurObjIndex = 9
Index   TaskID   TaskPrio   TaskName 
0       0x10000  0          Swt_Task
1       0x10001  31         IdleCore000
2       0x10002  1          system_wq
3       0x10003  10         app_Task
4       0x10004  5          TestTraceTsk
5       0x10005  0          Swt_Task
6       0x10006  31         IdleCore000
7       0x10007  10         SendToSer
8       0x10008  5          tcpip_thread
9       0x0      0          

CurEvtIndex = 7
Index   Time(cycles)      EventType      CurTask   Identity      params    
0       0x78142f2         0x45           0x10004   0x10006       0x5          0x20         0x1f         
1       0x788b982         0x45           0x10006   0x10004       0x1f         0x4          0x5          
2       0x788ba2e         0x45           0x10004   0x10006       0x5          0x20         0x1f         
3       0x7905aa7         0x45           0x10003   0x10004       0xa          0x4          0x5          
4       0x7905b82         0x45           0x10004   0x10003       0x5          0x20         0xa          
5       0x7908402         0x45           0x10006   0x10008       0x1f         0x4          0x5          
6       0x79084a9         0x45           0x10003   0x10004       0xa          0x4          0x5   
 *******TraceInfo end*******

两段数据分别为任务信息和事件信息。

  • 任务信息:任务id, 任务优先级, 任务名称。

  • 事件信息:发生时间、事件类型、在哪个任务中发生、事件操作的主体对象、事件的其他参数。

    事件类型EventType表示的具体事件可查阅头文件los_trace.h中enum LOS_TRACE_TYPE了解。

    主体对象Identity描述的对象及params表示的事件参数可查阅头文件los_trace.h中#TYPE#_PARAMS了解。

完整实例代码

sample_trace.c

LMS

概述

基本概念

LMS全称为Lite Memory Sanitizer,是一种实时检测内存操作合法性的调测工具。LMS能够实时检测缓冲区溢出(buffer overflow),释放后使用(use after free),多重释放(double free)和释放野指针(wild pointer),在异常发生的第一时间通知操作系统,结合操作系统Backtrace等定位手段,能准确定位到产生内存问题的代码行,大大提升内存问题定位效率。

运作机制

LMS使用影子内存映射标记系统内存的状态,一共可标记为四个状态:可读写,不可读写,部分可读写,已释放。影子内存存放在内存池的尾节点中。

  • 编译程序时,会在数组和结构体等局部变量两侧插入红区,并将红区所映射的影子内存设置为“不可读写”。
  • 内存在堆上被释放时,会将被释放内存的影子内存设置为“已释放”状态。
  • 编译代码时,会在代码中的读写指令前插入检测函数,对地址的合法性进行检验。主要是检测访问内存的影子内存的状态值,若检测到影子内存为不可读写,则会报溢出错误;若检测到影子内存为已释放,则会报释放后使用(Use After Free)错误;若在调用free时检测到影子内存为已释放,则会报重复释放(Double Free)错误。

使用指南

使用场景

  • 疑似系统内存问题需要定位时。
  • 开发过程中需要验证是否存在内存问题时。

使用流程

  1. 在被检测模块的Makefile文件里,增加LMS检测编译选项-fsanitize=kernel-address。

  2. 通过menuconfig,开启LMS配置项:

    LOSCFG_KERNEL_LMS=y 【Debug-->Enable Lite Memory Sanitizer】

  3. 为输出Backtrace信息,需要开启配置项:

    LOSCFG_BACKTRACE=y 【Debug-->Enable Backtrace】

  4. 为避免编译器优化,需要配置编译器选项:

    LOSCFG_COMPILER_OPTIMIZE_NONE=y 【Compiler-->Optimize Option --> Optimize None】

  5. LMS只适配了bestfit内存算法,且不支持SLAB算法,需要开启如下配置项:

    LOSCFG_KERNEL_MEM_BESTFIT=y【Kernel-->Memory Management-->Dynamic Memory Management Algorithm】

    LOSCFG_KERNEL_MEM_SLAB_EXTENTION is not set【Kernel-->Memory Management-->Enable Mem SLAB Extension】

  6. 重新编译,查看串口输出。如果检测到内存问题,会输出检测结果。LMS输出的检测信息类似下图所示:

    LMS检测信息包含下述几类信息:

    • 检测到的内存问题,示例图中为释放后使用Use After Free
    • 发生问题的内存地址,示例图中为0x804308cb。
    • 发生问题的内存地址对应的影子内存及该影子内存的值,示例图中影子内存地址为0x83c7b74c,该地址的值为01。
    • 发生问题的内存地址前后内存的取值。
    • 当前运行的任务,示例图中为IT_TST_INI,任务ID为4。
    • 发生问题时的backtrace回溯栈。

平台差异性

无。

注意事项

  • LMS属于调测特性,在产品发布时,需关闭LMS模块的裁剪开关LOSCFG_KERNEL_LMS,删除LMS检测编译选项-fsanitize=kernel-address,恢复为开启LMS特性开启的其他配置项,参见使用流程
  • 如果被检测的模块有大量堆内存读写操作,需要增加任务栈大小。
  • 内存模块和LMS模块不需要增加LMS检测编译选项-fsanitize=kernel-address。
  • 如果要检测memcpy、memmove、strcat、strcpy、memcpy_s、memmove_s、strcat_s、strcpy_s这些函数的使用是否会引入内存问题,需要include头文件los_lms.h。
  • LMS不支持栈上内存、全局变量的溢出检测,需要编译器支持。
  • LMS当前只适配了bestfit内存算法,且不支持SLAB算法。

Shell

参见Shell使用教程

调度统计

功能说明

用于统计CPU的一些调度信息,包括idle任务启动时间、idle任务运行时长、调度切次数等。

使用方法

  1. 通过menuconfig开启该调度统计功能,即配置LOSCFG_DEBUG_SCHED_STATISTICS=y,该功能默认关闭。

  2. 将以下函数注册为Shell命令。Shell命令注册方法详见Shell使用教程中的“新增命令开发流程”。

    OsShellStatisticsStart---调度统计功能开启函数。

    OsShellStatisticsStop---调度统计功能关闭函数。关闭后,会自动调用OsStatisticsShow输出调度统计信息。

    OsShellCmdDumpSched---显示CPU调度信息的函数。

  3. 调度信息查看。

    在Shell窗口中调用注册的命令。

注意事项

先执行OsShellStatisticsStart对应的Shell命令,开启调度统计功能后,再查看调度信息。

输出说明

图1 调用OsShellStatisticsStop后输出

图中的mpstop是OsShellStatisticsStop注册的Shell命令,仅用于举例,实际上系统中并不存在该命令。图中各输出项说明如下表所示:

Passed Time

调度功能运行时长

CPU

CPU名称

Idle(%)

idle任务运行时长百分比

ContexSwitch

任务调度切换次数

HwiNum

中断触发次数

Avg Pri

切入任务不为idle任务的任务优先级平均值

HiTask(%)

高优先级任务运行时长所占百分比,定义优先级小于16为高优先级

HiTask SwiNum

切入新任务为高优先级的切换次数,定义优先级小于16为高优先级

HiTask P(ms)

高优先级任务运行的平均时长,定义优先级小于16为高优先级

MP Hwi

核间中断触发次数,仅用于多核

图2 调用OsShellCmdDumpSched输出

图中的mpstat是OsShellCmdDumpSched注册的Shell命令,仅用于举例,实际上系统中并不存在该命令。图中各输出项说明如下表所示:

Task

任务名称

TID

任务ID

Total Time

所有CPU的任务运行时长

Total CST

所有CPU任务上下文切换次数

CPU

CPU名称

Time

单CPU的任务运行时长

CST

单CPU的任务上下文切换次数

内存调测方法

多模块内存统计

使用场景

系统业务模块化清晰,用户需统计各模块的内存占用情况。

功能说明

Huawei LiteOS提供了一套基于内核内存接口的封装接口,增加模块ID作为入参。不同业务模块进行内存操作时,调用对应封装接口,可统计各模块的内存使用情况,并通过模块ID获取指定模块的内存使用情况。

功能分类

接口名

描述

为指定模块申请、释放动态内存,并纳入模块统计

LOS_MemMalloc

从指定动态内存池分配size长度的内存给指定模块

LOS_MemMfree

释放指定模块的内存块

LOS_MemMallocAlign

从指定动态内存池中申请长度为size且地址按boundary字节对齐的内存给指定模块

LOS_MemMrealloc

按size大小重新分配内存块给指定模块,并将原内存块内容拷贝到新内存块。如果新内存块申请成功,则释放原内存块

获取指定模块的内存使用量

LOS_MemMusedGet

获取指定模块的内存使用量,单位为Byte

使用方法

  1. 通过make menuconfig打开多模块内存统计功能。

    该功能依赖于LOSCFG_MEM_MUL_MODULE,使用时需要在配置项中开启“Enable Memory module statistics”:

    Debug  ---> Enable a Debug Version ---> Enable MEM Debug ---> Enable Memory module statistics
    
  2. 每个业务模块配置唯一module ID,模块代码中在内存操作时调用对应接口,并传入相应模块ID。

  3. 通过LOS_MemMusedGet接口获取指定模块的内存使用情况,可用于模块内存占用优化分析。

注意事项

  • 模块ID由宏MEM_MODULE_MAX限定,当系统模块个数超过该值时,需修改MEM_MODULE_MAX。
  • 模块中所有内存操作都需调用封装接口,否则可能导致统计不准确。
  • 目前只有bestfit内存管理算法支持该功能,需要使能LOSCFG_KERNEL_MEM_BESTFIT。

编程实例

void test(void)
{
    void *ptr = NULL;
    void *ptrTmp = NULL;
    UINT32 size = 0x10;
    UINT32 module = 0;
    UINT32 memUsed = 0;

    ptr = LOS_MemMalloc(OS_SYS_MEM_ADDR, size, module);
    if (ptr == NULL) {
        PRINTK("module %d malloc failed\n", module);
    } else {
        PRINTK("module %d malloc successed\n", module);
    }

    memUsed = LOS_MemMusedGet(module);
    PRINTK("module %d mem used size %d\n", module, memUsed);

    module = 1;
    ptrTmp = LOS_MemMalloc(OS_SYS_MEM_ADDR, size, module);
    if (ptrTmp == NULL) {
        PRINTK("module %d malloc failed\n", module);
    } else {
        PRINTK("module %d malloc successed\n", module);
    }

    memUsed = LOS_MemMusedGet(module);
    PRINTK("module %d mem used size %d\n", module, memUsed);

    module = 0;
    LOS_MemMfree(OS_SYS_MEM_ADDR, ptr, module);
    module = 1;
    LOS_MemMfree(OS_SYS_MEM_ADDR, ptrTmp, module);
}

log:

module 0 malloc successed
module 0 mem used size 32
module 1 malloc successed
module 1 mem used size 32

多内存池机制

使用场景

系统中使用多个动态内存池时,需对各内存池进行管理和使用情况统计。

功能说明

系统内存机制中通过链表实现对多个内存池的管理。内存池需回收时可调用对应接口进行去初始化。

通过多内存池机制,可以获取系统各个内存池的信息和使用情况,也可以检测内存池空间分配交叉情况,当系统两个内存池空间交叉时,第二个内存池会初始化失败,并给出空间交叉的提示信息。

功能分类

接口名

描述

初始化内存池

LOS_MemInit

初始化一块指定的动态内存池,大小为size

删除内存池

LOS_MemDeInit

删除指定内存池,仅打开LOSCFG_MEM_MUL_POOL时有效

显示系统内存池

LOS_MemPoolList

打印系统中已初始化的所有内存池,包括内存池的起始地址、内存池大小、空闲内存总大小、已使用内存总大小、最大的空闲内存块大小、空闲内存块数量、已使用的内存块数量,仅打开LOSCFG_MEM_MUL_POOL时有效

使用方法

  1. 通过make menuconfig打开多内存池机制。

    功能依赖于LOSCFG_MEM_MUL_POOL,使用时在配置项中开启“Enable Memory multi-pool control”:

    Debug  ---> Enable a Debug Version---> Enable MEM Debug---> Enable Memory multi-pool control
    
  2. 调用LOS_MemInit接口进行内存池初始化,内存池回收时调用LOS_MemDeInit接口进行去初始化。

  3. 调用LOS_MemInfoGet获取指定内存池的信息和使用情况。

  4. 调用LOS_MemPoolList获取系统所有内存池信息和使用情况。

注意事项

  • 初始化内存池时,需保证各内存池空间无交叉,若交叉则会导致初始化失败。
  • malloc/free系列接口默认从OS系统内存池申请和释放内存,其它内存池的操作必须调用Huawei LiteOS内存接口(LOS_MemAlloc等),不能调用malloc/free系列接口及其相关封装接口。
  • 内存池回收必须调用LOS_MemDeInit接口去初始化(回收前需确保池中内存块均已释放),否则二次初始化该内存池空间会失败,导致该内存池不能被重新使用。
  • 内存池大小需根据业务实际情况合理分配。

编程实例

void test(void)
{
    UINT32 ret = 0;
    UINT32 size = 0x100000;

    VOID *poolAddr1 = LOS_MemAlloc(OS_SYS_MEM_ADDR, size);
    ret = LOS_MemInit(poolAddr1, size);
    if (ret != 0) {
        PRINTK("LOS_MemInit failed\n");
        return;
    }

    VOID *poolAddr2 = LOS_MemAlloc(OS_SYS_MEM_ADDR, size);
    ret = LOS_MemInit(poolAddr2, size);
    if (ret != 0) {
        PRINTK("LOS_MemInit failed\n");
        return;
    }

    PRINTK("\n********step1 list the mem poll\n");
    LOS_MemPoolList();

    LOS_MemDeInit(poolAddr1);
    if (ret != 0) {
        PRINTK("LOS_MemDeInit failed\n");
        return;
    }

    PRINTK("\n********step2 list the mem poll\n");
    LOS_MemPoolList();

    LOS_MemDeInit(poolAddr2);
    if (ret != 0) {
        PRINTK("LOS_MemDeInit failed\n");
        return;
    } 

    PRINTK("\n********step3 list the mem poll\n");
    LOS_MemPoolList(); 
}

log:

********step1 list the mem poll
pool0 :
pool addr          pool size    used size     free size    max free node size   used node num     free node num
---------------    --------     -------       --------     --------------       -------------      ------------
0x8017b2c0         0x100000     0x2e1fc       0xd1d20      0xd1d20              0x2b               0x1            
pool1 :
pool addr          pool size    used size     free size    max free node size   used node num     free node num
---------------    --------     -------       --------     --------------       -------------      ------------
0x8027b2c0         0x7d84d40    0x7070c8      0x767db94    0x767db94            0x1026             0x1            
pool2 :
pool addr          pool size    used size     free size    max free node size   used node num     free node num
---------------    --------     -------       --------     --------------       -------------      ------------
0x8078244c         0x100000     0x10          0xfff0c      0xfff0c              0x1                0x1            
pool3 :
pool addr          pool size    used size     free size    max free node size   used node num     free node num
---------------    --------     -------       --------     --------------       -------------      ------------
0x8088245c         0x100000     0x10          0xfff0c      0xfff0c              0x1                0x1            

********step2 list the mem poll
pool0 :
pool addr          pool size    used size     free size    max free node size   used node num     free node num
---------------    --------     -------       --------     --------------       -------------      ------------
0x8017b2c0         0x100000     0x2e1fc       0xd1d20      0xd1d20              0x2b               0x1            
pool1 :
pool addr          pool size    used size     free size    max free node size   used node num     free node num
---------------    --------     -------       --------     --------------       -------------      ------------
0x8027b2c0         0x7d84d40    0x7070c8      0x767db94    0x767db94            0x1026             0x1            
pool2 :
pool addr          pool size    used size     free size    max free node size   used node num     free node num
---------------    --------     -------       --------     --------------       -------------      ------------
0x8088245c         0x100000     0x10          0xfff0c      0xfff0c              0x1                0x1            

********step3 list the mem poll
pool0 :
pool addr          pool size    used size     free size    max free node size   used node num     free node num
---------------    --------     -------       --------     --------------       -------------      ------------
0x8017b2c0         0x100000     0x2e1fc       0xd1d20      0xd1d20              0x2b               0x1            
pool1 :
pool addr          pool size    used size     free size    max free node size   used node num     free node num
---------------    --------     -------       --------     --------------       -------------      ------------
0x8027b2c0         0x7d84d40    0x7070c8      0x767db94    0x767db94            0x1026             0x1   

内存备份机制

使用场景

业务代码中出现踩内存、释放野指针问题,通过异常dump信息较难定位内存非法操作的位置。

功能说明

备份动态内存节点控制头信息:在前一内存节点控制头中备份当前节点控制头信息。在内存申请和释放操作中增加对当前节点的控制头信息与备份信息的检测,在节点控制头被踩而备份信息未踩时,输出节点控制头备份信息及被踩节点前一内存节点信息,用于进一步分析是否为越界踩内存问题。在释放野指针时可及时输出提示信息和调用栈信息,快速定位释放野指针的位置。

使用方法

通过make menuconfig打开内存备份机制。目前只有bestfit内存管理算法支持该功能,需要使能LOSCFG_KERNEL_MEM_BESTFIT。同时该功能依赖于LOSCFG_MEM_HEAD_BACKUP,使用时在配置项中开启“Enable Node Head Backup”:

Debug  ---> Enable a Debug Version ---> Enable MEM Debug ---> Enable Node Head Backup

注意事项

该功能开启后会增加系统内存占用(占用大小=节点个数*节点控制头大小),且影响内存操作性能。建议仅在问题检测时开启,默认关闭。

内存合法性检查

使用场景

业务发生踩内存导致内存节点控制头被踩,长时间后才触发业务异常,业务逻辑复杂,难以定位发生踩内存的位置。

功能说明

开启该功能后,在动态内存申请接口中增加内存合法性检查,对动态内存池中所有节点控制头的合法性进行检查,若已发生动态内存节点被踩,及时触发异常,输出error信息,缩小问题定位范围。

使用方法

  1. 通过make menuconfig打开内存合法性检查。

    功能依赖LOSCFG_BASE_MEM_NODE_INTEGRITY_CHECK,使用时在配置项中开启“Enable integrity check or not”:

    Debug  ---> Enable a Debug Version ---> Enable MEM Debug ---> Enable integrity check or not
    
  2. 发生踩内存后,下一次内存申请操作即触发异常,并给出被踩节点和前一节点信息,可初步分析定位是否是前一节点越界踩内存。踩内存发生范围为上一次内存申请和此次内存申请之间。异常信息可以通过执行Shell命令memcheck查看。

注意事项

该功能开启时,系统内存申请操作的性能下降明显,建议仅在定位问题时开启,默认关闭。

踩内存问题定位实例

通过构造超出内存长度的memset操作,构造越界踩内存,造成内存节点损坏,构造代码如下:

VOID SampleFunc(VOID *p)
{
    memset(p, 0, 0x110); // 超出长度的memset,设置踩内存场景
}

UINT32 Test(UINT32 argc, CHAR **args)
{
    void *p1, *p2;

    p1 = LOS_MemAlloc((void*)OS_SYS_MEM_ADDR, 0x100);
    p2 = LOS_MemAlloc((void*)OS_SYS_MEM_ADDR, 0x100);
    dprintf("p1 = %p, p2 = %p \n", p1, p2);
    SampleFunc(p1); // 因为p1和p2相邻,当memset的长度超过p1的内存大小,就会越界踩到p2内存

    LOS_MemFree(OS_SYS_MEM_ADDR, (void *)p1);
    LOS_MemFree(OS_SYS_MEM_ADDR, (void *)p2);
    return 0;
}

执行上述代码后,执行Shell命令memcheck,其输出内容如下:

从上图可以看到打印了错误信息。

  • 标记2所指“cur node:0x8034fbfc”表示该节点内存被踩,“pre node:0x8034faec”表示被踩节点前面的节点。标记3所示“pre node was allocated by task:SerialShellTask”表示在SerialShellTask任务中发生了踩内存。
  • 标记1打印的是p1和p2内存的起始地址,“p2 = 0x8034fc0c”,减去控制头大小0x10,即p2-0x10=0x8034fbfc,就是cur node打印出的地址,即p2内存被踩。从代码可以看到p1和p2是两个相邻的节点(这也可以从打印的p1和p2地址看出来,即p1+p1的size+控制头大小=p2,0x8034fafc+0x100+0x10=0x8034fc0c),所以“pre node:0x8034faec”应该就是p1的地址,从标记1获取p1地址为“p1 = 0x8034fafc”,即pre node加上控制头大小0x10(0x8034faec+0x10=0x8034fafc)。

内存size检查

使用场景

memset和memcpy操作动态内存,发生越界踩内存问题。

功能说明

对于memset和memcpy操作,当入参为动态内存节点时,增加对内存节点实际大小与入参指定大小的检查,若指定大小大于节点实际大小时,输出error信息,并且取消该次memset或memcpy操作,所以能够防止操作越界。动态内存越界场景下,可开启该功能定位问题。

接口名

描述

LOS_MemCheckLevelSet

设置内存检查级别

LOS_MemCheckLevelGet

获取内存检查级别

LOS_MemNodeSizeCheck

获取指定内存块的总大小和可用大小

错误码

序号

定义

实际数值

描述

参考解决方案

1

LOS_ERRNO_MEMCHECK_PARA_NULL

0x02000101

LOS_MemNodeSizeCheck的入参中存在空指针

传入有效指针

2

LOS_ERRNO_MEMCHECK_OUTSIDE

0x02000102

内存地址不在合法范围内

输入内存地址本身不在内存管理范围之内,不做处理

3

LOS_ERRNO_MEMCHECK_NO_HEAD

0x02000103

内存地址已经被释放或者是野指针

输入非法地址,内存size检测不做处理

4

LOS_ERRNO_MEMCHECK_WRONG_LEVEL

0x02000104

内存检测等级不合法

通过LOS_MemCheckLevelGet检查等级,并通过LOS_MemCheckLevelSet来配置合法的等级

5

LOS_ERRNO_MEMCHECK_DISABLED

0x02000105

内存检测被关闭

通过LOS_MemCheckLevelSet使能内存检测

使用方法

通过make menuconfig打开内存size检查的配置项LOSCFG_BASE_MEM_NODE_SIZE_CHECK,即在menuconfig中开启“Enable size check or not”。目前只有bestfit内存管理算法支持该功能,所以还需要使能LOSCFG_KERNEL_MEM_BESTFIT。

Debug  ---> Enable a Debug Version ---> Enable MEM Debug ---> Enable size check or not

注意事项

开启该功能后,memset和memcpy性能下降,建议仅在需要定位越界问题时开启,默认关闭。

内存越界问题定位实例

通过构造超出内存长度的memset和memcpy操作,构造越界踩内存问题,构造代码如下:

VOID test(VOID)
{
    UINT32 size = 0x100;

    VOID *poolAddr1 = LOS_MemAlloc((VOID *)OS_SYS_MEM_ADDR, size);
    if (poolAddr1 == NULL) {
        PRINTK("malloc poolAddr1 failed\n");
        return;
    } else {
        PRINTK("malloc poolAddr1 %p successed\n", poolAddr1);
    }

    VOID *poolAddr2 = LOS_MemAlloc((VOID *)OS_SYS_MEM_ADDR, size);
    if (poolAddr2 == NULL) {
        PRINTK("malloc poolAddr2 failed\n");
        return;
    } else {
        PRINTK("malloc poolAddr2 %p successed\n", poolAddr2);
    }

    LOS_MemCheckLevelSet(LOS_MEM_CHECK_LEVEL_LOW);      // 开启对memset和memcpy的长度检测

    PRINTK("memset poolAddr1 overflow\n"); 
    memset(poolAddr1, 0x10, size * 2);                  // 超出长度的memset
    PRINTK("memset poolAddr1\n"); 
    memset(poolAddr1, 0x10, size);                      // 合理长度的memset

    PRINTK("memcpy poolAddr2 overflow\n"); 
    memcpy(poolAddr2, poolAddr1, size * 2);            // 超出长度的memcpy

    PRINTK("memcpy poolAddr2\n"); 
    memcpy(poolAddr2, poolAddr1, size);                // 合理长度的memcpy

    LOS_MemCheckLevelSet(LOS_MEM_CHECK_LEVEL_DISABLE); // 关闭对memset和memcpy的长度检测

    LOS_MemFree((VOID *)OS_SYS_MEM_ADDR, (VOID *)poolAddr1);
    LOS_MemFree((VOID *)OS_SYS_MEM_ADDR, (VOID *)poolAddr2);

    return 0;
}

log:

malloc poolAddr1 0x80349514 successed
malloc poolAddr2 0x80349624 successed
LOS_MemCheckLevelSet: LOS_MEM_CHECK_LEVEL_LOW
memset poolAddr1 overflow
[ERR] ---------------------------------------------
memset: dst inode availSize is not enough availSize = 0x100, memcpy length = 0x200
runTask->taskName = osMain
runTask->taskId = 64
*******backtrace begin*******
traceback 0 -- lr = 0x80209798    fp = 0x802c6930
traceback 1 -- lr = 0x80210fc4    fp = 0x802c6954
traceback 2 -- lr = 0x8020194c    fp = 0x802c6994
traceback 3 -- lr = 0x80201448    fp = 0x802c699c
traceback 4 -- lr = 0x802012fc    fp = 0x0
[ERR] ---------------------------------------------
memset poolAddr1
memcpy poolAddr2 overflow
[ERR] ---------------------------------------------
memcpy: dst inode availSize is not enough availSize = 0x100, memcpy length = 0x200
runTask->taskName = osMain
runTask->taskId = 64
*******backtrace begin*******
traceback 0 -- lr = 0x80209798    fp = 0x802c6930
traceback 1 -- lr = 0x8020dbc4    fp = 0x802c6954
traceback 2 -- lr = 0x8020194c    fp = 0x802c6994
traceback 3 -- lr = 0x80201448    fp = 0x802c699c
traceback 4 -- lr = 0x802012fc    fp = 0x0
[ERR] ---------------------------------------------
memcpy poolAddr2

由于开启size检测,非法的memset和memcpy操作被取消,输出error信息。“runTask->taskName = osMain”显示了该非法操作发生在 osMain函数中,并打印寄存器lr和fp的值。此时可以打开编译后生成的 vs_server.asm(默认生成在Huawei_LiteOS/out/<platform>目录下,其中的platform为具体的平台名),通过对比“寄存器lr”的值,查看函数的嵌套调用。

内存泄露检测

使用场景

业务运行中发生内存泄露,业务逻辑复杂或者长时间运行才出现。

功能说明

申请内存和释放申请时,在内存节点控制头中记录函数调用栈,发生内存泄露时,通过分析used节点信息,可定位疑似内存泄露的位置。

使用方法

  1. 通过make menuconfig打开内存泄漏检测。

    目前只有bestfit内存管理算法支持该功能,需要使能LOSCFG_KERNEL_MEM_BESTFIT。同时该功能依赖于LOSCFG_MEM_LEAKCHECK,可以在menuconfig中配置“Enable Function call stack of Mem operation recorded”:

    Debug  ---> Enable a Debug Version ---> Enable MEM Debug ---> Enable Function call stack of Mem operation recorded
    
  2. 配置调用栈回溯信息。

    • LOS_OMIT_LR_CNT:调用栈回溯忽略层级,默认配置为2。
    • LOS_RECORD_LR_CNT:调用栈回溯记录个数,默认配置为3。

    默认配置下,获取0~4层LR信息,忽略0和1两层(调用封装接口的节点0层和1层LR信息相同),记录2、3、4三层。

    说明: 宏配置LOS_OMIT_LR_CNT 和LOS_RECORD_LR_CNT位于los_memory.h中。

  3. 使用Shell命令** memused **获取used节点数据。

    系统稳定运行后,若used节点个数随时间一直增加,极大可能存在内存泄露,对数据进行对比分析,重点关注LR重复的节点是否存在内存泄露,泄漏点可通过LR信息进行回溯查找。

    打印log信息如下,memused命令说明详见memused

    Huawei LiteOS # memused
    node         LR[0]       LR[1]       LR[2]
    0x802d7b34:  0x8006d86c  0x8011c604  0x8011c758
    0x802dab6c:  0x8006d16c  0x8006d8a0  0x8011c604
    

注意事项

该功能开启时会增加系统内存占用(内存占用=节点个数*LOS_RECORD_LR_CNT*指针大小),影响内存操作性能,建议仅在定位问题时开启,默认关闭。

任务间通信调测方法

队列调测方法

功能说明

队列是一种生产者消费者模型,生产者生产消息放入队列,等待被消费者使用。如果队列已满,生产者被挂起,如果队列已空,消费者被挂起。Huawei LiteOS中使用队列传递消息时,可以设置超时时间,队列的主要作用是实现任务间的异步通信。通过Shell命令queue可以查看队列的使用情况。

使用方法

queue命令依赖LOSCFG_DEBUG_QUEUE,使用时需要在menuconfig中开启"Enable Queue Debugging"。

Debug ---> Enable a Debug Version ---> Enable Debug LiteOS Kernel Resource ---> Enable Queue Debugging

使用实例

在Shell窗口中执行命令queue,打印系统中的队列信息如下:

其输出项的含义见Shell使用教程中queue命令的使用示例,调试过程中主要使用上图中的标识项TaskEntry of creator,即创建队列的接口函数地址(0x0x80242df8)。在vs_server.asm反汇编文件(默认在Huawei_LiteOS/out/<platform>目录下,其中的platform为具体的平台名)中找到该地址,可以看到创建队列的函数名,比如这里的app_init(0x0x80242df8),见下图。

互斥锁调测方法

使用场景

多任务系统使用互斥锁达到资源互斥的目的,其他任务不能强行抢占任务已经占有的资源。使用互斥锁时,可能存在任务间相互等对方释放资源的情况,从而造成死锁。死锁会使任务陷入无限循环等待,导致业务功能障碍。

功能说明

开启互斥锁死锁检测功能后,每个任务在成功获取互斥锁时,会记录该互斥锁为本任务持有,因此通过任务ID可以得知持有的互斥锁。此外,互斥锁控制块本身会记录那些因申请不到该锁而被阻塞的任务。执行Shell命令dlock可以输出系统中所有任务持有互斥锁的信息及任务调用栈信息,再结合系统反汇编vs_server.asm文件和代码就可以确定哪些任务发生了死锁。

互斥锁死锁检测机制

任务发生死锁后,无法得到调度,通过记录任务上次调度的时间,设置一个超时时间阈值,如果任务在这段时间内都没有得到调度,则怀疑该任务发生了死锁。

使用方法

配置宏LOSCFG_DEBUG_DEADLOCK,即在menuconfig配置项中开启“Enable Mutex Deadlock Debugging”,若关闭该选项,则关闭死锁检测功能。

Debug->Enable Debug LiteOS Kernel Resource->Enable Mutex Deadlock Debugging

注意事项

死锁检测输出的是超过时间阈值(默认为10min)的任务信息,但不代表这些任务都发生了死锁,需要通过互斥锁信息及任务调用栈信息进一步确认。

死锁定位实例

构造ABBA互斥锁死锁场景,具体如下:

有两个任务,分别为app_Task和mutexDlock_Task,同时系统中还存在其他系统默认初始任务。在任务app_Task中执行MutexDlockDebug函数,并在该函数中创建任务mutexDlock_Task。函数MutexDlockDebug(即任务app_Task)创建了个互斥锁mutex0,并持有mutex0,接着创建更高优先级的任务mutexDlock_Task,休眠一段时间后去申请mutex1被阻塞(任务mutexDlock_Task已经率先持有mutex1)。任务mutexDlock_Task创建并持有mutex1,然后申请mutex0被阻塞(任务app_Task已经率先持有mutex0)。代码如下:

#include "unistd.h"
#include "los_mux.h"
#include "los_task.h"

static UINT32 mutexTest[2];
extern UINT32 OsShellCmdMuxDeadlockCheck(UINT32 argc, const CHAR **argv);

VOID DlockDebugTask(VOID)
{
    UINT32 ret;

    ret = LOS_MuxPend(mutexTest[1], LOS_WAIT_FOREVER);
    if (ret != LOS_OK) {
        PRINT_ERR("pend mutex1 error %u\n", ret);
    }

    ret = LOS_MuxPend(mutexTest[0], LOS_WAIT_FOREVER);
    if (ret != LOS_OK) {
        PRINT_ERR("pend mutex0 error %u\n", ret);
    }

    ret = LOS_MuxPost(mutexTest[1]);
    if (ret != LOS_OK) {
        PRINT_ERR("post mutex1 error %u\n", ret);
    }

    ret = LOS_MuxPost(mutexTest[0]);
    if (ret != LOS_OK) {
        PRINT_ERR("post mutex0 error %u\n", ret);
    }
}

// MutexDlockDebug函数在用户任务app_Task中被调度
STATIC UINT32 MutexDlockDebug(VOID)
{
    UINT32 ret;
    UINT32 taskId;
    TSK_INIT_PARAM_S debugTask;

    ret = LOS_MuxCreate(&mutexTest[0]);
    if (ret != LOS_OK) {
        PRINT_ERR("create mutex0 error %u\n", ret);
    }

    ret = LOS_MuxCreate(&mutexTest[1]);
    if (ret != LOS_OK) {
        PRINT_ERR("create mutex1 error %u\n", ret);
    }

    ret = LOS_MuxPend(mutexTest[0], LOS_WAIT_FOREVER);
    if (ret != LOS_OK) {
        PRINT_ERR("pend mutex0 error %u\n", ret);
    }

    (VOID)memset_s(&debugTask, sizeof(TSK_INIT_PARAM_S), 0, sizeof(TSK_INIT_PARAM_S));
    debugTask.pfnTaskEntry = (TSK_ENTRY_FUNC)DlockDebugTask;
    debugTask.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
    debugTask.pcName = "mutexDlock_Task";
    debugTask.usTaskPrio = 9;
    debugTask.uwResved = LOS_TASK_STATUS_DETACHED;

    ret = LOS_TaskCreate(&taskId, &debugTask);   // 创建mutexDlock_Task任务,任务入口函数DlockDebugTask,优先级9高于app_Task任务
    if (ret != LOS_OK) {
        PRINT_ERR("create debugTask error %u\n", ret);
    }

    sleep(2);
    ret = LOS_MuxPend(mutexTest[1], LOS_WAIT_FOREVER);
    if (ret != LOS_OK) {
        PRINT_ERR("pend mutex1 error %u\n", ret);
    }

    ret = LOS_MuxPost(mutexTest[0]);
    if (ret != LOS_OK) {
        PRINT_ERR("post mutex0 error %u\n", ret);
    }

    ret = LOS_MuxPost(mutexTest[1]);
    if (ret != LOS_OK) {
        PRINT_ERR("post mutex1 error %u\n", ret);
    }
    return ret;
}

针对以上场景出现的问题,在怀疑发生死锁后,可以参考一下步骤定位问题:

  1. 在Shell中运行dlock命令检测死锁。

    输出结果如下图:

    • “Task_name:app_Task, ID:0x5, holds the Mutexs below:”和“Task_name:mutexDlock_Task, ID:0xc, holds the Mutexs below:”这两行后面有mutex信息,表示可能是任务app_Task(任务ID为5)和mutexDlock_Task (任务ID为c)发生了死锁。
    • <Mutex0 info>:其后几行是该互斥锁的详细信息,包括“Ptr handle”为互斥锁句柄、“Owner”为锁的持有者、“Count”为该锁的引用计数、“Pended task”为阻塞在这把锁上的任务。如果该任务持有多把锁,会逐个打印这些锁的信息(Mutex0~MutexN)。当前app_Task和mutexDlock_Task俩任务分别只持有一把锁。
  2. 在Shell中运行task命令显示当前所有正在运行的任务状态和信息。

    输出结果如下图:

    根据步骤1,任务app_Task和mutexDlock_Task是找到的疑似发生死锁的任务。上图中的TaskEntryAddr列为发生死锁时互斥锁pend的任务入口函数地址,如本例中为任务app_Task(0x8026f28c)和mutexDlock_Task(0x8026eef4)。

  3. 在反汇编文件中找到相应函数。

    打开反汇编文件 vs_server.asm(默认在Huawei_LiteOS/out/<platform>目录下,其中的platform为具体的平台名),在vs_server.asm中找到相应的地址,以mutexDlock_Task(0x8026eef4)为例如下图所示,即可定位到互斥锁pend的位置及调用的接口:

  4. 查看单个任务的栈调用信息。

    如果需要进一步确认任务的调用关系,可以在Shell中运行task命令加ID号查看该任务的栈调用信息,最后再根据上下文判断是否存在死锁。

自旋锁调测方法

功能说明

多核环境下,多任务系统使用自旋锁达到互斥访问资源的目的。自旋锁的检测模块(lockdep),能够检测以下几种类型的错误(包括使用错误):

  1. 重复上锁。

  2. 死锁,以ABBA为例进行说明:

    1. 任务A持有自旋锁X,并永久等待自旋锁Y。
    2. 任务B持有自旋锁Y,并永久等待自旋锁X。

    此时任务A和任务B死锁。

  3. 未持锁情况下释放锁。

  4. lockdep记录信息溢出。

使用方法

通过配置项LOSCFG_KERNEL_SMP_LOCKDEP打开自旋锁的检测模块lockdep,开启自旋锁调测功能。

死锁定位实例

  1. 打开自旋锁检测后,检测到死锁时会打印死锁信息,死锁检测的打印信息示例如下。

    图 1 死锁模块打印信息

  2. 复制request addr的值(本例中为0x802a6528),在系统镜像的反汇编文件vs_server.asm(默认在Huawei_LiteOS/out/<platform>目录下,其中的platform为具体的平台名)中找到相应的地址,如下图所示,即可定位到调用spinlock的位置及调用函数(本例中为task_fx02)。

    图 2 反汇编文件中找到对应地址

  3. 根据图一死锁打印信息中第二个蓝框的自旋锁,通过代码逻辑找到另一个任务持有该锁的情况,再结合代码,调整spinlock调用的时序,从而解决死锁问题。

建议与总结

自旋锁死锁一般发生在某个CPU卡住、任务不再发生调度时。若不开启自旋锁死锁检查,可以使用JLink等调试工具halt住CPU或者触发看门狗异常,查看PC值是否为自旋锁代码,以此确定是否发生了自旋锁死锁。

临终遗言使用方法

功能说明

临终遗言日志存储钩子函数,实现日志存储的读写函数的注册,接口详细信息可以查看API参考。

接口名

描述

LOS_ExcInfoRegHook

注册读写临终遗言日志的钩子函数,并设置记录临终遗言日志的起始地址、大小和缓冲区。

说明: LOS_ExcInfoRegHook有4个参数:

  • startAddr:临终遗言日志的存储起始地址。
  • space:临终遗言日志的存储空间大小。
  • buf:临终遗言日志在内存中的buffer地址,大小space。
  • LogReadWriteFunc:读写异常信息的钩子函数。该函数的入参包括startAddr、space、rwFlag和buf。rwFlag是读写标记,0为写,1为读。其余3个入参同LOS_ExcInfoRegHook。

使用方法

  1. 通过menuconfig开启临终遗言记录功能,即配置LOSCFG_SHELL_EXCINFO_DUMP=y,该功能默认关闭。

  2. 注册读写钩子函数。

    1. 编写读写临终遗言日志的钩子函数,示例代码如下。

      #include "los_base.h"
      #if defined(LOSCFG_SHELL_EXCINFO) && defined(LOSCFG_DRIVERS_MTD_SPI_NOR)
      #include "linux/mtd/mtd.h"
      #include "linux/module.h"
      #include "linux/mtd/mtd_list.h"
      #include "spinor.h"
      #endif
      #include "los_hwi.h"
      #ifdef LOSCFG_FS_VFS
      #include "fs/fs.h"
      #endif
      
      #ifdef LOSCFG_SHELL_EXCINFO_DUMP
      STATIC struct mtd_info *g_mtdSpinor = NULL;
      STATIC VOID OsSpiflashErase(UINT32 start, size_t size)
      {
          struct erase_info eraseInfo;
          (VOID)memset_s(&eraseInfo, sizeof(struct erase_info), 0, sizeof(struct erase_info));
          eraseInfo.mtd = g_mtdSpinor;
          eraseInfo.callback = NULL;
          eraseInfo.fail_addr = (UINT64)MTD_FAIL_ADDR_UNKNOWN;
          eraseInfo.addr = start;
          eraseInfo.len = size;
          eraseInfo.time = 1;
          eraseInfo.retries = 1;
          eraseInfo.dev = 0;
          eraseInfo.cell = 0;
          eraseInfo.priv = 0;
          eraseInfo.state = 0;
          eraseInfo.next = NULL;
          eraseInfo.scrub = 0;
          (VOID)g_mtdSpinor->erase(g_mtdSpinor, &eraseInfo);
      }
      STATIC INT32 OsWriteExcInfoToSpiFlash(UINT32 startAddr, UINT32 space, const CHAR *buf)
      {
          UINT32 outLen;
          OsSpiflashErase(startAddr, LOS_Align(space, g_mtdSpinor->erasesize));
          return g_mtdSpinor->write(g_mtdSpinor, (loff_t)startAddr, space, &outLen, buf);
      }
      STATIC INT32 OsReadExcInfoForSpiFlash(UINT32 startAddr, UINT32 space, CHAR *buf)
      {
          UINT32 outLen;
          return g_mtdSpinor->read(g_mtdSpinor, (loff_t)startAddr, space, &outLen, buf);
      }
      
      /* Be called in the exception. */
      VOID OsReadWriteExceptionInfo(UINT32 startAddr, UINT32 space, UINT32 flag, CHAR *buf)
      {
          if ((buf == NULL) || (space == 0)) {
              PRINT_ERR("buffer is null or space is zero\n");
              return;
          }
      
          g_mtdSpinor = get_mtd("spinor");
          if (g_mtdSpinor == NULL) {
              (VOID)spinor_init();
              g_mtdSpinor = get_mtd("spinor");
              if (g_mtdSpinor == NULL) {
                  PRINT_ERR("Init spinor is failed\n");
                  return;
              }
          }
      
          if (flag == 0) {
              if (OsWriteExcInfoToSpiFlash(startAddr, space, buf) != LOS_OK) {
                  PRINT_ERR("Exception information written to 0x%x flash failed\n", startAddr);
              }
      #ifndef LOSCFG_EXC_INTERACTION
              /*
               * When system is in the exception interaction, this buf was free,
               * but this feature is still running. This buffer may be used again
               * without malloc.
               * So, consider whether or not the "buf" is released according to actual use.
               */
              free(buf);
      #endif
          } elseif (flag == 1) {
              if (OsReadExcInfoForSpiFlash(startAddr, space, buf) != LOS_OK) {
                  PRINT_ERR("Exception information read from 0x%x flash failed\n", startAddr);
              }
          } else {
              PRINT_ERR("flag is error\n");
          }
      }
      #endif
      #endif
    2. 在初始化函数如app_init中注册钩子函数,示例代码如下。

      #ifdef LOSCFG_SHELL_EXCINFO_DUMP
      #define EXCINFO_RECORD_BUF_SIZE     (16 * 1024)   /* 用户自己定义 */
      #define EXCINFO_RECORD_ADDR         (0xffffffff)  /* 此处是非法值,需要用户自己定义 */
          CHAR *buf = (CHAR *)malloc(EXCINFO_RECORD_BUF_SIZE);
          if (buf != NULL) {
              (VOID)memset_s(buf, EXCINFO_RECORD_BUF_SIZE, 0, EXCINFO_RECORD_BUF_SIZE);
              LOS_ExcInfoRegHook(EXCINFO_RECORD_ADDR, EXCINFO_RECORD_BUF_SIZE, buf, OsReadWriteExceptionInfo);
          } else {
              PRINTK("shell excinfo malloc failed!\n");
          }
      #ifdef LOSCFG_FS_VFS
          los_vfs_init();
      #endif
      #endif

      须知:

      • OsReadWriteExceptionInfo函数是对Nor Flash进行读写的功能函数,宏EXCINFO_RECORD_BUF_SIZE,EXCINFO_RECORD_ADDR需要用户根据实际情况进行配置,建议该段代码放在用户业务代码前。
      • Nor Flash写数据之前要进行块擦除,块大小为64k,因此设置的临终遗言存储起始地址之后的64k区域,在写临终遗言信息时会被擦除,建议此区域不保存数据。
  3. 查看异常信息。

    系统复位重启后,Shell窗口中执行excInfo命令,会打印出已记录的临终遗言日志。

注意事项

保存临终遗言信息的buffer需要用户进行维护,重复调用注册函数时,需要用户自行释放前一次注册时传入的buffer。

魔法键使用方法

使用场景

系统未输出挂死相关信息,但是无响应时,可以通过魔法键查看中断是否有响应。在中断有响应的情况下,可以通过魔法键查看task信息中 的CPUP(CPU占用率),找到是哪个任务长时间占用CPU导致系统其他任务无响应(一般为比较高优先级任务一直抢占CPU,导致低优先级任务无响应)。

功能说明

在uart中断和usb转虚拟串口中断中,嵌入魔法键检查功能,对特殊按键进行识别,输出相关信息。

使用方法

  1. 通过menuconfig开启魔法键功能,即配置LOSCFG_ENABLE_MAGICKEY=y。若关闭该选项,则魔法键失效。

    说明: 可以在menuconfig中光标移动到该选项上,输入“?”,查看帮助信息:

    Answer Y to enable LiteOS Magic key.
     ctrl + r : Magic key check switch;
     ctrl + z : Show all magic op key;
     ctrl + t : Show task information;
     ctrl + p : System panic;
     ctrl + e : Check system memory pool.
    
  2. 输入“ctrl + r ” 键,打开或者关闭魔法键检测功能。

    在连接uart或者usb转虚拟串口的情况下,输入“ctrl + r ” 键,打开魔法键检测功能,输出 “Magic key on”;再输入一次后,则关闭魔法键检测功能,输出“Magic key off”。魔法键功能如下:

    • ctrl + z:帮助键,输出相关魔法键简单介绍。
    • ctrl + t:输出任务相关信息。
    • ctrl + p:系统主动进入panic,输出panic相关信息后,系统会挂住。
    • ctrl + e:系统进行简单的内存池完整性检查,检查发现错误则输出相关错误信息,检查正常则输出“system memcheck over, all passed!”。

注意事项

魔法键检测功能打开情况下,如果需要通过uart或者usb转虚拟串口输入特殊字符需避免与魔法键值重复,否则魔法键会被误触发,而原有设计功能可能出现错误。

调试案例

踩内存定位方法

全局变量踩内存定位方法

调试过程中,发现一个全局变量只在一处赋值为0,但使用时打印发现变成一个非零异常值,大概率是该全局变量被踩。

如果已知一个全局变量被踩内存,可在Huawei_LiteOS/out/<platform>/vs_server.map文件中找到该全局变量所在的地址。注意该地址前面最近被使用的变量,排查是否前面变量操作不当引发踩内存,比如对该前面变量进行memcpy,memset操作时越界,溢出覆盖了当前全局变量。

这里列举一个测试的例子,在文件中定义了两全局变量,并且初始化。

UINT32 g_uwEraseMap[16] = {0};
UINT32 g_uwEraseCount = 0;

在vs_server.map中可以找到这些全局变量在bss段对应的位置。

若g_uwEraseMap被踩,在vs_server.map中找到其地址,再查找该地址前面的变量,即g_uwEraseCount。特别注意分析g_uwEraseCount变量的使用情况,观察是否存在某处,对变量g_uwEraseCount进行了越界操作。

task状态判断是否踩内存

Shell命令task“”,可以查看当前系统所有任务的状态。命令输出的stackSize、WaterLine、StackPoint、Top0fStack信息,可以作为判断任务栈是否踩内存的指标。

这里举例说明如何通过task命令判断是否踩内存,如下图所示,有一任务名为shellTask。

StackSize = 0x3000(创建该任务时分配的栈大小)

WaterLine = 0x2810(水线,目前为止该任务栈已经被使用的内存大小)

StackPoint = 0x80d10084 (任务栈指针, 指向该任务当前的地址)

Top0fStack = 0x80d0d768(栈顶)

MaxStackPoint = Top0fStack + StackSize = 0x80d10768(得到该任务栈最大的可访问地址)

  • 若WaterLine > StackSize,则说明该任务踩内存。
  • 若StackPoint > MaxStackPoint 或 StackPoint < Top0fStack,则说明该任务踩内存。