本文基于本仓库 vendored 的 EasyLogger v2.2.0 源码(easylogger/EasyLogger-2.2.0/)以及官方文档撰写,所有 API 与示例均可在该版本上直接编译运行。官方链接见文末「官方资源汇总」。
引言
做 开发的朋友,几乎都踩过同一个坑:程序跑得越久越慢,最后发现是日志在拖后腿。
原因是很多人图省事直接用 syslog(),而 syslog 走的是 Unix domain socket 同步 IPC,每次调用都要陷入内核、等守护进程处理。在低端 Cortex-A7 上,单条 syslog() 调用轻松 50~100µs,高频打印直接把业务线程堵死。
本文要讲的 EasyLogger,是 RT-Thread 社区大牛 armink 的作品。它的设计哲学和 syslog 完全相反:只负责把日志格式组装好,然后用一个环形缓冲区 + 后台线程(或自己的 sink)把“业务线程”和“真正的落盘”解耦。结果就是:业务线程打日志几乎零等待,慢速的文件/串口/Flash 写入交给后台线程慢慢做。
一、EasyLogger 介绍
| 维度 |
说明 |
| 作者 / 协议 |
armink / MIT(商用无忧) |
| 定位 |
超轻量级、高性能 C/C++ 日志库 |
| 体积宣称 |
ROM < 1.6K、RAM < 0.3K(指 MCU 裸机最小配置;Linux 端开启异步+文件后会大些,但依然是"轻"量级) |
| 支持平台 |
RT-Thread、UCOS、Linux、Windows、Nuttx,以及裸机 |
| 级别 |
6 级:Assert / Error / Warn / Info / Debug / Verbose(参考 Android Logcat) |
| 核心能力 |
用户自定义输出(终端/文件/Flash/串口…)、线程安全、异步输出、缓冲输出、RAW 与 hexdump、按标签/级别/关键词动态过滤、彩色、插件扩展 |
为什么在 Arm Linux 上值得用它?
- 异步非阻塞:业务线程只往内存环形缓冲写,后台 pthread 负责真正的输出。高频日志不再卡业务。
- 源码级集成,零第三方依赖:把几个
.c 加进工程编就行,交叉编译也只需要一个 -lpthread。
- 一份代码多端复用:同时维护 RT-Thread 和 Arm Linux 应用时,日志接口完全一致,移植负担最低。
- 可裁剪:不需要的功能(异步、缓冲、颜色、文件、时间)关掉对应宏即可,体积可控。
⚠️ 适用边界:EasyLogger 是单例、全局配置的库(配置和输出后端全进程只有一套)。它不适合"同一进程里不同模块要输出到完全不同地方并各自独立配置"的复杂场景——那种需求看 zlog。它最适合"进程级统一日志 + 轻量高性能"的嵌入式 Linux 业务。
二、常用 API
2.1 六级日志
0 [A] 断言 Assert
1 [E] 错误 Error
2 [W] 警告 Warn
3 [I] 信息 Info
4 [D] 调试 Debug
5 [V] 详细 Verbose
两级过滤机制(理解这个,调日志才不懵):
- 静态(编译期):
elog_cfg.h 里的 ELOG_OUTPUT_LVL。低于该级别的代码在编译时直接被替换成空。
- 动态(运行期):
elog_set_filter_lvl() 等 API。低于该级别的日志在运行时被丢弃。
优先级:ELOG_OUTPUT_ENABLE(总开关) > ELOG_OUTPUT_LVL(静态) > elog_set_filter_*(动态)。
2.2 打印日志的两种写法
EasyLogger 提供了两套宏,本质一样,只是一个要每次写 tag,一个靠文件顶部预先定义的 LOG_TAG:
/* 方式一:elog_x(tag, ...) —— 每次显式传标签 */
elog_a("net", "assert: %s", expr); /* 注意第一个参数是 tag */
elog_e("net", "connect failed, errno=%d", errno);
elog_w("net", "retry later");
elog_i("net", "connected");
elog_d("net", "send seq=%u", seq);
elog_v("net", "heartbeat");
/* 方式二:log_x(...) —— 文件顶部先 #define LOG_TAG "net",
之后无需再写 tag,LOG_TAG 自动填入 */
#define LOG_TAG "net"
#include <elog.h>
log_e("connect failed, errno=%d", errno); /* 等价于 elog_e("net", ...) */
log_i("connected");
实战经验:每个 .c 文件顶部按模块定义不同的 LOG_TAG(如 "wifi.proto"、"can.disp"),再统一用 log_x(),日志天然按模块分组,后期过滤/排查极方便。还可以配合 LOG_LVL 做文件级静态级别控制(详见官方 API 文档「技巧二」)。
2.3 生命周期与配置 API(常用)
下面这些都在 easylogger/inc/elog.h 声明,是最常用的几个:
| API |
作用 |
ElogErrCode elog_init(void) |
库初始化,任何日志调用前必须先调用 |
void elog_start(void) |
启动日志输出(初始化后必须调用,否则不输出) |
void elog_set_fmt(uint8_t level, size_t set) |
设置某级别的输出格式位(级别/标签/时间/进程/线程/文件/函数/行号) |
void elog_set_filter_lvl(uint8_t level) |
动态设置过滤级别(低于该级不输出) |
void elog_set_filter_tag(const char *tag) |
按标签过滤 |
void elog_set_filter_kw(const char *kw) |
按关键词过滤 |
void elog_set_filter_tag_lvl(const char *tag, uint8_t lvl) |
按模块(标签)单独设级别,非常实用 |
void elog_set_output_enabled(bool en) |
运行时整体开/关输出 |
void elog_set_text_color_enabled(bool en) |
开/关彩色(需 ELOG_COLOR_ENABLE) |
void elog_raw(const char *fmt, ...) |
输出 RAW 原始日志(无格式化头) |
void elog_hexdump(const char *name, uint8_t width, uint8_t *buf, uint16_t size) |
十六进制 dump,调试协议报文神器 |
void elog_buf_enabled(bool en) / void elog_flush(void) |
缓冲输出模式开关 / 把缓冲全部刷出 |
void elog_async_enabled(bool en) |
异步输出模式开/关 |
格式位宏(ELOG_FMT_*):ELOG_FMT_LVL、ELOG_FMT_TAG、ELOG_FMT_TIME、ELOG_FMT_P_INFO(进程)、ELOG_FMT_T_INFO(线程)、ELOG_FMT_DIR、ELOG_FMT_FUNC、ELOG_FMT_LINE,以及组合 ELOG_FMT_ALL。
典型初始化:
setbuf(stdout, NULL); /* 关掉 stdout 缓冲,避免日志看不到 */
elog_init();
/* 不同级别给不同格式:错误/警告带时间,调试给全量 */
elog_set_fmt(ELOG_LVL_ERROR, ELOG_FMT_LVL | ELOG_FMT_TAG | ELOG_FMT_TIME);
elog_set_fmt(ELOG_LVL_WARN, ELOG_FMT_LVL | ELOG_FMT_TAG | ELOG_FMT_TIME);
elog_set_fmt(ELOG_LVL_INFO, ELOG_FMT_LVL | ELOG_FMT_TAG | ELOG_FMT_TIME);
elog_set_fmt(ELOG_LVL_DEBUG, ELOG_FMT_ALL & ~ELOG_FMT_FUNC);
elog_set_fmt(ELOG_LVL_VERBOSE,ELOG_FMT_ALL & ~ELOG_FMT_FUNC);
#ifdef ELOG_COLOR_ENABLE
elog_set_text_color_enabled(true);
#endif
elog_start();
💡 想临时拉高某个模块的调试级别,不用重新编译:elog_set_filter_tag_lvl("wifi", ELOG_LVL_DEBUG); 打开 wifi 模块全部调试日志;elog_set_filter_tag_lvl("wifi", ELOG_FILTER_LVL_SILENT); 直接静默它。
三、 Arm Linux 移植的详细步骤
EasyLogger 的边界非常清晰:库本身只做"过滤 + 格式化",真正的输出(往哪打、怎么加锁、时间从哪来)全部由实现的一个 elog_port.c 决定。所以移植 = 改一个配置头 + 写一个 port 文件。
3.1 目录结构(从源码里取这些文件)
easylogger/
├── inc/
│ ├── elog.h # 公共 API(不要改)
│ └── elog_cfg.h # ★ 唯一要改的配置文件
├── src/
│ ├── elog.c # 核心
│ ├── elog_utils.c # 工具
│ ├── elog_async.c # 异步模式(按需加入编译)
│ └── elog_buf.c # 缓冲模式(按需加入编译)
├── port/
│ └── elog_port.c # ★ 移植接口(Linux 下自己实现)
└── plugins/
└── file/ # 文件输出插件(可选)
├── elog_file.c
├── elog_file.h
├── elog_file_cfg.h # 文件插件配置
└── elog_file_port.c
导入到工程的动作(官方移植文档第 2 章):
- 把
easylogger/ 整个目录拷进工程;
- 把
src/elog.c、src/elog_utils.c、port/elog_port.c 加入编译(elog_async.c/elog_buf.c 视是否开启异步/缓冲决定);
- 把
easylogger/inc/ 加入头文件搜索路径;
- 启用文件插件时,再把
plugins/file/elog_file.c、elog_file_port.c 加入编译,并把 plugins/ 加入搜索路径。

