系列目录 :第一篇:全景图与架构概览 | 第二篇:logd守护进程---启动、初始化与Socket通信 | 第三篇:liblog库---日志写入的完整链路 | 第四篇:日志写入接口---Java层与Native层 | 第五篇:日志读取---logcat源码深度分析 | 第六篇:日志缓冲区管理---容量、裁剪与统计机制 | 第七篇:实战调试与常见问题分析
日志写进去了,然后呢?本篇回答三个问题:日志存在哪里、容量满了怎么办、chatty 是怎么来的。
一、存储结构:一条链表,五个容量
整个 logd 只有一个 LogBuffer 实例。内部用一条 std::list<LogBufferElement*> 存放所有日志条目,按全局序列号 mSequence 排序。不同 log_id(main/system/events/radio/crash)的条目交错存放在同一条链表中。
每个条目只保留最少的元数据:
源码路径 :system/core/logd/LogBufferElement.h
cpp
class LogBufferElement {
const log_id_t mLogId; // 属于哪个缓冲区
const uid_t mUid; // 写入进程 UID
const pid_t mPid; // 写入进程 PID
const pid_t mTid; // 写入线程 TID
char *mMsg; // 消息体(tag\0msg\0)
union {
const unsigned short mMsgLen; // mMsg != NULL 时:消息长度
unsigned short mDropped; // mMsg == NULL 时:被丢弃的条数
};
const uint64_t mSequence; // 全局递增序列号(原子操作,确定全局顺序)
log_time mRealTime; // 时间戳
};
最关键的设计 :
mMsgLen和mDropped是 union 。正常条目有消息体(mMsg != NULL),被裁剪后消息体释放(mMsg = NULL),union 变为mDropped记录被丢弃了多少条。这就是 chatty 消息的数据来源。
LogBuffer 为每个 log_id 维护独立的容量上限,默认 256KB:
源码路径 :system/core/logd/LogBuffer.h
cpp
class LogBuffer {
LogBufferElementCollection mLogElements; // 唯一链表,所有 log_id 混合
LogStatistics stats; // 统计模块(按 UID/PID/TID/TAG 多维度)
PruneList mPrune; // 白名单/黑名单
unsigned long mMaxSize[LOG_ID_MAX]; // 每个 log_id 独立的容量限制
LastLogTimes &mTimes; // 所有 logcat 客户端的读取位置
};
二、写入流程:按时间插入,触发裁剪
源码路径 :system/core/logd/LogBuffer.cpp
cpp
int LogBuffer::log(log_id_t log_id, log_time realtime,
uid_t uid, pid_t pid, pid_t tid,
const char *msg, unsigned short len) {
// 1. 创建条目(构造时自动分配全局递增的 sequence)
LogBufferElement *elem = new LogBufferElement(log_id, realtime,
uid, pid, tid, msg, len);
// 2. isLoggable 过滤(不通过则只统计不入链,返回 -EACCES)
if (log_id != LOG_ID_SECURITY) {
if (!__android_log_is_loggable(prio, tag, ANDROID_LOG_VERBOSE)) {
stats.add(elem); stats.subtract(elem); delete elem;
return -EACCES;
}
}
pthread_mutex_lock(&mLogElementsLock);
// 3. 按时间戳找到正确的插入位置(从尾部向前遍历,保持时间有序)
LogBufferElementCollection::iterator it = mLogElements.end();
while (it != mLogElements.begin()) {
--it;
if ((*it)->getRealTime() <= realtime) break;
}
mLogElements.insert(it, elem);
// 4. 更新统计,判断是否需要裁剪
stats.add(elem);
maybePrune(log_id);
pthread_mutex_unlock(&mLogElementsLock);
return len;
}
两个关键点 :插入不是简单的 push_back,而是按时间戳找到正确位置 保持链表有序;chatty 检测不在 log() 中进行------它发生在裁剪阶段。
三、裁剪机制:什么时候裁、怎么裁
这是整个 LogBuffer 中最核心的部分。当某个 log_id 的已用字节数超过容量上限时,maybePrune() 计算需要裁剪多少行,然后调用 prune() 执行三段式裁剪。
3.1 触发条件与裁剪量计算
cpp
void LogBuffer::maybePrune(log_id_t id) {
size_t sizes = stats.sizes(id); // 当前该 log_id 的已用字节数
unsigned long maxSize = mMaxSize[id]; // 该 log_id 的容量上限
if (sizes > maxSize) {
// 降到 90% 容量:超出量按比例换算为行数
size_t sizeOver = sizes - ((maxSize * 9) / 10);
size_t elements = stats.realElements(id);
unsigned long pruneRows = elements * sizeOver / sizes;
// 限制每次裁剪数量:最少 4 条,最多 256 条
if (pruneRows < 4) pruneRows = 4;
if (pruneRows > 256) pruneRows = 256;
prune(id, pruneRows);
}
}
不是一次性清空,而是每次裁 4~256 条,避免锁持有时间过长。目标是降到 90% 容量。
3.2 三段式裁剪:prune()
裁剪从链表头部(最旧)向尾部遍历,按优先级分三个阶段:
阶段一:黑名单 + 最坏 UID 优先裁剪(含 chatty 合并)
│
├── 1. stats.sort() 找出占用最大的 UID
├── 2. 判断是否为"最坏 UID":占用 > 12.5% 容量,且 > dropped 量的 10 倍
├── 3. 如果是 AID_SYSTEM,进一步找出最坏 PID
└── 4. 遍历链表:
├── 前导 dropped 条目 → 直接删除
├── 相邻 dropped 条目 → coalesce 合并(chatty 机制)
├── 黑名单条目 → 直接删除
└── 最坏 UID/PID 的条目 → setDropped(1)(释放消息体,保留占位符)
阶段二:从最旧条目过期 + 白名单保护
│
└── 遍历链表:
├── 被 reader 锁定的条目 → 停止(或触发 reader 跳过/释放)
├── 白名单条目(nice)→ 跳过
└── 其他条目 → 直接删除
阶段三:白名单回退(阶段二因白名单保护不够裁时触发)
│
└── 重新遍历,这次连白名单条目也删除
裁剪优先级 :黑名单 > 最坏 UID/PID > 普通旧条目 > 白名单。最坏 UID 的条目不直接删除,而是通过
setDropped(1)变成占位符------这就是 chatty 消息的来源。
3.3 读者保护:被 logcat 正在读的条目不删
源码路径 :system/core/logd/LogTimes.h
每个 logcat 客户端在 LastLogTimes 中注册一个 LogTimeEntry,记录其当前读取到的 mStart(sequence 号)。prune() 在裁剪前会查找所有 reader 的最小 mStart,遇到该位置就停止裁剪。
对于慢速 reader(读得太慢导致日志积压),logd 有三种处理策略:
cpp
class LogTimeEntry {
uint64_t mStart; // 当前读取到的 sequence 号
const bool mNonBlock; // 是否为非阻塞模式(-d 参数)
unsigned int skipAhead[LOG_ID_MAX]; // 将要跳过的条目数
// 策略1:唤醒 reader 赶快读(有超时设置时)
void triggerReader_Locked() {
pthread_cond_signal(&threadTriggeredCondition);
}
// 策略2:让 reader 跳过 N 条(无超时但积压不严重)
void triggerSkip_Locked(log_id_t id, unsigned int skip) { skipAhead[id] = skip; }
// 策略3:释放 reader(积压 > 2 倍容量)
void release_Locked() {
mRelease = true;
pthread_cond_signal(&threadTriggeredCondition);
}
};
logd 根据积压程度选择策略:
积压 > 2 倍容量 → release_Locked() → 踢掉 reader
有超时设置 → triggerReader_Locked() → 唤醒 reader
否则 → triggerSkip_Locked() → 让 reader 跳过 N 条
四、chatty 的完整链路
chatty 不是"插入时检测重复",而是裁剪时合并连续被丢弃的条目。完整链路:
裁剪时:
最坏 UID 的条目 → setDropped(1) → mMsg 释放,mDropped=1
连续多条同 UID 条目 → coalesce 合并 → mDropped 累加,多余条目删除
logcat 读取时:
flushTo() 检测到 mMsg == NULL
→ populateDroppedMessage() 动态生成消息
→ 格式:"uid=10123(com.app) 0x3039 expire 47 lines"
→ 级别 INFO,tag 固定为 "chatty"
100 条重复日志在链表中只占 2 个节点(1 个 dropped 占位符 + 1 条下一条不同消息)。chatty 机制让日志系统在高频重复写入时不会 OOM。
五、统计系统:logcat -S 的背后
源码路径 :system/core/logd/LogStatistics.cpp
cpp
class LogStatistics {
size_t mSizes[LOG_ID_MAX]; // 每个 log_id 当前字节数
size_t mElements[LOG_ID_MAX]; // 每个 log_id 当前条目数
size_t mDroppedElements[LOG_ID_MAX]; // 每个 log_id 的 dropped 条目数
uidTable_t uidTable[LOG_ID_MAX]; // 按 UID 统计
pidTable_t pidSystemTable[LOG_ID_MAX]; // 按 PID 统计(仅 AID_SYSTEM)
};
三条更新路径对应三种操作:
| 操作 | 方法 | 效果 |
|---|---|---|
| 插入条目 | stats.add(elem) |
sizes ↑、elements ↑、uid/pet 统计 ↑ |
| 删除条目 | stats.subtract(elem) |
sizes ↓、elements ↓ |
| 条目被 dropped | stats.drop(elem) |
sizes ↓、droppedElements ↑、elements 不变 |
logcat -S 调用 LogStatistics::format() 生成报告,按 UID→PID→TID→TAG 层级输出。统计功能由 ro.logd.statistics 属性控制开启。
六、配置速查
| 属性 | 作用 | 默认值 |
|---|---|---|
persist.logd.size |
全局缓冲区大小 | 256K |
persist.logd.size.main |
main 缓冲区大小 | 256K |
persist.logd.size.system |
system 缓冲区大小 | 256K |
ro.logd.size |
编译时默认值(persist 为空时使用) | 256K |
ro.config.low_ram |
低内存设备:所有缓冲区降为 64K | false |
persist.logd.filter |
裁剪过滤规则 | ~! ~1000/! |
ro.logd.statistics |
启用 logcat -S 统计 | svelte+ |
运行时也可通过 logcat -G <size> 即时调整,无需重启 logd。
七、本篇总结
| 问题 | 答案 |
|---|---|
| 日志存在哪里? | 一条 std::list,所有 log_id 混合,按 sequence 排序 |
| 每个缓冲区多大? | 默认 256KB,每个 log_id 独立配置 |
| 插入时怎么做? | 按时间戳找位置,保持时间有序 |
| 满了怎么办? | maybePrune() 触发三段式裁剪,每次 4~256 条 |
| 先裁谁? | 黑名单 → 最坏 UID(>12.5%容量)→ 普通旧条目 → 白名单 |
| chatty 怎么来的? | 裁剪时最坏 UID 条目被 setDropped(),logcat 读取时动态生成 |
| 正在读的条目会被删吗? | 不会,LogTimeEntry range lock 保护 |
| 怎么定位日志大户? | logcat -S,背后是 LogStatistics 的多维度统计 |
下一篇将聚焦实战调试,用这些原理解决日志丢失、chatty 泛滥、开机日志缺失等常见问题。