RTOS成功取代Linux成为天猫精灵OS的关键 -- AliOS Things 维测专题

背景

在2018年下半年,天猫精灵系统团队开始研发新一代方糖系列智能音箱,当时业内主流的音箱产品如百度小米等均采用Linux来做智能音箱,Linux系统优势无需多说——开源稳定,三方库完备,芯片厂商硬件支持完备,是快速研发进入市场的首选。但随着智能音箱大战的进一步升级,价格战愈演愈烈,成本就成为了智能音箱快速普及占领市场的关键因素之一,如何降低智能音箱BOM成本就成为了很重要的考量因素。

这时使用RTOS替换Linux进入了我们的视野,因为RTOS在Footprint(RAM/Flash占用)方面优势巨大,两者对比:

RTOS成功取代Linux成为天猫精灵OS的关键 -- AliOS Things 维测专题

从上图可以看到,一方面使用RTOS可以保持同样的功能,采用更小规格的硬件,降低硬件成本。据统计,单独内存这块的BOM成本,就可以节省5块RMB/台,对拥有千万台量级出货量的天猫精灵音箱产品来说,节省的总成本还是非常可观的!

另一方面,我们也可以保持硬件成本不变,用腾出来的75%的RAM空间、87.5%的Flash空间,开发新的功能。

因此,RTOS成为了天猫精灵方糖系列智能音箱的设备端OS。接着问题又来了,同样是RTOS,业内也有很多RTOS并存,主流的有FreeRTOS、RT-Thread,并且天猫精灵团队已经基于FreeRTOS 研发出了音箱demo,但是后来为什么又切换到了AliOS Things?一是AliOS Things 是我们自己的RTOS;其次(也是最重要的),事实证明,在RTOS替换Linux的研发过程中,解决产品稳定性问题的时间占据了研发周期的绝大多数,AliOS Things拥有的众多维测手段,是天猫精灵RTOS版音箱产品从demo走向顺利量产的关键保证,而这些是其他RTOS根本无法比拟的。AliOS Things 维测特性也成为了IOT RTOS领域具有差异化价值的重要体现。

维测概述

维测是什么

维测即对系统异常和错误处理。代码中出现bug,常导致系统异常崩溃无法正常运行。维测组件将异常和错误现场展现出来,指导问题定位,快速找出问题原因。

维测能解决什么问题

简单的说,维测的价值在于可以缩短bug定位时间。如果一个bug出现导致系统异常后,用户可以不用连仿真器、不用加打印、不用打开gdb单步调试的情况下,可以快速找到bug原因,或者帮助用户指出可能的异常点,进而修复节省开发时间。

举例说明:

  1. 代码中访问了非法内存(比如:在不可写的地址处写了数据,如访问了0地址)导致系统奔溃,维测可以记录访问非法内存时的pc值,告诉用户挂在了哪一行;
  2. 代码跑飞了(pc=0),维测记录了函数调用的栈,并根据栈向上回溯可以找到A->B->C的函数调用过程;
  3. 用户内存申请时malloc 失败,维测可以记录用户此时申请了多少内存导致了内存池不够、此时还有多少字节内存可以供申请、用户是在哪个任务中申请的内存、从系统启动开始内存的申请情况等信息,帮助用户查看是否有组件申请了过大内存但没有释放等内存泄漏的情况;
  4. 已经有明显的踩内存现象,但是无法具体定位踩内存根因,只知道一段内存被非法改写,复现现象不一致,定位相当耗时。维测可以提供接口,设置该段内存的属性为不可访问,从而制造memory访问异常,结合异常现场打印,快速定位踩内存的元凶;
  5. 在一次长达数小时的压测中(如天猫精灵语音唤醒72小时压测)出现了系统内存缓慢释放后内存耗尽,问题长时间压测才复现,维测可以在bug首次出现但系统还在正常运行的时候(如内存已经发生泄漏但是系统仍在正常运行),主动触发异常告警,提醒用户系统存在隐患,并打印出异常现场信息供分析,不用等待长时间系统崩溃后才复现问题。

