Android 7系统日志(三)liblog库—日志写入的完整链路

系列目录第一篇:全景图与架构概览 | 第二篇:logd守护进程---启动、初始化与Socket通信 | 第三篇:liblog库---日志写入的完整链路 | 第四篇:日志写入接口---Java层与Native层 | 第五篇:日志读取---logcat源码深度分析 | 第六篇:日志缓冲区管理---容量、裁剪与统计机制 | 第七篇:实战调试与常见问题分析


本篇是全系列最核心的一篇,逐函数拆解从 __android_log_buf_write() 到 Socket writev() 的完整调用链。

一、liblog 模块全景

源码路径system/core/liblog/

复制代码
system/core/liblog/
├── logger_write.c       // 对外接口:__android_log_buf_write() + 传输器管理
├── logger_read.c        // 读取接口
├── logd_writer.c        // logd 通道写实现(logdWrite → writev → /dev/socket/logdw)
├── pmsg_writer.c        // pstore 内核兜底(/dev/pmsg0 → ramoops)
├── logd_reader.c        // logd 通道读实现
├── config_write.c       // transport 注册与配置
├── event_tag_map.c      // events 日志 tag 编号映射
├── logger_lock.c        // 线程安全锁
├── log_is_loggable.c    // isLoggable 属性检查
├── log_event_write.c    // 二进制 events 日志写入
└── logprint.c           // 日志格式化输出

二、入口函数:__android_log_buf_write()

源码路径system/core/liblog/logger_write.c

c 复制代码
int __android_log_buf_write(int bufID, int prio,
                            const char *tag, const char *msg)
{
    struct iovec vec[3];
    char tmp_tag[32];

    if (!tag) tag = "";

    // 步骤1:tag 自动路由到 radio 缓冲区
    if ((bufID != LOG_ID_RADIO) &&
         (!strcmp(tag, "HTC_RIL") ||
        !strncmp(tag, "RIL", 3) ||
        !strncmp(tag, "IMS", 3) ||
        !strcmp(tag, "AT") ||
        !strcmp(tag, "GSM") ||
        !strcmp(tag, "STK") ||
        !strcmp(tag, "CDMA") ||
        !strcmp(tag, "PHONE") ||
        !strcmp(tag, "SMS"))) {
            bufID = LOG_ID_RADIO;
            snprintf(tmp_tag, sizeof(tmp_tag), "use-Rlog/RLOG-%s", tag);
            tag = tmp_tag;
    }

    // 步骤2:FATAL 级别日志设置 abort message(写入 tombstone)
#if __BIONIC__
    if (prio == ANDROID_LOG_FATAL) {
        android_set_abort_message(msg);
    }
#endif

    // 步骤3:构建 iovec 数组
    // vec[0] = prio 字节(1 字节)
    // vec[1] = tag\0(以 nul 结尾)
    // vec[2] = msg\0(以 nul 结尾)
    vec[0].iov_base = (unsigned char *)&prio;
    vec[0].iov_len  = 1;
    vec[1].iov_base = (void *)tag;
    vec[1].iov_len  = strlen(tag) + 1;
    vec[2].iov_base = (void *)msg;
    vec[2].iov_len  = strlen(msg) + 1;

    // 步骤4:调用 write_to_log 函数指针
    return write_to_log(bufID, vec, 3);
}

关键设计

  • __android_log_buf_write() 只负责参数校验和 iovec 构建,不构建 android_log_header_t (header 由 logdWrite() 构建)
  • iovec 格式为 [prio, tag\0, msg\0],简洁高效
  • radio 自动路由在构造 iovec 之前完成,确保重定向后的 tag 进入正确的 iovec

三、函数指针机制:初始化与分发

liblog 使用函数指针替换模式实现懒初始化:首次调用时触发初始化,之后直接走快速路径。

源码路径system/core/liblog/logger_write.c

c 复制代码
// 函数指针:初始指向 __write_to_log_init,初始化后替换为 __write_to_log_daemon
static int (*write_to_log)(log_id_t, struct iovec *vec, size_t nr) = __write_to_log_init;

static int __write_to_log_init(log_id_t log_id, struct iovec *vec, size_t nr)
{
    __android_log_lock();

    if (write_to_log == __write_to_log_init) {
        int ret = __write_to_log_initialize();
        if (ret < 0) {
            __android_log_unlock();
            return ret;
        }
        write_to_log = __write_to_log_daemon;  // 替换函数指针
    }

    __android_log_unlock();
    return write_to_log(log_id, vec, nr);  // 尾递归调用新函数
}

__write_to_log_initialize() 遍历 __android_log_transport_write 链表,调用每个传输器的 open()

  • logd_writerlogdOpen() → 创建 SOCK_DGRAM socket,connect 到 /dev/socket/logdw
  • pmsg_writerpmsgOpen() → 打开 /dev/pmsg0

__write_to_log_daemon() 是初始化后的快速路径,流程如下:

