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

📅 2026/7/4 4:01:26 👁️ 阅读次数 📝 编程学习
Android 7系统日志(三)liblog库—日志写入的完整链路

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


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

一、先看全局:一条日志的旅程

在深入源码之前,先建立全局视角。无论是 Java 层的Log.d()、Native 层的ALOGD(),还是Slog.d()EventLog.writeEvent(),最终都收敛到同一个函数:

Log.d() / ALOGD() / Slog.d() / EventLog.writeEvent() │ ▼ __android_log_buf_write(bufID, prio, tag, msg) ← 唯一入口 │ ▼ write_to_log(bufID, vec, 3) ← 函数指针 │ ├── 首次调用:初始化 → 打开 socket → 替换指针 │ └── 后续调用:直接分发 │ ├── logdWrite() → writev(/dev/socket/logdw) ← 主通道 └── pmsgWrite() → writev(/dev/pmsg0) ← 兜底通道

liblog 模块的源码结构:

system/core/liblog/ ├── logger_write.c // 入口 + 函数指针机制 + transport 管理 ├── logd_writer.c // logd 通道(socket 连接 + writev 发送) ├── pmsg_writer.c // pstore 兜底(/dev/pmsg0) ├── config_write.c // transport 注册 ├── logger_lock.c // 线程安全(单把 mutex) └── log_is_loggable.c // 运行时级别过滤

二、入口函数:__android_log_buf_write()

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

所有日志接口最终都调用这个函数。它只做四件事:tag 路由、FATAL 处理、构建 iovec、调用函数指针。

int__android_log_buf_write(intbufID,intprio,constchar*tag,constchar*msg){structiovecvec[3];chartmp_tag[32];if(!tag)tag="";// 步骤1:tag 自动路由到 radio 缓冲区// RIL/IMS/CDMA/PHONE/SMS 等无线相关 tag 自动重定向到 LOG_ID_RADIOif((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 级别日志写入 tombstone#if__BIONIC__if(prio==ANDROID_LOG_FATAL){android_set_abort_message(msg);}#endif// 步骤3:构建 iovec 数组:[prio, tag\0, msg\0]vec[0].iov_base=(unsignedchar*)&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:调用函数指针returnwrite_to_log(bufID,vec,3);}

关键设计:这个函数不构建 headerandroid_log_header_t由下游logdWrite()构建),只负责参数校验和 iovec 打包。iovec 格式为[prio, tag\0, msg\0],简洁高效。


三、函数指针机制:一次初始化,永久复用

liblog 使用函数指针替换实现懒初始化。核心只有三行代码:

// 初始指向初始化函数staticint(*write_to_log)(...)=__write_to_log_init;// 初始化完成后,在 __write_to_log_init 内部替换write_to_log=__write_to_log_daemon;

流程

