☰
Linux C/C++错误日志系统设计:从崩溃排查到高性能异步落盘
2026/10/9 17:09:51 网站建设 项目流程

1. 从一次线上服务崩溃说起:为什么日志记录远不止是写文件

凌晨两点,监控告警突然响了——某台服务器上的核心服务进程挂了,重启之后一切正常,但没人知道它为什么挂。翻遍系统日志,只有一句干巴巴的"segmentation fault",没有任何上下文。这就是我入行早期踩过的最典型的坑:服务在跑,但日志没记全,出了问题等于两眼一抹黑。

后来我花了整整一周时间重构了那套服务的日志模块,才真正理解了一件事:在Linux环境下记录错误信息日志,本质上不是"调用一个函数把字符串写进文件"这么简单,它涉及日志分级、异步写入、文件轮转、信号安全、性能开销控制等一系列工程问题。你写的日志模块能不能在进程崩溃前的最后一刻把关键信息落盘,能不能在高并发下不拖慢主流程,能不能在磁盘写满时不把整个服务拖死——这些才是区分"能用"和"可靠"的分水岭。

这篇文章面向的是有一定Linux C/C++开发基础、正在为自己的项目搭建日志系统的开发者。无论你是在写一个后台守护进程、一个网络服务,还是一个嵌入式采集程序,只要它运行在Linux上并且需要记录错误信息,这里的内容都能直接参考。我会从最基础的设计决策讲起,一直讲到生产环境中真正会遇到的那些坑,尽量把每一步"为什么这么做"说清楚,而不是只丢一段代码让你抄。

需要提前说明的是,日志系统的设计没有银弹,不同的场景对性能、可靠性、可维护性的要求完全不同。我会在关键节点给出取舍分析,你可以根据自己的实际情况做选择。文中涉及的具体参数和阈值,都是基于常见实践的合理估算,实际使用时需要结合你的硬件和业务特点做压测调整。

2. 动手之前先想清楚:日志系统的四个核心设计决策

很多人一上来就开始写fprintf,结果写到一半发现需求变了,推倒重来。我在动手写任何日志模块之前,一定会先把下面四个问题想明白。这四个决策会直接影响你后面所有的代码结构,想清楚了再写,能省掉大量返工。

2.1 日志分级:不是越多越好,而是要能过滤

日志分级是最基础的设计。常见的分级从低到高一般是:DEBUG、INFO、WARN、ERROR、FATAL。但我要提醒的是,分级数量本身不是重点,重点是运行时可过滤。

我见过一些项目把分级写死在代码里,比如#define LOG_LEVEL 3,编译时就把低级别日志裁掉了。这样做的好处是零运行时开销,但坏处是一旦线上出问题需要临时打开DEBUG日志,就得重新编译、重新部署。对于需要长时间运行的服务,这几乎不可接受。

我的建议是采用运行时可配置的分级:日志级别作为一个全局变量或者配置项,程序启动时从配置文件或环境变量读取。每条日志在写入前先做一次级别比较,低于当前级别的直接返回。这个比较的开销极小,一次整数比较而已,完全可以接受。

typedef enum { LOG_DEBUG = 0, LOG_INFO, LOG_WARN, LOG_ERROR, LOG_FATAL } log_level_t; static log_level_t g_current_level = LOG_INFO; #define LOG_WRITE(level, ...) do { \ if ((level) >= g_current_level) { \ log_output(level, __FILE__, __LINE__, __VA_ARGS__); \ } \ } while(0)

这里有个细节值得说:为什么用宏而不是函数?因为如果用函数,那么即使日志被过滤掉,参数表达式的求值、函数调用的压栈开销依然存在。用宏包一层if判断,被过滤的日志几乎零开销。这是性能敏感场景下的标准做法。

2.2 同步还是异步:性能与可靠性的第一场博弈

这是日志系统设计中最核心的取舍。同步写日志就是调用日志函数时直接写文件,简单直接,但每次写都可能触发磁盘I/O,在高频日志场景下会严重拖慢主线程。异步写日志则是把日志先放进一个缓冲区(通常是内存队列),由单独的后台线程负责落盘,主线程只管往队列里塞。

我做过一个粗略的测试:在普通机械硬盘上,同步写一条日志的平均耗时在几十微秒到几百微秒不等,如果每秒写一万条日志,光日志就能吃掉主线程相当可观的时间。而异步方式下,主线程写队列的耗时通常在亚微秒级别。