3.2 配置 elog_cfg.h(关键宏)
下面是一份 Arm Linux 用户态推荐配置(在官方 Linux demo 配置基础上加了中文注释):
/* ===== 总开关与级别 ===== */
#define ELOG_OUTPUT_ENABLE /* 总开关,关掉则所有日志变空 */
#define ELOG_OUTPUT_LVL ELOG_LVL_VERBOSE /* 静态级别:编译期最低可见级 */
#define ELOG_ASSERT_ENABLE /* 开启断言检查 */
/* ===== 缓冲区与过滤长度 ===== */
#define ELOG_LINE_BUF_SIZE 512 /* 单行日志最大字符数 */
#define ELOG_LINE_NUM_MAX_LEN 5 /* 行号最大位数 */
#define ELOG_FILTER_TAG_MAX_LEN 16 /* 标签最大长度 */
#define ELOG_FILTER_KW_MAX_LEN 16 /* 关键词最大长度 */
#define ELOG_FILTER_TAG_LVL_MAX_NUM 5 /* 支持多少组"标签+级别"过滤 */
#define ELOG_NEWLINE_SIGN "\n" /* 换行符,可改 "\r\n" */
/* ===== 彩色 ===== */
#define ELOG_COLOR_ENABLE /* 终端彩色(需配合 elog_set_text_color_enabled(true)) */
/* ===== 异步输出(强烈推荐开启) ===== */
#define ELOG_ASYNC_OUTPUT_ENABLE /* 开启异步模式 */
#define ELOG_ASYNC_OUTPUT_LVL ELOG_LVL_DEBUG /* 高于此级的日志走同步,保证高优先级实时性 */
#define ELOG_ASYNC_OUTPUT_BUF_SIZE (ELOG_LINE_BUF_SIZE * 100) /* 环形缓冲 51KB */
#define ELOG_ASYNC_OUTPUT_USING_PTHREAD /* 用 pthread 实现后台刷出线程 */
/* #define ELOG_ASYNC_LINE_OUTPUT */ /* 可选:保证每次 port_output 都是完整的一行 */
/* ===== 文件插件(可选) ===== */
/* #define ELOG_FILE_ENABLE */ /* 取消注释即启用文件输出插件 */
/* #define ELOG_FILE_FLUSH_CAHCE_ENABLE */ /* 注意:上游宏名就是 CAHCE(拼写如此),开启即每次 fflush+fsync */
注意 ELOG_FILE_FLUSH_CAHCE_ENABLE 的拼写——上游源码里就是 CAHCE,照抄即可,别自己"修正"成 CACHE,否则宏不生效。
3.3 实现 elog_port.c(Linux 版)
这是移植的核心。需要提供的接口(官方移植文档第 3 章):
| 接口 |
必须 |
说明 |
elog_port_init() |
是 |
初始化移植所需资源(如锁、打开文件) |
elog_port_output(log, size) |
是 |
日志最终落点:往终端/文件/串口写 |
elog_port_output_lock() |
是 |
输出加锁(多线程保护) |
elog_port_output_unlock() |
是 |
输出解锁 |
elog_port_get_time() |
否 |
返回时间戳字符串(开 ELOG_TIME 时用) |
elog_port_get_p_info() |
否 |
进程信息(多进程排查很有用) |
elog_port_get_t_info() |
否 |
线程信息(多线程排查很有用) |
下面是一份生产可用的基础 port(终端 + 进程/线程信息),也是示例 1、2 用的:
/* elog_port.c —— Arm Linux 用户态基础移植(终端输出) */
#include <elog.h>
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <time.h>
/* 进程内线程安全锁(保护输出不被多线程交错撕碎) */
static pthread_mutex_t s_lock = PTHREAD_MUTEX_INITIALIZER;
ElogErrCode elog_port_init(void) {
/* 基础移植无需额外资源,预留扩展点 */
return ELOG_NO_ERR;
}
/* 日志最终出口:这里打到终端。要写文件/串口,在此扩展即可 */
void elog_port_output(const char *log, size_t size) {
printf("%.*s", (int)size, log);
}
void elog_port_output_lock(void) { pthread_mutex_lock(&s_lock); }
void elog_port_output_unlock(void) { pthread_mutex_unlock(&s_lock); }
/* 时间戳:启用 ELOG_TIME_ENABLE 时会被调用 */
const char *elog_port_get_time(void) {
static char buf[24] = {0};
time_t t = time(NULL);
struct tm *tm = localtime(&t);
if (tm == NULL) return "";
snprintf(buf, sizeof(buf), "%02d-%02d %02d:%02d:%02d",
tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec);
return buf;
}
/* 进程信息:多进程场景用它一眼看出日志来自哪个进程 */
const char *elog_port_get_p_info(void) {
static char buf[16] = {0};
snprintf(buf, sizeof(buf), "pid:%04d", (int)getpid());
return buf;
}
/* 线程信息:多线程场景区分日志来源 */
const char *elog_port_get_t_info(void) {
static char buf[16] = {0};
snprintf(buf, sizeof(buf), "tid:%04ld", (long)pthread_self());
return buf;
}
把时间、进程、线程信息都打开后,一条日志会长这样:\033[32;22mI/var 04-18 11:20:01 pid:1234 tid:5678 main: hello\033[0m 级别、时间、pid、tid、tag 一目了然,多进程多线程排障时省太多事。
3.4 文件输出插件(可选)
想让日志落盘,启用 ELOG_FILE_ENABLE 并补全两个文件:
elog_file_cfg.h(插件自己的配置):
#define ELOG_FILE_NAME "/var/log/myapp.log" /* 日志文件路径 */
#define ELOG_FILE_MAX_SIZE (1024 * 1024) /* 单文件最大 1MB */
#define ELOG_FILE_MAX_ROTATE 5 /* 超过后滚动 5 个备份 */
elog_file_port.c(插件移植,加锁即可):
#include "elog_file.h"
ElogErrCode elog_file_port_init(void) { return ELOG_NO_ERR; }
void elog_file_port_lock(void) { /* 进程内锁,需要可加 pthread_mutex */ }
void elog_file_port_unlock(void) { /* 与上面配对 */ }
void elog_file_port_deinit(void) { /* 清理 */ }
启用后,基础 port 的 elog_port_output 里顺便调一句 elog_file_write(log, size); 即可同时写文件(官方 Linux demo 的 elog_port.c 就是这样做的)。
3.5 编译(本机 & 交叉编译)
# 本机编译(示例 1/2 用到的文件)
gcc main.c \
easylogger/src/elog.c easylogger/src/elog_utils.c \
easylogger/src/elog_async.c easylogger/src/elog_buf.c \
elog_port.c \
-I./easylogger/inc -lpthread -o app
# 启用文件插件时再加:
# easylogger/plugins/file/elog_file.c easylogger/plugins/file/elog_file_port.c
# -I./easylogger/plugins -I./easylogger/plugins/file
# Arm Linux 交叉编译(把 gcc 换成工具链前缀即可)
arm-linux-gnueabihf-gcc main.c \
easylogger/src/elog.c easylogger/src/elog_utils.c \
easylogger/src/elog_async.c easylogger/src/elog_buf.c \
elog_port.c \
-I./easylogger/inc -lpthread -o app_arm
四、代码示例
下面三个示例都基于上面的 elog_port.c。编译时请确保 elog_cfg.h 里 ELOG_ASYNC_OUTPUT_ENABLE 已开(否则异步相关行为不同)。
4.1 示例一:单线程(终端 + 文件)
最基础的用法:初始化 → 打各级别日志 → hexdump 一段报文 → 退出。
/*
* 示例一:单线程(终端输出 + hexdump + 动态过滤)
* 编译:make demo_single 运行:./demo_single
*/
#define LOG_TAG "main"
#include <elog.h>
#include <stdio.h>
#include <unistd.h>
int main(void) {
/* 关掉 stdout 缓冲,避免日志“攒批”看不到 */
setbuf(stdout, NULL);
elog_init();
/* 不同级别给不同格式:信息级带 级别/标签/时间/进程,调试级给全量 */
elog_set_fmt(ELOG_LVL_INFO, ELOG_FMT_LVL | ELOG_FMT_TAG | ELOG_FMT_TIME | ELOG_FMT_P_INFO);
elog_set_fmt(ELOG_LVL_DEBUG, ELOG_FMT_ALL & ~ELOG_FMT_FUNC);
#ifdef ELOG_COLOR_ENABLE
elog_set_text_color_enabled(true);
#endif
elog_start();
log_a("assert demo"); /* 一般只在真断言时出现 */
log_e("open %s failed, errno=%d", "/dev/ttyS1", 2);
log_w("cpu load high: %d%%", 92);
log_i("system started, pid=%d", getpid());
log_d("rx buffer free=%u", 1024u);
log_v("heartbeat tick");
/* 协议调试:把一帧原始数据 dump 出来 */
uint8_t frame[16];
for (int i = 0; i < 16; i++) {
frame[i] = (uint8_t)(i * 16 + i);
}
elog_hexdump("uart_rx", 16, frame, sizeof(frame));
/* 动态调级演示:只保留 WARN 及以上 */
elog_set_filter_lvl(ELOG_LVL_WARN);
log_i("这条 INFO 不会被输出");
log_w("这条 WARN 仍可见");
/* ★ 异步模式退出注意:EasyLogger 2.2.0 没有“等待异步刷完”的 API。
本程序会马上退出,若不稍等,分离的异步后台线程会被一起杀掉,
环形缓冲里最后的日志就丢了。长时间运行的服务无需担心(线程持续排空)。
演示/工具类程序可用下面两种方式之一:
1) 像这里留一点排空时间; 2) 调 elog_async_enabled(false) 切回同步。 */
usleep(200000);
return 0;
}

编译运行:
gcc demo_single.c easylogger/src/elog.c easylogger/src/elog_utils.c \
easylogger/src/elog_async.c easylogger/src/elog_buf.c elog_port.c \
-I./easylogger/inc -lpthread -o demo_single
./demo_single
4.2 示例二:多线程(N 个线程并发打日志)
EasyLogger 在 Linux 下是线程安全的,前提是 elog_port_output_lock/unlock 用了互斥锁(上面 port 已经做了)。异步模式下,真正的写文件/终端是后台 pthread 在做,业务线程只是往环形缓冲塞数据,几乎无锁竞争。
下面起 3 个线程,每个循环打 50 条日志,验证不会"串味":
/*
* 示例二:多线程(N 个线程并发打日志)
* 编译:make demo_mt 运行:./demo_mt
*
* 结论:EasyLogger 在 Linux 下是线程安全的,前提是 elog_port_output_lock/unlock
* 用了互斥锁(基础 port 已经做了)。异步模式下真正的写终端由后台 pthread
* 完成,业务线程只往环形缓冲塞数据,几乎无锁竞争。
*/
#define LOG_TAG "mt"
#include <elog.h>
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
static void *worker(void *arg) {
long id = (long)arg;
for (int i = 0; i < 50; i++) {
log_i("thread-%ld loop-%d", id, i);
usleep(1000);
}
return NULL;
}
int main(void) {
setbuf(stdout, NULL);
elog_init();
elog_set_fmt(ELOG_LVL_INFO, ELOG_FMT_LVL | ELOG_FMT_TAG | ELOG_FMT_T_INFO | ELOG_FMT_P_INFO);
#ifdef ELOG_COLOR_ENABLE
elog_set_text_color_enabled(true);
#endif
elog_start();
pthread_t t[3];
for (long i = 0; i < 3; i++) {
pthread_create(&t[i], NULL, worker, (void *)i);
}
for (int i = 0; i < 3; i++) {
pthread_join(t[i], NULL);
}
log_i("all threads done");
/* 同 demo_single:退出前给异步后台线程留排空时间,避免最后几条日志丢失 */
usleep(200000);
return 0;
}
实战经验:
- 多线程下不要在
elog_port_output 里做耗时极长的同步 IO 且不开异步——开了异步后重活都在后台线程,业务线程瞬间返回。
- 环形缓冲写满时,EasyLogger 会丢弃超出部分(不会阻塞业务),所以极端高频场景要酌情调大
ELOG_ASYNC_OUTPUT_BUF_SIZE,或把最关键的日志级别设到 ELOG_ASYNC_OUTPUT_LVL 以下走同步,确保不丢。
- 进程退出前,若担心最后几条还在缓冲里没刷完,可让主线程在
return 前 sleep 一小会儿,给后台线程排空时间(2.2.0 没有提供"等待异步刷完"的 API,这是最稳妥的做法)。

4.3 示例三:多进程(fork 后各自独立实例,写同一文件)
这是最容易踩坑的地方,先把原理讲清:
- EasyLogger 是"进程内"库。它的缓冲区、异步后台线程、配置都在进程私有内存里。所以 fork 之后,子进程必须重新
elog_init() + elog_start(),否则子进程既没初始化,也没有那个后台刷出线程(fork 只复制调用线程,异步线程不会被复制)。
- 多个进程写同一个文件必须跨进程加锁。基础 port 里的
pthread_mutex 只保护"进程内"的并发,挡不住另一个进程的写入交错。正确做法:打开文件时用 O_APPEND(Linux 对 write() 到普通文件保证原子追加),再叠加 flock() 做跨进程互斥。
- fork 后子进程应重新
open() 文件,得到独立的"文件描述/锁",否则父子共享同一个 flock 锁对象,反而失去互斥效果。
下面给一份多进程安全的 port + 示例(不依赖文件插件,自己用 open+flock 落盘,最直观):
多进程 port(elog_port_mp.c):
/* elog_port_mp.c —— 多进程安全移植:共享文件 + flock */
#include <elog.h>
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <time.h>
#include <fcntl.h>
#include <sys/file.h>
static pthread_mutex_t s_lock = PTHREAD_MUTEX_INITIALIZER; /* 进程内 */
static int g_fd = -1; /* 各进程独立打开 */
ElogErrCode elog_port_init(void) {
/* 每个进程各自 open 一次,拿到独立的文件描述,flock 才能跨进程互斥 */
g_fd = open("/var/log/elog_mp.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
return ELOG_NO_ERR;
}
void elog_port_output(const char *log, size_t size) {
/* 终端也打一份,方便现场看 */
printf("%.*s", (int)size, log);
/* 文件:flock 跨进程加锁 + O_APPEND 原子追加,保证不交错、不丢字节 */
if (g_fd >= 0) {
flock(g_fd, LOCK_EX);
write(g_fd, log, size);
flock(g_fd, LOCK_UN);
}
}
void elog_port_output_lock(void) { pthread_mutex_lock(&s_lock); }
void elog_port_output_unlock(void) { pthread_mutex_unlock(&s_lock); }
const char *elog_port_get_time(void) {
static char buf[24] = {0};
time_t t = time(NULL); struct tm *tm = localtime(&t);
if (tm == NULL) return "";
snprintf(buf, sizeof(buf), "%02d-%02d %02d:%02d:%02d",
tm->tm_mon+1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec);
return buf;
}
const char *elog_port_get_p_info(void) {
static char buf[16] = {0};
snprintf(buf, sizeof(buf), "pid:%04d", (int)getpid());
return buf;
}
const char *elog_port_get_t_info(void) {
static char buf[16] = {0};
snprintf(buf, sizeof(buf), "tid:%04ld", (long)pthread_self());
return buf;
}
多进程示例代码(demo_mp.c):
/* demo_mp.c —— 父进程 fork 出 2 个子进程,三者并发写同一日志文件 */
/*
* 示例三:多进程(fork 后各自独立实例,并发写同一日志文件)
* 编译:make demo_mp 运行:./demo_mp ; 之后 cat /var/log/elog_mp.log
*
* ★★★ 多进程必读:fork 与异步模式的坑 ★★★
* 1) EasyLogger 是“进程内”库:缓冲区、异步后台线程、配置都在进程私有内存。
* fork 之后子进程必须重新 elog_init() + elog_start(),否则子进程既没有
* 初始化,也没有后台刷出线程(fork 只复制调用线程)。
* 2) 更隐蔽的坑:elog_async.c 里的 init_ok 是 static,父进程初始化后置为
* true,fork 把它复制到子进程;于是子进程 elog_init() 看到 init_ok==true
* 会直接返回,不会创建异步线程 → 子进程的日志塞进环形缓冲却永远没人排空,
* 最后全丢。所以本例在父、子进程里都显式 elog_async_enabled(false),
* 改成同步输出(写文件这种慢操作交给端口里的 flock,本就不靠后台线程)。
* 3) 写同一文件必须跨进程互斥:用 open(O_APPEND) + flock(),且每个进程各自
* open(见 elog_port_mp.c),不能继承父进程的 fd。
*/
#define LOG_TAG "mp"
#include <elog.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
setbuf(stdout, NULL);
/* 父进程先初始化(会 open 自己的 fd) */
elog_init();
elog_set_fmt(ELOG_LVL_INFO, ELOG_FMT_LVL | ELOG_FMT_TAG | ELOG_FMT_TIME | ELOG_FMT_P_INFO);
#ifdef ELOG_COLOR_ENABLE
elog_set_text_color_enabled(true);
#endif
elog_start();
/* 见上方说明:父进程也关闭异步,避免 fork 后子进程异步线程缺失导致丢日志 */
elog_async_enabled(false);
log_i("parent ready, going to fork");
for (int c = 0; c < 2; c++) {
pid_t pid = fork();
if (pid == 0) {
/* ===== 子进程:必须重新 init/start ===== */
elog_init(); /* 内部再次 open,获得独立 fd/锁 */
elog_set_fmt(ELOG_LVL_INFO, ELOG_FMT_LVL | ELOG_FMT_TAG | ELOG_FMT_TIME | ELOG_FMT_P_INFO);
#ifdef ELOG_COLOR_ENABLE
elog_set_text_color_enabled(true);
#endif
elog_start();
elog_async_enabled(false); /* 关键:子进程强制同步输出 */
for (int i = 0; i < 30; i++) {
log_i("child-%d iter-%d", c, i);
usleep(5000);
}
log_i("child-%d exit", c);
exit(0);
}
}
/* 父进程自己也打一些 */
for (int i = 0; i < 30; i++) {
log_i("parent iter-%d", i);
usleep(5000);
}
/* 收尸,确保子进程日志都刷完 */
int status;
while (wait(&status) > 0) { /* 等待所有子进程 */ }
log_i("all children reaped, parent exit");
return 0;
}
编译运行:
gcc demo_mp.c easylogger/src/elog.c easylogger/src/elog_utils.c \
easylogger/src/elog_async.c easylogger/src/elog_buf.c elog_port_mp.c \
-I./easylogger/inc -lpthread -o demo_mp
./demo_mp
cat /var/log/elog_mp.log # 父/子进程的日志按行完整、互不交错
多进程更简单的替代方案(推荐小项目用):每个进程写自己的文件,文件名带 PID(如 /var/log/myapp_%d.log)。完全不需要跨进程锁,排查时 grep 也清爽。上面的 flock 共享文件方案适合"希望所有进程日志汇聚到一个文件"的场景。
五、避坑与实践总结
- ❌ 不要在高频循环里用同步日志(原生 syslog、
printf 直接刷盘)。EasyLogger 开了异步后,业务线程只是内存拷贝,几乎不阻塞。
- ✅ 异步模式写满会丢日志,不会卡业务。高频场景调大
ELOG_ASYNC_OUTPUT_BUF_SIZE;关键日志设到 ELOG_ASYNC_OUTPUT_LVL 以下走同步保不丢。
- ✅ 多线程天然安全:只要
elog_port_output_lock/unlock 用互斥锁(Linux demo 已做)。
- ⚠️ 多进程必须各 init 各的,
fork 后子进程一定要重新 elog_init()+elog_start(),否则既没有初始化也没有后台刷出线程。
- ⚠️ 多进程写同一文件要跨进程锁:
pthread_mutex 管不了别的进程,用 open(O_APPEND)+flock,或干脆每进程一个文件。
- 静态 vs 动态级别:
ELOG_OUTPUT_LVL 是编译期裁剪(更狠、零运行时开销),elog_set_filter_lvl() 是运行期过滤(可随时改)。线上想"临时拉满某模块日志",用 elog_set_filter_tag_lvl("模块名", ELOG_LVL_DEBUG)。
- 彩色只在终端有效:输出到文件时颜色转义符会污染日志,落盘场景可在
elog_cfg.h 关掉 ELOG_COLOR_ENABLE,或仅对终端后端启用。
- 交叉编译记住三件事:工具链前缀替换 gcc、保留
-lpthread、头文件路径包含 easylogger/inc(以及插件目录)。
官方资源汇总(建议 Watch & Star)
- 项目主页 / 源码:
https://github.com/armink/EasyLogger
- 国内镜像(下载慢时用):
https://git.oschina.net/armink/EasyLogger
- 核心 API 文档:
https://github.com/armink/EasyLogger/blob/master/docs/zh/api/kernel.md
- 移植文档:
https://github.com/armink/EasyLogger/blob/master/docs/zh/port/kernel.md
- 文件插件源码:
https://github.com/armink/EasyLogger/tree/master/easylogger/plugins/file
- 本仓库 vendored 版本:本地
easylogger/EasyLogger-2.2.0/(API 以本地源码为准)
- 协议:MIT(Copyright armink.ztl@gmail.com)
总结
- 移植 EasyLogger = 改
elog_cfg.h + 写 elog_port.c,零第三方依赖。
- 生命周期:
elog_init() → elog_set_fmt(...) → elog_start(),少一步都不输出。
- 打日志用
elog_x(tag,...) 或 log_x(...)(配合文件顶部 LOG_TAG)。
- 异步是多线程/高频场景的必选项;环形缓冲写满丢日志不阻塞。
- 多线程安全靠 port 里的互斥锁;多进程各自
init 且写共享文件要 flock。
- 调级别:编译期
ELOG_OUTPUT_LVL,运行期 elog_set_filter_*,按模块用 elog_set_filter_tag_lvl。
更多嵌入式开发实战与资源共享,欢迎访问云栈社区与开发者一起交流。