第1次调用: write_to_log → __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 ← 替换指针 └── 尾递归 → 进入快速路径 第2次及以后: write_to_log → __write_to_log_daemon() ← 直接干活,零初始化开销
staticint__write_to_log_init(log_id_tlog_id,structiovec*vec,size_tnr){__android_log_lock();if(write_to_log==__write_to_log_init){// 双重检查intret=__write_to_log_initialize();// 打开 socket/pmsgif(ret<0){__android_log_unlock();returnret;}write_to_log=__write_to_log_daemon;// ★ 替换指针 ★}__android_log_unlock();returnwrite_to_log(log_id,vec,nr);// 尾递归 → 快速路径}

初始化完成后,每次写日志走__write_to_log_daemon()

staticint__write_to_log_daemon(log_id_tlog_id,structiovec*vec,size_tnr){// 步骤0:空消息检查// 步骤1:权限检查(SECURITY 日志校验 UID/GID,普通日志校验 isLoggable)// 步骤2:clock_gettime() 获取时间戳// 步骤3:遍历 transport 链表,逐个调用 node->write()// → logdWrite() → writev(/dev/socket/logdw)// → pmsgWrite() → writev(/dev/pmsg0)}

三个函数的角色__write_to_log_init是初始化守卫(只执行一次),__write_to_log_initialize做实际初始化(打开 socket),__write_to_log_daemon是快速路径(每次调用)。函数指针替换后,后续调用零额外开销。


四、logdWrite():通往 logd 的主通道

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

这是日志写入的核心函数,负责构建 header、拼接 iovec、发送到 logd。

4.1 打开连接:logdOpen()

staticintlogdOpen(){// 创建 SOCK_DGRAM socket(无连接,每条消息独立边界)inti=socket(PF_UNIX,SOCK_DGRAM|SOCK_CLOEXEC,0);fcntl(i,F_SETFL,O_NONBLOCK);// connect() 绑定到 /dev/socket/logdwstructsockaddr_unun;un.sun_family=AF_UNIX;strcpy(un.sun_path,"/dev/socket/logdw");connect(i,(structsockaddr*)&un,sizeof(un));logdLoggerWrite.context.sock=i;}

SOCK_DGRAM保证消息边界,connect()绑定远端地址后可以直接用writev()发送,无需每次指定目标。O_NONBLOCK避免阻塞。

4.2 发送日志:logdWrite()

staticintlogdWrite(log_id_tlogId,structtimespec*ts,structiovec*vec,size_tnr){structiovecnewVec[nr+1];// header + 原始 iovecandroid_log_header_theader;// 1. 构建 header:id + tid + realtimeheader.id=logId;header.tid=gettid();header.realtime.tv_sec=ts->tv_sec;header.realtime.tv_nsec=ts->tv_nsec;newVec[0].iov_base=&header;newVec[0].iov_len=sizeof(header);// 2. 将原始 iovec(prio + tag + msg)追加到 header 之后for(size_ti=0;i<nr;i++){newVec[i+1]=vec[i];}// 3. writev() 一次性发送:header + prio + tag + msgintret=writev(logdLoggerWrite.context.sock,newVec,nr+1);// 4. ENOTCONN:logd 重启 → 关闭旧 socket,重新打开,重试if(ret==-ENOTCONN){logdClose();logdOpen();ret=writev(logdLoggerWrite.context.sock,newVec,nr+1);}// 5. EAGAIN:socket 缓冲区满 → 丢失计数 +1if(ret==-EAGAIN){atomic_fetch_add(&dropped,1);}}

Socket 上的实际数据布局

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

关键点

  • __android_log_buf_write()不构建 header,header 在这里由logdWrite()构建
  • writev()一次系统调用发送所有数据,高效
  • EAGAIN时只计数不阻塞(设置了O_NONBLOCK),丢失计数在下一次写入时以 EVENT 格式上报给 logd
  • ENOTCONN自动重连,应对 logd 重启

五、pmsgWrite():内核 panic 时的兜底

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

pmsg 通道在正常运行时将日志写入/dev/pmsg0,内核暂存于 RAM。系统崩溃(kernel panic)时,pstore 机制将其刷入 ramoops 区域,重启后可从/sys/fs/pstore/读取。

staticintpmsgWrite(log_id_tlogId,structtimespec*ts,structiovec*vec,size_tnr){android_pmsg_log_header_tpmsgHeader;// 额外头:magic + len + uid + pidandroid_log_header_theader;// 标准头:id + tid + realtimepmsgHeader.magic=LOGGER_MAGIC;pmsgHeader.len=sizeof(pmsgHeader)+sizeof(header);pmsgHeader.uid=__android_log_uid();pmsgHeader.pid=getpid();header.id=logId;header.tid=gettid();header.realtime=*ts;// 组装 newVec = [pmsgHeader, header, prio, tag, msg]newVec[0]={&pmsgHeader,sizeof(pmsgHeader)};newVec[1]={&header,sizeof(header)};// ... 追加原始 iovec ...writev(pmsgLoggerWrite.context.fd,newVec,nr+2);}

双写机制的生存策略

正常情况: logdWrite() → writev(logdw) → logd 接收 → LogBuffer ✓ pmsgWrite() → writev(/dev/pmsg0) → 内核暂存 ✓ 系统 crash (内核 panic): logdWrite() → logd 进程已死 ✗ pmsgWrite() → pstore 刷入 ramoops → 重启后可读 ✓

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


六、Transport 注册与线程安全

6.1 Transport 链表

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

两个 transport 在编译时静态注册到全局链表:

// logd 通道structandroid_log_transport_writelogdLoggerWrite={.node={&logdLoggerWrite.node,&logdLoggerWrite.node},.name="logd",.available=logdAvailable,.open=logdOpen,.close=logdClose,.write=logdWrite,};// pmsg 通道structandroid_log_transport_writepmsgLoggerWrite={.node={&pmsgLoggerWrite.node,&pmsgLoggerWrite.node},.name="pmsg",.available=pmsgAvailable,.open=pmsgOpen,.close=pmsgClose,.write=pmsgWrite,};

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

6.2 线程安全

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

整个 liblog 只有一把锁log_init_lock,仅用于保护初始化过程。初始化完成后,__write_to_log_daemon()没有锁——因为 transport 链表在初始化后不再变更,各 transport 的write()内部自行处理并发(logd 端通过 epoll 和 LogBuffer 的mLogElementsLock来保证)。


七、完整调用链

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

八、本篇总结

函数职责位置
__android_log_buf_write()唯一入口:路由、FATAL、构建 ioveclogger_write.c
__write_to_log_init()初始化守卫:加锁、双重检查、触发初始化、替换指针logger_write.c
__write_to_log_daemon()快速路径:权限检查、时间戳、遍历 transport 写入logger_write.c
logdWrite()主通道:构建 header + writev 发送到 logdwlogd_writer.c
pmsgWrite()兜底:写入 /dev/pmsg0,panic 时通过 pstore 恢复pmsg_writer.c

核心设计亮点:

  • 函数指针替换:一次初始化,之后零开销
  • 双写机制:logd(主通道)+ pmsg(兜底),系统崩溃日志不丢失
  • header 延迟构建:入口函数只打包 iovec,header 由 logdWrite 构建,职责分离
  • DGRAM socket:天然消息边界,无需拆包