结合AliOS Things谈嵌入式系统通用问题定位方法(2):内核相关基础

1、任务

1.1、任务创建

  • 任务自启动参数:

内核提供的任务创建接口,会存在参数指定当前任务创建完立即运行还是需要显示调用start运行,需要注意。如果在创建任务时指定了立即执行,而在创建任务后去设置任务参数,可能是不生效的。(尤其posix的pthread接口经常遇到这种问题)

  • 任务的栈参数:

任务的栈大小是由用户来指定的,一定要确认开栈大小是否满足自身需求。OS虽然有栈溢出检测,但是不是100%能检测出来的。引起的问题会非常混乱。

1.2、任务的调度策略

目前rhino内核提供的几种调度策略包括:

  • 优先级;
  • RR时间片;
  • CFS公平调度

需要注意的是,在前两种调度的任务中,如果用户任务内在死循环,且没有主动退出的动作,如sleep或者其他Pend动作,那这个任务会一直运行,直到中断的到来,才可能切换到更高优先级的任务,且低优先级任务永远无法运行。

1.3、任务和中断关联

任务执行中,如果不关中断,中断会随时打断任务上下文现场;

中断退出时,会发生任务调度,可能会有任务切换。

1.4、任务退出

需要关注的是,用户的任务处理函数内结束后,任务即会自动退出;

不需要用户手动调用删除任务接口;

也不能在任务自己退出后,还在使用任务的资源;

2、内存

2.1、内存空间管理

内存的堆一般是在ld链接脚本中预留的,通过固定的数据结构告诉OS。

链接脚本中定义堆空间:


  1. PROVIDE (heap_start = __stack); //end of stack
  2. heap_end = ORIGIN(RAM) + LENGTH(RAM);
  3. PROVIDE (heap_len = heap_end - heap_start);

堆的起点heap_start定义为栈的结尾,堆的结尾heap_end定义为RAM的结尾,这样剩余RAM的空间都交给OS管理。

对应的krhino的堆空间初始化为:


k_mm_region_t g_mm_region[] = {{(uint8_t *) &heap_start, (size_t) &heap_len}};

注意:这段内存分配给堆使用,并不是表示内存都耗尽了,而是将其交给OS管理,用户通过malloc出来的内存都是从其中申请。

2.2、内存分配管理

内核提供的内存申请返回地址和大小是8字节对齐的,上限受到总的堆空间的限制。

每一块的内存blk有管理头,用来提供基本的管理和维测信息。

内部会自动完成相关块的拆分合并,但是不可避免会有内存碎片的问题,需要通过下面的快速小内存来优化。

2.3、内存维测

通过内存头部的维测模块的信息,来对该内存块进行监控,主要包含的信息包括:

内存魔术字 => 判断是否被踩

申请的任务号 => 跟踪任务申请状况

申请的时间段 => 跟踪某时间段的内存申请释放情况

申请的backtrace => 具体是哪个位置申请的内存

当前内存状态: => 申请还是释放

具体参考:k_mm.h中k_mm_list_t数据结构

2.4、小内存快速申请

内核还额外提供快速内存申请功能。其提供可配置的2^N固定大小的内存快速申请和释放,优点是效率高,没有碎片,缺点是没有维测信息。

2.5、内存踩踏

内核中提供了自动对内存块的检测,看其内存头是否正确,来达到检测内存踩踏的目的,一般检测在下面的时间点发生:

内存释放:会检测当前内存块的魔术字是否被踩;检测下一块内存头是否正确;检测内存空闲/忙状态是否正确。

在发生内存致命错误或者异常时:

系统会遍历所有内存块,将可疑内存给检测输出出来。

会存在检测遗漏的现场:

尤其是用户空间被飞踩,则检测内存头部检测不出来;

也会经常存在内存头部的非魔术字信息被踩的情况。

从串口打印看是否有内存踩踏:

目前从串口的输出信息查找下面两个关键字,可快速查看是否系统检测出内存踩踏:


  1. !used !free
  2. ======== all memory error blocks =========
  3. kernel space mem layout:
  4. g_kmm_head = 40ccce60
  5. ALL BLOCKS
  6. Blk_Addr Stat Len Chk Caller Point
  7. 0x4f9176e0 used 2368 OK 0x40526c58 (0x40526c54 <- 0x40626730 <- 0x40627160 <- 0x4061aba0 <- 0x4061b228 <- 0x4061b8a8 <- 0x4061be40 <- 0x40615450)
  8. 0x4f918050 free 48 OK 0x0 (0x40654b90 <- 0x40664964 <- 0x40664b64 <- 0x40561aa8 <- 0x0 <- 0x0 <- 0x0 <- 0x0)
  9. 0x4f9180b0 !used 56 a848 0x406528d4 (0x406528d0 <- 0x40664964 <- 0x40664b64 <- 0x40664b60 <- 0x0 <- 0x0 <- 0x0 <- 0x0)

如上图,检测出0x4f9180b0内存状态不对,此时一般首先怀疑被前面的内存块给踩了。当然这只是最基本的情况。

可以通过上面Caller打印出来的调用栈,通过addr2line命令输出。也可通过OS提供的自动解析脚本和IDE工具输出。

2.6、内存泄漏

当内存申请失败的时候,系统报致命错误,并将当前剩余空间,已经当前的内存状态都打印出来。

内存不足报错:


WARNING, malloc failed!!!! need size:80, but free size:40