c 复制代码
static int __write_to_log_daemon(log_id_t log_id, struct iovec *vec, size_t nr)
{
    // 1. 权限检查:LOG_ID_SECURITY 需要 log UID,LOG_ID_EVENTS 需要 tag 可记录
    // 2. isLoggable 过滤:检查 log.tag.<Tagname> 属性
    // 3. clock_gettime() 获取时间戳
    // 4. 遍历 __android_log_transport_write 链表,调用每个 transport->write()
    // 5. 遍历 __android_log_persist_write 链表,调用每个 transport->write()
    return ret;
}

四、logd_writer --- 通往 logd 的核心通道

4.1 logdOpen() --- 打开 Socket 连接

源码路径system/core/liblog/logd_writer.c

c 复制代码
static int logdOpen()
{
    // 创建 Unix domain datagram socket
    int i = socket(PF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0);

    // 设置为非阻塞模式
    fcntl(i, F_SETFL, O_NONBLOCK);

    // connect() 绑定到远端地址 /dev/socket/logdw
    struct sockaddr_un un;
    memset(&un, 0, sizeof(un));
    un.sun_family = AF_UNIX;
    strcpy(un.sun_path, "/dev/socket/logdw");
    connect(i, (struct sockaddr *)&un, sizeof(un));

    return i;
}

注意 :虽然是 SOCK_DGRAM 无连接协议,但 connect() 在这里的作用是绑定远端地址,之后可以直接用 writev() 发送,无需每次指定目标地址。

4.2 logdWrite() --- 发送日志

源码路径system/core/liblog/logd_writer.c

c 复制代码
static int logdWrite(log_id_t logId, struct timespec *ts,
                     struct iovec *vec, size_t nr)
{
    struct iovec newVec[nr + 1];  // 多一个 slot 给 header
    android_log_header_t header;

    // 过滤 logd 自身的日志
    if (__android_log_uid() == AID_LOGD) {
        return 0;
    }

    // 构建 header:tid + 时间戳
    header.tid = gettid();
    header.realtime.tv_sec = ts->tv_sec;
    header.realtime.tv_nsec = ts->tv_nsec;

    // 将 header 放在 newVec[0],调用方的 vec 紧随其后
    newVec[0].iov_base = (unsigned char *)&header;
    newVec[0].iov_len  = sizeof(header);

    // 处理之前丢失的日志计数(向 LOG_ID_SECURITY/LOG_ID_EVENTS 发送 dropped 事件)
    // ...

    // 设置正确的 log_id
    header.id = logId;

    // 将调用方的 iovec 追加到 newVec,同时截断超长日志
    for (size_t i = 1; i < nr + 1; i++) {
        newVec[i].iov_base = vec[i - 1].iov_base;
        newVec[i].iov_len = vec[i - 1].iov_len;
        // 累计 payload 大小,超过 LOGGER_ENTRY_MAX_PAYLOAD (4068) 则截断
    }

    // writev() 发送:header + prio + tag + msg
    ret = writev(sock, newVec, i);

    // ENOTCONN 错误处理:如果 logd 重启,关闭旧 socket 重新打开
    if (ret == -ENOTCONN) {
        logdClose();
        logdOpen();
        ret = writev(sock, newVec, i);
    }

    // EAGAIN 错误:logd 过载,记录丢失计数
    if (ret == -EAGAIN) {
        atomic_fetch_add(&dropped, 1);
    }

    return ret;
}

Socket 上的实际数据布局

复制代码
┌──────────────────────────┐
│ android_log_header_t     │  ← newVec[0]: 11 字节 (packed)
│   id:      1 字节        │
│   tid:     2 字节        │
│   realtime: 8 字节       │
├──────────────────────────┤
│ prio (1 byte)            │  ← vec[0]: 日志级别
├──────────────────────────┤
│ tag\0                    │  ← vec[1]: 标签字符串
├──────────────────────────┤
│ msg\0                    │  ← vec[2]: 消息字符串
└──────────────────────────┘

五、pmsg_writer --- 内核 panic 的兜底通道

源码路径system/core/liblog/pmsg_writer.c

c 复制代码
static int pmsgOpen()
{
    // 打开 /dev/pmsg0(persistent message)
    // 写入的数据在内核 panic 重启后可通过 pstore 读取
    int fd = open("/dev/pmsg0", O_WRONLY | O_CLOEXEC);
    return fd;
}

static int pmsgWrite(log_id_t logId, struct timespec *ts,
                     struct iovec *vec, size_t nr)
{
    // 向 /dev/pmsg0 写入日志
    // 正常运行时:数据暂存于内核 RAM
    // 内核 panic 时:数据被刷入 pstore(ramoops 区域)
    // 重启后可通过 /sys/fs/pstore/ 读取
    for (size_t i = 0; i < nr; i++) {
        write(fd, vec[i].iov_base, vec[i].iov_len);
    }
}

双写机制的生存策略

复制代码
正常情况:
    logdWrite() → writev(logdw) → logd → LogBuffer ✓
    pmsgWrite() → write(/dev/pmsg0) → 内核暂存 ✓

