日志系统架构:多级别日志、异步日志、日志轮转
日志这东西,说实话,很多嵌入式工程师一开始都不太重视。我早年也踩过这个坑——程序跑着跑着突然挂了,连个现场信息都没留下,那叫一个抓瞎。后来我养成了一个习惯:任何交付给客户的产品,日志系统必须到位。今天我们就来聊聊,怎么在嵌入式环境里,搭一套靠谱的日志系统。
为什么需要多级别日志?
你想想看,如果所有日志都用一个级别打印,调试信息、警告、错误全混在一起,那日志文件基本没法看。我见过一个项目,DEBUG 日志刷得飞起,结果真正有用的错误信息被淹没了。排查问题的时候,翻了几万行日志才找到关键线索。
所以,日志级别是刚需。常见的分级方式是这样的:
| 级别 | 含义 | 使用场景 |
|---|---|---|
| FATAL | 致命错误 | 系统无法继续运行,必须立即处理 |
| ERROR | 错误 | 功能异常,但系统还能跑 |
| WARN | 警告 | 潜在问题,值得关注 |
| INFO | 信息 | 关键流程节点,比如启动、初始化完成 |
| DEBUG | 调试 | 开发阶段用的详细输出 |
| TRACE | 跟踪 | 最细粒度,函数调用追踪 |
我个人习惯,发布版本只保留 INFO 及以上级别。DEBUG 和 TRACE 只在开发阶段打开。这样既能保证现场信息够用,又不会把存储空间撑爆。
异步日志:别让日志拖慢你的系统
很多新手写日志,直接 printf 或者 fprintf 到文件。这在简单场景下没问题,但一旦系统负载上来,同步写日志会严重拖慢主流程。我记得有一次做工业控制器,日志打印一多,控制周期直接超时了。查了半天,罪魁祸首就是同步日志。
解决方案很简单:异步日志。核心思路是:
- 日志生产方(业务代码)只管把日志丢进缓冲区
- 日志消费方(后台线程)负责从缓冲区取数据,写入文件
- 生产者和消费者之间用队列解耦
来看一个简化版的实现:
// 日志队列结构
typedef struct {
char buffer[LOG_BUF_SIZE][MAX_LOG_LEN];
int head;
int tail;
int count;
pthread_mutex_t lock;
pthread_cond_t not_empty;
} log_queue_t;
// 生产者:写入日志
void log_write(const char *msg) {
pthread_mutex_lock(&queue.lock);
while (queue.count >= LOG_BUF_SIZE) {
// 队列满了,可以丢弃或等待
pthread_cond_wait(&queue.not_full, &queue.lock);
}
strncpy(queue.buffer[queue.tail], msg, MAX_LOG_LEN);
queue.tail = (queue.tail + 1) % LOG_BUF_SIZE;
queue.count++;
pthread_cond_signal(&queue.not_empty);
pthread_mutex_unlock(&queue.lock);
}
// 消费者:后台线程
void *log_flusher(void *arg) {
while (1) {
pthread_mutex_lock(&queue.lock);
while (queue.count == 0) {
pthread_cond_wait(&queue.not_empty, &queue.lock);
}
char msg[MAX_LOG_LEN];
strncpy(msg, queue.buffer[queue.head], MAX_LOG_LEN);
queue.head = (queue.head + 1) % LOG_BUF_SIZE;
queue.count--;
pthread_mutex_unlock(&queue.lock);
// 写入文件
fputs(msg, log_file);
fflush(log_file);
}
return NULL;
}
日志轮转:别让日志撑爆存储
嵌入式设备的存储空间通常有限。如果不做日志轮转,日志文件会一直增长,直到把 Flash 或 SD 卡写满。我遇到过一台设备,运行半年后日志文件占了 2GB,系统直接报存储不足。从那以后,日志轮转成了我项目的标配。
常见的轮转策略有两种:
- 按大小轮转:单个日志文件达到指定大小(比如 10MB),就自动切换到下一个文件
- 按时间轮转:每天或每小时生成一个新的日志文件
实际项目中,我通常两种策略结合使用。比如:每个文件最大 10MB,最多保留 5 个文件。超过 5 个就覆盖最旧的那个。
代码实现思路:
#define MAX_LOG_FILES 5
#define MAX_FILE_SIZE (10 * 1024 * 1024) // 10MB
void log_rotate(void) {
// 检查当前文件大小
fseek(log_file, 0, SEEK_END);
long size = ftell(log_file);
if (size < MAX_FILE_SIZE) return;
// 关闭当前文件
fclose(log_file);
// 删除最旧的文件(如果有)
char oldest[64];
snprintf(oldest, sizeof(oldest), "log_%d.txt",
(current_index - MAX_LOG_FILES + 1));
remove(oldest);
// 创建新文件
current_index++;
char new_name[64];
snprintf(new_name, sizeof(new_name), "log_%d.txt", current_index);
log_file = fopen(new_name, "w");
}
整体架构图
下面这张图,展示了日志系统的整体流程。从应用层产生日志,到异步队列缓冲,再到后台线程写入文件,最后按策略轮转。一目了然。
避坑指南
- 日志队列满了怎么办?我建议直接丢弃新日志,而不是阻塞业务线程。系统正常运行比记录日志更重要。
- 多线程写日志要加锁吗?异步模式下,生产者之间需要互斥,但消费者只从队列取数据,不需要额外锁。
- 日志文件不要放在根目录。我习惯单独建一个 /log 分区,防止日志写满导致系统崩溃。
总结
日志系统看似简单,但要做好其实不容易。多级别日志让你能快速定位问题,异步日志保证系统性能不受影响,日志轮转防止存储被撑爆。这三者结合起来,就是一套靠谱的嵌入式日志方案。
嗯,这套架构我在好几个项目里都用过,效果不错。你可以在自己的项目里试试,根据实际需求调整队列大小和轮转策略。记住,日志系统是为排查问题服务的,别让它成为新的问题来源。
公众号:蓝海资料掘金营,微信deep3321