.......

.......

还有很多问题可以通过维测能力来帮助定位,更高级的维测功能也在持续开发中。

下面,我们详细介绍AliOS Things的常见的维测点,具体包括:

  1. 异常接管
  2. 调用栈解析
  3. 硬件watchpoint
  4. 内存维测
  5. 系统打断机制
  6. 伙伴任务
  7. 常用维测命令
  8. AliOS Things维测大图

维测点剖析

1. 异常接管

原理

AliOS Things的死机维测入口即是系统异常后的接管,OS接管系统产生的任何异常和用户主动触发的异常,将异常时系统快照详细输出,OS接管系统的整体框架如下图:
RTOS成功取代Linux成为天猫精灵OS的关键 -- AliOS Things 维测专题

系统快照

系统快照就是 AliOS Things维测模块提供的异常现场信息(设备端log)

  !!!!!!!!!! Exception  !!!!!!!!!!          
crash time   : 1970-01-01 08:01:46       
current task : ucli_cmd_exc_task   //记录异常任务名称、时间、类型等
========== coreID: 0  ==========      
Exception Type: Undefined Instruction    
uspace smartbox exception         
========== Regs info  ==========  //记录通用寄存器现场
 PC      0x80000618
 LR      0x80000618
 SP      0x80EF8AF0
 CPSR    0x400D0010
 R0      0x0000001C
 R1      0x80EF89B0
 R2      0x00000000
 R3      0x0000001C
 R4      0x8046A604
 R5      0x80000608
 R6      0x06060606
 R7      0x07070707
 R8      0x08080808
 R9      0x09090909
 R10     0x10101010
 R11     0x11111111
 R12     0x80EF8B18
========== Fault info ==========  //记录错误寄存器现场
DFSR    0x0C88D939
DFAR    0x6AF935F7
IFSR    0x00000010
IFAR    0xC3502E16
========== Uspace Stack info ==========  //记录栈现场,内核态标识字符串为Stack info
stack(0x4F9DDDC0) : 0xDEADBEAF 0x00000000 0x00000000 0x00000000
.........................( All Zeros ).........................
stack(0x4F9DE980) : 0x41280902 0x40A0112D 0x00000001 0x4F9DEB60
stack(0x4F9DE990) : 0x00000001 0x00000001 0x4F9DEA74 0x4F9DEA60
中间略
stack(0x4F9DEDB0) : 0x04040404 0x05050505 0x06060606 0x40653C1C
========== Uspace Call stack ======  //记录栈回溯,内核态标识字符串为Call stack
backtrace : 0x40665168 
backtrace : 0x40663168 
backtrace : 0x40663340 
backtrace : 0x40663CD8 
backtrace : ^task entry^
========== Heap Info  ==========   //记录堆信息
---------------------------------------------------------------------------
[HEAP]| TotalSz    | FreeSz     | UsedSz     | MinFreeSz  | MaxFreeBlkSz  |
      | 0x02710318 | 0x01D77740 | 0x00998BD8 | 0x01A37BB8 | 0x01D66730    |
---------------------------------------------------------------------------
[POOL]| PoolSz     | FreeSz     | UsedSz     | MinFreeSz  | MaxFreeBlkSz  |
      | 0x00002000 | 0x00000674 | 0x0000198C | 0x000004E8 | 0x00000020    |
