系列目录:第一篇:全景图与架构概览 | 第二篇: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);}关键设计:这个函数不构建 header(
android_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 格式上报给 logdENOTCONN自动重连,应对 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、构建 iovec | logger_write.c |
__write_to_log_init() | 初始化守卫:加锁、双重检查、触发初始化、替换指针 | logger_write.c |
__write_to_log_daemon() | 快速路径:权限检查、时间戳、遍历 transport 写入 | logger_write.c |
logdWrite() | 主通道:构建 header + writev 发送到 logdw | logd_writer.c |
pmsgWrite() | 兜底:写入 /dev/pmsg0,panic 时通过 pstore 恢复 | pmsg_writer.c |
核心设计亮点:
- 函数指针替换:一次初始化,之后零开销
- 双写机制:logd(主通道)+ pmsg(兜底),系统崩溃日志不丢失
- header 延迟构建:入口函数只打包 iovec,header 由 logdWrite 构建,职责分离
- DGRAM socket:天然消息边界,无需拆包