找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

5369

积分

0

好友

692

主题
发表于 3 小时前 | 查看: 3| 回复: 0

本文基于本仓库 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 上值得用它?

  1. 异步非阻塞:业务线程只往内存环形缓冲写,后台 pthread 负责真正的输出。高频日志不再卡业务。
  2. 源码级集成,零第三方依赖:把几个 .c 加进工程编就行,交叉编译也只需要一个 -lpthread
  3. 一份代码多端复用:同时维护 RT-Thread 和 Arm Linux 应用时,日志接口完全一致,移植负担最低。
  4. 可裁剪:不需要的功能(异步、缓冲、颜色、文件、时间)关掉对应宏即可,体积可控。

⚠️ 适用边界: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_LVLELOG_FMT_TAGELOG_FMT_TIMEELOG_FMT_P_INFO(进程)、ELOG_FMT_T_INFO(线程)、ELOG_FMT_DIRELOG_FMT_FUNCELOG_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 章):

  1. easylogger/ 整个目录拷进工程;
  2. src/elog.csrc/elog_utils.cport/elog_port.c 加入编译(elog_async.c/elog_buf.c 视是否开启异步/缓冲决定);
  3. easylogger/inc/ 加入头文件搜索路径;
  4. 启用文件插件时,再把 plugins/file/elog_file.celog_file_port.c 加入编译,并把 plugins/ 加入搜索路径。

EasyLogger移植需修改文件目录

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.hELOG_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 以下走同步,确保不丢。
  • 进程退出前,若担心最后几条还在缓冲里没刷完,可让主线程在 returnsleep 一小会儿,给后台线程排空时间(2.2.0 没有提供"等待异步刷完"的 API,这是最稳妥的做法)。

多线程日志输出无串味

4.3 示例三:多进程(fork 后各自独立实例,写同一文件)

这是最容易踩坑的地方,先把原理讲清:

  1. EasyLogger 是"进程内"库。它的缓冲区、异步后台线程、配置都在进程私有内存里。所以 fork 之后,子进程必须重新 elog_init() + elog_start(),否则子进程既没初始化,也没有那个后台刷出线程(fork 只复制调用线程,异步线程不会被复制)。
  2. 多个进程写同一个文件必须跨进程加锁。基础 port 里的 pthread_mutex 只保护"进程内"的并发,挡不住另一个进程的写入交错。正确做法:打开文件时用 O_APPEND(Linux 对 write() 到普通文件保证原子追加),再叠加 flock() 做跨进程互斥。
  3. 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)

总结

  1. 移植 EasyLogger = 改 elog_cfg.h + 写 elog_port.c,零第三方依赖。
  2. 生命周期:elog_init()elog_set_fmt(...)elog_start(),少一步都不输出。
  3. 打日志用 elog_x(tag,...)log_x(...)(配合文件顶部 LOG_TAG)。
  4. 异步是多线程/高频场景的必选项;环形缓冲写满丢日志不阻塞。
  5. 多线程安全靠 port 里的互斥锁;多进程各自 init 且写共享文件要 flock
  6. 调级别:编译期 ELOG_OUTPUT_LVL,运行期 elog_set_filter_*,按模块用 elog_set_filter_tag_lvl

更多嵌入式开发实战与资源共享,欢迎访问云栈社区与开发者一起交流。




上一篇:垂类产品与用户自训大模型的核心差异:履约、合同与连接人的价值
下一篇:Nacos 双写问题与关闭实战:2.x 升级后如何避免数据不一致
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-7-30 05:19 , Processed in 0.749791 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表