崩溃时日志易丢失,须用预分配环形缓冲区+信号拦截+原子操作+write系统调用实现无锁安全日志;单条日志≤4KB,导出前需按\n截取完整行。

崩溃时没日志?得用双缓冲+信号拦截
直接写磁盘的日志在进程崩溃瞬间大概率丢失,因为 std::ofstream 或 fprintf 的缓冲未刷新、文件句柄可能已损坏。必须把日志先攒在内存里,再通过 signal 捕获(如 SIGSEGV、SIGABRT)触发强制导出。但注意:信号处理函数里只能调用异步信号安全函数,std::string、std::vector、new、malloc 全部禁止——这意味着你不能在信号处理函数里做任何动态内存分配或 STL 操作。
用预分配的环形缓冲区 + 原子索引控制写入
避免运行时分配,整个缓冲区必须在程序启动时静态/全局分配。推荐用 std::array<char n></char> 或裸 char buffer[N],配合两个原子整数记录写入位置和已提交长度:
-
std::atomic<size_t> write_pos{0}</size_t>:当前可写起始偏移(多线程写入时用fetch_add) -
std::atomic<size_t> commit_len{0}</size_t>:已完整写入且可被导出的日志字节数(仅由写入线程更新) - 每次写入前检查剩余空间:
if (write_pos.load() + len > buffer.size())→ 回绕或丢弃(不阻塞) - 写入后用
commit_len.store(write_pos.load())标记完成(确保内存序为memory_order_release)
示例关键片段:
static char g_log_buffer[1024 * 1024];
static std::atomic<size_t> g_write_pos{0};
static std::atomic<size_t> g_commit_len{0};
<p>void safe_log(const char* msg) {
size_t pos = g_write_pos.fetch_add(len, std::memory_order_relaxed);
if (pos + len >= sizeof(g_log_buffer)) {
// 回绕:从头开始,覆盖旧日志
pos = 0;
g_write_pos.store(len, std::memory_order_relaxed);
}
memcpy(g_log_buffer + pos, msg, len);
g_commit_len.store(pos + len, std::memory_order_release); // 标记有效终点
}信号处理函数里只能用 write() 导出,且必须重入安全
signal() 处理器中禁止调用 fopen、fprintf、std::cout、std::string::c_str() 等——它们内部可能锁、分配、调用非 async-signal-safe 函数。唯一可靠方式是用系统调用 write() 直接写文件描述符:
立即学习“C++免费学习笔记(深入)”;
- 提前用
open()打开日志文件(如log_fd = open("crash.log", O_WRONLY | O_CREAT | O_APPEND, 0644)),并在main()开头完成 - 信号处理器中只做三件事:读
g_commit_len、计算有效日志范围、调用write(log_fd, ...) - 必须用
volatile sig_atomic_t标记信号标志位(如volatile sig_atomic_t g_crashed = 0),避免编译器优化掉轮询判断 - 不要在 handler 中调用
exit()或abort()—— 它们本身可能触发二次崩溃;导出后用_exit(128)(不刷 stdio 缓冲、不调析构)立即终止
多线程写入需规避锁,但要注意内存可见性
多个线程并发调用 safe_log() 时,不能用互斥锁(pthread_mutex_lock 在信号上下文中不安全,且可能死锁)。环形缓冲区靠原子索引 + 无锁写入即可,但有两点硬约束:
- 单条日志必须短于缓冲区一半(否则回绕时无法保证原子写入),建议限制单条 ≤ 4KB
- 写入内容需是纯 ASCII 或 UTF-8 字节流,避免宽字符、locale 依赖操作
- 若需格式化(如带时间戳、线程ID),必须在进入
safe_log()前完成,并存为 C 风格字符串(snprintf(buf, sizeof(buf), "[%ld][%d] %s", time(NULL), gettid(), msg)),再传入 - 导出时,
g_commit_len.load(std::memory_order_acquire)必须配对memory_order_release,否则可能读到中间态数据
真正棘手的是:崩溃可能发生在日志写入一半时(比如 memcpy 被中断)。所以导出逻辑要跳过不完整行——按换行符切分,只保存以 \n 结尾的完整行。这需要在信号 handler 中手动扫描,不能依赖 strtok。



















