4.4.7. Log 系统介绍

4.4.7.1. Linux log 系统简介

Linux 的日志系统主要可以分为三个主要部分:

  1. 日志缓冲区:日志缓冲区(log buffer)是日志系统核心组件。

  2. 日志写入:通过 printkprintk_deferreddevkmsg_writedev_printk_emit接口日志写入日志缓冲区。

  3. 日志提取:通过注册控制台(console)、/dev/kmsg 以及 syslogd提取日志信息。

三个部分可以描述下图:

log_system

log buffer

在 Linux 系统中,日志缓冲区(log buffer)是内核用来临时存储日志消息环形区域。它主要作用提供一个快速日志消息收集机制,允许内核影响系统性能情况记录重要信息。

log buffer 作为一个静态全局变量定义,它__LOG_BUF_LEN大小控制:

// kernel/printk.c
#define __LOG_BUF_LEN (1 << CONFIG_LOG_BUF_SHIFT)     // 即 2 的 CONFIG_LOG_BUF_SHIFT 次幂
static char __log_buf[__LOG_BUF_LEN] __aligned(LOG_ALIGN);

__LOG_BUF_LENCONFIG_LOG_BUF_SHIFT 影响,CONFIG_LOG_BUF_SHIFT 在 kernel config 中设置大小:

log_buf_size

可以看到,当前系统设置CONFIG_LOG_BUF_SHIFT 大小是17,所以 log buffer 的大小就是 2^17B(128KB)。若需修改 log buffer 大小,只在 menuconfig 中修改 CONFIG_LOG_BUF_SHIFT,然后重新编译内核镜像即可。

需要注意,在当前系统内核版本中(Linux V6.1.83) log buffer 的范围是 4KB ~ 32MB (CONFIG_LOG_BUF_SHIFT范围是12~25):

# init/Kconfig
config LOG_BUF_SHIFT
	int "Kernel log buffer size (16 => 64KB, 17 => 128KB)"
	range 12 25
	default 17
	depends on PRINTK
	help
	  Select the minimal kernel log buffer size as a power of 2.
	  The final size is affected by LOG_CPU_MAX_BUF_SHIFT config
	  parameter, see below. Any higher size also might be forced
	  by "log_buf_len" boot parameter.

	  Examples:
		     17 => 128 KB
		     16 => 64 KB
		     15 => 32 KB
		     14 => 16 KB
		     13 =>  8 KB
		     12 =>  4 KB

log 写入接口

printk 函数

printk 是 Linux 内核用于输出日志信息核心函数,它用户空间printf 类似,但用于内核空间。printk内核调试、日志记录信息报告重要工具。它能够调试信息、错误消息输出内核日志缓冲区,然后系统日志守护进程(如 dmesg)将这些信息展示用户。

1) printk 基本概述

printk 是 Linux 内核提供一个用于输出日志信息函数,其原型如下:

int printk(const char *fmt, ...);
  • fmt:格式化字符串,类似printf格式化字符串。

  • 后续参数:与格式化字符串相对数据。

printk行为printf 相似,但是输出结果保存内核日志缓冲区中,而不是标准输出。

2)printk日志级别

printk 支持多种日志级别,帮助开发人员根据重要性、严重性用途区分不同日志消息。日志级别内核日志系统定义,并且可以影响信息显示方式以及是否保存。常用日志级别如下:

  • KERN_EMERG (0):紧急事件消息,通常用于系统崩溃严重错误。这些消息立即打印,并可能触发系统重启。

  • KERN_ALERT (1):警报消息,表示需要立即处理严重系统问题。

  • KERN_CRIT (2):严重错误消息,通常用于严重硬件软件操作失败。

  • KERN_ERR (3):错误消息,表示需要进一步调试处理系统错误,驱动程序常用级别报告硬件错误。

  • KERN_WARNING (4):警告消息,表示可能存在通常不会导致系统崩溃问题。

  • KERN_NOTICE (5):通知消息,表示正在执行重要操作,但属于错误警告,常用安全相关提示。

  • KERN_INFO (6):提示信息,表示正常操作状态变更,例如驱动程序启动打印硬件信息。

  • KERN_DEBUG (7):调试信息,用于开发过程输出详细调试信息。

示例:

printk(KERN_INFO "Device initialized successfully.\n");
printk(KERN_ERR "Failed to allocate memory for device.\n");
printk(KERN_DEBUG "Debugging device register read.\n");

指定 log level,默认 loglevel 是 default_message_loglevel,默认为 4,也就是只会显示 KERN_WARNING 级别以上日志:

// kernel/include/linux/printk.h
#define default_message_loglevel (console_printk[1])
/* printk's without a loglevel use this.. */
#define MESSAGE_LOGLEVEL_DEFAULT CONFIG_MESSAGE_LOGLEVEL_DEFAULT

// kernel/kernel/printk/printk.c
int console_printk[4] = {
	CONSOLE_LOGLEVEL_DEFAULT,	/* console_loglevel */
	MESSAGE_LOGLEVEL_DEFAULT,	/* default_message_loglevel */
	CONSOLE_LOGLEVEL_MIN,		/* minimum_console_loglevel */
	CONSOLE_LOGLEVEL_DEFAULT,	/* default_console_loglevel */
};

default_message_loglevel实际就是 MESSAGE_LOGLEVEL_DEFAULT ,而 MESSAGE_LOGLEVEL_DEFAULT在 kernel config 中设置的:

default_message_loglevel

MESSAGE_LOGLEVEL_DEFAULT取值范围是 1~7:

# lib/Kconfig.debug
config MESSAGE_LOGLEVEL_DEFAULT
	int "Default message log level (1-7)"
	range 1 7
	default "4"
	help
	  Default log level for printk statements with no specified priority.

	  This was hard-coded to KERN_WARNING since at least 2.6.10 but folks
	  that are auditing their logs closely may want to set it to a lower
	  priority.

	  Note: This does not affect what message level gets printed on the console
	  by default. To change that, use loglevel=<x> in the kernel bootargs,
	  or pick a different CONSOLE_LOGLEVEL_DEFAULT configuration value.

3)printk线程安全

printk 函数系统中是线程安全的。这意味着多个内核线程可以同时调用 printk,并且内核保证日志信息不会混乱。

但是,printk输出使用一个全局日志锁(logbuf_lock)来同步访问日志缓冲区。这个确保多个日志消息不会互相干扰,但同时可能高频率日志输出带来一定性能开销,特别系统负载高时。所以,我们开发过程可以选择使用更为精细化、灵活可控日志接口,而不是驱动直接使用 printk

4)动态调试

内核允许动态启用调试输出(printk调试信息),通过修改 /sys/kernel/debug/dynamic_debug/control 文件控制哪些模块文件调试信息启用。例如,可以启用某个驱动程序调试输出:

echo "file drivers/net/* +p" > /sys/kernel/debug/dynamic_debug/control

这会启用 drivers/net/ 目录所有源文件调试信息输出。

关闭相应调试信息打印,可以dynamic_debug/control 文件写入关闭控制指令 -p。示例如下:

echo "file drivers/net/* -p" > /sys/kernel/debug/dynamic_debug/control

5)printk限制