---------------------------------------------------------------------------
========== Task Info  ==========   //任务信息
----------------------------------------------------------------------------------------------------------------
TaskName             State    Prio       Stack      StackSize (MinFree)       Ustack      UstackSize (MinFree)      cpu_binded    cpu_num    cur_exc
----------------------------------------------------------------------------------------------------------------
[kernel task]
dyn_mem_proc_task    PEND     0x00000006 0x40CBDF80 0x00001000(0x00000DEC)   0x00000000  0x00000000(0x00000000)        0         1         0   !
idle_task            RDY      0x0000008C 0x40CBFA44 0x00000320(0x000001BC)   0x00000000  0x00000000(0x00000000)        1         0         1   !
idle_task            RDY      0x0000008C 0x40CBFD64 0x00000320(0x000001BC)   0x00000000  0x00000000(0x00000000)        1         1         0   !
DEFAULT-WORKQUEUE    PEND     0x00000014 0x40CC2608 0x00000C00(0x00000A54)   0x00000000  0x00000000(0x00000000)        0         0         0   !
timer_task           PEND     0x00000005 0x40CC0DA0 0x000012C0(0x000010EC)   0x00000000  0x00000000(0x00000000)        0         0         0   !
aos-init             SLP      0x00000078 0x4F700498 0x00002000(0x000010E8)   0x00000000  0x00000000(0x00000000)        1         0         0   !
ipi_r_0              PEND     0x00000078 0x4F706D08 0x00002000(0x00001CE4)   0x00000000  0x00000000(0x00000000)        0         1         0   !
ipi_s_0              PEND     0x00000078 0x4F7093B8 0x00002000(0x00001CDC)   0x00000000  0x00000000(0x00000000)        0         0         0   !
========== Queue Info ==========      //队列信息
-------------------------------------------------------
QueAddr    TotalSize  PeakNum    CurrNum    TaskWaiting
======== Buf Queue Info ========         //缓存队列信息
------------------------------------------------------------------
BufQueAddr TotalSize  PeakNum    CurrNum    MinFreeSz  TaskWaiting
------------------------------------------------------------------
0x40CC07E8 0x000001E0 0x00000002 0x00000000 0x000001B0 timer_task          
0x40CBD868 0x00000100 0x00000003 0x00000000 0x000000FD                     
0x4F706C78 0x00000010 0x00000000 0x00000000 0x00000010 ipi_r_0             
0x4F709328 0x00000010 0x00000000 0x00000000 0x00000010 ipi_s_0             
0x4F70EC90 0x00000010 0x00000000 0x00000000 0x00000010 ipi_r_1             
0x4F711340 0x00000010 0x00000000 0x00000000 0x00000010 ipi_s_1             
0x4F715780 0x00001540 0x00000000 0x00000000 0x00001540 wrap_dsp_msg        
========= Sem Waiting ==========     //信号量信息
--------------------------------------------
SemAddr    Count      PeakCount  TaskWaiting
--------------------------------------------
0x40CC0754 0x00000000 0x00000000 dyn_mem_proc_task   
0x40CC25D0 0x00000000 0x00000000 DEFAULT-WORKQUEUE   
0x4F70D3C0 0x00000000 0x00000000 ipc notify          
0x4F71F540 0x00000000 0x00000000 conn-log    
======== Mutex Waiting =========      //互斥信号量信息
--------------------------------------------
MutexAddr  TaskOwner            NestCnt    TaskWaiting
--------------------------------------------
0x4FADF5E8 touch_event_handle_t 0x00000001                      
Total: 0x00000272 
!!!!!!!!!! dump end   !!!!!!!!!!   //日志尾部

-------------------------------------------------------

异常后CLI 接管

进入了系统异常模式后,一般CLI(cmd line命令行)已无法使用,为了更好的定位问题,我们优化了串口驱动和CLI的使用,使得异常后的CLI命令仍然可用,并且保证了如果在交互过程中发生了二次异常,如访问了非法地址,则继续进入异常后重复CLI过程,用户无感知,相当与OS帮助用户过滤了debug过程中新增的其他异常。

2. 调用栈解析

调用栈解析是AliOS Thing维测点的核心手段,我们遇到crash问题中一半以上是可以直接通过上面“系统快照”中列出的一栏中的调用栈解析,直接看出来此时系统挂在哪里的,大大节省了问题定位时间。以下图显示的调用栈为例(设备端log中输出了调用栈bakctrace地址,我们可以直接用PC端的toolchain中arm-none-eabi-addr2line看解析这个地址,查看对应代码在哪)

