2. 日志系统基础:日志级别、日志格式、日志缓冲区设计原则
做过多核异构调试的工程师都知道,日志系统是调试的「眼睛」。没有一套好的日志系统,你就像在黑夜里摸黑修电路——全靠运气。今天我就把日志系统最核心的三个东西掰开揉碎了讲:日志级别、日志格式、还有缓冲区设计。
2.1 日志级别:别什么都往串口打
日志级别这东西,说白了就是给日志分个三六九等。我见过不少新手,调试时直接 printf("xxx") 一把梭,结果产品上线后日志满天飞,关键信息反而被淹没了。
我个人习惯用这五个级别,够用且不冗余:
| 级别 | 数值 | 含义 | 典型场景 |
|---|---|---|---|
| FATAL | 0 | 致命错误 | 系统即将崩溃,必须立即处理 |
| ERROR | 1 | 错误 | 功能异常,但系统还能跑 |
| WARN | 2 | 警告 | 潜在风险,比如缓冲区快满了 |
| INFO | 3 | 信息 | 关键流程节点,如任务启动/结束 |
| DEBUG | 4 | 调试 | 开发阶段用的详细输出 |
2.2 日志格式:统一模板,别搞花活
日志格式这件事,团队里必须统一。你想想看,A 同事打的是 [时间] 信息,B 同事打的是 信息 (时间),后期用脚本分析日志时,光解析格式就够你喝一壶的。
我推荐的标准格式:
[级别] [时间戳] [模块名] [任务ID] 日志内容
举个例子:
[INFO] [2025-01-15 14:23:45.123] [UART_DRV] [TASK_03] DMA传输完成,长度=1024字节
为什么要有任务ID?在多核异构系统里,同一个模块可能被多个核、多个任务同时调用。没有任务ID,你根本分不清这条日志是谁打的。嗯,这里要注意——时间戳最好带毫秒,否则两个核的日志混在一起,你根本排不出先后顺序。
2.3 日志缓冲区设计原则:别让日志拖死系统
日志缓冲区设计,是嵌入式日志系统里最容易翻车的地方。我见过三种常见的死法:
- 死法一: 缓冲区太小,日志频繁丢失
- 死法二: 缓冲区太大,内存被吃光
- 死法三: 锁竞争严重,日志写入成了性能瓶颈
怎么设计?我总结了三原则:
原则一:环形缓冲区 + 水位线
环形缓冲区是嵌入式日志的标配。它固定大小,写满后覆盖最旧的数据。但要注意——加一个水位线机制。当缓冲区使用量超过 80% 时,主动触发一次批量输出,防止关键日志被覆盖。
原则二:无锁设计或极简锁
多核同时写日志时,锁竞争是性能杀手。我个人的做法是:每个核分配独立的日志缓冲区,核间不共享。这样完全不需要加锁。等日志输出时,由专门的日志任务统一收集。
原则三:异步输出,不阻塞业务
日志写入缓冲区后,立刻返回。真正的输出(写到串口、SD卡或网络)由后台任务处理。这样即使输出设备慢,也不会拖慢主业务。
2.4 知识体系结构图
下面这张图,把日志系统的三个核心模块串起来了。你可以看到它们之间的依赖关系:
2.5 代码示例:一个轻量级日志模块
下面是我在一个 Cortex-M4 + RISC-V 异构项目里用过的日志模块骨架。它实现了环形缓冲区 + 异步输出:
/* log.h */
#define LOG_LEVEL_FATAL 0
#define LOG_LEVEL_ERROR 1
#define LOG_LEVEL_WARN 2
#define LOG_LEVEL_INFO 3
#define LOG_LEVEL_DEBUG 4
/* 运行时日志级别,可在启动时配置 */
extern int g_log_level;
#define LOG_FATAL(fmt, ...) \
do { if (g_log_level >= LOG_LEVEL_FATAL) \
log_output(LOG_LEVEL_FATAL, __MODULE__, fmt, ##__VA_ARGS__); \
} while(0)
/* log.c */
#define RING_BUF_SIZE 4096
static char ring_buf[RING_BUF_SIZE];
static uint32_t write_idx = 0;
static uint32_t read_idx = 0;
void log_output(int level, const char* module, const char* fmt, ...) {
char entry[256];
va_list args;
va_start(args, fmt);
vsnprintf(entry, sizeof(entry), fmt, args);
va_end(args);
/* 写入环形缓冲区(简化版,实际需处理溢出) */
uint32_t next = (write_idx + 1) % RING_BUF_SIZE;
if (next != read_idx) {
ring_buf[write_idx] = entry[0]; /* 实际应拷贝完整字符串 */
write_idx = next;
}
/* 触发后台任务输出 */
task_notify(LOG_TASK_ID);
}
日志系统基础就讲到这里。记住三个关键词:级别过滤、格式统一、缓冲区不阻塞。把这三点吃透了,后面讲多核日志同步时,你才能游刃有余。
公众号:蓝海资料掘金营,微信deep3321