但异步不是没有代价的。最大的风险是进程崩溃时缓冲区里的日志可能丢失。如果你的服务因为段错误直接挂掉,后台线程还没来得及把队列里的日志写出去,那这些日志就永远丢了——而这恰恰是你最需要的那部分日志。

我的实际经验是采用混合策略:ERROR及以上级别的日志同步写,确保关键错误一定落盘;DEBUG和INFO级别走异步队列,保证性能。这样既不会因为大量调试日志拖慢服务,又能保证出问题时的关键信息不丢。

2.3 输出目标:文件、标准错误还是系统日志

日志往哪里写,也是个需要想清楚的问题。常见的选择有三种:

  • 写普通文件:最灵活,可以自定义格式、自定义轮转策略,适合大多数自研服务。
  • 写标准错误(stderr):适合被systemd或supervisor管理的服务,由上层工具负责收集和轮转。
  • 写系统日志(syslog):适合需要统一收集、集中管理的场景,但格式受限于syslog协议。

对于自研的后台服务,我通常推荐写普通文件,因为控制力最强。但要注意,如果服务是被systemd拉起的,直接写文件时要确保工作目录正确,否则可能写到意想不到的地方。我踩过一次坑:服务手动运行时日志正常,用systemd拉起后日志文件跑到了根目录下,排查了半天才发现是工作目录的问题。

2.4 文件轮转:别让日志把磁盘撑爆

这是最容易被忽视、但后果最严重的问题。一个每秒写几百条日志的服务,如果不做轮转,几天就能把磁盘写满。磁盘一满,不仅日志写不进去,整个服务可能都会因为写失败而异常。

轮转策略通常有两种维度:按大小和按时间。按大小就是单个文件超过阈值(比如100MB)就切分;按时间就是每天或每小时切一个文件。实际生产中我一般两个都用:单文件超过100MB或者跨天,都触发轮转,同时保留最近N个文件,超出的自动删除。

这里有个关键细节:轮转时的文件重命名和删除操作,必须考虑多进程或多线程的并发安全。如果你的服务是多进程的,多个进程同时往一个文件写,轮转时就会出现混乱。这种情况要么用单进程专门负责写日志,要么用文件锁保护轮转操作。

3. 核心实现拆解:一个可靠的错误日志模块长什么样

想清楚了上面的设计决策,就可以动手实现了。这一部分我把一个可靠的日志模块拆成几个关键组件,逐个讲清楚实现要点和背后的考量。

3.1 日志格式设计:让每一条日志都能自证身份

日志格式看起来是小事,但它直接决定了你排查问题时的效率。一条好的错误日志,应该包含足够的信息让你在不看代码的情况下就能定位问题。我常用的格式是这样的:

[2024-01-15 14:23:45.123] [ERROR] [pid:12345] [worker.c:287] connection timeout: fd=18, peer=192.168.1.100:54321

拆开来看,每个字段都有它的作用:

  • 时间戳(精确到毫秒):排查问题时经常需要对齐多个服务的日志,毫秒级精度是必须的。秒级精度在并发场景下根本不够用。
  • 日志级别:方便过滤,一眼看出严重程度。
  • 进程ID:多进程服务必备,能快速定位是哪个进程出的问题。
  • 文件名和行号:直接定位到代码位置,省去大量搜索时间。
  • 具体信息:错误描述加上关键上下文(比如文件描述符、对端地址等)。

时间戳的格式化有个性能陷阱:localtime和strftime这类函数不是线程安全的,而且每次调用都有开销。在高频日志场景下,我通常会用localtime_r(可重入版本),并且做一层缓存——同一秒内的日志复用已经格式化好的时间字符串前缀,只更新毫秒部分。

static void format_timestamp(char *buf, size_t len) { struct timeval tv; gettimeofday(&tv, NULL); struct tm tm_buf; localtime_r(&tv.tv_sec, &tm_buf); snprintf(buf, len, "%04d-%02d-%02d %02d:%02d:%02d.%03d", tm_buf.tm_year + 1900, tm_buf.tm_mon + 1, tm_buf.tm_mday, tm_buf.tm_hour, tm_buf.tm_min, tm_buf.tm_sec, (int)(tv.tv_usec / 1000)); }

3.2 异步队列的实现:无锁还是加锁

如果选择异步写日志,队列的实现方式直接决定了性能上限。最简单的做法是用互斥锁保护一个链表或环形缓冲区,生产者加锁入队,消费者加锁出队。这种方式实现简单,但在高并发下锁竞争会成为瓶颈。