有时,日志信息频繁输出可能导致系统性能问题,尤其同一个错误警告反复发生时。为了避免这种情况,内核提供以下两种机制:

  • printk_once:确保某个日志打印一次,无论函数调用多少次。这适用需要打印一次错误信息。

    printk_once("This error message will be printed only once.\n");
    
  • printk_ratelimited:限制日志输出频率。默认情况下,内核每 5 秒打印 10 次相同错误信息。这对于那些重复触发错误非常有用,能够减少日志冗余输出。

    printk_ratelimited("Resource usage high, please check.\n");
    

驱动常用日志接口

内核提供一些专门用于设备驱动日志接口,这些接口基于设备结构体(struct device)进行设计,能够日志设备实例关联,从而方便追踪设备行为。

驱动开发常用日志接口有:

dev_err、dev_warn、dev_info、dev_dbg

四个接口主要用于设备相关日志输出,它们依赖于 struct device 结构体,因此需要驱动代码使用 dev(设备结构)指针,下面分别说明四个接口:

  • dev_err

    • dev_err(dev, fmt, ...) 用于打印错误信息,通常设备操作失败或者发生严重错误使用。此函数错误信息输出系统日志中,供开发人员进行问题排查。

    • 示例: dev_err(dev, "Failed to initialize device: %d\n", ret);

  • dev_warn

    • dev_warn(dev, fmt, ...) 用于打印警告信息,适用致命错误异常情形,提示系统存在潜在问题影响设备继续运行。

    • 示例: dev_warn(dev, "Device is running low on resources\n");

  • dev_info

    • dev_info(dev, fmt, ...) 用于打印常规信息状态报告。适用设备初始化、状态变更较为普通日志输出。

    • 示例: dev_info(dev, "Device initialized successfully\n");

  • dev_dbg

    • dev_dbg(dev, fmt, ...) 用于打印调试信息,适合开发调试阶段使用,提供详细设备操作信息。注意,dev_dbg 一般默认禁用,因此调试模式有效。

    • 示例: dev_dbg(dev, "Reading register value: 0x%X\n", value);

上述接口下面几个优点:

  • 关联性强:这些接口设备结构体 dev 绑定,能够清晰记录哪个设备发生什么事件,方便调试排查问题。

  • 日志级别区分明确:根据不同日志级别(错误、警告、信息、调试),可以有效控制日志详细程度。

  • 便于扩展维护:使用这些设备相关接口,可以保证日志格式一致性,增强代码可维护性。

设备结构使用接口

某些情况下,日志输出可能依赖于特定设备结构,例如内核模块其他部分通用代码中。这时,可以使用 pr_* 系列接口,比如:pr_errpr_warnpr_infopr_debug

  • pr_err(fmt, ...):用于输出错误信息。

    示例:

    pr_err("Memory allocation failed\n");
    
  • pr_warn(fmt, ...):用于输出警告信息。

    示例:

    pr_warn("The configuration value is set to the default\n");
    
  • pr_info(fmt, ...):用于输出普通信息。

    示例:

    pr_info("Kernel module loaded successfully\n");
    
  • pr_debug(fmt, ...):用于输出调试信息,适合开发阶段。

    示例:

    pr_debug("Debugging internal state: %d\n", state);
    

pr_debugdev_dbg调试输出默认情况关闭的,以避免生产环境产生过多日志信息。在开发调试过程中,可以通过动态调试功能打开这些日志。

echo "file drivers/mmc/host/* +p" > /sys/kernel/debug/dynamic_debug/control

应用程序 log 接口

  1. 推荐使用 ALOG 的接口,对应打印级别有 ALOGV、 ALOGD、 ALOGI、 ALOGW、 ALOGE、 ALOGV_TAG、 ALOGD_TAG、 ALOGI_TAG、 ALOGW_TAG、 ALOGE_TAG、 ALOGF_TAG。

  2. 使用 ALOG* 接口时,各个模块输出 Log 时,可以通过定义 LOG_TAG 宏来打印自身模块名, logcat 支持使用 tag 筛选日志。其通过 Makefile 传入,如: DLOG_TAG=camera;或者代码开头声明。

  3. 使用 ALOG*_TAG 接口时,各个模块输出 Log 时,可以通过第一个参数打印自身模块名, logcat 支持使用 tag 筛选日志。

  4. log 系统支持 1 个 2MB 的 log_main 缓冲区, 1 个 256KB 的 log_radio、 log_system 和 log_event 和缓冲区,建议使用 log_main 缓冲区。

    • 当 bufID 是 LOG_ID_RADIO 时,日志保存到 log_radio 缓冲区, bufID 是 LOG_ID_SYSTEM 时,日志保存到 log_system 缓冲区,默认是 log_main 缓冲区。

代码示例:

完整代码在 hbre/liblog/log_test.cpp 文件查看

#define LOG_TAG "alog_test"

#include <stdio.h>
#include <logging.h>

int main(int argc, char *argv[])
{
printf("\n  logcat test !!!!!!!!!!!!!!!\n");

if (argc == 2 && 0 == strcmp(argv[1], "--test")) {
  logprint_run_tests();
  exit(0);
}

if (argc == 2 && 0 == strcmp(argv[1], "--help")) {
  android::show_help(argv[0]);
  exit(0);
}

printf("\n LogV LogD test start!!! \n");

ALOGV("**************************ALOGV test start***************************");
ALOGV("**************************1***************************");
ALOGV("**************************2***************************");
ALOGV("**************************3***************************");
ALOGV("**************************4***************************");
ALOGV("**************************5***************************");
ALOGV("**************************ALOGV test  end ***************************");

ALOGD("**************************ALOGV test start***************************");
ALOGD("**************************1***************************");
ALOGD("**************************2***************************");
ALOGD("**************************3***************************");
ALOGD("**************************4***************************");
ALOGD("**************************5***************************");
ALOGD("**************************ALOGV test  end ***************************");

RLOGV("**************************RLOGV test log_radio start***************************");
RLOGD("**************************1*****************************************************");
RLOGE("**************************RLOGV test log_radio end******************************");

SLOGV("**************************SLOGV test log_system start***************************");
SLOGD("**************************2*****************************************************");
SLOGE("**************************RLOGV test log_system end**************************** *");
printf("\n LogV LogD write test success!!! \n");

return 0;
}

测试结果:

# ./logtest

不设置过滤:
# logcat
logcat test start !!!
--------- beginning of /dev/log_main
V/alog_test(21590): *ALOG test start*
D/alog_test(21590): ********1********
I/alog_test(21590): ********2********
W/alog_test(21590): ********3********
E/alog_test(21590): ********4********
V/tag     (21590): ********1********
D/tag     (21590): ********1********
I/tag     (21590): ********2********
W/tag     (21590): ********3********
E/tag     (21590): ********4********
F/tag     (21590): ********1********
V/alog_test(21590): *ALOG test end***

设置过滤 -s *:F
# logcat -s *:F
logcat test start !!!
--------- beginning of /dev/log_main
F/tag     (21590): ********1********

设置过滤 -s *:E
# logcat -s *:E
logcat test start !!!
--------- beginning of /dev/log_main
E/alog_test(21590): ********4********
E/tag     (21590): ********4********
F/tag     (21590): ********1********

设置过滤 -s tag:E
# logcat -s tag:E
logcat test start !!!
--------- beginning of /dev/log_main
E/tag     (21590): ********4********
F/tag     (21590): ********1********

切换缓存区为 log_radio:

