系列目录 :第一篇:全景图与架构概览 | 第二篇: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_writer→logdOpen()→ 创建 SOCK_DGRAM socket,connect 到/dev/socket/logdwpmsg_writer→pmsgOpen()→ 打开/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()。