跳转至

Linux System Debug SOP


REVISION HISTORY

Revision No.
Description
Date
1.0
  • Initial release
  • 12/21/2023

    前言

    本文为FAE及软件开发相关人员而写,旨在介绍开发过程中客户遇到Linux系统相关问题时,如何自行进行初步排查,确定是Sigmastar SDK问题后再提供相关信息给RD分析。


    1. 内存泄露问题

    当系统在运行过程中出现OOM,或者在/proc/meminfo看到available或者free memory一直在减少,这种情况下可以先排查是否有内存泄漏的问题。

    1.1 判断是user还是kernel内存泄漏

    OOM打印的信息和meminfo的信息类似,从打印信息里面可以获取内存使用相关的数据。

    OOM

    OOM的 active_anoninactive_anon 比较大,可以怀疑是应用层内存泄漏。

    OOM的 slab_reclaimableslab_unreclaimable 比较大,可以怀疑是内核内存泄漏。

    /proc/meminfo

    /proc/meminfo的 Active (anon)和 Inactive (anon)或者 AnonPages 比较大,可以怀疑是应用层内存泄漏。

    /proc/meminfo的 SReclaimableSUnreclaim 比较大,可以怀疑是内核内存泄漏。

    1.2 应用层内存泄漏

    1.2.1 应用没有显式调用malloc类接口

    用户没有显式地调用malloc这些接口去申请内存,而是通过其他接口造成的内存泄漏,像是线程结束之后,没有主动回收线程。

    1.2.1.1 /proc/$pid/maps 或者 pmap

    如果是这种情况,可以通过看 /proc/$pid/maps 或者 pmap 中的8192K大小的匿名页是否有增长。(8192 是glibc 用户线程的栈默认大小,uclibc 则是 2044K,之所以没有 1024K 对齐,是因为使用了 4K 作为 guard page)

    线程栈的泄漏:

    • 粗略计算当前进程的所有线程栈的数量:pmap pid | grep 8192 | wc -l,如果该数字一直变大,说明可能存在线程栈的泄漏
    • 但是如果 app 仅比开始时多了几个线程栈但没有一直增长,且退出所有线程后仍发现比开始的线程栈多,则不一定是线程栈泄漏,可能是 glibc stack cache(默认是 40M,即 5 个线程)
    • 一般是线程退出后没有 pthread_join() 或线程退出前没有调用 pthread_detach() 导致的
    1.2.1.2 /proc/$pid/status

    也可以通过看 /proc/$pid/status ,查看Threads字段是否有增长来判断。

    1.2.2 tcmalloc heap profile

    如果 app 没有退出逻辑,无法用 asan 扫描内存泄漏,可以考虑使用 tcmalloc

    所用到的库需要从 gperftools 和 libunwind 下载 release 版本并解压:

    # 编译 libunwind,so 位置是 build/lib/libunwind.so.8
    ./configure --host=arm-linux-gnu CXX=arm-linux-gnueabihf-g++ CC=arm-linux-gnueabihf-gcc --enable-cxx-exceptions --disable-tests --prefix=$PWD/build
    make install
    
    # 编译 gperftools,so 位置是 build/lib/libtcmalloc.so.4.5.16
    ./configure --host=arm-linux-gnu CXX=arm-linux-gnueabihf-g++ CC=arm-linux-gnueabihf-gcc --prefix=$PWD/build \
                    CPPFLAGS="-I$PWD/../libunwind-1.8.1/build/include" \
                    CFLAGS="-I$PWD/../libunwind-1.8.1/build/include" \
                    LDFLAGS="-L$PWD/../libunwind-1.8.1/build/lib" LIBS="-lunwind"
    make install
    

    app 需要使用 -g 编译,板子上的 app 可以 strip,但是需要保留 .ARM.exidx 段,可以通过 arm-linux-gnueabihf-readelf -S app 去确认,本地用 pprof 分析用的 app 必须要有 -g(因为 backtrace unwind 只需要 .ARM.exidx 段得到每个函数的地址,但是 pprof 分析结果时需要打印行号,则需要 app 要有调试信息才能看到行号)

    还有就是如果是 uclibc,除了 -g,还需要添加 -funwind-tables,因为貌似 uclibc toolchain 默认不会生成 .ARM.exidx 段

    编译 app 注意事项:

    • app 需要使用 -g 编译,因为 pprof 分析结果时需要用来看行号
    • 板子上的 app 可以 strip,但是需要保留 .ARM.exidx section,可以通过 arm-linux-gnueabihf-readelf -S app 去确认
    • 本地用 pprof 分析用的 app 必须要有 -g(因为记录 backtrace 只需要函数地址信息,但是 pprof 分析结果需要打印函数名,需要有调试信息)
    • 如果是 uclibc toolchain,除了 -g,还需要添加 -funwind-tables,因为貌似 uclibc toolchain 默认不会生成 .ARM.exidx

    运行 app(记住需要重命名为 libunwind.so.8)增加以下环境变量:

    # 先把 libunwind.so.8 和 libtcmalloc.so.4.5.16 拷贝到设备上
    mkdir /mnt/mmc/profile
    export HEAPPROFILESIGNAL=12 HEAPPROFILE=/mnt/mmc/profile/profile UNW_ARM_UNWIND_METHOD=4
    # 也可以加上 HEAP_PROFILE_MMAP=true HEAP_PROFILE_MMAP_LOG=yes 去抓 app mmap 信息,但是无法抓到线程栈的 mmap,因为 glibc 调用的是 __mmap
    export LD_LIBRARY_PATH=/dir/to/libunwind.so.8 # libunwind.so.8 所在的目录
    LD_PRELOAD=/path/to/libtcmalloc.so.4.5.16 ./app
    

    运行过程中隔一段时间去 kill -12 pid 就会在 /mnt/mmc/profile/ 目录下生成某一时刻的 malloc 使用情况

    在 host 端分析结果(这里的 app 需要是带 -g 且没有 strip 的:

    • pprof --lines --text --show_bytes app profile.0001.heap --lib_prefix=$PWD,如果不加 --show_bytes 显示的是 MB
    • 如果第一步的结果里面只有地址没有函数名,那可能需要拷贝对应的未 strip 的 so 到 host,比如 /config/lib/libxx.so 就是 mkdir ./config/lib 并拷贝
    • 第一步的输出结果主要看第一列,第一列是每个函数已分配但是未释放的字节数

    更直接的方法是对比 2 次结果:pprof --lines --text --show_bytes [app] profile.0002.heap --lib_prefix=$PWD --base=profile.0001.heap,如果出现某一行第一列的值增长很明显,则说明对应行的函数可能有内存泄漏

    1.3. 内核内存泄漏

    1.3.1 slab内存

    常见的,OOM看slab_reclaimable 和 slab_unreclaimable,meminfo看SReclaimable和SUnreclaim,特别是unreclaim的内存。如果这些项耗内存过多,可以开启kernel原生的slub debug机制做debug。

    slub debug配置项:

    CONFIG_SLUB=y

    CONFIG_SLUB_DEBUG=y

    CONFIG_SLUB_DEBUG_ON=y

    CONFIG_SLUB_STATS=y

    # 定位是 slab 中哪一个内存泄漏,泄漏的通常是 active_objs 不断变大的那一项
    cat /proc/slabinfo
    slabinfo - version: 2.1
    # name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
    mi_vif_internal        0     18    448   18    2 : tunables    0    0    0 : slabdata      1      1      0
    mi_vif_internal        0     18    448   18    2 : tunables    0    0    0 : slabdata      1      1      0
    mi_scl_internal        8     20   1600   20    8 : tunables    0    0    0 : slabdata      1      1      0
    cmdq_pool              5     20    400   20    2 : tunables    0    0    0 : slabdata      1      1      0
    mi_sys_ringpool        0      0    576   28    4 : tunables    0    0    0 : slabdata      0      0      0
    mi_sys_bufhandl      520    576    512   16    2 : tunables    0    0    0 : slabdata     36     36      0
    mi_sys_meta_buf        0      0   1152   28    8 : tunables    0    0    0 : slabdata      0      0      0
    mi_sys_cust_all        0      0   1152   28    8 : tunables    0    0    0 : slabdata      0      0      0
    g_miSysChunkCac      540    616    576   28    4 : tunables    0    0    0 : slabdata     22     22      0
    g_miSysMmaAlloc    34396  34408   1408   23    8 : tunables    0    0    0 : slabdata   1496   1496      0
    
    # xxx 是上面的 name,比如 g_miSysMmaAlloc
    cat /sys/kernel/slab/xxx/alloc_calls
    

    1.3.2 vmalloc内存

    在meminfo的VmallocUsed可以查看通过vmalloc申请内存的使用情况。

    虽然有些kernel版本不会对这个项进行统计,也可以通过以下方法查看相关的内存使用:

    cat /proc/vmallocinfo
    

    Note: 主要对比vmallocinfo中pages的分配数量,其中vmap的page分配数量可以忽略

    1.3.3 通过CamOs API申请的内存

    通过CamOs API申请的内存可能是上面提到的slab内存,也可能是vmalloc申请的内存。

    对于这部分内存的使用,可以使用监测CAM OS API分配内存的机制。

    机制打开的方法:

    1. 在kernel打开宏 CONFIG_TRACE_CAM_OS_MEM

    2. 通过Alkaid的menuconfig选项打开TRACE_CAM_OS_MEM功能,在project路径下执行make menuconfig,选项路径:Customer_Options -→> Trace Cam Os Mem ,如下图:

    该功能会在以下API中记录trace信息:

    CamOsMemAlloc
    CamOsMemAllocAtomic
    CamOsMemCalloc
    CamOsMemCallocAtomic
    CamOsMemCacheAlloc
    CamOsMemCacheAllocAtomic
    CamOsContiguousMemAlloc
    

    如果enable成功,在平台的/proc/mi_modules/common路径会有 mem_filtermem_stat 两个proce entry

    • cat /proc/mi_modules/common/mem_filter:可以看到能够监测的模块。
    • echo trace 5 > /proc/mi_modules/common/mem_filter:可以选择需要关心的模块以监测内存消耗,比如这里选择模块MI_SYS,序号为 5。
    • echo trace 40 > /proc/mi_modules/common/mem_filter:设置大于39的数值就可以查看所有模块的内存监测信息,比如这里设置40。
    • echo showall 1 > /proc/mi_modules/common/mem_filter:定位模块调用CAM OS API申请内存的地方
    信息条目 意思
    ModuleName 被监测的模块
    TotalSize 对应模块所申请的内存大小
    TraceInfo 每做一个记录所耗的kernel内存大小,属于本功能所耗的内存
    TotalCount 本次监测统计一共记录的个数
    TraceCost 本功能在本次监测统计所耗的内存,等于TraceInfo 乘以TotalCount
    HighTotal 在本次统计中,各个模块申请内存的总和峰值
    CamTotal 在本次统计中,各个模块申请内存的总和

    如果需要查看所有模块申请内存的函数信息,可以用以下几条命令:

    echo trace 10 > /proc/mi_modules/common/mem_filter
    echo showall 1 > /proc/mi_modules/common/mem_filter
    
    # 23 表示中间省略1次同样API申请同样size内存的打印。
    cat /proc/mi_modules/common/mem_stat
    --------------------------- CamOsMemory Usage state ---------------------------
    Function Caller                                                         Ptr             BytesReq
    --------------------------------------------------------------------------------
    [kernel]:
    _CamProcCreateEntryNode+0x6f/0x12a                                      c2199a00        38
    ...(23)
    0xbfdc907f                                                              c20a0200        d0
    ...(1)
    _CamProcCreateEntryNode+0x6f/0x12a                                      c1f79100        38
    .....
    

    通过对比 mem_stat 前后 2 次的结果就可以得到内存泄漏的位置

    1.3.4 kmemleak工具

    对于kernel内存泄漏问题,可以用kernel原生的工具kmemleak做分析。

    kmemleak打开方法:

    Kernel hacking → Memory Debugging → Kernel memory leak detector

    kmemleak打开对应的配置项:

    1)清除kmemleak历史记录,以便记录新数据:

    echo clear > /sys/kernel/debug/kmemleak
    

    2)使能kmemleak进行内存泄漏检测扫描:

    echo scan > /sys/kernel/debug/kmemleak
    
    echo scan=n > /sys/kernel/debug/kmemleak 设置n秒内自动记忆扫描,默认600s
    

    3)查看kmemleak内存泄漏检测结果:

    cat /sys/kernel/debug/kmemleak
    

    运行步骤:

    1)echo clear > /sys/kernel/debug/kmemleak       //清除缓存
    
    2)echo scan=10 > /sys/kernel/debug/kmemleak     //开启扫描,设置扫描时间间隔为10s
    3)insmod kmemleak_test.ko
    
    4)cat /sys/kernel/debug/kmemleak
    
    unreferenced object 0xe1be4000 (size 4000):
      comm "insmod", pid 869, jiffies 4294948352 (age 31.890s)
      hex dump (first 32 bytes):
        0d 00 00 00 04 00 00 00 ff ff ff ff ad 00 00 00  ................
        01 00 00 00 02 00 00 00 ec 51 75 bf 98 01 00 00  .........Qu.....
      backtrace:
        [<c0092a8b>] __vmalloc_node+0x2f/0x38
        [<c0092ab7>] vmalloc+0x23/0x30
        [<bf75501f>] kmemleak_test+0x1e/0x54 [kmemleak_test]          //gdb可定位到vmalloc行
        [<bf75700d>] 0xbf75700d
        [<c00095bf>] do_one_initcall+0xcf/0x104
        [<c005ebc1>] do_init_module+0x39/0x12c
        [<c005ffc7>] load_module+0x12d1/0x13b6
        [<c0060159>] SyS_init_module+0xad/0xb8
        [<c000d2e1>] ret_fast_syscall+0x1/0x4c
        [<ffffffff>] 0xffffffff
    

    注意:kernel config 打开 CONFIG_TRACE_CAM_OS_MEM 后,kmemleak 会追踪不到 cam os api 的内存泄漏,因为所有 camos 分配内存的指针都会被添加到 hash list 里面,如果有内存泄漏,hashlist 中仍会存在泄漏内存的指针,kmemleak 扫描内存时也会扫描到该指针,导致 kernel 误认为没有内存泄漏

    1.3.5 使用page_owner

    当以上方法都查不到kernel memleak的root cause时,可以采用kernel原生的page owner机制,它记录了每个申请page的owner,以及记录了owner申请page的callstack。

    打开page_owner机制需要的配置:

    1. 添加kenel配置 CONFIG_PAGE_OWNER=y
    2. 在bootargs参数添加 page_owner=on

    功能开启后我们可以在proc中看到如下节点:

    /sys/kernel/debug/page_owner
    

    之后的使用教程我们就可以参考:

    Documentation/vm/page_owner.rst
    ...
    
    1) Build user-space helper::
    
        cd tools/vm
        make page_owner_sort
    
    2) Enable page owner: add "page_owner=on" to boot cmdline.
    
    3) Do the job what you want to debug
    
    4) Analyze information from page owner::
    
        cat /sys/kernel/debug/page_owner > page_owner_full.txt
        ./page_owner_sort page_owner_full.txt sorted_page_owner.txt
    
       See the result about who allocated each page
       in the ``sorted_page_owner.txt``.
    

    编译page_owner_sort排序工具,然后cat节点最后排序。然后肉眼看一大堆数据

    下面就是page_owner的一个输出结果:

    /mnt/slab_diff/0804 # tail -n 20 ./sorted_page_owner2.txt
    
    1 times:
    Page allocated via order 8, mask 0x52dc0(GFP_KERNEL|__GFP_NOWARN|__GFP_NORETRY|__GFP_COMP|__GFP_ZERO), pid 2298, ts 7180405565150 ns, free_ts 7170863960659 ns
     get_page_from_freelist+0x1b5/0x402
     __alloc_pages_nodemask+0xc9/0x5de
     kmalloc_order+0x1f/0x44
     kvmalloc_node+0x39/0x88
     CamOsMemCalloc+0x35/0x44
     IqInit+0x11/0x8c [mi_isp]
     CameraIspAlgo_3A_IQ_Init+0x481/0x53e [mi_isp]
     CameraCreateIspInstance+0x29b/0x2f8 [mi_isp]
     MHalVpeCreateIspInstance+0xff/0x158 [mi_isp]
     MI_ISP_DRVCFG_CreateInstance+0xb9/0x19c [mi_isp]
     MI_ISP_IMPL_CreateChannel+0x619/0x6c4 [mi_isp]
     MI_ISP_IOCTL_CreateChannel+0x31/0x74 [mi_isp]
     MI_DEVICE_Ioctl+0xfd/0x1f0 [mi_common]
     vfs_ioctl+0x11/0x1c
     sys_ioctl+0x8b/0x4b2
     ret_fast_syscall+0x1/0x5c
    

    每个order(页框)对应的size如下:

    - order 0: 4KB
    - order 1: 8KB
    - order 2: 16KB
    - order 3: 32KB
    - order 4: 64KB
    - order 5: 128KB
    - order 6: 256KB
    - order 7: 512KB
    - order 8: 1MB
    - order 9: 2MB
    

    也是跟buddyinfo页框数量的顺序一一对应的:

     cat /proc/buddyinfo
    Node 0, zone   Normal     28     22     15     14      5      7     10      5      5    128
    

    根据剩余页框数量我们可以计算出meminfo中的LowFree大小了。MemFree=HighFree+LowFree 所以我们可以计算出内存剩余量。

    回到上面的page_owner结果,我们可以信息很清楚的看到:Page allocated via order 8, mask 0x52dc0(GFP_KERNEL|__GFP_NOWARN|__GFP_NORETRY|__GFP_COMP|__GFP_ZERO), pid 2298, ts 7180405565150 ns, free_ts 7170863960659 ns

    这个页框的信息:

    • order 8 申请了1M内存
    • mask 掩码是多少
    • pid 是多少。
    • ts 表示分配该页面的时间戳,即页面被分配的时间点。
    • free_ts 表示最后一次释放该页面的时间戳,即页面最后一次被释放的时间点。

    通过比较这两个时间戳,您可以计算出页面从分配到最后一次释放所经过的时间。这在分析页面的使用情况、内存回收等方面可能会有用。

    我们可以看到free_ts小于ts 那就说明申请了没释放。 根据调用栈我们看到是IqInit时申请了1M内存没有释放。

    2. CPU loading高问题

    2.1 top

    top 可以简单分析总体的 cpu loading 和内存,确认是哪个线程或进程的 cpu loading 较高

    输入 top 后再按 1,就可以列出每个 cpu 的 loading:

    • us 表示 user,sy 表示 system,hi 表示 hard irq,si 表示 soft irq,st 表示 steal time (hypervisor)
    • 通过 top CPUx 那一行可以定位到非线程上下文的 cpu loading 高的问题,比如 si 和 hi 过高也会导致线程调度不及时
      Mem: 74864K used, 1720804K free, 853776K shrd, 0K buff, 17924K cached
      CPU0:  0.0% us  5.3% sy  0.0% ni 94.6% idle  0.0% io  0.0% hi  0.0% si  0.0% st
      CPU1:  0.1% us  3.5% sy  0.0% ni 96.2% idle  0.0% io  0.0% hi  0.0% si  0.0% st
      CPU2:  0.0% us  0.0% sy  0.0% ni  100% idle  0.0% io  0.0% hi  0.0% si  0.0% st
      CPU3:  0.7% us  1.5% sy  0.0% ni 97.6% idle  0.0% io  0.0% hi  0.0% si  0.0% st
      Load average: 14.05 14.05 14.01 1/108 1028
      PID  PPID USER     STAT   VSZ %VSZ CPU %CPU COMMAND
      996   993 root     S    58088  3.2   1  1.2 ./prog_vif_isp_scl_ut param_snr0_venc.ini
      1003     2 root     DW       0  0.0   1  1.0 [isp0_P0_MAIN]
      942     2 root     DW       0  0.0   1  0.1 [IspMidThreadWq]
      997     2 root     DW       0  0.0   0  0.1 [vif0_P0_MAIN]
      1028  1014 root     R     2432  0.1   2  0.1 top
      1009     2 root     DW       0  0.0   2  0.0 [venc0_P0_MAIN]
      937     2 root     DW       0  0.0   0  0.0 [IspDriverThread]
      12     2 root     IW       0  0.0   3  0.0 [rcu_preempt]
      

    2.2 perf

    top 只能定位到大概出问题的位置,但是无法定位造成 cpu loading 高的具体位置

    需要用到 perf 和 FlameGraph 去定位具体的代码位置:

    # 将 perf 拷贝到设备上执行以下 2 条命令,生成的 perf.unfold 拷贝出来
    # -a 表示 --all-cpus,-g 表示抓取 kenrel 和 user;sleep 1 表示 1 秒后退出抓取,可以加大该值加长抓取时间
    perf record -a -g sleep 1
    perf script -i perf.data > perf.unfold
    # 使用抓到的 perf 数据生成火焰图
    # https://github.com/brendangregg/FlameGraph.git,进入该项目的目录下后执行命令
    ./stackcollapse-perf.pl ./perf.unfold > report.data && ./flamegraph.pl report.data > report.svg
    

    若想要抓到的部分是 ? (unknown) 或地址,则可能需要针对对应的 app binary 和 so 开启 -g 去编译

    2.3 瞬时 cpu loading 高的问题

    由于 FlameGraph 显示的是一段时间内的 cpu loading,但是无法观察到瞬时升高的 cpu loading

    这里推荐使用 flamescope,依赖只有 python,因此可以跑在 linux 或 windows:

    # 在设备上抓取的方式一致
    perf record -a -g sleep 1
    perf script -i perf.data > perf.unfold
    
    # 环境搭建
    git clone https://github.com/Netflix/flamescope
    cd flamescope
    # master 分支的 flamescope 有问题,需要替换 tag v0.2.0 的 app/public
    git checkout -b v2.0.0 v2.0.0
    cp app/public ..
    git checkout master
    cp -r ../public app
    pip install -r requirements.txt
    
    # 将抓取的数据拷贝到 example 进行可视化
    cp [path/to/perf.unfold] example/
    python run.py
    

    打开 127.0.0.1:5000 再打开 perf.unfold,就能看到 x 轴为秒,y 轴为毫秒的图,点击其中任意 2 个点既可打开这段时间内的火焰图

    3. 死锁问题

    3.1 死锁概念

    死锁是指多个进程(线程)因为长久等待已被其他进程占有的的资源而陷入阻塞的一种状态。当等待的资源一直得不到释放,死锁会一直持续下去。死锁一旦发生,程序本身是解决不了的,只能依靠外部力量使得程序恢复运行,例如重启,看门狗复位等。

    3.2 互斥死锁分析

    主要分为 D 状态死锁和 R 状态死锁。

    3.2.1 D 状态死锁分析

    D 状态死锁指的是 进程等待 I/O 资源无法得到满足,长时间(系统默认配置 120 秒)处于 TASK_UNINTERRUPTIBLE 睡眠状态。 可以通过mi_sys的 show_threads 命令来定位具体问题。 使用方式: 要在死锁出现的现场:

    1. 新建一个窗口,在这个窗口中:

      cat /proc/kmsg
      

      这一步很重要,因为抓取ksmg的log相比起串口log要齐全。

    2. 在另外的窗口中:

      echo show_threads > /proc/mi_modules/mi_sys/mi_sys0
      
    3. 分析kmsg的输出

    3.2.2 R 状态死锁分析

    进程长时间(系统默认配置 60 秒)处于 TASK_RUNNING 状态垄断 CPU 而不发生切换,一般情况下是进程关抢占或关中断后长时候执行任务、死循环,此时往往会导致多 CPU 间互锁,整个系统无法正常调度,导致喂狗线程无法执行,无法喂狗而最终看门狗复位的重启。该问题多为原子操作,spinlock 等 CPU 间并发操作处理不当造成。后面所介绍的 Lockdep 死锁检测工具检测的死锁类型就是 R 状态死锁。

    3.3 自旋死锁分析

    spinlock死锁大致可以分为soft lockup和hard lockup。

    soft lockup: 指CPU运行在kernel space,并且超过一定时间都没有发生过任务调度。

    hard lockup:指CPU超过指定时间,都没有发生过interrupt。 对于不同lockup,有不同解决分析手段。

    3.3.1 soft lockup分析

    针对 soft lockup 可以使用linux kernel的soft lockup detector。

    • 原理 soft lockup detector会为每一个CPU启动一个watchdog thread,并设置调度属性,只允许在对应的CPU上执行,这个thread的作用类似hardware watchdog,喂狗(更新时间戳watchdog_touch_ts),如果这个thread有被CPU正常的调度,则watchdog_touch_ts会被更新,从而不会触发soft lockup报警。

      检查是否喂狗超时,是在CPU arch timer中断处理函数里面,如果系统异常,如:某个thread使用了preempt_disable()并长时间没有调用preempt_enable(),就会导致喂狗的thread得不到CPU执行,就会导致超时。

    • 使用方式 Kernel Config Option:

      CONFIG_LOCKUP_DETECTOR=y
      

      Kernel重编后,重烧开机去复现问题,抓取log进行分析。

    3.3.2 hard lockup分析

    对于 hard lockup ,目前原生 ARM Linux 上并没有其他很好的Feature来定位问题。可以通过下面的 Trace32 或者 Lockdep 来定位问题。

    3.3.3 Trace32分析

    除了上述方案外,可以接入Trace32,看下各个core跑的真实上下文,最为直观。 在连接Trace后,输入命令'frame',默认弹出core0的堆栈信息,通过右下角切换core,来查看想要观察的core上下文。 更多具体使用方法参见Trace32培训。

    3.4 死锁通用分析方法

    无论是自旋锁还是互斥(不包括信号量),对于可以再复现的场景,可以打开Linux内核的一个检测死锁的feature "Lockdep"。用后面介绍的方法打开Lockdep,然后复现问题,分析log得出导致死锁的真凶。

    3.4.1 Lockdep 简介

    Linux内核提供死锁调试模块Lockdep,跟踪每个锁的自身状态和各个锁之间的依赖关系,经过一系列的验证规则来确保锁之间依赖关系是正确的。 Lockdep检测的锁包括spinlock、rwlock、mutex、rwsem的死锁,锁的错误释放,原子操作中睡眠等错误行为。

    3.4.2 Lockdep 使用

    下面两种方式任选其一,推荐使用2.比较方便快捷。

    1. 单独编译Kernel打开 在menuconfig或者defconfig中打开如下选项:

      CONFIG_DEBUG_LOCK_ALLOC=y
      CONFIG_PROVE_LOCKING=y
      CONFIG_LOCKDEP=y
      CONFIG_LOCK_STAT=y
      CONFIG_DEBUG_LOCKDEP=y
      CONFIG_TRACE_IRQFLAGS=y
      CONFIG_DEBUG_ATOMIC_SLEEP=y
      
    2. 整包编译Alkaid打开 可参见L3 WIKI《编译alkaid debug版本》,编译整个image时带上DEBUG=4,即:

      make image DEBUG=4 -jN
      

    4. 踩内存问题

    崩溃是踩内存问题最为常见的现象(包括访问无效指针,断言失败,打印的变量值不符合预期等)

    4.1 sdk 对齐

    若调用 mi api 时发现怪异表现(比如在 kernel 打印的值和 user 拿到的不一样),先去排查 release sdk 是否对齐了,包括头文件、ko、so 等

    若已经确认对齐后还是会有问题,再确认是否还有预编译的算法库 so 等单独使用了静态链接的 sdk 的 .a,确认方法如下:

    # MI_SYS_ChnInputPortGetBufPa 只是一个例子,具体应该是要改成表现怪异的 mi api
    # 若能在 so 里面找到相同的 api(T 表示 test 段),证明 libxxxx.so 链接了 sdk 的 .a,需要用新的 sdk 去编译 libxxxx.so
    arm-linux-gnueabihf-nm libtest.so | grep MI_SYS_ChnInputPortGetBufPa
    0000000000002a20 T MI_SYS_ChnInputPortGetBuf
    arm-linux-gnueabihf-nm libxxxx.so | grep MI_SYS_ChnInputPortGetBufPa
    0000000000002a20 T MI_SYS_ChnInputPortGetBuf
    

    4.2 asan (userspace)

    userspace 常见的踩内存现象:

    • SIGSEGV:空指针,指针未初始化就使用,内存越界
    • libc 或非常稳定的第三方库出现判断失败导致 abort:可能是内存越界或者 used after free
    • 没有 crash 但是某些变量的值不正常:可能是内存越界或者 used after free

    userspace 定位踩内存问题最有效的工具就是 asan

    4.2.1 asan 的基本使用方法

    userspace 使用 asan 扫描踩内存问题:

    • 需要针对所有编译的库和 binary 开启 3 个编译选项 -g -fsanitize=address -fno-omit-frame-pointer;也可以不加 -fno-omit-frame-pointer 但需要 strip 时保留 .ARM.exidx section
    • 打包可以去 strip,编译一定要有 -g 并保留未 strip 的 binary 或 so,否则 addr2line 无法解出文件和行号
    • asan 也可以静态链接,只需要加 ld flags:-static-libasan,这样就不需要在运行时加 LD_PRELOAD 环境变量;如果是 uclibc,可能还需要添加 ld flags:-lasan
      # main.c 参考
      # int main(int argc, char *argv[])
      # {
      #     char *a = malloc(10);
      #     free(a);
      #     a[1] = 1;
      #     return 0;
      # }
      arm-linux-gnueabihf-gcc -g -fsanitize=address -fno-omit-frame-pointer main.c
      
      # 拷贝 /tools/toolchain/gcc-11.1.0-20210608-sigmastar-glibc-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/lib/libasan.so.6.0.0 到设备,
      # 并重命名位 libasan.so.6;在设备上运行上一步编译出来的 a.out
      LD_PRELOAD=/path/to/libasan.so.6 ./a.out
      
      # asan 运行结果如下,需要关注的就是崩溃时的栈的地址
      # ==803==ERROR: AddressSanitizer: heap-use-after-free on address 0xb41007b1 at pc 0x00010653 bp 0xbed15c80 sp 0xbed15c84
      # WRITE of size 1 at 0xb41007b1 thread T0
      #     #0 0x10650  (/mnt/a.out+0x10650)
      #     #1 0xb68b6bc4 in __libc_start_main (/lib/libc.so.6+0x17bc4)
      
      # 下面的 a.out 需要是没有 strip 过,并且 -g 编译的
      arm-linux-gnueabihf-addr2line -e a.out 0x10650
      main.c:8
      

    经历以上步骤通常能够抓到踩内存的代码位置,但是为何会踩内存则需要去详细地分析代码

    4.2.2 开启 asan 后 oom

    asan 会占用原本的 2 倍内存(还有 2 倍的 CPU 开销)

    • 若开启 asan 后,app 跑了很短时间就 oom 了,此时可能需要缩小 mma 去开启;若还是因为内存不够无法开启 asan,能否试下裁剪掉一些功能;也可以尝试开启 swap
    • 若 app 在跑了很长一段时间才出现 oom,可以设置环境变量 ASAN_OPTIONS=quarantine_size_mb=50 去限制隔离区的大小(默认是 250MB)

    内存不足时,开启 swap 的方法:

    • 打开 kernel config: CONFIG_SWAP,sdk ko 无需重新编译
    • 创建 swap 文件(可以在 linux 服务器上执行):dd if=/dev/zero of=./swap bs=1M count=250 && mkswap ./swap(调整 count 值可以调整 swap 大小),将 swap 文件拷贝到 sd 卡或 emmc
    • 在设备上开启 swap:swapon /mnt/sd/swap,确认 /proc/meminfoSwapFree 大于 0,证明开启成功;echo 200 > /proc/sys/vm/swappiness 调整倾向使用 swap

    注意是开启 swap 后还是有可能会内存不足

    若客户是 arm64,但是内存不够去开启 asan,可以考虑使用 hwasan(使用的内存仅会增加 30%-50%,且没有隔离区的内存消耗,但功能和 asan 完全一致),和 asan 的使用差别:

    • -fsanitize=address 改为 -fsanitize=hwaddress
    • 拷贝的 libasan.so 改为 libhwasan.so

    4.2.3 开启 asan 后无法复现问题

    若在开启 asan 后复现不到问题,大概率是要对 预编译的库 (.so 或 .a)重新用 asan 编译,比如 sdk 或预编译算法库重新使用 asan 编译

    编译带 asan 的 sdk:make DEBUG=256 image -j8

    4.2.4 uclibc asan

    由于 uclibc 默认无法编译出 asan 版本,需要修改 libsanitizer 以适配 uclibc toolchain。

    使用方法:将 libasan.alibasan_preinit.o 放到 uclibc toolchain 的目录下 arm-sigmastar-linux-uclibcgnueabihf-9.1.0/arm-sigmastar-linux-uclibcgnueabihf/lib,同时由于是静态链接,不需要在运行时加 LD_PRELOAD 参数了,其他使用方法一致

    4.3 kasan (kernel)

    kernel 踩内存的现象:

    • BUGON:原生 kernel code BUGON,或 BUGON 经确认是绝对不可能出现的,可能是 used after free 或内存越界
    • 变量变成了不该出现的值(可能不会 panic):大概率是 used after free,也有可能是内存越界
    • 读写非法地址,可能会有如下打印:
      • Unable to handle kernel paging request at virtual address
      • address between user and kernel address ranges
      • 原因可能是:used after free 或内存越界把指针的值改掉了,或者是使用了未初始化的指针

    kasan 开关 CONFIG_KASAN,kernel 和 userspace 不太一样,不能只是单个 ko 去使用 kasan 编译,必须 release 带 kasan 的 sdk,编译命令是 make DEBUG=2 image -j8

    开启 kasan 后需要注意的几个问题:

    • flash 空间不足:可以开启 CONFIG_KASAN_OUTLINE,生成较小的二进制;默认是 CONFIG_KASAN_INLINE=y,其速度比 outline 快 2 倍
    • 若出现 oom,请参考 asan 的处理方式
    • 通过 gdb 看到文件和行号需要 debug info,即 CONFIG_DEBUG_INFO=y

    这里故意写一个 used after free:

    MI_S32 MI_SYS_Cmd_ProcShowThreads(/*...*/)
    {
        char * a = kmalloc(10, GFP_KERNEL);
        // ...
        kfree(a);
        a[0] = 1;
        return MI_SUCCESS;
    }
    

    kasan log:

    BUG: KASAN: use-after-free in MI_SYS_Cmd_ProcShowThreads+0x100/0x110 [mi_sys]
    Write of size 1 at addr c3e34600 by task sh/1521
    
    CPU: 0 PID: 1521 Comm: sh Tainted: P           O      5.10.117-android12-9-00500-gfdf36a84d0fb-dirty #1
    Hardware name: SStar Soc (Flattened Device Tree)
    [<c0214440>] (unwind_backtrace) from [<c020dd6c>] (show_stack+0x10/0x14)
    [<c020dd6c>] (show_stack) from [<c122bdf8>] (dump_stack_lvl+0x80/0xac)
    [<c122bdf8>] (dump_stack_lvl) from [<c04b9380>] (print_address_description+0x5c/0x2ec)
    [<c04b9380>] (print_address_description) from [<c04b999c>] (kasan_report+0x16c/0x1a4)
    [<c04b999c>] (kasan_report) from [<bf1149bc>] (MI_SYS_Cmd_ProcShowThreads+0x100/0x110 [mi_sys])
    [<bf1149bc>] (MI_SYS_Cmd_ProcShowThreads [mi_sys]) from [<bf120260>] (MI_SYS_DEBUG_ProcOnExecCmd+0x80/0x98 [mi_sys])
    [<bf120260>] (MI_SYS_DEBUG_ProcOnExecCmd [mi_sys]) from [<bf11af88>] (_MI_SYS_Proc_DevCommonWrite+0x1d8/0x17f8 [mi_sys])
    [<bf11af88>] (_MI_SYS_Proc_DevCommonWrite [mi_sys]) from [<c0dda378>] (_CamProcWriteLinux+0x11c/0x180)
    [<c0dda378>] (_CamProcWriteLinux) from [<c05b7e8c>] (proc_reg_write+0xc8/0x124)
    [<c05b7e8c>] (proc_reg_write) from [<c04ea264>] (vfs_write+0x1c8/0x51c)
    [<c04ea264>] (vfs_write) from [<c04ea744>] (ksys_write+0x8c/0xfc)
    [<c04ea744>] (ksys_write) from [<c0200140>] (ret_fast_syscall+0x0/0x50)
    Exception stack(0xe0057fa8 to 0xe0057ff0)
    7fa0:                   0000000d a5e02064 00000001 a5e02064 0000000d ffffffff
    7fc0: 0000000d a5e02064 00000080 00000004 a6399138 b69b694c 0149b31c b69b6955
    7fe0: 00000000 b69b6918 0147fb5d a6379ce0
    
    Allocated by task 1521:
    __kasan_kmalloc+0xa0/0xac
    MI_SYS_Cmd_ProcShowThreads+0x30/0x110 [mi_sys]
    MI_SYS_DEBUG_ProcOnExecCmd+0x80/0x98 [mi_sys]
    _MI_SYS_Proc_DevCommonWrite+0x1d8/0x17f8 [mi_sys]
    _CamProcWriteLinux+0x11c/0x180
    proc_reg_write+0xc8/0x124
    vfs_write+0x1c8/0x51c
    ksys_write+0x8c/0xfc
    ret_fast_syscall+0x0/0x50
    0xb69b6918
    
    Freed by task 1521:
    kasan_set_track+0x28/0x30
    kasan_set_free_info+0x20/0x34
    ____kasan_slab_free+0xec/0x114
    __kasan_slab_free+0x14/0x1c
    kfree+0xa8/0x42c
    MI_SYS_Cmd_ProcShowThreads+0xf8/0x110 [mi_sys]
    MI_SYS_DEBUG_ProcOnExecCmd+0x80/0x98 [mi_sys]
    _MI_SYS_Proc_DevCommonWrite+0x1d8/0x17f8 [mi_sys]
    _CamProcWriteLinux+0x11c/0x180
    proc_reg_write+0xc8/0x124
    vfs_write+0x1c8/0x51c
    ksys_write+0x8c/0xfc
    ret_fast_syscall+0x0/0x50
    0xb69b6918
    
    # gdb 根据 BUG 那一行的栈信息去查看代码位置
    arm-linux-gnueabihf-gdb interface/src/sys/mi_sys.ko
    (gdb) b *(MI_SYS_Cmd_ProcShowThreads+0x100)
    Breakpoint 1 at 0x4ab70: file xxxxx.c, line 575.
    

    CONFIG_KASAN_SW_TAGS 对应 user 的 hwasan,只能在 64 位平台上使用,打开后可以减小 kasan 的内存占用

    4.4 HW Watchpoints (kernel)

    若开启 kasan 后无法复现,并且踩的变量一直是同一个,该变量的值很少发生变化,可以考虑使用 HW Watchpoints,因此该方法的使用范围很小

    如果需要在 sdk 的 ko 里面加,需要将 kernel/events/hw_breakpoint.cregister_wide_hw_breakpoint() gpl 改为 EXPORT_SYMBOL

    #include <linux/perf_event.h>
    #include <linux/hw_breakpoint.h>
    
    static int len = 0;
    static void sample_hbp_handler(struct perf_event *bp, struct perf_sample_data *data, struct pt_regs *regs)
    {
        pr_info("value is changed\n");
        dump_stack();
    }
    MI_S32 MI_SYS_Cmd_ProcShowThreads(/*...*/)
    {
        struct perf_event_attr attr;
    
        hw_breakpoint_init(&attr);
        attr.bp_type = HW_BREAKPOINT_W | HW_BREAKPOINT_R;
        attr.bp_addr = (u32)&len;
        attr.bp_len = HW_BREAKPOINT_LEN_4;
        register_wide_hw_breakpoint(&attr, sample_hbp_handler, NULL);
    
        len = 1; // 此处对变量的写入会触发中断去调用 sample_hbp_handler
        // ...
        return MI_SUCCESS;
    }
    

    log 显示最后对内存的写入函数是 MI_SYS_Cmd_ProcShowThreads

    Exception stack(0xc2461ca8 to 0xc2461cf0)
    1ca0:                   00000000 2e58b000 401c8642 00000001 c09ea6c8 00000000
    1cc0: bf0f3d58 bf0dee34 bf0b4f8c 00000000 00000001 c2461d9c c2461cc0 c2461cf8
    1ce0: c01093c0 bf0b502c 60000013 ffffffff
    r7:c2461cdc r6:ffffffff r5:60000013 r4:bf0b502c
    [<bf0b4f8c>] (MI_SYS_Cmd_ProcShowThreads [mi_sys]) from [<bf0bbac8>] (MI_SYS_DEBUG_ProcOnExecCmd+0x74/0x90 [mi_sys])
    r6:c2461e0c r5:c2461da8 r4:00000010
    [<bf0bba54>] (MI_SYS_DEBUG_ProcOnExecCmd [mi_sys]) from [<bf0bb0b8>] (_MI_SYS_Proc_DevCommonWrite+0x21c/0x2bc [mi_sys])
    r10:c35cbacc r9:00000000 r8:c2461e0c r7:00000000 r6:bf0bba54 r5:bf0f3db8
    r4:00000001
    [<bf0bae9c>] (_MI_SYS_Proc_DevCommonWrite [mi_sys]) from [<c058367c>] (_CamProcWriteLinux+0xac/0x118)
    r10:00000000 r9:c2461f58 r8:0000000d r7:000d2598 r6:c19b2cec r5:c35cbac0
    r4:0000000d
    [<c05835d0>] (_CamProcWriteLinux) from [<c01e3898>] (proc_reg_write+0x98/0xa8)
    r7:000d2598 r6:c1d34500 r5:c05835d0 r4:c1ba0e00
    [<c01e3800>] (proc_reg_write) from [<c0167f84>] (vfs_write+0xc0/0x1b0)
    r9:c01e3800 r8:000d2598 r7:c09ea6c8 r6:c2461f58 r5:c1d34500 r4:0000000d
    [<c0167ec4>] (vfs_write) from [<c01681cc>] (ksys_write+0x78/0xc4)
    r9:0000000d r8:c2461f64 r7:c09ea6c8 r6:000d2598 r5:c2461f58 r4:c1d34500
    [<c0168154>] (ksys_write) from [<c0168230>] (sys_write+0x18/0x1c)
    r9:c2460000 r8:c0008664 r7:00000004 r6:000d2598 r5:b6fce0c0 r4:000c5ea0
    [<c0168218>] (sys_write) from [<c0008420>] (ret_fast_syscall+0x0/0x50)
    

    4.5 Dump Kernel Image

    如果每次 kernel panic 位置不一样,而且经常是 kernel 原生 code 的 panic,可以怀疑是否 ddr 不稳定导致的

    通过 dump kernel 的 .text 段可以确认 ddr 是否不稳定:

    • 打开 kernel 目录下的 System.map,找到 __start_rodata_text,得到 .text 段的长度 size = __start_rodata - _text
    • cat /proc/iomem 获取 .text 段的起始地址,但是 dump 需要 MIU 地址,因此需要减去 System RAM 的起始地址,即 addr = kernel-code-start - system-ram-start
    • 关闭串口(串口输入 11111),打开 tv tool,执行下图的操作
    • 导出 vmlinux 的 .text 段:arm-linux-gnueabihf-objcopy -j .text -O binary vmlinux k_text.bin
    • 对比从 RAM 导出的文件和 vmlinux 导出的 .text,有许多的小段二进制不同是正常的(static key 造成的),若有一大段出现不同,可以怀疑 ddr 的稳定性

    linux kernel 目录下需要执行的 3 个步骤:

    # 1. size = 0xc06d0000 - 0xc0008000 = 0x6C8000
    cat System.map
    c0008000 T _text
    ...
    c06d0000 R __start_rodata
    
    # 2. addr = 0x1000008000 - 0x1000000000 = 0x8000
    cat /proc/iomem
    ...
    1000000000-107fffffff : System RAM
    1000008000-100097cfff : Kernel code
    10009e4000-1000b01d23 : Kernel data
    
    # 3. 导出 vmlinux .text
    arm-linux-gnueabihf-objcopy -j .text -O binary vmlinux k_text.bin
    

    5. CPU无异常log卡死问题

    5.1. dump log_buf

    当CPU 无异常log卡死时,有可能会有一些重要的log信息没有来得及打印,这时可以通过dump 系统的log buf获取这些log信息。

    确定log buf地址有以下几种方法:

    1. arm-linux-gnueabihf-sigmastar-9.1.0-gdb vmlinux Reading symbols from vmlinux... (gdb) info address __log_buf Symbol "__log_buf" is static storage at address 0xc041a090.

    2. cat System.map |grep __log_buf c041a090 b __log_buf

    3. 不同chip可能有差别,I6C可以用TV TOOL dump bank 0x1004数据,在offset 0x8和0x9记录了__log_buf地址。

    查到__log_buf地址之后,可以使用TV TOOL 或者 Trace32等工具dump对应的内存log信息。

    上面查到的log_buf地址是0xc041a090,如果用TV TOOL dump log buf就需要注意一点,就是dump的log_buf地址需要改成0x41a090(减去0xc0000000),dump log buf的长度一般用0x8000。参考下图的步骤1、2、3。

    5.2. 接Trace32做debug

    系统卡死时,可以通过Trace32 debug工具查看系统各线程的信息、CPU寄存器信息以及cache line信息和dram数据等信息。

    一般情况下,使用Trace32前的准备如下:

    1. EVB board有留出JTAG排插的话可以直接接Trace32。

      如果像demo board或者客户板子没有留JTAG排插,可以请硬件owner帮忙。

    2. 准备好对应chip在Trace32上使用的cmm脚本,可以问SI提供。

    3. 切换适合Trace32的JTAG mode,具体命令可以问SI提供。

      Souffle EVB切换使用的命令是:/customer/riu_w 0x103c 0x60 0x0200

      Iford EVB切换使用的命令是:/customer/riu_w 0x103c 0x60 0x80

    6. RCU相关问题

    如果发生RCU stall warning,可以立马怀疑是interrupt戳太多导致(大多数情况是如此)。若 rcu stall 打印的 backtrace 里面有 2 个 gic_handle_irq 基本可以锁定是 interrupt 的问题 所以问题转化为对查明是什么interrupt发生次数过多。 对此可以分为两种情况:

    1. 串口或者telnet可以操作 多cat /proc/interrupts,从而找出可疑的IRQ
    2. 无法操作板子 (1)可以使用tv_tool查看main interrupt controller的registers,看是哪个interrupt一直发生

      [main interrupt controller registers]
         Souffle:  /customer/riu_r 0x1009
               查看fiq offset: 0x4c 0x4d 0x4e 0x4f
               查看irq offset: 0x5c 0x5d 0x5e 0x5f
         Iford: /customer/riu_r 0x1009
               查看fiq offset: 0x4c 0x4d 0x4e 0x4f
               查看irq offset: 0x5c 0x5d 0x5e 0x5f
      

      (2)打开CONFIG_SSTRA_IRQ_DEBUG_TRACE,它会统计每个interrupt发生的次数,然后在发生soft lockup时打印出统计信息