CppCon 2018 学习:Mini Dumps Efficient core dumps for FlashBlade

Mini Dumps” 指的是一种精简的 core dump(核心转储)机制,目的是在 高性能系统(如 Pure Storage FlashBlade)中,在出错时收集足够的调试信息,同时避免完整 core dump 带来的性能开销或空间浪费

什么是 Core Dump?

Core dump 是操作系统在程序崩溃时写出的一份进程内存快照,供开发者排查问题。但:

  • 完整 core dump 文件可能数百 MB 到数 GB
  • 写出耗时长,在高性能存储系统中容易影响正常运行
  • 包含很多没用的数据(如空闲堆栈、未使用堆内存等)

Mini Dump 的目的

Mini dump ≈ core dump 的“精简版”

特性 描述
智能筛选 只保留调试必要的数据(如崩溃线程的栈帧、寄存器、特定对象)
体积小 几 MB 到几十 MB
快速写出 快速转储,最小影响运行时
足够排查 能满足多数故障诊断场景
安全合规 减少用户数据泄露风险

FlashBlade 场景中应用 Mini Dump 的原因

FlashBlade 是一个分布式、并发性极高的系统,特点:

  • I/O 密集,核心 dump 会拖慢服务
  • 数百个并发线程/组件,不需要整个内存内容,只需崩溃上下文
  • 保持运行可用性更重要 → 不希望因一个 core dump 影响系统可用性
    因此:

FlashBlade 更倾向于使用 “Mini Dump” 方案,快速记录关键状态,并自动上报分析,而非阻塞性地写出完整 core 文件。

Mini Dump 的内容通常包括:

  • 崩溃线程的寄存器状态
  • 调用栈(stack trace)
  • 局部变量、函数参数快照
  • 关键静态全局变量
  • 特定 allocator 状态(内存分配堆状态)
  • 系统版本、构建号、时间戳等元数据