======= Call stack =======          
backtrace : 0x401111AA 
test_panic at /home/yx170385/code/aos/app/example/helloworld/helloworld.c:20

backtrace : 0x401111C7 
application_start at /home/yx170385/code/aos/app/example/helloworld/helloworld.c:37

backtrace : 0x4008D521 
app_entry at /home/yx170385/code/aos/platform/mcu/esp32/bsp/entry.c:30

backtrace : ^task entry^

可清晰看到发生异常的函数调用过程:
app_entry  -- >  application_start --->test_panic

3. 硬件watchpoint

当我们在调试程序时,如果发现所定义的一个数据结构中的某一个值总是被意外的更改。根据我们的经验,这种意外更改是因为程序中存在bug的缘故。这种踩内存问题中,最困难的就是找出元凶。遇到这种问题时常见的作法如下:

  • 作法1:连上仿真器,通过gdb打内存断点(添加watchpoint), 看看谁非法访问了该内存区域。本方法的局限性在于:有些系统根本无法连接硬件仿真器,或者不支持gdb,或者被踩内存地址不固定,或者问题出现在启动阶段,来不及设置断点,限制太多;
  • 作法2:通过MMU对特定内存区域进行保护。本方法的局限性在于:MMU保护的最小单位是一个内存页(一般为4KB),保护粒度太大,有可能受害内存区域较小(经常是一个4字节指针),无法用MMU进行监控;
  • 作法3:dump事发现场周边的内存,通过关键字识别谁对这块内存进行了非法写入。比如受害内存区域中有0xAABB字样关键字,而只有某个模块会产生0xAABB的数据,基于此就可以锁定凶手。但是并非每个模块的数据都是有特征的,大部分情况下无法通过该方法找到凶手。

在这种情况下,我们很难找出根源在哪儿。如果处理器有一种功能,当某块内存区或具体的地址被意外更改时,停下来就好了。这就是硬件数据断点的作用!处理器如果提供这种功能,我们能更方便的找出出错的根源。 这时可以尝试芯片自带的硬件watchpoint功能, ARM平台和x86/64一般均支持。

AliOS Things下在线数据断点watchpoint说明

给出2个API:

API 说明
int debug_watch_on(struct watchpoint *watchpoint); 增加一个watch点,返回值:0 -- 增加成功
                             其他:增加失败
int debug_watch_off(struct watchpoint *watchpoint); 删除一个watch点,返回值:0 -- 删除成功
                             其他:增加失败

示例:定位踩内存

调用示例:定位一个全局变量被非法改写

/*watch一个全局变量, 长度4字节,访问模式是不可改写*/
int g_dwt_test = 0;
struct watchpoint test_watch = {(int)(&g_dwt_test), 4, DWT_WRITE, "test watchpoint"};

ret = debug_watch_on(&test_watch);

/*判断返回值*/
.... 

/*在watch的过程中,g_dwt_test这个变量一旦被改写,立即触发crash,根据callstack查看谁改写的*/
   
    
ret = debug_watch_off(&test_watch);
/*判断返回值*/
....

4. 内存维测

这里的内存指的是通过malloc 出来的动态内存,此类问题包括:内存泄漏(memory leak)、内存逻辑出错(use-after-free、double free)等。由于内存问题在稳定性问题中占有较大比重,AliOS Things维测专门针对内存问题提供了强大的分析诊断工具。

常见的内存状态统计

========== Heap Info  ========== 
---------------------------------------------------------------------------
[HEAP]| TotalSz    | FreeSz     | UsedSz     | MinFreeSz  | MaxFreeBlkSz  |
      | 0x02710318 | 0x01D77740 | 0x00998BD8 | 0x01A37BB8 | 0x01D66730    |
---------------------------------------------------------------------------
[POOL]| PoolSz     | FreeSz     | UsedSz     | MinFreeSz  | MaxFreeBlkSz  |
      | 0x00002000 | 0x00000674 | 0x0000198C | 0x000004E8 | 0x00000020    |
---------------------------------------------------------------------------

