1. 嵌入式日志记录的核心原则在嵌入式Linux系统开发中日志记录是调试和问题排查的生命线。经过多年实战我总结出几个铁律重要提示嵌入式系统资源有限但日志绝不能省。关键是要在信息量和资源消耗间找到平衡。1.1 必打日志的场景异常分支和重大操作必须记录日志这是底线。我见过太多因为这个错误应该不会发生而没打日志结果线上问题排查时抓瞎的案例。具体包括内存分配失败必须记录申请大小文件/网络操作错误记录路径、错误码参数校验失败打印具体参数值进程/线程创建销毁关键状态变更1.2 日志格式五要素完整的日志应包含以下字段这是多年踩坑总结出的黄金组合精确时间戳建议使用gettimeofday()获取微秒级时间。在多线程竞争场景下毫秒级精度可能无法区分事件顺序。struct timeval tv; gettimeofday(tv, NULL); strftime(time_buf, sizeof(time_buf), %Y-%m-%d %H:%M:%S, localtime(tv.tv_sec)); snprintf(log_header, [%s.%03ld], time_buf, tv.tv_usec/1000);源码位置__FILE__和__LINE__宏是必备的。曾经有个内存泄漏问题因为多个模块都有相似代码没有文件行号导致排查花了三天。进程ID通过getpid()获取。在守护进程重启或集群环境下特别有用。线程ID使用pthread_self()或syscall(SYS_gettid)。曾遇到过一个死锁问题靠线程ID才理清调用链。日志级别ERROR必须立即处理的问题WARN潜在问题INFO关键业务流程DEBUG调试信息2. 实战日志规范实现2.1 日志函数封装示例#define LOG(level, fmt, ...) do { \ struct timeval tv; \ gettimeofday(tv, NULL); \ char time_buf[32]; \ strftime(time_buf, sizeof(time_buf), %Y-%m-%d %H:%M:%S, localtime(tv.tv_sec)); \ fprintf(log_file, [%s.%03ld][%s:%d][pid%d][thread0x%lx][%s] fmt \n, \ time_buf, tv.tv_usec/1000, __FILE__, __LINE__, \ getpid(), (unsigned long)pthread_self(), #level, ##__VA_ARGS__); \ fflush(log_file); \ } while(0)2.2 内存操作日志要点内存问题是最难排查的日志要包含分配/释放的内存地址内存块大小调用栈可选void* my_malloc(size_t size, const char* tag) { void* ptr malloc(size); if (!ptr) { LOG(ERROR, malloc(%zu) failed for %s, size, tag); } else { LOG(DEBUG, malloc(%zu)%p for %s, size, ptr, tag); } return ptr; }3. 典型场景日志实践3.1 网络通信日志必须记录对端IP和端口本地socket信息错误码及描述int connect_to_server(const char* ip, int port) { int sockfd socket(AF_INET, SOCK_STREAM, 0); // ...初始化sockaddr_in... if (connect(sockfd, (struct sockaddr*)serv_addr, sizeof(serv_addr)) 0) { LOG(ERROR, connect(%s:%d) failed: %s (errno%d), ip, port, strerror(errno), errno); return -1; } LOG(INFO, Connected to %s:%d via fd%d, ip, port, sockfd); return sockfd; }3.2 文件操作日志关键信息完整文件路径操作模式读/写等文件描述符错误详情FILE* safe_fopen(const char* path, const char* mode) { FILE* fp fopen(path, mode); if (!fp) { LOG(ERROR, fopen(%s, %s) failed: %s, path, mode, strerror(errno)); return NULL; } LOG(DEBUG, Opened %s as fd%d, path, fileno(fp)); return fp; }4. 高级日志技巧4.1 日志分级控制通过环境变量控制日志级别typedef enum { LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_WARN, LOG_LEVEL_ERROR, LOG_LEVEL_NONE } log_level_t; static log_level_t current_level LOG_LEVEL_INFO; void set_log_level(log_level_t level) { current_level level; } #define LOG(level, fmt, ...) do { \ if (level current_level) { \ /* 之前的日志实现 */ \ } \ } while(0)4.2 日志轮转策略嵌入式设备存储有限需要实现按大小分割单个日志文件不超过10MB按时间分割每天生成新文件自动清理保留最近7天的日志void log_rotate() { if (ftell(log_file) 10*1024*1024) { fclose(log_file); char old_name[256]; snprintf(old_name, sizeof(old_name), %s.1, log_path); rename(log_path, old_name); log_file fopen(log_path, a); } }5. 常见问题排查指南5.1 日志丢失问题现象程序崩溃后最后几条日志没写入 解决方案每次写日志后调用fflush()使用同步IO模式打开日志文件考虑内存日志缓冲区5.2 性能优化高频日志可能成为性能瓶颈对高频路径使用LOG_DEBUG级别批量操作合并日志异步日志线程注意线程安全void* log_thread(void* arg) { while (!exit_flag) { pthread_mutex_lock(log_mutex); // 处理日志队列 pthread_mutex_unlock(log_mutex); usleep(10000); // 10ms } return NULL; }5.3 多线程日志安全确保线程安全的方法使用互斥锁保护日志文件每个线程独立日志文件不推荐无锁环形缓冲区pthread_mutex_t log_mutex PTHREAD_MUTEX_INITIALIZER; #define LOG(level, fmt, ...) do { \ pthread_mutex_lock(log_mutex); \ /* 写日志操作 */ \ pthread_mutex_unlock(log_mutex); \ } while(0)6. 日志分析工具链6.1 实时监控# 跟踪最新日志 tail -f app.log | grep -E ERROR|WARN # 统计错误频率 awk -F[][] /ERROR/ {print $6} app.log | sort | uniq -c | sort -nr6.2 离线分析推荐工具组合grep基础过滤awk字段提取sed文本替换logparser高级分析示例分析内存问题grep Failed to allocate app.log | awk {print $NF} | sort -n | uniq -c7. 项目实战经验在物联网网关项目中我们通过改进日志解决了几个棘手问题内存碎片问题通过记录每次内存分配大小发现大量1MB的分配请求失败尽管系统显示还有空闲内存。最终重构了内存管理模块引入内存池。死锁问题通过微秒级时间戳和线程ID发现两个线程在相差200us内尝试获取相同的锁顺序。调整锁获取顺序后解决。性能瓶颈日志显示某函数每秒被调用上万次优化后系统吞吐量提升3倍。关键教训日志要包含足够上下文时间精度要足够高重要错误要包含系统状态如free、top输出