更进一步的做法是无锁队列,用原子操作(CAS)实现多生产者单消费者的环形缓冲区。这种实现性能极好,但代码复杂度高,容易出微妙的bug。我的建议是:如果你的日志量没有大到每秒几十万条,用互斥锁的环形缓冲区就够了,别过早优化。我见过太多项目为了追求无锁队列,结果引入了难以复现的并发bug,得不偿失。

环形缓冲区的大小需要权衡:太小了容易满,生产者要么阻塞要么丢弃日志;太大了占用内存,而且崩溃时丢失的日志更多。我一般设置为能容纳几万条日志的规模,具体大小根据单条日志的平均长度和可接受的内存占用反推。

当队列满时怎么办?这是个必须明确的策略。常见的选择有:阻塞等待(保证不丢但拖慢主线程)、丢弃新日志(保性能但丢信息)、丢弃最旧日志(保留最新状态)。对于错误日志,我倾向于阻塞等待一小段时间,超时则丢弃并记录一条"日志队列溢出"的元日志。这样既不会无限阻塞,又能让你知道发生了溢出。

3.3 崩溃时的日志落盘:信号处理里的那些坑

这是整个日志系统里最考验功力的部分。当进程收到SIGSEGV、SIGABRT这类致命信号时,你希望能在进程死掉之前把关键信息写出去。但信号处理函数里能做的事情非常有限——很多函数不是异步信号安全的,在里面调用它们会导致未定义行为。

printf、malloc、free这些都不是信号安全的,在信号处理函数里调用它们可能死锁或崩溃。信号安全的函数包括write、open、close等系统调用。所以正确的做法是:在信号处理函数里直接用write系统调用把预先准备好的信息写到日志文件描述符,不经过任何缓冲和格式化库函数。

static int g_log_fd = -1; static void crash_handler(int sig) { char buf[256]; int len = snprintf(buf, sizeof(buf), "FATAL: caught signal %d, crashing\n", sig); if (g_log_fd >= 0) { write(g_log_fd, buf, len); } signal(sig, SIG_DFL); raise(sig); }

注意这里用snprintf其实也有争议,严格来说它不是异步信号安全的。更保守的做法是预先格式化好字符串,信号处理函数里只做write。另外,写完日志后要恢复默认信号处理并重新raise,让进程以正确的方式终止,这样core dump等机制才能正常工作。

还有一个容易被忽略的点:异步队列里的日志在崩溃时会丢失。所以我在信号处理函数里会额外做一件事——尝试把队列里剩余的日志flush出去。但这又涉及到队列的并发访问问题,在信号上下文里加锁是危险的。我的做法是维护一个"紧急日志区",关键错误在入队的同时也写一份到这个区域,崩溃时直接把这个区域的内容dump出来。

3.4 文件轮转的落地实现:重命名、删除与并发保护

文件轮转的实现逻辑不复杂,但细节很多。基本流程是:写日志前检查当前文件大小,超过阈值就关闭当前文件、重命名、打开新文件。重命名通常用带时间戳或序号的命名方式,比如app.log.20240115.001。

删除旧文件时要注意,不能简单地按文件名排序删除,因为文件名排序不一定等于时间排序。稳妥的做法是记录每个轮转文件的时间戳,按时间排序后删除最旧的。或者更简单:轮转时检查目录下匹配模式的文件数量,超过保留数量就删除最旧的。

并发保护方面,如果是单进程多线程,用一个互斥锁保护整个轮转过程即可。如果是多进程,情况就复杂了——多个进程可能同时判断需要轮转,然后同时重命名,导致混乱。这种场景下我通常会用文件锁(flock)来串行化轮转操作,或者干脆改成单进程写日志、其他进程通过IPC发送日志的模式。

static void rotate_if_needed(void) { struct stat st; if (fstat(g_log_fd, &st) < 0) return; if (st.st_size < MAX_LOG_SIZE) return; flock(g_log_fd, LOCK_EX); /* 双重检查,防止其他进程已经轮转过 */ if (fstat(g_log_fd, &st) == 0 && st.st_size >= MAX_LOG_SIZE) { close(g_log_fd); rename(g_current_path, g_rotated_path); g_log_fd = open(g_current_path, O_WRONLY | O_CREAT | O_APPEND, 0644); cleanup_old_logs(); } flock(g_log_fd, LOCK_UN); }