上面统计分成两部分,HEAP与POOL。HEAP是总的统计,POOL表示较小size的内存分配区域,是HEAP的一部分。

HEAP与POOL的区别是,当用户使用

malloc(size)

来分配内存的时候,size若小于32字节(RHINO_CONFIG_MM_BLK_SIZE,在k_config.h中定义),malloc会在POOL上固定分配32字节内存,反之则在HEAP上分配用户定义size的内存。

HEAP中的内容含义:

  • TotalSz,可供malloc的动态内存总大小;
  • FreeSz,当前堆的空闲大小;
  • UsedSz,当前堆的使用量,即UsedSz = TotalSz – FreeSz;
  • MinFreeSz,堆空闲历史最小值,即TotalSz – MinFreeSz 便是堆历史使用量峰值;
  • MaxFreeBlkSz,最大空闲块Size,表示系统此时可供分配出来的内存最大值。

内存问题分析

遇到内存问题,我们会将系统内存使用状况全部dump出来,如果log中出现了如下图所示的内存相关Log,一般有以下几种情况:

  • 内存泄漏导致内存空间不足
  • 内存逻辑出错(use-after-free、double free)

RTOS成功取代Linux成为天猫精灵OS的关键 -- AliOS Things 维测专题

使用维测解析脚本或者uTrace工具,可以帮助用户分析得出下面的统计:

RTOS成功取代Linux成为天猫精灵OS的关键 -- AliOS Things 维测专题

使用工具可以解析出下面几点有用的信息,对于定位内存泄漏很有用:

  • 当前malloc的地址,大小,次数
  • 解析哪个函数申请的内存,精确到了代码位置和行号
  • 按照从小到大排序,可以清楚看到当前系统申请内存的情况

5. 系统打断机制

遇到设备端无任何log输出、系统无任何交互反应的hung死问题时,我们往往束手无策,只能断电重启。AliOS Things维测针对这类问题,也创造性的支持了系统打断机制,简单的说,就是在任何情况下,我们都可以打断hung死的系统,让系统输出相应日志,从而帮助我们分析系统此时阻塞在什么地方。

AliOS Things支持Cortex-M/A 系列的系统打断机制,通过执行特殊的cli 命令$#@!,我们可以打断现场。

打断机制的原理其实不难,就是保证uart的输入在任何情况下都是可以被响应的,下图显示了基本原理:即调整uart输入中断优先级或者中断属性,使其达到NMI 不可屏蔽中断的效果,使得串口在系统卡死的时候也能响应中断。
RTOS成功取代Linux成为天猫精灵OS的关键 -- AliOS Things 维测专题

这里简单介绍了打断机制的原理和使用,关于系统打断机制的详细说明,请关注后续的文章——AliOS Things维测实战之“如何破解系统卡死”。

6. 伙伴监控

伙伴监控是RTOS上监控任务饿死(高优先级的任务占用CPU不放,导致低优先级的任务得不到执行,称为任务饿死)的一种机制。

背景

RTOS作为实时操作系统,高优先级的任务优先执行,如果高优先级的任务出现异常占用CPU不放,低优先级的任务就得不到CPU资源而被饿死。如果被饿死的任务是关键任务,例如天猫精灵智能音箱中负责响应“天猫精灵”唤醒词的任务,就会造成系统“假死”,严重影响用户体验。因此需要一种监控机制,能够恢复系统,并上报问题。

原理

  • 为每一个优先级的任务创建一个伙伴任务,固定时间间隔(例如10s)计数器+1。同优先级的任务时间片轮转分时执行,因此同一个优先级别只需要一个伙伴任务。只要这个优先级的伙伴任务正常计数,就表明这个优先级的所有任务都没有被饿死。
  • 最高优先级的伙伴任务负责喂狗。因为这个任务优先级最高,如果这个任务也不能执行,说明是硬件异常,watchdog触发系统reset。
  • 最高优先级的伙伴任务还负责监控其他伙伴任务的计数,如果某个关键任务(例如智能音箱唤醒任务,或者其他某个需要确保执行的任务,如果不指定,默认为最低优先级别的任务)所对应的伙伴任务的计数器长时间得不到响应(例如5分钟),则判定系统异常,记录所有的伙伴任务计数,并读取当前占用CPU最多的任务的PC地址,然后触发panic,系统静默重启。重启之后自动上传crash report,包含任务计数和占用CPU最多的任务PC地址。