logcat test start !!!
--------- beginning of /dev/log_radio
V/        ( 1319): **************************RLOGV test log_radio start***************************
D/        ( 1319): **************************1*****************************************************
E/        ( 1319): **************************RLOGV test log_radio end******************************

切换缓存区为 log_system:

  logcat test start !!!
--------- beginning of /dev/log_system

V/        ( 1319): **************************SLOGV test log_system start***************************
D/        ( 1319): **************************2*****************************************************
E/        ( 1319): **************************RLOGV test log_system end**************************** *

logcat 命令格式如下

参数 描述
1 -b <buffer> 加载一个使用日志缓冲区查看,比如 event 和 radio,默认值是 main
2 -c 清除缓冲区全部日志退出(清除完后可以使用 -g 查看缓冲区)
3 -d 缓冲区的 log 转存屏幕然后退出
4 -f <filename> 将 log 输出指定文件<文件名>,默认标准输出(stdout)
5 -g 打印日志缓冲区大小退出
6 -n <count> 设置日志最大数目 <count>,默认值是4,需要-r 选项一起使用
7 -r <kbytes> 输出 <kbytes>轮替日志文件,默认值是16,需要-f 选项一起使用
8 -s 设置过滤器
9 -v <format> 设置输出格式日志消息,默认短暂格式

log 提取

在 Linux 系统中,日志输出途径多种,其中包括通过 console/dev/kmsgsyslogd 提取日志。以下详细介绍三种主要日志提取方式。

Console(控制台)

console连接系统终端设备,通常用于输出内核日志信息系统消息。在 Linux 系统中,控制台基础日志输出方式之一,尤其没有图形界面情况下。

一般常用console 有 uart、net、pstore,但是需要注意 uart 的 write 函数受限于串口波特率的,过低波特率(比如 115200),打印一行 log 是导致毫秒中断,如果串口打印大量 log,会导致 CPU 一直中断状态,其它进程很难调度机会,会导致系统时序异常、softlockup 等问题。而且大量日志串口打印速度慢,需要快速打印日志场景,建议使用 net console(即,使用 ssh 窗口)。

控制台输出特点:

  • 实时显示:控制台输出直接显示终端的,通常用于用户交互式系统诊断。

  • 内核消息:通过 printk()函数输出内核日志信息发送到控制台。控制台显示日志包括启动过程内核日志、硬件信息、错误信息等。

  • 控制台支持:Linux 支持多个虚拟控制台(如 /dev/tty1/dev/tty6)。用户可以这些虚拟终端查看日志信息,或者通过 dmesg 命令查看内核环形缓冲区日志。

  • 控制台日志级别:内核允许用户设置不同日志级别控制哪些信息输出控制台。可以通过内核启动参数或者运行修改 console_loglevel 变量配置输出详细程度。

控制台配置使用:

控制台可以使用 dmesg 命令查看内核缓冲区日志,并根据需要设置控制台输出日志级别。

查看系统使用日志等级信息:

root@buildroot:~# cat /proc/sys/kernel/printk
6       4       1       6

四个数字依次对应 console_logleveldefault_message_loglevelminimum_console_loglevel 以及 default_console_loglevel

  • console_loglevel:控制台使用日志级别;

  • default_message_loglevel:调用 printk() 未指定日志级别使用日志级别;

  • minimum_console_loglevel:允许设置控制台日志级别(console_loglevel)最小值;

  • default_console_loglevel:系统启动使用日志级别。

通常情况下,为了加速系统启动,系统启动信息非常少。比如 default_console_loglevelconsole_loglevel 为 4 的情况下,只能显示 err、crit、alert 和 emerg 等调式信息,所以,在内核模块调式需要调整日志显示级别。

控制台中,一般常用下面两种方式调整控制台输出日志详细程度:

1) 修改 proc/sys/kernel/printk

echo "8" > /proc/sys/kernel/printk

上述命令控制台日志级别设置8,显示所有级别日志。这里需要注意,控制台只会显示大于设置等级信息,所以,想要显示 debug 信息,console_loglevel 应设置大于 7值。另外,这种修改方式系统重启失效。

2)dmesg 命令修改

dmesg一个用于显示控制内核缓冲区(kernel ring buffer)内容用户空间命令。它能够显示系统启动以来内核消息(日志信息),包括内核启动信息、驱动加载、硬件检测、文件系统挂载、设备驱动状态等。

dmesg -n <value>echo x > proc/sys/kernel/printk 能够达到同样效果。同样,系统重启失效。

dmesg -n 7

dmesg 命令格式如下:

