Ramoops oops/panic 日志记录器¶
Sergiu Iordache <sergiu@chromium.org>
更新:2021 年 2 月 10 日
简介¶
Ramoops 是一个 oops/panic 日志记录器,它在系统崩溃前将日志写入 RAM。它的工作原理是将 oops 和 panic 记录在循环缓冲区中。Ramoops 需要一个具备持久化 RAM 的系统,以便该区域的内容在重启后能够保留下来。
Ramoops 概念¶
Ramoops 使用预定义的内存区域来存储转储。内存区域的起始地址、大小和类型是通过以下变量来设置的
mem_address用于指定起始地址
mem_size用于指定大小。内存大小会被向下舍入为 2 的幂次方。
mem_type用于指定内存类型(默认值为 pgprot_writecombine)。
mem_name用于指定由reserve_mem命令行参数定义的内存区域。
通常应使用 mem_type=0 的默认值,因为这会将 pstore 映射设置为 pgprot_writecombine。设置 mem_type=1 会尝试使用 pgprot_noncached,这仅在某些平台上有效。这是因为 pstore 依赖于原子操作。至少在 ARM 上,pgprot_noncached 会导致内存被映射为强序(strongly ordered),而对强序内存的原子操作是与实现相关的,在许多 ARM(例如 omap)上无法工作。设置 mem_type=2 会尝试将内存区域视为普通内存,从而在其上启用完整缓存。这可以提高性能。
内存区域被划分为 record_size 大小的块(同样向下舍入为 2 的幂次方),每个 kmesg 转储都会写入一个大小为 record_size 的信息块。
可以通过 max_reason 值来限制存储哪些种类的 kmsg 转储,如 include/linux/kmsg_dump.h 的 enum kmsg_dump_reason 中所定义的。例如,要同时存储 Oops 和 Panic,应将 max_reason 设置为 2 (KMSG_DUMP_OOPS);要仅存储 Panic,max_reason 应设置为 1 (KMSG_DUMP_PANIC)。将其设置为 0 (KMSG_DUMP_UNDEF) 意味着原因过滤将由启动参数 printk.always_kmsg_dump 控制:如果未设置,它将是 KMSG_DUMP_OOPS,否则为 KMSG_DUMP_MAX。
该模块使用一个计数器来记录多个转储,但该计数器会在重启时重置(即重启后的新转储会覆盖旧转储)。
Ramoops 还支持持久化内存区域的软件 ECC 保护。当使用硬件重置来恢复机器(例如触发了看门狗)时,这可能会非常有用。在这种情况下,RAM 可能会有一定程度的损坏,但通常是可以恢复的。
设置参数¶
可以通过几种不同的方式来设置 ramoops 参数
A. 使用模块参数(其名称与前面描述的变量相同)。为了快速调试,您还可以在引导期间保留部分内存,然后将保留的内存用于 ramoops。例如,假设一台机器拥有 > 128 MB 的内存,以下内核命令行将指示内核仅使用前 128 MB 的内存,并将受 ECC 保护的 ramoops 区域放置在 128 MB 的边界处
mem=128M ramoops.mem_address=0x8000000 ramoops.ecc=1B. 使用设备树(Device Tree)绑定,如
Documentation/devicetree/bindings/reserved-memory/ramoops.yaml中所述。例如reserved-memory { #address-cells = <2>; #size-cells = <2>; ranges; ramoops@8f000000 { compatible = "ramoops"; reg = <0 0x8f000000 0 0x100000>; record-size = <0x4000>; console-size = <0x4000>; }; };C. 使用平台设备并设置平台数据。然后可以通过该平台数据来设置参数。一个这样做的示例如下
#include <linux/pstore_ram.h> [...] static struct ramoops_platform_data ramoops_data = { .mem_size = <...>, .mem_address = <...>, .mem_type = <...>, .record_size = <...>, .max_reason = <...>, .ecc = <...>, }; static struct platform_device ramoops_dev = { .name = "ramoops", .dev = { .platform_data = &ramoops_data, }, }; [... inside a function ...] int ret; ret = platform_device_register(&ramoops_dev); if (ret) { printk(KERN_ERR "unable to register platform device\n"); return ret; }
使用通过
reserve_mem命令行参数保留的内存区域。地址和大小将由reserve_mem参数定义。请注意,reserve_mem可能并不总是在相同的位置分配内存,因此不能完全对其产生依赖。需要进行测试,并且它可能并非在每台机器或每个内核上都能工作。请将其视为一种“尽力而为”的方法。reserve_mem选项接受大小、对齐方式和名称作为参数。该名称用于将内存映射到一个可由 ramoops 检索的标签。reserve_mem=2M:4096:oops ramoops.mem_name=oops
您可以指定 RAM 内存或外设设备的内存。但是,当指定 RAM 时,请务必在架构代码的极早期通过调用 memblock_reserve() 来保留内存,例如
#include <linux/memblock.h>
memblock_reserve(ramoops_data.mem_address, ramoops_data.mem_size);
转储格式¶
数据转储以一个头部开始,当前定义为 ====,后跟时间戳和换行符。随后转储会继续输出实际数据。
读取数据¶
可以从 pstore 文件系统中读取转储数据。这些文件的格式为 dmesg-ramoops-N,其中 N 是内存中的记录编号。要从 RAM 中删除存储的记录,只需删除(unlink)相应的 pstore 文件即可。
持久化函数跟踪¶
持久化函数跟踪对于调试软件或硬件相关的挂起(hang)可能很有用。函数调用链日志存储在 ftrace-ramoops 文件中。以下是使用示例
# mount -t debugfs debugfs /sys/kernel/debug/
# echo 1 > /sys/kernel/debug/pstore/record_ftrace
# reboot -f
[...]
# mount -t pstore pstore /mnt/
# tail /mnt/ftrace-ramoops
0 ffffffff8101ea64 ffffffff8101bcda native_apic_mem_read <- disconnect_bsp_APIC+0x6a/0xc0
0 ffffffff8101ea44 ffffffff8101bcf6 native_apic_mem_write <- disconnect_bsp_APIC+0x86/0xc0
0 ffffffff81020084 ffffffff8101a4b5 hpet_disable <- native_machine_shutdown+0x75/0x90
0 ffffffff81005f94 ffffffff8101a4bb iommu_shutdown_noop <- native_machine_shutdown+0x7b/0x90
0 ffffffff8101a6a1 ffffffff8101a437 native_machine_emergency_restart <- native_machine_restart+0x37/0x40
0 ffffffff811f9876 ffffffff8101a73a acpi_reboot <- native_machine_emergency_restart+0xaa/0x1e0
0 ffffffff8101a514 ffffffff8101a772 mach_reboot_fixups <- native_machine_emergency_restart+0xe2/0x1e0
0 ffffffff811d9c54 ffffffff8101a7a0 __const_udelay <- native_machine_emergency_restart+0x110/0x1e0
0 ffffffff811d9c34 ffffffff811d9c80 __delay <- __const_udelay+0x30/0x40
0 ffffffff811d9d14 ffffffff811d9c3f delay_tsc <- __delay+0xf/0x20