这里的双重检查很关键:加锁之前可能已经有别的进程完成了轮转,加锁后必须重新检查一次,否则会重复轮转。

4. 实测中踩出来的经验:那些文档不会告诉你的坑

代码写完了能跑,和在生产环境里稳定运行,中间隔着无数个坑。这一部分我把自己和身边同行踩过的典型问题整理出来,希望能帮你少走弯路。

4.1 磁盘写满时的连锁反应

这是最惨烈的一类故障。磁盘满了之后,日志写失败,如果代码里没处理好写失败的情况,可能会触发一连串异常。我见过一个服务,磁盘满了之后日志写入返回错误,代码里没检查返回值,结果缓冲区状态错乱,最终导致整个服务崩溃。

正确的做法是:每次写日志都要检查返回值,写失败时要有降级策略。降级策略可以是:写失败时直接丢弃日志(保证服务不崩),同时通过其他渠道(比如标准错误)输出一条告警。另外,可以在启动时和运行中定期检查磁盘剩余空间,空间不足时主动降低日志级别或触发轮转清理。

还有一个细节:如果日志文件所在的磁盘满了,连轮转时的重命名和新建文件都可能失败。所以轮转逻辑里也要处理这些失败情况,不能假设一定成功。

4.2 多线程下的日志交错问题

多线程同时写日志,如果不加保护,会出现日志内容交错的情况——两条日志的字符混在一起,完全没法看。用互斥锁保护写入操作是最直接的解决方案,但要注意锁的粒度。

如果锁保护的是"格式化+写入"整个过程,那么格式化(尤其是时间格式化)的开销也在锁内,会降低并发度。更好的做法是先在锁外完成格式化到线程局部缓冲区,然后只在真正写入时加锁。这样锁的持有时间极短,并发性能好很多。

void log_output(int level, const char *file, int line, const char *fmt, ...) { char buf[1024]; int offset = 0; /* 锁外格式化 */ offset += format_timestamp(buf + offset, sizeof(buf) - offset); offset += snprintf(buf + offset, sizeof(buf) - offset, " [%s] [%s:%d] ", level_name(level), file, line); va_list ap; va_start(ap, fmt); offset += vsnprintf(buf + offset, sizeof(buf) - offset, fmt, ap); va_end(ap); buf[offset++] = '\n'; /* 只在写入时加锁 */ pthread_mutex_lock(&g_write_lock); write(g_log_fd, buf, offset); pthread_mutex_unlock(&g_write_lock); }

4.3 日志本身成为性能瓶颈的排查过程

有一次我负责的一个服务,压测时QPS怎么都上不去,CPU占用却不高。用性能分析工具一看,大量时间花在了日志相关的系统调用上。排查下来发现两个问题:一是DEBUG日志在生产环境没关,每条请求都写好几条;二是同步写日志,每次写都触发一次write系统调用。

解决过程分两步:首先把生产环境的日志级别调到INFO,DEBUG日志直接过滤掉,QPS立刻提升了一大截。然后把ERROR以下的日志改成异步写入,又提升了一截。最后把多条日志合并成一次write(用writev或者先拼接到缓冲区再一次性写),系统调用次数大幅下降。

这个经历让我深刻体会到:日志系统的性能问题往往是"温水煮青蛙"式的,平时量小看不出来,一旦压力上来就成了瓶颈。所以日志模块一定要做压测,模拟高并发场景下的表现。

4.4 日志文件权限与安全

日志文件里可能包含敏感信息,比如用户ID、请求参数等。文件权限设置不当,可能造成信息泄露。我一般把日志文件权限设为0640,属主可读写,同组可读,其他用户无权限。目录权限设为0750。

另外,日志内容本身也要注意脱敏。密码、密钥、完整的身份证号这类信息绝对不能写进日志。我见过有项目把数据库连接字符串(含密码)直接打进日志的,这是严重的安全隐患。在日志格式化函数里做一层过滤,对敏感字段做掩码处理,是个好习惯。

4.5 日志时间戳的时区陷阱

时间戳用本地时间还是UTC,是个容易引发混乱的问题。如果服务部署在不同时区的机器上,用本地时间会导致日志时间对不上。我的建议是统一用UTC时间记录,在展示时再转换成需要的时区。这样无论服务部署在哪里,日志的时间线都是一致的。

如果因为历史原因必须用本地时间,那至少要在日志里标明时区,比如2024-01-15 14:23:45.123+08:00。否则跨时区排查问题时,光是对时间就能让人崩溃。