选项 描述
-C, --clear 清空内核环形缓冲区
-c, --read-clear 读取清空所有消息
-D, --console-off 禁用控制台打印消息
-E, --console-on 启用控制台打印消息
-F, --file <file> 使用指定文件内核日志缓冲区
-f, --facility <list> 限制输出定义设施列表
-H, --human 显示人类输出格式
-J, --json 使用 JSON 输出格式
-k, --kernel 显示内核消息
-L, --color[=<when>] 启用消息着色(auto、always 或 never),默认启用颜色
-l, --level <list> 限制输出定义日志级别
-n, --console-level <level> 设置打印控制台消息级别
-P, --nopager 输出通过分页程序传递
-p, --force-prefix 强制每行多行消息输出时间
-r, --raw 打印原始消息缓冲区内容
--noescape 转义不可打印字符
-S, --syslog 强制使用 syslog(2) 而非 /dev/kmsg
-s, --buffer-size <size> 设置查询内核环形缓冲区缓冲区大小
-u, --userspace 显示用户空间消息
-w, --follow 等待消息
-W, --follow-new 等待打印消息
-x, --decode 解码设施级别字符串
-d, --show-delta 显示打印消息之间时间差
-e, --reltime 显示本地时间时间差格式
-T, --ctime 显示人类时间戳(可能准确)
-t, --notime 显示任何时间
--time-format <format> 使用指定格式显示时间戳:[delta
--since <time> 显示指定时间以来日志
--until <time> 显示直到指定时间日志
-h, --help 显示帮助信息
-V, --version 显示版本信息

支持日志设施:

设施 描述
kern 内核消息
user 随机用户消息
mail 邮件系统
daemon 系统守护进程
auth 安全/授权消息
syslog syslogd 生成内部消息
lpr 打印机子系统
news 网络新闻子系统

支持日志级别(优先级):

级别 描述
emerg 系统不可
alert 必须立即采取行动
crit 严重条件
err 错误条件
warn 警告条件
notice 正常重要条件
info 信息性消息
debug 调试级别消息

/dev/kmsg

/dev/kmsg 是 Linux 中一个虚拟设备文件,提供一个接口读取内核日志。它主要作用允许用户空间进程直接内核日志读取消息以及内核日志发送消息。

/dev/kmsg使用方式:

  • 查看内核日志: 可以通过 cat 命令查看 /dev/kmsg日志:

    sudo cat /dev/kmsg
    

    命令输出内核日志内容,包括通过 printk() 打印所有日志信息。cat /dev/kmsgdmesg -w执行效果相似,它们用于实时显示内核日志。

  • 写入内核日志: 可以使用 echo 命令消息写入内核日志。例如:

    echo "Custom log message from user space" | sudo tee /dev/kmsg
    dmesg | tail -n 5
    

许多日志收集工具(如 rsyslogsystemd-journald)会定期读取 /dev/kmsg 文件提取内核日志存储日志文件转发远程服务器。

注意,与 dmesg 不同, 第一次执行 sudo cat /proc/kmsg 打印目前位置所有内核信息,再次执行 sudo cat /proc/kmsg 时,则不会打印再次之前打印信息。

syslogd

syslogd(System Log Daemon)是一个用于管理记录系统日志守护进程。在 Unix 和 Linux 系统中,syslogd 是一个核心组件,它负责收集、存储、转发系统日志消息,并这些日志消息传送文件、远程服务器其他地方。系统日志通常包含操作系统、应用程序、设备驱动程序以及其他软件生成各种信息,如错误、警告、调试信息等。

syslogd 的历史可以追溯到 1980 年代早期,是 Unix 系统一个核心重要组件,下面是 syslogd 发展简要历史概述:

  • 1980年代初:syslog 出现,作为 Unix 系统日志基础,syslogd 守护进程负责处理日志。

  • 1983年:syslog 被引入到 BSD 4.2 Unix 系统中,成为标准化日志管理工具。

  • 1990年代:syslogd 在 Unix 和 Linux 系统得到广泛应用,并开始支持远程日志转发。

  • 2000年代:rsyslog 作为 syslogd 的增强版本,提供多功能,如高性能、日志加密、数据库集成等。

  • 2010年代:systemd 作为现代 Linux 系统核心组件,逐步取代传统的 syslogd,引入了 systemd-journald,但依然兼容传统的 syslog 协议。

syslogd 配置说明

内核中, /etc/syslog-startup.conf 文件配置了 syslogd 进程以及其子服务,该文件内容如下:

# This configuration file is used by the busybox syslog init script,
# /etc/init.d/syslog[.busybox] to set syslog configuration at start time.

DESTINATION=file                        # log destinations (buffer file remote)
LOGFILE=/userdata/log/kernel/message     # where to log (file)
REMOTE=loghost:514                      # where to log (syslog remote)
REDUCE=no                               # reduce-size logging
DROPDUPLICATES=no                       # whether to drop duplicate log entries
BUFFERSIZE=64                           # size of circular buffer [kByte]
FOREGROUND=no                           # run in foreground (don't use!)
#LOGLEVEL=5                             # local log level (between 1 and 8)

下面每个配置详细解析:

  • DESTINATION=file

    • 含义:定义日志目标地点,可以file(日志文件)、buffer(环形缓冲区)或 remote(远程 syslog 服务器)。

    • 解释:此配置设置日志记录目标文件。这意味着日志写入指定文件,而不是存在内存缓冲区通过网络发送到远程服务器。

  • LOGFILE=/userdata/log/kernel/message

    • 含义:指定日志文件路径。

    • 解释:这个配置设置日志文件存储路径/userdata/log/kernel/message。系统文件存储日志信息。如果 DESTINATION 配置file,则文件实际日志输出文件。

  • REMOTE=loghost:514

    • 含义:指定远程 syslog 服务器地址端口。

    • 解释:如果选择 DESTINATION=remote,则日志发送到指定远程主机。在这里,日志发送到 loghost 主机的 514 端口(默认的 syslog 端口)。此项配置可能用于日志信息转发集中管理远程日志服务器。

  • REDUCE=no

    • 含义:是否减少日志大小。

    • 解释:这个选项决定是否进行日志压缩其他方式减少日志大小。设置no 表示压缩日志,日志原样存储。

  • DROPDUPLICATES=no

    • 含义:是否丢弃重复日志条目。

    • 解释:该选项控制是否日志丢弃重复条目。no 表示丢弃重复条目,因此日志可能包含相同日志消息多次。

  • BUFFERSIZE=64

    • 含义:环形缓冲区大小(以 KB 为单位)。

    • 解释:指定用于存储日志环形缓冲区大小,这里设置为 64KB。环形缓冲区一个有限大小缓冲区,用于临时存储日志数据,缓冲区时会覆盖日志条目。

  • FOREGROUND=no

    • 含义:是否前台运行 syslog

    • 解释:设置no 表示 syslog后台运行,而不是前台运行。通常情况下,后台运行服务不会阻塞终端,而前台运行服务占用终端输出日志。

  • #LOGLEVEL=5

    • 含义:设置本地日志级别(1 到 8),和 printk日志级别一致。

    • 解释:这一行注释了,因此没有启用。LOGLEVEL 配置允许设置本地日志详细级别,数值从 1 到 8,数字越大,日志输出详细。通常,级别 5 表示“警告”级别日志,显示重要警告错误信息。

/etc/syslog-startup.conf 最终/etc/init.d/S91syslogd 脚本解析用来设置日志记录行为。

syslogd 日志格式说明

配置/etc/syslog-startup.conf运行syslogdklogd 后,一般所有 log 的信息追加/userdata/log/kernel/message 中。 下面其中一些日志内容:

root@buildroot:~# cat /userdata/log/kernel/message
Jan  1 00:00:04 buildroot kern.debug kernel: [    0.140925] dr-power-domain 31030000.power-controller: Looking up bpu-supply from device tree
Jan  1 00:00:04 buildroot kern.info kernel: [    0.141973] horizon-aon-pinctrl 31040000.aon_iomuxc: Initialized D-Robotics pinctrl driver
Jan  1 00:00:04 buildroot kern.err kernel: [    0.144257] (NULL device *): no horizon,gpio-banks in node /soc/disp_apb/disp_iomuxc@3e0a0054
……
Jan  1 00:51:52 buildroot user.notice ptp4l: [3112.607] selected local clock 8ea1d5.fffe.4ca67d as best master
Jan  1 00:51:55 buildroot kern.debug kernel: [ 3115.366849] sdhci-dwcmshc 35040000.sdhci: dwcmshc_runtime_resume
Jan  1 00:51:55 buildroot kern.debug kernel: [ 3115.366971] sdhci-dwcmshc 35040000.sdhci: Get fixed-drv-type: 2

/userdata/log/kernel/message 文件日志目的组成格式如下:

<timestamp> <hostname> <level> <label>: [duration] <message>

下面日志为例详细说明日志格式:

Jan  1 00:51:52 buildroot user.notice ptp4l: [3112.607] selected local clock 8ea1d5.fffe.4ca67d as best master
  • timestamp (时间戳)Jan 1 00:51:52,日志消息记录时间,通常包括月份(MMM)、日期(dd)和时间(hh:mm:ss)。

  • hostname (主机名)buildroot,产生日志消息主机名。

  • level (日志级别)user.notice,表示日志消息级别,这里表示这是一条来自用户的 notice 级别信息。

  • label (标签)ptp4l,表示产生日志进程、服务系统组件名称,这里表示消息来自 PTP 时间同步守护进程。

  • duration (持续时间)[3112.607],它表示系统启动持续第 3112.607 秒打印日志。

  • message (消息内容)selected local clock 8ea1d5.fffe.4ca67d as best master,日志消息具体内容。

有关 syslogd更加详细信息可以官方文档获取。

4.4.7.2. log 目录结构

log 分区

平台日志系统分区如下:

log 类型 存储位置 单个 log 大小 是否压缩 内容
基础系统 log kernel /userdata/log/kernel 2M 内核 log 信息
pstore /userdata/log/pstore 最大3M 内核 crash 的 log 信息
coredump /userdata/log/coredump 限制 应用 crash 的 log 信息
remoteproc /userdata/log/dsp 2M DSP 输出的 log 信息
uboot /userdata/log/uboot 4KB uboot 输出的 log 信息
reset /userdata/log/reset_reason.txt 1M 记录每次系统重启原因
/userdata/log/reset_count.txt 4KB 记录当前系统重启次数
ALOG 系统 ALOG /userdata/log/usr 2M 应用日志

log 分区内容说明

  1. Kernel log

    • 内核 log:通过 klogd 和 syslogd 转存到 /userdata/log/kernel 目录

    • pstore log:当内核 crash 重启时,移动 /sys/fs/pstore 目录日志到 /userdata/log/pstore 目录下,记录系统 panic 前后内核日志

  2. 启动原因 log

    • reset-reason.txt 信息介绍

      • COLD_BOOT: 掉电上电

      • UBOOT_RESET: uboot 下中重启

      • PANIC:发生了 panic

      • WATCHDOG: 触发了 watchdog

      • REBOOT_CMD: kernel 下的 reboot 命令

    • reset_count.txt:当前系统重启次数

  3. remoteproc log

    • dsp:ADSP 输出的 log 信息

  4. ALOG 系统

    • 使用 ALOG 的接口打印的 log 信息

    • 应用软件推荐使用 ALOG 接口

  5. 应用 crash log

    • coredump:储存进程运行突然崩溃那一刻一个内存快照,会进程此刻内存、寄存器状态、运行堆栈信息转储保存目录文件

4.4.7.3. log 管理

log 进程

log 进程信息说明

  • 板端 log 进程运行信息

    # ps -aux | grep log
    root       817  0.0  0.2   3852  2764 ?        S    00:00   0:00 /bin/bash /usr/bin/hobot-log start
    root       826  0.0  0.0   3008   332 ?        S    00:00   0:02 /sbin/syslogd -n -O /userdata/log/kernel/message -s 2048 -b 99
    root       892  0.0  0.0   2724   332 ?        S    00:00   0:00 /usr/hobot/bin/hrut_remoteproc_log -b dsp -f /userdata/log/dsp/message -r 2048 -n 50
    root       896  0.0  0.1   5088  1756 ?        S    00:00   0:00 /usr/hobot/bin/logcat -v time -f /userdata/log/usr/message -r2048 -n 100
    root       900  0.0  0.0   3008   308 ?        S    00:00   0:01 /sbin/klogd -n
    
  • kernel: klogd+syslogd

    • 获取内核记录消息,将消息数据转存文件。

  • usr: ALOG(libalog.so)+logcat

    • 通过 ALOG 接口向 log buffer 写入 log 信息,logcat 从 log buffer 中提取数据文件中。

  • remoteproc_log:hrut_remoteproc_log 进程

    • log 记录:通过 remoteproc 节点获取 ADSP 的 log 信息,将写入文件中。

    • log 管控:根据输入参数的 log 文件大小数量限制管控 log 的存储空间。

  • 日志管理:hobot-log

    • log 记录:记录 reset,pstore,uboot 的 log 信息。

    • log 管理:定期原始 log 文件转存固定格式文件,管控各个分区目录存储空间,超过固定容量进行删除,删除时间比较产生文件。

log 进程启动顺序说明

挂载所有分区后,系统索引 /etc/init.d/ 目录中 log 相应脚本,并按规定顺序启动相应这些脚本,具体如下:

S90log_daemon
S91syslogd
S92hobot_log_start
S92klogd

执行顺序如下:

|- etc/init.d/S90log_daemon
        |- /usr/bin/hobot-log first //转储上次启动的日志信息
                |- record_reset_count(shell function)
                |- system_config(shell function)
                |- wait_for_timesync(shell function)
                |- record_reset_reason(shell function)
                |- set_pstore(shell function)
                |- set_uboot(shell function)
                |- check_first_log(shell function)
        |- /usr/bin/hobot-log
                |- check_log(shell function, major cycle)

|- /etc/init.d/S91syslogd
        |- /sbin/syslogd -n $SYSLOG_ARGS

|- /etc/init.d/S92hobot_log_start
        |- /usr/bin/hrut_remoteproc_log -b dsp
        |- logcat -v time

|- /etc/init.d/S92klogd
        |- /sbin/klogd -n $KLOGD_ARGS

log 管理方式

log 的文件生成目录空间管控主要/usr/bin/hobot-log 脚本管理的。

hobot-log 脚本简介

hobot-log 脚本功能管理归档多种日志文件,它会定期检查日志文件大小数量,并根据配置进行文件循环保存,它可以处理不同日志源(如内核日志、用户空间日志、uboot、pstore 等),并需要备份日志指定目录。

  • log 生成:

    • 系统每次启动时去转存 log,log 文件命名格式如下

      • X5_Uboot-count-time.Log:当系统启动时,将 uboot log 转存/userdata/log/uboot/archive 目录

      • X5_Pstore-count-time(文件夹):当系统启动时,如果检测上次系统异常重启,则创建格式文件夹,并对应异常 log 信息记录文件夹

    • 系统运行过程每10分钟转存 log,文件转存各个模块的 archive 目录下,log 文件命名格式如下:

      • X5_Kernel-count-time_<inode>.Log

      • X5_Usr-count-time_<inode>.Log

      • X5_Bl31-count-time_<inode>.Log

      • X5_Dsp0-count-time_<inode>.Log

      • X5_Dsp1-count-time_<inode>.Log

      • X5_Mcore-count-time_<inode>.Log

    • log 文件命名格式解析示例: 以 /userdata/log/kernel/archive 目录日志文件 X5_Kernel-0003-2022_05_01_08_01_00_131599.Log 为例进行说明。

      • 命名说明:[board]_[module]-[count]-[time]_<inode>.Log

        • board: X5

        • module: Kernel, Usr, Dsp, Uboot, Pstore。首字母大写

        • count: 4位数字,0000到9999,示例代表第3次重启

        • time:如2022_05_01_08_01_00

        • inode:如131599。正常情况没有,只有出现同一时间转存多个文件命名存在重合时,加入 inode 节点区分

  • 容量管控:

    • 开始划分不同的 log 目录空间容量,每10分钟针对指定目录容量检索空间,如果容量超过,将会按照文件生成时间排序,删除产生时间比较文件

    • hobot-log

      管理着 usr、kernel、remoteproc_log、pstore、uboot 的空间,对应数量限制内部参数设定。 其中,每个类型日志文件大小说明log 分区,usr、kernel、pstore、uboot 日志文件系统默认数量限制为100个,dsp 日志文件系统默认数量限制为50个。 hobot-log 脚本相关内容定义如下:

      ROTATESIZE=2048 #KB
      ROTATEGENS_KER=100
      ROTATEGENS_USR=100
      ROTATEGENS_REMOTE=50
      ROTATEGENS_CHIP=50
      ROTATEGENS_ALL=$((${ROTATEGENS_REMOTE} + ${ROTATEGENS_KER} + ${ROTATEGENS_USR} + ${ROTATEGENS_CHIP}))
      ROTATESIZE_BYTES=$((${ROTATESIZE} * 1024))
      PSTORE_LOGMAX=100
      

      通过这样配置可以实现日志轮换,当日志文件达到一定大小(当前设定值为 2048KB)时,系统当前日志文件重命名,并创建一个日志文件继续记录日志消息。并且限制保存文件数量,在达到设置文件数量上限根据时间顺序删除最早日志,这样可以防止日志文件无限增长,占用过多磁盘空间。

    • hrut_remoteproc_log:

      log 的大小数量进程自身管理的,对应参数配置是 hobot-log 中设置

用户定制 log 系统

  • 日志命名修改

    • 在 hobot-log 中修改,具体修改功能逻辑check_log 函数实现

      • $1:log 产生原始目录

      • $2:log 根据原始文件转存目录

      • $3:log 文件命名前缀

      • $4:log 文件命名后缀

      • $5:log 文件限制最大数量

        #$1 origin log dir
        #$2 save log dir
        #$3 filename prefix
        #$4 filename suffix
        #$5 maximum log count
        function check_log()
        {
            local origin_dir="$1"
            local save_dir="$2"
            local prefix="$3"
            local suffix="$4"
            local log_cnt_max="$5"
            local save_log_dir max_file_size file_name time_type_name file_repeat file_inod
        

        check_log 函数目的检查日志目录是否符合特定规则日志文件,并根据设定日志轮换策略处理这些日志文件。

    • 具体使用范例

      start 函数描述新增日志文件,使用 check_log 第3,4,5个参数定义 log 的文件命名格式以及限制 log 的文件数量

      function start() {
          LOG_EXE_FLAG=1
          record_reset_count
      
          while true; do
              check_log ${KER_ORI_LOG_DIR}   ${KER_SAVE_LOG_DIR}   "${SOC}_Kernel-${RESET_COUNT}-" ".Log" ${ROTATEGENS_KER}
              check_log ${USR_ORI_LOG_DIR}   ${USR_SAVE_LOG_DIR}   "${SOC}_Usr-${RESET_COUNT}-"    ".Log" ${ROTATEGENS_USR}
              check_log ${DSP0_ORI_LOG_DIR}  ${DSP0_SAVE_LOG_DIR}  "${SOC}_Dsp-${RESET_COUNT}-"   ".Log" ${ROTATEGENS_REMOTE}
              check_log ${BL31_ORI_LOG_DIR}  ${BL31_SAVE_LOG_DIR}  "${SOC}_Bl31-${RESET_COUNT}-"   ".Log" ${ROTATEGENS_REMOTE}
              check_log ${CHIP_ORI_LOG_DIR}  ${CHIP_SAVE_LOG_DIR}  "${SOC}_Chip-${RESET_COUNT}-"   ".Log" ${ROTATEGENS_CHIP}
              check_log_cnt_with_keyword ${CORE_DUMP_LOG_DIR} "adsp" 20
              check_log_dir_size ${CORE_DUMP_LOG_DIR} ${CORE_DUMP_LOG_DIR_SIZE}
      
              sleep 600
          done
      }
      

      每个 check_log参数说明:

      • 源目录保存目录:每个调用两个目录参数,分别代表原始日志文件存放位置目标日志保存位置。例如,KER_ORI_LOG_DIR 表示内核日志原始目录,KER_SAVE_LOG_DIR 表示保存目标目录。

      • 日志文件名模板"${SOC}_Kernel-${RESET_COUNT}-" ".Log"日志文件名前缀(SOC 和 RESET_COUNT 是变量,通常表示系统信息重启次数)。这生成一个唯一日志文件名。

      • 旋转次数(例如 ROTATEGENS_KER:该参数控制日志文件旋转次数数量限制,用于控制日志文件大小备份数量。不同日志目录不同旋转设置。

      另外,最后sleep 600,每次循环结束后,start() 函数等待600秒(即10分钟)。这意味着每10分钟执行一次上述日志检查和管理操作。

  • log 进程裁剪

    • 可以通过控制 init.d 中 log 启动脚本,增删 log 进程启动。

    • 推荐开发功能加入时,也通过 init.d 中的 S92hobot_log_start shell 脚本集成。

  • log 回读

    • log-service demo 会针对 log 的分区目录每隔一段时间进行文件上传,此过程不去压缩 log。

pstore

pstore(Persistent Store)是 Linux 内核提供一种机制,旨在关键错误信息(如内核崩溃日志、崩溃内存快照等)保存持久存储中(如闪存中),即使系统重启后,这些信息仍然可以访问分析。这有利于调试分析 Linux 系统硬件故障、内核崩溃、或者严重系统错误。

pstore 具有以下特点:

  1. 持久存储:pstore 可以重要内核调试信息(如 Oops 信息、内核崩溃日志等)保存持久存储设备中,这样即使系统重启后,依然可以获取崩溃信息。

  2. 多种存储介质支持:pstore 支持多种存储介质,例如,内存(通过内存某些区域)、闪存(如 eMMC、NAND 闪存等)、以及其他内存设备(如 SPI Flash、NVRAM)。

  3. 内核崩溃信息记录:当内核发生崩溃时,pstore 可以崩溃堆栈信息、寄存器状态、内核错误信息保存下来,供后续调试分析。

  4. 支持多种崩溃场景:pstore 不仅支持内核崩溃(Oops),还可以用于记录其它类型错误信息,如内存泄漏、硬件错误等。

pstore 的机制

pstore 本质一个文件系统,它挂载/sys/fs/pstore 目录下:

root@buildroot:~# cat /proc/mounts |grep "^pstore"
pstore /sys/fs/pstore pstore rw,relatime 0 0

系统 panic 时,日志保存/sys/fs/pstore 目录中:

root@buildroot:/sys/fs/pstore# ls
console-ramoops-0  dmesg-ramoops-0  sched-ramoops-0
  • console-ramoops-0 :该文件通常存储系统崩溃控制台输出日志(例如内核 panic 信息、错误消息、调试信息等)。

  • dmesg-ramoops-0:该文件通常存储内核启动日志信息,包括硬件检测、驱动加载、系统启动期间发生错误警告等。

  • sched-ramoops-0:该文件通常存储调度器(scheduler)相关日志信息,记录内核调度崩溃状态。

配置 pstore

端系统预留了 256KB 的空间用来保存 Ramoops 日志,相关描述设备树文件中:

# kernel/arch/arm64/boot/dts/hobot/x5-memory.dtsi
ramoops@A4080000 {
    compatible = "ramoops";
    reg = <0x0 0xA4080000 0x0 0x00040000>;
    console-size = <0x8000>;
    pmg-size = <0x8000>;
    ftrace-size = <0x8000>;
    sched-size  = <0x8000>;
    record-size = <0x4000>;
    ecc-size = <0x0>;
};
  • ramoops@A4080000 { ... }

    • 这是一个设备节点,定义一个名为 ramoops设备,节点地址A4080000。这个节点表明系统一个 ramoops 设备,它位于物理内存地址 0xA4080000 处。

  • compatible = "ramoops";

    • 定义设备ramoops 驱动兼容性。这里,它表明设备使用 ramoops 驱动,ramoops一种用于保存内核崩溃信息(如控制台输出、调度信息等)的机制。

  • reg = <0x0 0xA4080000 0x0 0x00040000>;

    • 这是设备地址大小配置,格式<base address size>。 - 0x0:表示地址空间起始地址(通常为 0,表示物理地址)。 - 0xA4080000:表示设备起始物理地址,这里0xA4080000。 - 0x0:表示偏移量(通常为 0)。 - 0x00040000:表示设备内存区域大小,这里0x00040000,即 256 KB。

    • 表示设备 ramoops内存区域0xA4080000 地址开始,占用 256 KB 大小。

  • console-size = <0x8000>;

    • 配置控制台日志缓冲区大小。console-size 指定保存内核控制台日志内存区域大小,单位字节。

    • 0x8000:表示 32 KB 的大小。意味着 ramoops控制台日志分配 32 KB 的内存空间。

  • pmg-size = <0x8000>;

    • pmg-size 指定保存内核 panic 信息内存区域大小。

    • 0x8000:表示 32 KB 大小。意味着 ramoops内核 panic 信息分配 32 KB 的内存空间。

  • ftrace-size = <0x8000>;

    • ftrace-size 配置ftrace(内核追踪功能)日志缓冲区大小。

    • 0x8000:表示 32 KB 大小。意味着 ramoops内核ftrace 追踪日志分配 32 KB 的内存空间。

  • sched-size = <0x8000>;

    • sched-size 配置内核调度日志缓冲区大小。

    • 0x8000:表示 32 KB 大小。意味着 ramoops内核调度相关信息(如任务调度日志)分配 32 KB 的内存空间。

  • record-size = <0x4000>;

    • record-size 配置记录日志内存区域大小。

    • 0x4000:表示 16 KB 大小。意味着 ramoops记录崩溃信息日志区域分配 16 KB 的内存空间。

  • ecc-size = <0x0>;

    • ecc-size 配置了 ECC(错误更正码)日志区域大小。

    • 0x0:表示没有为 ECC 分配内存。也就是说,在配置没有为 ECC 错误记录预留内存区域。

启用 pstore 功能,需要内核配置启用相关选项(默认开启):

pstore

注意,这里配置 pstore 日志保存位置是 RAM 上,所以系统 panic 后那次自动重启才能/sys/fs/pstore看到相应日志,一旦掉电重启后,/sys/fs/pstore文件便会清空:

<*>     Log panic/oops to a RAM buffer

掉电重启需要转存目录 /userdata/log/pstore/查看最新一份日志。

启用了 pstore 支持后,对应驱动便会加载,在系统启动日志可以看到相关内容:

root@buildroot:~# dmesg | grep "ramoops"
[    0.114818] printk: console [ramoops-1] enabled
[    0.114822] pstore: Registered ramoops as persistent store backend
[    0.114827] ramoops: using 0x40000@0xa4080000, ecc: 0

pstore 日志转存

端系统中,hobot-log 脚本/sys/fs/pstore日志文件转存/userdata/log/pstore/ 目录下,具体实现依赖 set_pstore 函数:

#pstore log information
PSTORE_FS=$(cat /proc/mounts |grep "^pstore" |awk '{print $2}')
PSTORE_LOG=${LOG_SOURCE_DIR}/pstore
PSTORE_LOGMAX=100

function set_pstore()
{
	if [ -n "${PSTORE_FS}" ] && [ ! -e ${PSTORE_LOG}/disable ]; then
		if [ "$(ls -A ${PSTORE_FS})" == "" ];then
			return 0
		fi

		local UBOOTCMD=$(cat /proc/cmdline| sed 's/ /\n/g' | grep -i hobotboot.reason)
		local BOOTREASON=$(echo ${UBOOTCMD#*=})
		if [ "${BOOTREASON}" == "COLD_BOOT" ] || [ "${BOOTREASON}" == "UBOOT_RESET" ] || [ "${BOOTREASON}" == "WATCHDOG" ] || [ "${BOOTREASON}" == "REBOOT_CMD" ]; then
			return 0
		fi

		local pstore_log_date=$(date +%Y_%m_%d_%H_%M_%S)
		local pstore_log_dir="${PSTORE_LOG}/${SOC}_Pstore-${RESET_COUNT_NOW}-${pstore_log_date}"
		mkdir -p ${pstore_log_dir}
		output "pstore log to ${pstore_log_dir}"
		cp ${PSTORE_FS}/* ${pstore_log_dir}/
		/usr/hobot/bin/hrut_sched_log_parse ${PSTORE_FS}/sched-ramoops-0 > ${pstore_log_dir}/sched-ramoops-0
		change_file_time ${pstore_log_dir}

		local pstore_log_cnt=$(ls -l ${PSTORE_LOG} | grep "^d" | wc -l)
		while [ ${pstore_log_cnt} -gt ${PSTORE_LOGMAX} ]; do
			local deldir_name=$(ls -ltr ${PSTORE_LOG} | grep "^d" | head -n 1 | awk '{print $9}')
			rm -rf ${PSTORE_LOG}/${deldir_name}
			pstore_log_cnt=$(ls -l ${PSTORE_LOG} | grep "^d" | wc -l)
		done
		sync
	fi
}

set_pstore 函数只有系统启动原因PANIC (不能COLD_BOOTUBOOT_RESETWATCHDOGREBOOT_CMD)的时候将 pstore 存储日志文件移动pstore_log_dir 目录(/userdata/log/pstore/),并日志文件数量进行控制,一旦容量超过设定值,就删除日志,确保不会超出设定容量。

转存后,在/userdata/log/pstore/生成本次 panic 的日志目录:

root@buildroot:/userdata/log/pstore# ls
X5_Pstore-0074-1970_01_01_00_00_03  X5_Pstore-0075-1970_01_01_00_00_03

每次 panic 都新增一个目录,每个目录日志文件是从 /sys/fs/pstore 目录复制的:

root@buildroot:/userdata/log/pstore/X5_Pstore-0075-1970_01_01_00_00_03# ls
console-ramoops-0  dmesg-ramoops-0  sched-ramoops-0

pstore 使用示例

崩溃发生后,可以通过以下命令查看日志信息:

# 触发 panic
echo c > /proc/sysrq-trigger

# 查看 pstore 日志
cat /sys/fs/pstore/console-ramoops-0

# 或者查看转存到 /userdata/log/pstore/ 目录下的最新的一个子目录中的日志
 cd /userdata/log/pstore/X5_Pstore-0075-1970_01_01_00_00_03/console-ramoops-0

4.4.7.4. log debug

log 调试注意

  1. 调试大量日志可以单独保存,防止丢失,方便查看:

    • 内核日志:dmesg -w > /userdata/dmesg.log &

    • ALOG 日志:logcat -v time -f /userdata/logcat.log &

  2. 每个转存周期(10min)内产生超过 rotate 数量日志造成日志丢失:

    • 首先输出必要日志。

    • 如果输出多 log,在设置 rotate 的数量时候,要考虑大小。

  3. 不要串口、ssh 窗口实时查看日志。可能发现日志,原因输出慢,覆盖导致。如果必须,建议 ssh 窗口实时查看日志。

    • 比如使用 logcat 在串口实时查看输出,内核输出 “logcat lost message”,则说明日志丢失。

  4. 日志写入存储设备保存。考虑存储设备寿命有限大量存储日志对 IO/CPU 性能影响,应输出必要日志,正式版本输出大量调试日志:

    • 比如存储设备是 eMMC(64GB、MLC、3000次擦写)时,若每分钟写入 10MB 日志,则连续工作十年消耗27%寿命,考虑写入放大,消耗多。

死机情况如何保存有效日志记录

  1. 出现系统 panic 死机时候,pstore 的机制可以发生 panic 的内核 log 信息存储到 pstore 的目录的,但需要注意 BL31 的 panic 信息没法保存。

  2. 关于 panic 的具体原因可以系统稳定性排查指南按照指导进一步分析。

有效获取问题时刻日志

  1. 在 log 分区文件名字包含对应 log 文件最后修改时间信息,可以文件查找问题时刻的 log。

  2. 如果当前机器对应的 log 没有对应文件,可能出现问题时刻的 log 已经覆盖了,可以云端获取上传的 log,寻找对应文件名的 log 去分析问题。

4.4.7.5. 记录 AB/BAK 异常切换原因

介绍

目前,启动失败,并且造成 AB 或 BAK 切换原因以下几种:

  • miniboot 中的 BL3x 被 corrupted(BAK 切换)

  • misc 分区记录 AB 信息区域被 corrupted(包含 misc 分区为空情况,切换到 A)

  • Uboot 被 corrupted(AB 切换)

  • boot 被 corrupted(AB 切换)

  • system 被 corrupted(即 dm-verity 失败,AB 切换)

各级启动程序检测以上这些 corrupted 时,会对应 flag 写入到 AON(Always-On)寄存器,在下次启动时 BL2 会读取 AON 寄存器,将 corrupted 原因写入 misc 分区中,在 misc 分区只会保存最近10次启动失败原因。在板端文件系统中,提供了 hrut_switch_reason 工具,可以解析 misc 分区或 dump 后的 misc 分区文件,打印上次启动失败原因,或将10次原因全部打印出来。

另外提供了 Python3 脚本,支持在 PC 上解析 dump 后的 misc 分区文件,打印启动失败原因。路径位于 BSP 源码hbre/hbutils/utility/pc_tools/hrut_switch_reason.py

hrut_switch_reason 工具使用

板端工具

root@buildroot:~# hrut_switch_reason -h
Usage: hrut_switch_reason [misc_path] <--current|--all>
misc_path: misc partition or file path (default: /dev/block/platform/by-name/misc)
--current: The reason for the last abnormal switch
--all: The reason for the all abnormal switch
example: hrut_switch_reason --all
  • misc_file:可参数。指定 dump 出的 misc 分区文件路径。如果为空,默认路径是 misc 的分区节点 /dev/block/platform/by-name/misc,如果指定路径,必须作为第一个参数

  • <–current|–all>:必选参数。–current 表示打印上次启动失败原因;–all 表示打印所有启动失败原因,共10次

PC工具

 ./hrut_switch_reason.py -h
Usage: ./hrut_switch_reason.py <misc_file> <--current|--all>
  • misc_file:必选参数。指定 dumpmisc 分区文件路径

  • <–current|–all>:必选参数。–current 表示打印上次启动失败原因;–all 表示打印所有启动失败原因,共10次

使用示例

# 板端工具
root@buildroot:~# hrut_switch_reason --current  # 打印上次启动失败原因
dm-verity corruped

root@buildroot:~# hrut_switch_reason --all  # 打印全部启动失败原因
All Reasons (From the latest to the oldest):
1: dm-verity corruped
2: misc broken
3: unused
4: unused
5: unused
6: unused
7: unused
8: unused
9: unused
10: unused

# PC工具 ./hrut_switch_reason.py ~/misc_file --current   # 打印上次启动失败原因
miniboot corruped

❯ ./hrut_switch_reason.py ~/misc_file --all  # 打印全部启动失败原因
All Reasons (From the latest to the oldest):
1: miniboot corruped
2: miniboot corruped
3: miniboot corruped
4: uboot corruped
5: uboot corruped
6: dm-verity corruped
7: boot corruted
8: boot corruted
9: dm-verity corruped
10: misc broken

测试用例构造

测试 misc 分区 corrupted 可以板子烧写 disk 镜像后,这种情况下会将 misc 分区全部清空,记录启动失败原因区域全部清空。

mmc
# system corrupted (dm-verity) 需要开启AVB验证
mount / -o rw,remount
echo 132 > ~/test
sync

# boot corrupted 需要开启AVB验证
dd if=/dev/random of=/dev/block/platform/by-name/boot_a bs=1 count=4

# uboot corrupted
dd if=/dev/random of=/dev/block/platform/by-name/uboot_a bs=1 count=4

# miniboot中的bl3x corrupted
dd if=/dev/random of=/dev/block/platform/by-name/miniboot seek=262144 bs=1 count=512
flash
# system corrupted (dm-verity)
# flash 上不支持 dm-verity

# boot corrupted 需要开启AVB验证
# uboot cmd 执行
mtd write boot_a ${kernel_addr} 0 0x1000

# uboot corrupted
mtd write uboot_a ${kernel_addr} 0 0x1000

# miniboot 中的 bl3x corrupted
mtd write miniboot ${kernel_addr} 0x40000 0x1000

通过 cmdline 查看

Uboot 也读取 AON 寄存器,把上次启动失败原因通过 bootargs 传递给 Kernel,并清除 AON 寄存器。

root@buildroot:~# cat /proc/cmdline
console=ttyS0,921600n8 root=/dev/mmcblk0p13 ro rootwait hobotboot.slot_suffix=_b hobotboot.reason=WATCHDOG hobotboot.medium=MMC hobotboot.mode=normal hobotboot.ab_switch_reason=dm-verity-corruted pmic_type=single-pmic

以下日志各个参数解析:

  • console=ttyS0,921600n8:这个参数指定系统控制台输出哪个串行端口。ttyS0 是串行端口名称,921600是波特率,n8 表示没有奇偶校验位(no parity),8个数据位。

  • root=/dev/mmcblk0p13:这个参数指定系统文件系统所在设备分区。这里指定是 /dev/mmcblk0 设备第13个分区(p13)。

  • ro:这个参数表示文件系统只读模式挂载。

  • rootwait:这个参数告诉内核启动过程等待文件系统设备准备好。这通常用于网络启动或 USB 启动,其中文件系统设备可能需要一些时间才能准备好。

  • hobotboot.slot_suffix=_b:这是一个特定某个系统参数,表示启动使用的 slot(插槽)后缀是 _b。

  • hobotboot.reason=WATCHDOG:这个参数表示启动原因由于看门狗(WATCHDOG)触发的。看门狗一种硬件软件机制,用于检测系统是否正常运行,如果系统停止响应,看门狗可以触发重启。

  • hobotboot.medium=MMC:这个参数表示启动介质多媒体卡(MMC)。

  • hobotboot.mode=normal:这个参数表示启动模式正常启动模式。

  • hobotboot.ab_switch_reason=dm-verity-corruted:这个参数表示 AB 切换(可能双系统切换)的原因是 dm-verity 校验失败。dm-verity 是一种用于验证启动分区完整性技术,如果校验失败,可能意味着启动分区篡改损坏。

  • pmic_type=single-pmic:这个参数表示电源管理集成电路(PMIC)的类型单 PMIC。

cmdline 中启动原因标志位是 hobotboot.ab_switch_reason

通过系统 log 查看

系统 log 文件中,也记录每次启动命令上次启动失败原因,log 文件位于端的 /userdata/log/reset_reason.txt。例如第一次启动时 log 如下:

root@buildroot:~# cat /userdata/log/reset_reason.txt
1970-01-01-00-00-08: UBOOT_RESET        misc-broken             LNX6.1.83_PL5.1_V1.0.11_20240912-1542   0000

其中 misc-broken 表示此次启动misc 分区发生初始化,从 A slot 启动。