所有的内存状态信息打印:


  1. ------------------------------- all memory blocks ---------------------------------
  2. g_kmm_head = 805985a0
  3. ALL BLOCKS
  4. Blk_Addr Stat Len Chk Caller Point
  5. 0x80598688 used 8 OK 0x0 (0x0 <- 0x0 <- 0x0 <- 0x0 <- 0x0 <- 0x0 <- 0x0 <- 0x0)
  6. 0x805986c0 used 1049952 OK 0x0 (0x0 <- 0x0 <- 0x0 <- 0x0 <- 0x0 <- 0x0 <- 0x0 <- 0x0)
  7. 0x80698c50 used 3112 OK 0x8000a448 (0x8000a444 <- 0x800084fc <- 0x80000780 <- 0x0 <- 0x0 <- 0x0 <- 0x0 <- 0x0)

通过我们提供的python解析脚本,能自动检测可以内存泄漏点,使用方法:

把上面log 和对应 elf取出,和AliOS Things工程内提供的解析脚本 coredump_parser_mmleak.py放在同一目录

执行脚本:


./coredump_parser_mmleak.py err.log alios_kernel\@mt8153a-mk.kernel.elf

分析结果样例:


  1. ========== Show MM LEAK Info : Top 10 of Malloc-Cnt Backtrace ==========
  2. --------------------------------------------------------------------------------------------------------------------------------------------
  3. {'Backtrace': '0x80290880 <- 0x8029f8b4 <- 0x802a0f2c <- 0x802a131c <- 0x80296ad8 <- 0x0 <- 0x0 <- 0x0', 'Blk Cnt': 18320, 'Total Size': 1124424}
  4. js_def_malloc at thirdparty/quickjs/quickjs.c:34194 (discriminator 1)
  5. ========== Show MM Statistic Info ==========
  6. Caller | Func | Alloc Cnt | Total Size | Line | File
  7. 0x802fd8c4 | sk_malloc_flags(unsigned int, unsigned int) | 249 | 22265888 | 263 | thirdparty/skia/src/core/SkMemory_stdlib.cpp

可以将目前申请次数最大,以及申请内存总数最大的位置导出,供用户进一步分析。

2.7、堆和栈

内核中的堆指的是划分给内存malloc申请的内存空间;

栈一般是任务运行的自身的栈空间,用作用户函数执行过程中的压栈出栈处理;

另外还有CPU相关的模式栈、系统栈之类的,用户暂不用关注。

3、中断

3.1、中断必调接口

一般在CPU架构代码中已经默认调用,但是一定要确保中断上下文调用过。


  1. krhino_intrpt_enter(); //指示系统进入中断上下文
  2. isr_handler();
  3. krhino_intrpt_exit(); //内部会进行切换动作

3.2、中断中失效接口

Sem、mutex、queue的等待接口不能在中断中使用,因为其会调度当前的task,而目前正处于中断上下文;当然OS可以封装一层不触发调度的基本等待接口,但是一旦没有等待信息,会立即返回失败,没有等待的效果。

对应的释放接口可以在中断中使用。

3.3、中断优先级

目前中断设定为同样的优先级,不支持中断抢占。

3.4、中断开关

用户接口不应该直接调用开关中断的接口,而全部内核接管。

3.5、Fiq和irq

普通的中断一般叫irq,位于GIC架构的Group1;另外ARM还提供的快速中断fiq,位于Group0,其默认优先级高于所有的irq中断,能打断任意中断和任务现场

4、多核相关

4.1、任务相关接口

内核提供所有的对外接口都可以在单核或者多核场景下使用,且能到达相关的目的。

如mutex接口能同时保障单核和多核的互斥。

同时还提供了4个基本的多核扩展接口:

krhino_task_cpu_create

任务静态创建,并指定核运行

krhino_task_dyn_create

任务动态创建,并指定核运行

krhino_task_cpu_bind

任务绑定核运行

krhino_task_cpu_unbind

任务解绑核运行,运行核随机

4.2、多核互斥接口

原则上,用户使用通用的mutex接口来进行互斥,不直接使用和系统运行深度关联的关中断、加锁功能接口,以免造成不必要的死锁。

对于死锁问题,尤其要排查用户显示调用加锁的地方是否有问题。

4.3、加锁和关中断

加锁接口前必定要关中断:

对于普通核内不可嵌套锁,中断触发后再次加锁会导致死锁;

对于核内嵌套锁,如果不关中断,内部发生调度,该任务被调度到其他核,那么就会导致解锁失败。

同样的原理:关中断加锁接口内,只能有普通的数据、寄存器操作,不能调用会发生切换的接口,如mutex、printf、malloc。

5、用户态/内核态

内核态与用户态权限隔离、页表隔离

任务的三种运行状态:用户态任务、内核态任务、用户态任务的内核模式;

用户态异常现场在SVC,说明进入了内核态之后异常,得查看内核态的现场;

拿到异常现场需要首先确认异常发生在用户态还是内核态。

判断方法:

  • 查看异常PC和相关通用寄存器空间处于用户态还是内核态;
  • 从异常打印现场区分

内核会在异常时检测是用户态还是内核态异常,区分关键字 :

“kernel space exception” : 内核态发生异常

“uspace \*\* exception” : 用户态发生异常

“uspace task result exception in kernel” : 用户态任务系统调用进入内核发生异常

开发者技术支持

如需更多技术支持,可加入钉钉开发者群,或者关注微信公众号

结合AliOS Things谈嵌入式系统通用问题定位方法(2):内核相关基础

更多技术与解决方案介绍,请访问阿里云AIoT首页https://iot.aliyun.com/

上一篇:Chaos带你快速上手混沌工程-学习记录


下一篇:【玩转微信公众平台之三】 基本操作