5. 从能用走向好用:日志模块的进阶优化方向

基础功能跑通之后,如果想让日志系统更上一层楼,还有不少可以打磨的地方。这些优化不是必须的,但在特定场景下能带来明显收益。

5.1 结构化日志:让机器也能读懂

传统的文本日志是给人看的,但现代运维越来越依赖自动化分析。结构化日志(比如JSON格式)让日志既能被人读,也能被程序解析。每条日志是一个JSON对象,字段固定,方便用日志分析工具做聚合、过滤、告警。

{"ts":"2024-01-15T14:23:45.123Z","level":"ERROR","pid":12345,"file":"worker.c","line":287,"msg":"connection timeout","fd":18,"peer":"192.168.1.100:54321"}

结构化日志的代价是体积更大、格式化开销更高。所以通常只在需要集中收集分析的场景下使用,本地调试日志还是用传统格式更轻量。

5.2 日志采样:高频日志的降噪手段

有些错误会在短时间内大量重复出现,比如网络抖动导致的连接失败,可能一秒内出现几千次。这种日志如果全部记录,既占空间又淹没真正重要的信息。日志采样就是在这种情况下只记录一部分,比如同样的错误每秒最多记录N条,其余的只计数不记录详情。

实现上可以用一个哈希表记录每种错误最近的出现时间,超过阈值就进入"限流"状态,只记录计数。这样既保留了错误发生的证据,又不会让日志爆炸。

5.3 与监控系统的联动

日志和监控是排查问题的两条腿。日志记录"发生了什么",监控记录"指标怎么样"。把两者联动起来,能大幅提升排查效率。比如当日志中出现特定级别的错误时,自动上报一个监控指标;或者反过来,当监控指标异常时,自动提升日志级别,捕获更多细节。

这种联动通常通过一个中间层实现:日志模块在写入ERROR日志时,除了写文件,还调用监控上报接口。上报接口本身要做异步和限流,避免监控系统故障反过来影响日志写入。

5.4 日志的集中收集与检索

单机日志排查问题,grep和tail就够了。但服务一旦上了规模,几十上百台机器的日志分散在各处,就需要集中收集。常见的方案是每台机器上跑一个日志收集代理,把日志发送到中心存储,再用检索工具做查询。

这个方向涉及的技术栈比较广,不是日志模块本身能解决的。但日志模块在设计时可以为集中收集做点准备:比如输出结构化日志、在日志里带上机器标识和服务标识、支持通过配置动态调整输出目标等。这些准备能让后续接入集中收集系统时省不少事。

6. 一套可以直接参考的完整实现骨架

讲了这么多原理和坑,最后给出一套相对完整的实现骨架,把前面的要点串起来。这套代码不是生产级的完整实现,但结构清晰,你可以在此基础上按自己的需求扩展。