系统 crash (内核 panic):
    logdWrite() → writev(logdw) → logd 进程已死 ✗
    pmsgWrite() → write(/dev/pmsg0) → 刷入 pstore → 重启后可读 ✓

这就是为什么即便系统崩溃,通过 pstore 仍能恢复部分关键日志。


六、完整调用链总结

复制代码
__android_log_buf_write(bufID, prio, tag, msg)
    │
    ├── tag 自动路由(RIL/IMS/CDMA/PHONE 等 → LOG_ID_RADIO)
    ├── FATAL 级别设置 abort message(android_set_abort_message)
    ├── 构建 iovec[3] = [prio, tag\0, msg\0]
    │
    └── write_to_log(bufID, vec, 3)  ─── 函数指针
            │
            ├── 首次调用:__write_to_log_init()
            │   ├── __write_to_log_initialize()
            │   │   ├── logdOpen() → socket() + connect("/dev/socket/logdw")
            │   │   └── pmsgOpen() → open("/dev/pmsg0")
            │   └── write_to_log = __write_to_log_daemon  // 替换指针
            │
            └── 后续调用:__write_to_log_daemon()
                ├── 权限检查(SECURITY) / isLoggable 过滤
                ├── clock_gettime() 获取时间戳
                └── 遍历 transport 链表:
                    ├── logdWrite(logId, &ts, vec, nr)
                    │   ├── 构建 android_log_header_t {id, tid, realtime}
                    │   ├── 组装 newVec = [header, prio, tag, msg]
                    │   └── writev(sock, newVec) → /dev/socket/logdw
                    │                       │
                    │                       ▼
                    │             ┌──────────────────┐
                    │             │  logd 守护进程    │
                    │             │  LogListener     │
                    │             │  recvmsg()       │
                    │             │  +SCM_CREDENTIALS│
                    │             │  → LogBuffer.log()│
                    │             └──────────────────┘
                    │
                    └── pmsgWrite(logId, &ts, vec, nr)
                        └── write(/dev/pmsg0)  → 内核暂存 → panic 时刷入 pstore

七、transport 传输器注册机制

源码路径system/core/liblog/config_write.c

c 复制代码
// 两个传输器在编译时静态注册到链表
LIBLOG_HIDDEN struct android_log_transport_write logdLoggerWrite = {
    .node = { &logdLoggerWrite.node, &logdLoggerWrite.node },
    .name = "logd",
    .available = logdAvailable,
    .open = logdOpen,
    .close = logdClose,
    .write = logdWrite,
};

LIBLOG_HIDDEN struct android_log_transport_write pmsgLoggerWrite = {
    .node = { &pmsgLoggerWrite.node, &pmsgLoggerWrite.node },
    .name = "pmsg",
    .available = pmsgAvailable,
    .open = pmsgOpen,
    .close = pmsgClose,
    .write = pmsgWrite,
};

每个 transport 提供统一接口 {available, open, close, write}write_to_log 遍历链表时通过 node->logMask 按位判断该 transport 是否处理当前 log_id


八、线程安全机制

源码路径system/core/liblog/logger_lock.c

c 复制代码
static pthread_mutex_t __android_log_lock = PTHREAD_MUTEX_INITIALIZER;

int __android_log_lock() {
    return pthread_mutex_lock(&__android_log_lock);
}
int __android_log_unlock() {
    return pthread_mutex_unlock(&__android_log_lock);
}

这个锁保护的是 liblog 端 的状态(函数指针替换、socket fd 的重连),不保护 logd 端的并发访问。logd 端用 LogBuffer::lock() 独立保护。


九、本篇总结

函数 层级 职责
__android_log_buf_write() 入口 参数校验、radio 自动路由、FATAL abort message、构建 iovec
__write_to_log_init() 初始化 首次调用时初始化所有 transport,替换函数指针
__write_to_log_initialize() 初始化 遍历 transport 链表,调用 open() 打开 socket/文件
__write_to_log_daemon() 分发 权限检查、isLoggable 过滤、获取时间戳、遍历 transport 写入
logdWrite() 写端 构建 header、组装 newVec、writev 发送到 logdw
pmsgWrite() 兜底 写入 /dev/pmsg0,panic 时通过 pstore 恢复
logdOpen() 连接 创建 SOCK_DGRAM socket,connect 到 /dev/socket/logdw

核心设计亮点:

  • 函数指针替换实现零开销的懒初始化(首调之后无额外判断)
  • transport 链表实现多通道同时写入(正常 + 兜底 + persist)
  • DGRAM socket 天然消息边界,无需拆包
  • 双写机制(logd + pmsg)确保系统崩溃日志不丢失
  • header 由 logdWrite 构建 而非入口函数,保持 __android_log_buf_write 的简洁

下一篇将聚焦 Java 层和 Native 层的各类写入接口 (Log/Slog/EventLog 和 ALOGD 等宏),看它们如何最终收敛到 __android_log_buf_write()