Mini Dump 的技术实现方式(一般思路):

  1. 信号捕获(如 SIGSEGVSIGABRT
  2. 使用 lightweight dumper 工具
    • 比如 Google 的 minidump 或 LLVM 的 llvm-symbolizer
  3. 只转储特定线程和内存区域
    • 通过 /proc/self/maps 只采集 .text, .data, .stack
  4. 压缩、上传或保存本地小文件

开发者如何利用 Mini Dump?

  • 本地使用 gdblldb + 符号文件调试:
    gdb -c mini.dmp my_binary
    
  • 也可借助 crash dump 分析框架(如 Breakpad、ABRT、Microsoft WinDbg mini dump loader)

总结一句话:

Mini Dump 是为大规模系统稳定运行设计的一种“轻量级核心转储”,能快速捕获崩溃关键信息,而不会拖垮系统性能或存储资源。

这段内容讲的是为什么需要 Mini Dumps(精简 core dump) ——它是从工程实用角度出发,为了改善传统 Linux core dump 的问题。

动机(Motivation)解析:

1. Core dumps 非常有用

  • 当程序崩溃(如 segmentation fault),core dump 是排查根因最直接的手段
  • gdb 可以用 core dump 查看崩溃现场(变量、堆栈、指令地址等)。

2. 传统 core dump 的问题

问题 描述
隐私泄露 dump 会包含整个内存,可能有 客户数据、密码、私钥 等敏感信息
非常慢 写出所有内容通常 需要 30 秒或更久,影响正常服务
非常大 一个系统的 dump 可高达 50GB+,严重消耗磁盘空间或网络传输时间
无法控制内容 kernel 自动转储,无法选择性排除某些数据区域

3. 我们真正想要的东西:

目标 解释
更快的 dump 几秒内生成
更小的文件 只包含调试必要信息,几 MB 到几十 MB
有选择性地排除区域 比如跳过客户数据/缓存等无关区域
仍能用 gdb 分析 mini dump 不能是自定义格式,还得兼容 GDB 分析器

关键要点总结:

传统 core dump 优点 缺点
可用 gdb 调试 太大、太慢、太危险(含敏感数据)
完整崩溃现场信息 无法控制 dump 内容

因此,我们希望能生成一个“可被 GDB 分析的、但更小、更安全、更快的 dump”——这就是 Mini Dump

Mini Dump 的设计目标

  • 避免用户数据被泄露
  • 快速生成,不阻塞服务运行
  • 仅保留崩溃线程和关键全局对象
  • 能用 GDB 排查问题

为什么不用 MADV_DONTDUMP 来排除内存区域?

什么是 MADV_DONTDUMP

madvise(addr, length, MADV_DONTDUMP) 是一个 Linux 系统调用,用于标记指定的内存区域在程序崩溃时 不要写入 core dump
听起来似乎正好符合“减少 dump 大小”的需求,但这段说明了 为什么它不适合用在实际大型系统中,尤其是 FlashBlade 这类性能敏感的环境中

为什么 不能仅靠 MADV_DONTDUMP

原因 解释
调用开销大 这个系统调用在 dump 时是不安全的(不能临时调用),
所以必须在程序运行期间反复调用它来设置区域 → 增加运行时负担。
需要加锁 内核在处理 MADV_DONTDUMP 时必须 获取锁来保护数据结构(可能影响性能)。
和内存分配器有冲突 应用使用大页(2MB)批量分配 64MB 块,这些块会被频繁复用
所以哪些区域该 dump 是动态变化的,用 MADV_DONTDUMP 很难精确控制。
粒度太粗 MADV_DONTDUMP按页(≥4KB)生效,
但有时候你一个页面里
既有重要系统状态,也有客户数据
→ 你不能全扔也不能全保。
最终仍然太大 即使用了 MADV_DONTDUMP,也可能还会有 几 GB 的数据 被 dump,
而你可能只关心其中很少部分。

总结:

虽然 MADV_DONTDUMP 是内核提供的排除机制,但它:

  • 对运行时性能有影响
  • 控制太粗,粒度不够细
  • 不适合频繁变动的内存结构
  • 最终结果还是太大
    因此,为了更高效、细粒度、应用感知的控制,团队更倾向于自己实现一套 精细选择区域的 mini dump 机制,而不是依赖 MADV_DONTDUMP。

core dump 文件(通常是 core)内部到底包含了什么?

什么是 Core Dump?

当程序崩溃时,系统可以将其内存状态、寄存器状态等保存到一个文件中,叫做 core file(core dump)。
这个文件可以被 gdb 等工具读取和分析,帮助你 重现崩溃时的上下文状态

如何查看 core 文件内容?

你可以用这个命令查看 core 文件结构:

readelf -a core

Core 文件结构解析:

区域 内容
ELF header Core 文件是 ELF 格式:
e_type = ET_CORE 表示这是一个 core 文件
e_machine = x86_64 表示适用于哪种架构
Program headers (PHDRs) 描述内存映射等内容,每个 header 可能是:
- PT_NOTE: 包含元信息(进程、线程、信号)
- PT_LOAD: 真实内存数据段
Section headers Core dump 通常 没有 section headers(section hdr cnt = 0)
因为这些对调试不是必须的
Notes 区域 (PT_NOTE) 这是最重要的元信息区域,类型为 NT_* 开头的结构:

Notes 区域内容详解

Note 类型 含义
NT_PRPSINFO 进程信息:uid, gid, pid 等
NT_SIGINFO 崩溃的信号信息:signo, errno 等(例如 SIGSEGV)
NT_AUXV 辅助向量(auxv_t),来自内核的启动参数
NT_FILE 映射的文件区域列表(类似 /proc/self/maps
包含:start/end address, file offset, 路径等
NT_PRSTATUS 每个线程的状态(tid, 当前寄存器值等)
NT_FPREGSET 浮点寄存器状态(x87)
NT_X86_XSTATE 扩展寄存器(例如 AVX、SSE、XSAVE)

PT_LOAD 段

  • 每个 PT_LOAD 表示一段真实的内存映射区域
    • 包括:
      • 权限(读写执行)
      • 虚拟地址(vaddr
      • 实际大小(p_memsz
      • 文件中大小(p_filesz
      • 在 core 文件中的偏移量(p_offset
        有些段的 p_filesz = 0 表示该内存段被“标记”了,但并没有保存具体内容(例如被裁剪了)。

总结图示:

Core File (ELF)
├── ELF Header
├── Program Headers
│   ├── PT_NOTE (metadata)
│   │   ├── NT_PRPSINFO
│   │   ├── NT_SIGINFO
│   │   ├── NT_AUXV
│   │   ├── NT_FILE
│   │   ├── NT_PRSTATUS (per thread)
│   │   └── ...
│   └── PT_LOAD (memory segment)
│       └── contains actual memory content (heap, stack, etc)
└── No section headers

你可以实际尝试:

ulimit -c unlimited
./your_app_crashing
readelf -a core

或者用 GDB 分析:

gdb your_app core

在信号处理函数(signal handler)里做什么 —— “轻量 mini core dump” 的关键技术点之一

信号处理函数是什么?

当程序出现严重错误时,操作系统会发送一个信号,比如:

信号 意义
SIGSEGV 段错误(非法内存访问)
SIGABRT 调用了 abort()
SIGINT 中断(比如 Ctrl+C)
SIGILL 非法指令执行
你可以**注册一个信号处理器(handler)**来在程序崩溃前做点事情,比如打印日志、生成 mini core dump 等。

在 signal handler 中能干什么?

参考:man 7 signal 文档,有一个非常重要的限制:

“Signal-safe functions”(异步信号安全函数)

这些函数是明确可以在信号处理器中调用的,包括:

类别 可用函数
文件操作 open(), read(), write(), close(), fsync()
进程信息 getpid(), kill(), signal()
字符串 strlen(), strerror_r()
内存 不能调用 malloc()new,也不能使用 STL 容器
输出 可以 write(2, ...) 输出错误信息到 stderr
时间 time()gettimeofday() 是安全的

不能做的事(常见坑)

禁止事项 原因
malloc() / free() 不是异步安全的,可能内部持有锁,会死锁
C++ 异常抛出 不合法,在 handler 里抛异常会直接终止
STL 容器使用 所有 std::vector / std::string 都依赖动态分配
printf() / std::cout 会触发缓冲区刷新和 malloc不安全

这意味着什么?

在 signal handler 中你不能做复杂逻辑,但是:

可以写出简化版 core 文件

  • 你知道 core dump 是 ELF 格式,文件结构是固定的(如前面分析)
  • 你可以直接 open() 创建一个文件
  • 然后用 write() 写入一些你需要的信息:
    • 当前寄存器快照
    • 部分用户态内存(stack / heap)
    • 映射文件信息(用 /proc/self/maps 获取)
    • 崩溃信号类型
  • 最后 close() 退出即可

示例:注册一个最小 signal handler

#include 
#include 
#include 
#include 
void handler(int sig) {
    const char msg[] = "Fatal signal received, dumping...\n";
    write(STDERR_FILENO, msg, sizeof(msg) - 1);
    // 假设打开文件并写入关键内存块等
    int fd = open("/tmp/mini_dump.raw", O_CREAT | O_WRONLY, 0644);
    if (fd >= 0) {
        write(fd, "DUMP", 4);  // 模拟
        close(fd);
    }
    _exit(1);  // 直接退出,避免进入未知状态
}
int main() {
    signal(SIGSEGV, handler);
    signal(SIGABRT, handler);
    // 故意制造崩溃
    *(int*)0 = 42;
    return 0;
}

总结:信号处理器中能做的事情

可做 不可做
写文件(write、open) malloc / new
输出错误信息 std::string、std::vector
保存寄存器快照 抛异常
写 mini core dump std::cout / printf

改进这个 dump 文件内容,让它更像一个实用的 mini core dump,可参考以下几个扩展方向:

目标:写一个更实用的 mini dump 文件

下面是改进目标:

  1. 记录触发信号类型
  2. 记录当前进程 ID
  3. 记录栈顶地址和栈内部分数据(近似 backtrace)
  4. 写入 /proc/self/maps 映射信息(可用 gdb 分析)

改进后的 handler 示例代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 
void handler(int sig) {
    const char msg[] = "Fatal signal received, dumping...\n";
    write(STDERR_FILENO, msg, sizeof(msg) - 1);
    int fd = open("/tmp/mini_dump.raw", O_CREAT | O_WRONLY | O_TRUNC, 0644);
    if (fd < 0) {
        _exit(1);
    }
    // 1. 写信号类型
    write(fd, "SIGNAL:\n", 8);
    dprintf(fd, "%d\n", sig);
    // 2. 写进程 ID
    write(fd, "PID:\n", 5);
    dprintf(fd, "%d\n", getpid());
    // 3. 写当前栈顶地址及内容(栈的“快照”)
    write(fd, "STACK (partial):\n", 18);
    void* sp = __builtin_frame_address(0);  // 栈帧指针
    write(fd, &sp, sizeof(sp));
    write(fd, "STACK DATA:\n", 12);
    write(fd, sp, 128);  // 写入栈顶附近128字节(注意可能非法)
    // 4. 写当前映射文件内容:/proc/self/maps
    write(fd, "\nMEMORY MAP:\n", 13);
    int maps_fd = open("/proc/self/maps", O_RDONLY);
    if (maps_fd >= 0) {
        char buf[256];
        ssize_t r;
        while ((r = read(maps_fd, buf, sizeof(buf))) > 0) {
            write(fd, buf, r);
        }
        close(maps_fd);
    }
    close(fd);
    _exit(1);
}

main() 保持不变:

int main() {
    signal(SIGSEGV, handler);
    signal(SIGABRT, handler);
    *(int*)0 = 42;  // 故意崩溃
    return 0;
}

编译和运行

g++ -g -o mini_dump mini_dump.cpp
./mini_dump

输出:

Fatal signal received, dumping...
Segmentation fault (core dumped)

你会看到文件 /tmp/mini_dump.raw 包含如下内容:

SIGNAL:
11
PID:
12345
STACK (partial):
[二进制数据]
MEMORY MAP:
00400000-00401000 r-xp ... main
...
  • 每个线程的状态
    • 崩溃时的寄存器状态(比如指令指针、栈指针等)
    • 崩溃信号的信息(比如SIGSEGV相关信息)
  • “有趣”的内存内容
    • 栈内容(要往回保存多少栈帧数据)
    • 寄存器里看起来像指针的值附近的内存
    • 栈里看起来像指针的值附近的内存
    • 递归地追踪这些内存块中可能存在的指针,继续保存对应内存
    • 每个“锚点”地址(anchor address)前后保存多少内存数据
  • 流程大致是
    • 由崩溃线程的信号处理器开始获取主线程信息
    • 发送信号(如SIGUSR1)给其他线程,让它们收集自己的寄存器和栈信息
    • 其中一个线程负责收集内存映射(/proc/self/maps)和实际内存数据,写入 dump 文件
      总结来说,就是通过信号处理和多线程协作,精确采集程序崩溃时必要且“有价值”的内存和寄存器状态,做一个更精简、高效的核心转储(mini dump)。

代码已经实现了基础的信号捕获与部分信息写入:

1. 写寄存器状态(而非只写栈帧指针)

当前只用 __builtin_frame_address(0) 得到栈帧指针,没抓寄存器。信号处理函数能拿到 ucontext_t,里面含寄存器完整信息。

void handler(int sig, siginfo_t *info, void *ucontext) {
    ucontext_t *uc = (ucontext_t *)ucontext;
    // 以 x86_64 为例,打印 RIP, RSP, RBP 寄存器
    uintptr_t rip = uc->uc_mcontext.gregs[REG_RIP];
    uintptr_t rsp = uc->uc_mcontext.gregs[REG_RSP];
    uintptr_t rbp = uc->uc_mcontext.gregs[REG_RBP];
    dprintf(fd, "RIP: %p\nRSP: %p\nRBP: %p\n", (void*)rip, (void*)rsp, (void*)rbp);
}

2. 信号处理函数注册用 sigaction 并使用 SA_SIGINFO

能拿到更多信息(如 siginfo_tucontext_t):

struct sigaction sa;
sa.sa_sigaction = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_SIGINFO | SA_RESTART;
sigaction(SIGSEGV, &sa, NULL);
sigaction(SIGABRT, &sa, NULL);

3. 对栈内容和指针进行更安全的访问

你直接读栈指针附近的128字节,有非法地址风险会导致二次崩溃。改为:

  • /proc/self/maps 判断栈地址范围
  • 或用 mincore()mprotect()探测可读区域
  • 只dump安全可读内存

4. 多线程处理

  • 主线程信号处理器通过发送 SIGUSR1 给所有其他线程,让它们写寄存器和栈信息(可先写日志或预留共享内存区)
  • pthread_kill() 发送信号,避免崩溃时死锁
  • 由一个线程负责写 /proc/self/maps 和内存内容到dump文件

5. 设计“锚点”机制

  • 通过寄存器和栈里识别“可能是指针”的值,保存其附近内存(如寄存器指针附近1KB,栈指针附近128B等)
  • 递归跟踪指针,扩大dump覆盖
  • 这样能精准捕获对调试有用的内存片段,极大减少dump体积

6. 避免非信号安全函数

  • mallocprintf等不可在信号处理器调用,dprintfwrite相对安全
  • 可以在信号处理器写简单信息,更多复杂处理放到崩溃后重启时再做

7. 例子:改进后的注册与信号处理器

#include 
#include 
#include 
#include 
#include 
#include 
#include 
void handler(int sig, siginfo_t *info, void *ucontext) {
    int fd = open("/tmp/mini_dump.raw", O_CREAT | O_WRONLY | O_TRUNC, 0644);
    if (fd < 0) _exit(1);
    dprintf(fd, "SIGNAL:\n%d\n", sig);
    dprintf(fd, "PID:\n%d\n", getpid());
    ucontext_t *uc = (ucontext_t *)ucontext;
    dprintf(fd, "REGISTERS:\n");
    dprintf(fd, "RIP: %p\n", (void*)uc->uc_mcontext.gregs[REG_RIP]);
    dprintf(fd, "RSP: %p\n", (void*)uc->uc_mcontext.gregs[REG_RSP]);
    dprintf(fd, "RBP: %p\n", (void*)uc->uc_mcontext.gregs[REG_RBP]);
    // 其他寄存器...
    // 尝试写栈内容(假设128字节安全)
    void *rsp = (void*)uc->uc_mcontext.gregs[REG_RSP];
    dprintf(fd, "STACK DATA:\n");
    if (rsp != NULL) {
        // 小心访问,真实项目中需要判断可读范围
        write(fd, rsp, 128);
    }
    // 写内存映射
    dprintf(fd, "\nMEMORY MAP:\n");
    int maps_fd = open("/proc/self/maps", O_RDONLY);
    if (maps_fd >= 0) {
        char buf[256];
        ssize_t r;
        while ((r = read(maps_fd, buf, sizeof(buf))) > 0) {
            write(fd, buf, r);
        }
        close(maps_fd);
    }
    close(fd);
    _exit(1);
}
int main() {
    struct sigaction sa;
    sa.sa_sigaction = handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_SIGINFO;
    sigaction(SIGSEGV, &sa, NULL);
    sigaction(SIGABRT, &sa, NULL);
    *(int*)0 = 42;  // 故意崩溃
    return 0;
}

总结

  • sigaction + SA_SIGINFO 获取完整寄存器和信号信息
  • 多线程时,信号处理器通知其他线程收集寄存器栈信息
  • 对栈和指针附近内存采用“锚点”追踪,精准采集调试必要内容
  • 避免非信号安全操作,尽量写简单日志
  • 读取 /proc/self/maps 记录进程内存映射,方便调试

想做一个非常完善的多线程mini dump工具,包含:

  • 更完整的寄存器和栈遍历
  • 锚点地址内存块递归追踪
  • 多线程信号同步(无锁或条件变量)
  • 线程启动时自动登记tid
  • 清晰的dump文件格式(ELF core或自定义格式)
    这个项目相当庞大,通常一个成熟的mini dump库几千行代码,甚至更多。这里我帮你设计一个分步骤分模块的实现方案,并提供部分示范代码片段,方便你分阶段完成,也方便集成或替换:

1. 线程启动自动登记tid

用线程局部存储(TLS)自动登记tid,且在pthread_create时hook登记。

#include 
#include 
#include 
#include 
static std::atomic<int> g_thread_count{0};
static pthread_t g_threads[MAX_THREADS];
static pid_t g_tids[MAX_THREADS];
// 线程局部存储变量
static __thread int g_my_index = -1;
void register_current_thread() {
    int idx = g_thread_count.fetch_add(1);
    if (idx < MAX_THREADS) {
        g_threads[idx] = pthread_self();
        g_tids[idx] = syscall(SYS_gettid);
        g_my_index = idx;
    }
}
void* thread_start_wrapper(void* (*start_routine)(void*), void* arg) {
    register_current_thread();
    return start_routine(arg);
}
int pthread_create_wrapped(pthread_t* thread, const pthread_attr_t* attr,
                           void* (*start_routine)(void*), void* arg) {
    // 包装线程入口函数,使其先登记线程信息
    struct WrapperArg {
        void* (*start_routine)(void*);
        void* arg;
    };
    WrapperArg* warg = new WrapperArg{start_routine, arg};
    auto wrapper = [](void* warg_void) -> void* {
        WrapperArg* w = (WrapperArg*)warg_void;
        register_current_thread();
        void* ret = w->start_routine(w->arg);
        delete w;
        return ret;
    };
    return pthread_create(thread, attr, wrapper, warg);
}

2. 更完整寄存器和栈遍历

不同架构寄存器数量不同,比如 x86_64 寄存器集:

  • RIP, RSP, RBP, RAX, RBX, RCX, RDX, RSI, RDI, R8-R15 等。
    遍历栈:
  • 通过栈指针(RSP)和栈底地址确定栈范围(比如取8KB)
  • 逐字(8字节)扫描栈内容
  • 检查栈内容是否可能是有效指针(位于已映射内存区域)
  • 将这些指针加入锚点队列,递归追踪
    示范遍历栈代码片段(x86_64):
#include 
bool is_address_mapped(void* addr) {
    // 简单方法:用mincore探测页是否在内存中
    unsigned char vec;
    void* page = (void*)((uintptr_t)addr & ~(4095));
    if (mincore(page, 4096, &vec) == 0)
        return true;
    return false;
}
void scan_stack_for_pointers(void* stack_start, size_t stack_size,
                             std::vector<void*>& anchor_addrs) {
    uintptr_t* ptr = (uintptr_t*)stack_start;
    uintptr_t* end = (uintptr_t*)((uintptr_t)stack_start + stack_size);
    for (; ptr < end; ++ptr) {
        uintptr_t val = *ptr;
        if (val > 0x10000 && is_address_mapped((void*)val)) {
            anchor_addrs.push_back((void*)val);
        }
    }
}

3. 锚点内存块管理与递归追踪

  • 使用一个无重复的std::set来存储所有锚点基地址
  • 每发现一个新指针锚点,判断是否已存在,没存在则加入队列递归读内存
  • 每次读的内存范围例如:锚点地址向前1KB,向后8KB
  • 递归深度限制防止无限循环
    示例:
#include 
#include 
std::set<uintptr_t> visited_anchors;
std::queue<uintptr_t> anchor_queue;
void add_anchor(uintptr_t addr) {
    // 对齐4K页
    addr &= ~(4095);
    if (visited_anchors.insert(addr).second) {
        anchor_queue.push(addr);
    }
}
void recursive_anchor_dump(int fd) {
    while (!anchor_queue.empty()) {
        uintptr_t base = anchor_queue.front();
        anchor_queue.pop();
        // Dump内存[base - 1k, base + 8k]
        uintptr_t dump_start = base > 1024 ? base - 1024 : base;
        size_t dump_size = 1024 + 8192;
        // 读取内存
        char buf[dump_size];
        memcpy(buf, (void*)dump_start, dump_size);
        // 写入dump文件(简略)
        write(fd, &dump_start, sizeof(dump_start));
        write(fd, buf, dump_size);
        // 递归扫描这段内存,找新指针加入anchor_queue
        uintptr_t* ptr = (uintptr_t*)buf;
        uintptr_t* end = (uintptr_t*)(buf + dump_size);
        for (; ptr < end; ++ptr) {
            uintptr_t val = *ptr;
            if (val > 0x10000 && is_address_mapped((void*)val)) {
                add_anchor(val);
            }
        }
    }
}

4. 多线程信号同步(无锁或条件变量)

信号处理函数里不能使用普通锁,常见做法:

  • 主线程触发信号,其他线程在信号处理器中设置原子标志
  • 主线程轮询等待所有线程确认完成
  • 可以用std::atomic变量或信号量实现
    示范同步变量:
std::atomic<int> threads_done_count{0};
void other_thread_handler(int sig, siginfo_t*, void* uc_void) {
    // 保存寄存器等
    // ...
    threads_done_count.fetch_add(1);
    // 等待主线程通知退出信号处理
    while (!g_crash_handling_done) {
        usleep(100000);
    }
    _exit(0);
}
void crash_handler(...) {
    threads_done_count = 0;
    signal_other_threads();
    // 等待其他线程完成
    while (threads_done_count.load() < g_thread_count.load() - 1) {
        usleep(100000);
    }
    // 继续写dump
    // ...
    g_crash_handling_done = true;
}

5. dump文件结构(建议)

  • 自定义简单格式(JSON + 二进制块)
    优点:灵活易读,集成容易
    缺点:不够标准,工具支持差
  • ELF core dump格式
    优点:系统工具(gdb)支持,格式标准
    缺点:复杂度高,需完整ELF结构知识和代码实现
  • Google Breakpad mini dump格式
    Google开源项目,可以参考或直接使用Breakpad库

简单示例:自定义格式写法(伪代码)

FILE HEADER
THREAD INFO BLOCK (tid, registers, stack range, stack data)
ANCHOR MEMORY BLOCKS (address, size, raw data)
PROCESS MAPS BLOCK (parsed /proc/self/maps)

你写入每个block前加长度和标识符,方便后续解析。

总结

这已经是非常大的系统工程,建议你:

  • 分模块实现,先做线程登记和信号同步
  • 实现寄存器和栈扫描
  • 实现锚点追踪和内存保存
  • 做简单文件格式
  • 再逐步完善
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
// 最大线程数
#define MAX_THREADS 32
// 全局线程信息结构
struct ThreadDumpInfo {
    pid_t tid;
    pthread_t ptid;
    volatile sig_atomic_t signaled = 0;
    ucontext_t context;
    volatile sig_atomic_t done = 0;
};
static ThreadDumpInfo g_thread_infos[MAX_THREADS];
static std::atomic<int> g_thread_count{0};
static volatile sig_atomic_t g_handling_crash = 0;
static volatile sig_atomic_t g_crash_done = 0;
// 简单信号安全打印
static void sig_safe_print(const char* msg) { write(STDERR_FILENO, msg, strlen(msg)); }
// 注册当前线程
void register_thread() {
    pid_t tid = syscall(SYS_gettid);
    pthread_t ptid = pthread_self();
    int idx = g_thread_count.fetch_add(1);
    if (idx < MAX_THREADS) {
        g_thread_infos[idx].tid = tid;
        g_thread_infos[idx].ptid = ptid;
    }
}
// 发送SIGUSR1给其他线程
void signal_other_threads() {
    pid_t self_tid = syscall(SYS_gettid);
    for (int i = 0; i < g_thread_count; ++i) {
        if (g_thread_infos[i].tid != self_tid) {
            pthread_kill(g_thread_infos[i].ptid, SIGUSR1);
        }
    }
}
// 其他线程信号处理器,收到SIGUSR1后保存寄存器状态
void other_thread_handler(int sig, siginfo_t*, void* uc_void) {
    pid_t tid = syscall(SYS_gettid);
    ucontext_t* uc = (ucontext_t*)uc_void;
    for (int i = 0; i < g_thread_count; ++i) {
        if (g_thread_infos[i].tid == tid) {
            g_thread_infos[i].signaled = sig;
            memcpy(&g_thread_infos[i].context, uc, sizeof(ucontext_t));
            g_thread_infos[i].done = 1;
            break;
        }
    }
    // 等待主线程写完dump文件后退出
    while (!g_crash_done) {
        usleep(100000);
    }
    _exit(0);
}
// 写寄存器到fd,简化只写RIP寄存器(x86_64)
void dump_registers(int fd, ucontext_t* uc) {
#if defined(__x86_64__)
    char buf[128];
    int len = snprintf(buf, sizeof(buf), "RIP=0x%llx\n",
                       (unsigned long long)uc->uc_mcontext.gregs[REG_RIP]);
    write(fd, buf, len);
#endif
}
// 写简单栈快照128字节(栈指针附近)
void dump_stack(int fd, ucontext_t* uc) {
#if defined(__x86_64__)
    void* sp = (void*)uc->uc_mcontext.gregs[REG_RSP];
    write(fd, "STACK_DATA:\n", 11);
    ssize_t wr = write(fd, sp, 128);
    (void)wr;  // 可能失败,简单忽略
#endif
}
// 崩溃信号处理器
void crash_handler(int sig, siginfo_t* si, void* uc_void) {
    if (__sync_lock_test_and_set(&g_handling_crash, 1)) {
        _exit(1);  // 防止重入
    }
    sig_safe_print("Fatal signal received. Writing mini dump...\n");
    int fd = open("/tmp/minidump.raw", O_CREAT | O_WRONLY | O_TRUNC, 0644);
    if (fd < 0) {
        _exit(1);
    }
    // 写信号和进程信息
    dprintf(fd, "SIGNAL: %d\n", sig);
    dprintf(fd, "PID: %d\n", getpid());
    dprintf(fd, "CRASH_THREAD_TID: %d\n", syscall(SYS_gettid));
    // 写崩溃线程寄存器和栈
    ucontext_t* uc = (ucontext_t*)uc_void;
    dump_registers(fd, uc);
    dump_stack(fd, uc);
    // 通知其他线程写寄存器栈
    signal_other_threads();
    // 等待其他线程完成
    for (int i = 0; i < 50; ++i) {
        bool all_done = true;
        for (int j = 0; j < g_thread_count; ++j) {
            if (g_thread_infos[j].tid != syscall(SYS_gettid) && g_thread_infos[j].done == 0) {
                all_done = false;
                break;
            }
        }
        if (all_done) break;
        usleep(100000);
    }
    // 写其他线程寄存器简略信息
    write(fd, "\nOTHER_THREADS:\n", 15);
    for (int i = 0; i < g_thread_count; ++i) {
        if (g_thread_infos[i].tid != syscall(SYS_gettid) && g_thread_infos[i].done) {
            dprintf(fd, "Thread TID: %d\n", g_thread_infos[i].tid);
            dump_registers(fd, &g_thread_infos[i].context);
            // 栈数据写略
        }
    }
    // 读取 /proc/self/maps 写入
    write(fd, "\nPROC_SELF_MAPS:\n", 16);
    int maps_fd = open("/proc/self/maps", O_RDONLY);
    if (maps_fd >= 0) {
        char buf[256];
        ssize_t r;
        while ((r = read(maps_fd, buf, sizeof(buf))) > 0) {
            write(fd, buf, r);
        }
        close(maps_fd);
    }
    close(fd);
    g_crash_done = 1;
    _exit(1);
}
// 安装信号处理器
void setup_signal_handlers() {
    struct sigaction sa = {};
    sa.sa_sigaction = crash_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_SIGINFO | SA_RESTART;
    sigaction(SIGSEGV, &sa, NULL);
    sigaction(SIGABRT, &sa, NULL);
    struct sigaction sa_usr1 = {};
    sa_usr1.sa_sigaction = other_thread_handler;
    sigemptyset(&sa_usr1.sa_mask);
    sa_usr1.sa_flags = SA_SIGINFO | SA_RESTART;
    sigaction(SIGUSR1, &sa_usr1, NULL);
}
// 工作线程示范
void* worker_thread(void*) {
    register_thread();
    while (1) {
        unsigned int ret = sleep(1);
        if (ret == 0) break;  // 正常睡眠完毕
        // 否则被信号中断,继续睡剩余时间
    }
    return NULL;
}
int main() {
    register_thread();  // 主线程注册一次
    setup_signal_handlers();
    pthread_t t1, t2;
    pthread_create(&t1, NULL, worker_thread, NULL);
    pthread_create(&t2, NULL, worker_thread, NULL);
    sleep(1);
    // 故意崩溃触发
    *(volatile int*)0 = 42;
    // main函数中等待线程启动
    for (int i = 0; i < 10; ++i) {
        if (g_thread_count.load() >= 3) break;  // 主线程 + 两个工作线程
        usleep(100000);                         // 100ms,快速轮询等待
    }
    return 0;
}

主要内容与背景

这是关于一种崩溃(crash)时生成内存转储(core dump / minidump)机制的讨论,结合了当前设计状态和未来规划。

1. 现状 Caveats(限制/注意点)

  • 没有稀疏虚拟地址 (sparse virtual addresses)
    目前系统没有使用稀疏虚拟地址技术。
    意味着:
    • 如果一块内存区域在 /proc/self/maps 中列出,并且是以 2MB 为单位的连续块,那它肯定也对应物理内存。
    • 这样简化了内存转储过程,因为不用担心虚拟地址映射到的物理内存不连续或不存在。
    • 这是可以解决的问题,因为内存分配器是自家的,可以控制如何分配内存。

2. 未来工作(Future work)

a) 标记内存区域的转储策略的API

  • 想要提供接口,可以标记某些内存范围为:
    • “永远转储” (always dump)
    • “永不转储” (never dump)
  • 对“永远转储”的期望:
    • 只在启动时标记,不是在运行时动态改变。
    • 用于保存关键且有价值的数据结构(比如服务器或权限管理相关的核心数据)。
  • 对“永不转储”的期望:
    • 尽量少用。
    • 用于敏感信息(如AES密钥不在进程时),DMA缓冲区,消息缓冲区等不需要保存的区域。
  • 需要考虑设计一个无锁且崩溃安全的表结构,用于管理这些标记,保证崩溃时读取时不会出错。

b) 保存更多寄存器状态?

  • 是否保存更多CPU状态结构,比如 NT_FPREGSET (浮点寄存器) 和 NT_X86_XSTATE(扩展CPU状态)?
  • 目前还没发现需要额外保存这些。

3. 总结 Summary

  • 通过信号处理器(signal handler)可以写出一个有效的 core dump(或者 minidump)。
  • 在崩溃时通知其他线程收集它们的寄存器和栈信息。
  • 实际测试表明,生成的 minidump 文件在大小和效率上都优于传统 core 文件,甚至小了一个数量级(1.4G 的 core 文件,对比 313M 的 minidump)。

4. 你可以这样理解:

这段文字说明了:

  • 目前的内存转储方案比较简洁,依赖于某些假设(无稀疏虚拟内存),方便快速抓取核心数据。
  • 未来希望能更灵活,精细地控制哪些内存块要转储,哪些不要,尤其是为了保护敏感数据和优化转储文件大小。
  • 已经能写出比较紧凑且有用的转储文件,方便崩溃调试,同时避免传统core文件过大问题。

你可能感兴趣的:(CppCon,学习,c++,开发语言)