/* log.h */ #ifndef LOG_H #define LOG_H #include <stdio.h> #include <stdarg.h> #include <pthread.h> #include <time.h> #include <sys/time.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <stdlib.h> #include <signal.h> #include <sys/stat.h> #include <sys/file.h> typedef enum { LOG_DEBUG = 0, LOG_INFO, LOG_WARN, LOG_ERROR, LOG_FATAL } log_level_t; int log_init(const char *path, log_level_t level); void log_set_level(log_level_t level); void log_close(void); void log_write(log_level_t level, const char *file, int line, const char *fmt, ...); #define LOGD(...) log_write(LOG_DEBUG, __FILE__, __LINE__, __VA_ARGS__) #define LOGI(...) log_write(LOG_INFO, __FILE__, __LINE__, __VA_ARGS__) #define LOGW(...) log_write(LOG_WARN, __FILE__, __LINE__, __VA_ARGS__) #define LOGE(...) log_write(LOG_ERROR, __FILE__, __LINE__, __VA_ARGS__) #define LOGF(...) log_write(LOG_FATAL, __FILE__, __LINE__, __VA_ARGS__) #endif
/* log.c */ #include "log.h" #define MAX_LOG_SIZE (100 * 1024 * 1024) /* 100MB */ #define MAX_LOG_FILES 10 #define LOG_BUF_SIZE 1024 static int g_log_fd = -1; static log_level_t g_level = LOG_INFO; static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER; static char g_log_path[512]; static char g_log_dir[512]; static const char *level_names[] = { "DEBUG", "INFO", "WARN", "ERROR", "FATAL" }; static void format_timestamp(char *buf, size_t len) { struct timeval tv; gettimeofday(&tv, NULL); struct tm tm_buf; localtime_r(&tv.tv_sec, &tm_buf); snprintf(buf, len, "%04d-%02d-%02d %02d:%02d:%02d.%03d", tm_buf.tm_year + 1900, tm_buf.tm_mon + 1, tm_buf.tm_mday, tm_buf.tm_hour, tm_buf.tm_min, tm_buf.tm_sec, (int)(tv.tv_usec / 1000)); } static void rotate_log(void) { struct stat st; if (fstat(g_log_fd, &st) < 0) return; if (st.st_size < MAX_LOG_SIZE) return; char rotated[600]; char ts[32]; struct timeval tv; gettimeofday(&tv, NULL); struct tm tm_buf; localtime_r(&tv.tv_sec, &tm_buf); snprintf(ts, sizeof(ts), "%04d%02d%02d%02d%02d%02d", tm_buf.tm_year + 1900, tm_buf.tm_mon + 1, tm_buf.tm_mday, tm_buf.tm_hour, tm_buf.tm_min, tm_buf.tm_sec); snprintf(rotated, sizeof(rotated), "%s.%s", g_log_path, ts); close(g_log_fd); rename(g_log_path, rotated); g_log_fd = open(g_log_path, O_WRONLY | O_CREAT | O_APPEND, 0640); } static void crash_handler(int sig) { char buf[256]; int len = snprintf(buf, sizeof(buf), "FATAL: caught signal %d, process crashing\n", sig); if (g_log_fd >= 0) { write(g_log_fd, buf, len); } signal(sig, SIG_DFL); raise(sig); } int log_init(const char *path, log_level_t level) { g_level = level; strncpy(g_log_path, path, sizeof(g_log_path) - 1); g_log_fd = open(path, O_WRONLY | O_CREAT | O_APPEND, 0640); if (g_log_fd < 0) return -1; signal(SIGSEGV, crash_handler); signal(SIGABRT, crash_handler); signal(SIGBUS, crash_handler); signal(SIGFPE, crash_handler); return 0; } void log_set_level(log_level_t level) { g_level = level; } void log_write(log_level_t level, const char *file, int line, const char *fmt, ...) { if (level < g_level) return; char buf[LOG_BUF_SIZE]; int offset = 0; offset += snprintf(buf + offset, sizeof(buf) - offset, "["); format_timestamp(buf + offset, sizeof(buf) - offset - 1); offset = strlen(buf); offset += snprintf(buf + offset, sizeof(buf) - offset, "] [%s] [pid:%d] [%s:%d] ", level_names[level], getpid(), file, line); va_list ap; va_start(ap, fmt); offset += vsnprintf(buf + offset, sizeof(buf) - offset, fmt, ap); va_end(ap); if (offset < (int)sizeof(buf) - 1) { buf[offset++] = '\n'; } pthread_mutex_lock(&g_lock); if (g_log_fd >= 0) { rotate_log(); ssize_t n = write(g_log_fd, buf, offset); (void)n; } pthread_mutex_unlock(&g_lock); } void log_close(void) { pthread_mutex_lock(&g_lock); if (g_log_fd >= 0) { close(g_log_fd); g_log_fd = -1; } pthread_mutex_unlock(&g_lock); }

这套骨架覆盖了分级过滤、时间戳格式化、文件轮转、崩溃信号处理、多线程保护这几个核心点。使用方式也很简单:

int main(void) { if (log_init("/var/log/myapp/app.log", LOG_INFO) < 0) { fprintf(stderr, "log init failed\n"); return 1; } LOGI("service started, version=%s", "1.0.0"); LOGE("failed to connect to backend, retry=%d", 3); log_close(); return 0; }

有几个地方需要根据实际情况调整:MAX_LOG_SIZE和MAX_LOG_FILES要根据你的磁盘容量和日志量来定;崩溃信号处理里如果要dump更多信息,需要预先准备好数据;异步队列没有包含在这个骨架里,如果需要可以按第3.2节的思路加上。

我在实际项目里用这套结构跑了很长时间,稳定性没问题。但每次新项目我都会重新审视一遍参数和策略,因为不同场景的侧重点真的不一样。比如一个低频的批处理任务,同步写日志完全够用,没必要上异步;而一个高频的交易系统,异步队列和采样就是必须的。工具是死的,场景是活的,理解每个设计决策背后的原因,比记住代码本身重要得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询