应用实例

RTOS成功取代Linux成为天猫精灵OS的关键 -- AliOS Things 维测专题

如上图伙伴监控实例,分析如下:

  • Partner_1/2/7/8/9 为伙伴任务,最高优先级伙伴任务Partner_1负责喂狗和监测异常,其他伙伴任务负责计数;
  • 优先级为8的Task_8_A 异常进入死循环,导致优先级为9的关键任务Task_9_C被饿死;
  • 最高优先级伙伴任务Partner_1,监测到关键任务Task_9_C对应的伙伴Partner_9连续5分钟计数异常,记录计数器数值,同时高一个优先级的任务中找到CPU占用率最高的Task_8_A,记录PC地址,然后触发panic,系统重启。

优点

  • 多级伙伴任务

使用多个伙伴任务,覆盖每一个不同的优先级,可以快速定位是硬件异常还是哪一个级别的任务出现软件异常。

  • 通过计数准确判定任务饿死

使用计数器,准确判定任务饿死,避免了通过CPU负载判定而造成的误判。

  • 快速定位导致任务饿死的原因

异常处理采用静默重启,使得用户无感。同时能够上传crash report帮助研发人员快速定位问题。

7. 其他常用命令

工欲善其事必先利其器,要想定位分析稳定性问题,首先需要熟练使用一些调试工具。除了上面列出的维测点,AliOS Things 维测还提供了如下一些命令行工具:

  • mmlk:AliOS Things 提供的内存泄漏辅助定位工具,具体使用与原理详见后续文章
  • addr2line: 获取地址对应的代码行数, 这个工具应该是我们系统工程师最常用的工具之一,可以用uTrace取代
  • tasklist:AliOS Things 提供的查看所有线程的id以及状态、cpu使用情况
  • taskbt task_id:AliOS Things 提供的查看对应id线程的调用栈
  • debug: AliOS Things 提供的查看系统整体的进程、内存信息
  • dumpsys mm_info:AliOS Things 提供的查看当前系统内存的状态
  • mmc: AliOS Things 提供的内存踩踏异常扫描工具
  • p: AliOS Things 提供的内存地址的打印工具

8. AliOS Things 维测大图

上面列出的是AliOS Thing维测针对较典型场景使用功能一些命令和用法,还有其他很多命令比较分散,没有一一列出,特此梳理了一张作为附件,上面尽量列出了大多数使用场景和对应的命令。
RTOS成功取代Linux成为天猫精灵OS的关键 -- AliOS Things 维测专题

AliOS Things维测大图.pdf

专题预告

综合运用上面的维测机制,后面将继续给出——以“天猫精灵项目”研发过程中的实战经验, 继续带领大家更好的体验AliOS Thing维测的价值所在。

AliOS Things维测实战之“如何破解内存泄漏”

AliOS Things维测实战之“如何破解内存踩踏”

AliOS Things维测实战之“如何破解系统卡死”

AliOS Things维测实战之“如何破解系统卡顿”

AliOS Things维测实战之“RTOS超轻量级日志系统方案”

AliOS Things维测实战之“高阶数据断点能力使用方案”

......

总结和展望

每一个稳定性问题的定位和解决,也都是综合运用了多种维测手段才能做到的。AliOS Thing维测仍然在不断的向前演进,维测结合微内核“双态分离”、“进程加载卸载”,可以更好的扩展维测的边界,实现用户最为期待的“用不死机”、“永远在线”的需求。

上一篇:Oracle 11g安装步骤(二)


下一篇:Linux 下安装 Oracle9i