系列目录第一篇全景图与架构概览| 第二篇logd守护进程—启动、初始化与Socket通信 | 第三篇liblog库—日志写入的完整链路 | 第四篇日志写入接口—Java层与Native层 | 第五篇日志读取—logcat源码深度分析 | 第六篇日志缓冲区管理—容量、裁剪与统计机制 | 第七篇实战调试与常见问题分析本篇深入 LogBuffer 的内部实现日志条目数据结构、按时间排序的插入逻辑、三段式裁剪算法、chatty 合并机制以及 LogStatistics 统计。一、LogBuffer 源码概览system/core/logd/LogBuffer.cpp ← 环形缓冲区核心实现997 行 system/core/logd/LogBuffer.h ← 头文件数据结构 接口声明 system/core/logd/LogBufferElement.cpp ← 单个日志条目实现flushTo 等 system/core/logd/LogBufferElement.h ← 条目标头文件 system/core/logd/LogStatistics.cpp ← 统计模块多维度 UID/PID/TID/TAG system/core/logd/LogStatistics.h ← 统计模块头文件 system/core/logd/LogWhiteBlackList.cpp ← 白名单/黑名单PruneList system/core/logd/LogWhiteBlackList.h system/core/logd/LogTimes.cpp ← 客户端读取状态跟踪关键整个 logd 中只有一个LogBuffer实例内部用一条std::listLogBufferElement*存放所有 log_id 的日志条目按全局 sequence 编号排序。每个 log_id 有独立的容量限制mMaxSize[LOG_ID_MAX]。二、LogBufferElement — 日志条目的数据结构源码路径system/core/logd/LogBufferElement.hclassLogBufferElement{constlog_id_t mLogId;// 所属缓冲区 IDconstuid_t mUid;// 写入进程的 UIDconstpid_t mPid;// 写入进程的 PIDconstpid_t mTid;// 写入线程的 TIDchar*mMsg;// 消息体指针tag\0msg\0union{constunsignedshortmMsgLen;// mMsg ! NULL 时消息长度unsignedshortmDropped;// mMsg NULL 时被丢弃的条数};constuint64_tmSequence;// 全局递增序列号原子操作log_time mRealTime;// 时间戳monotonic 或 realtimestaticatomic_int_fast64_t sequence;// 全局序列号计数器};关键设计mMsgLen和mDropped是union当条目有实际消息时mMsg ! NULL使用mMsgLen当条目被裁剪后mMsg NULL使用mDropped记录被丢弃了多少条mSequence是全局原子递增的序列号用于确定日志的全局顺序没有引用计数条目被裁剪后读取中的客户端通过LogTimeEntry的 range lock 机制保护而不是 rc三、LogBuffer 类结构源码路径system/core/logd/LogBuffer.htypedefstd::listLogBufferElement*LogBufferElementCollection;classLogBuffer{LogBufferElementCollection mLogElements;// ★ 唯一链表混合存放所有 log_id 的条目pthread_mutex_t mLogElementsLock;// 保护链表的锁LogStatistics stats;// 统计模块内部维护 UID/PID/TID 多级统计PruneList mPrune;// 白名单/黑名单管理// 裁剪水位线优化避免每次从头遍历LogBufferElementCollection::iterator mLast[LOG_ID_MAX];// 上次裁剪到的位置boolmLastSet[LOG_ID_MAX];// 位置是否有效LogBufferIteratorMap mLastWorstUid[LOG_ID_MAX];// 按 UID 的水位线LogBufferPidIteratorMap mLastWorstPidOfSystem[LOG_ID_MAX];// 按 PID 的水位线unsignedlongmMaxSize[LOG_ID_MAX];// ★ 每个 log_id 独立的容量限制默认 256KBboolmonotonic;// 时间戳类型LastLogTimesmTimes;// 所有 logcat 客户端的读取位置// 核心方法intlog(...);// 写入一条日志voidmaybePrune(log_id_t id);// 判断是否需要裁剪boolprune(log_id_t id,unsignedlongpruneRows,uid_t uidAID_ROOT);// 三段式裁剪boolclear(log_id_t id,uid_t uidAID_ROOT);// 清空缓冲区};四、环形链表结构所有 log_id 的日志条目存放在同一个std::list双向链表中按mSequence全局序列号排序mLogElements (std::list 哨兵) │ ▼ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ seq100 │◄──┤ seq101 │◄──┤ seq102 │◄──┤ seq103 │ │ main │──►│ system │──►│ events │──►│ main │ │ oldest │ │ │ │ │ │ newest │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ ▲ │ └──────────────────────────────────────────────┘不同 log_id 的条目交错存放按全局 sequence 排序。裁剪时按 log_id 过滤只删除指定 log_id 的条目。五、log() — 写入流程源码路径system/core/logd/LogBuffer.cpp第 202–285 行intLogBuffer::log(log_id_t log_id,log_time realtime,uid_t uid,pid_t pid,pid_t tid,constchar*msg,unsignedshortlen){// 步骤1创建 LogBufferElement构造时自动分配全局 sequenceLogBufferElement*elemnewLogBufferElement(log_id,realtime,uid,pid,tid,msg,len);// 步骤2isLoggable 过滤SECURITY 缓冲区除外if(log_id!LOG_ID_SECURITY){// 提取 prio/tag 调用 __android_log_is_loggable()// 不可记录的日志仅统计不插入链表if(!__android_log_is_loggable(prio,tag,ANDROID_LOG_VERBOSE)){stats.add(elem);stats.subtract(elem);deleteelem;return-EACCES;}}pthread_mutex_lock(mLogElementsLock);// 步骤3按时间戳找到正确的插入位置保持时间有序// 从链表尾部向前遍历找到第一个 realtime 新条目的位置LogBufferElementCollection::iterator itmLogElements.end();LogBufferElementCollection::iterator lastit;while(last!mLogElements.begin()){--it;if((*it)-getRealTime()realtime)break;lastit;}// 步骤4检查 reader range lock决定插入位置// 如果最后一个 reader 正在读取该位置附近则追加到末尾if(end_always||(end_set(end(*last)-getSequence()))){mLogElements.push_back(elem);}else{mLogElements.insert(last,elem);}// 步骤5更新统计 触发裁剪stats.add(elem);maybePrune(log_id);// 判断是否超出容量超出则调用 prune()pthread_mutex_unlock(mLogElementsLock);returnlen;}与原文的关键差异插入不是简单的 push_back而是按时间戳找到正确位置保持链表时间有序chatty 检测不在 log() 中进行而是在 prune() 裁剪时处理裁剪不是直接调用 prune()而是先调用maybePrune()判断是否需要六、maybePrune() — 裁剪触发判断voidLogBuffer::maybePrune(log_id_t id){size_t sizesstats.sizes(id);// 当前该 log_id 的已用字节数unsignedlongmaxSizelog_buffer_size(id);// 该 log_id 的容量上限if(sizesmaxSize){// 计算需要裁剪多少行超出量 / 10% 阈值size_t sizeOversizes-((maxSize*9)/10);size_t elementsstats.realElements(id);size_t minElementselements/100;if(minElementsminPrune)minElementsminPrune;// 至少 4 条unsignedlongpruneRowselements*sizeOver/sizes;if(pruneRowsminElements)pruneRowsminElements;if(pruneRowsmaxPrune)pruneRowsmaxPrune;// 最多 256 条prune(id,pruneRows);// 进入三段式裁剪}}裁剪不是一次性清空而是每次最多删除 256 条至少 4 条避免一次性锁持有时间过长。七、prune() — 三段式裁剪算法这是整个 LogBuffer 中最复杂的函数约 350 行按三个阶段依次裁剪阶段一黑名单 最坏 UID/PID 优先裁剪含 chatty 合并1. 通过 stats.sort() 计算当前占用最大的 UID 2. 判断是否为最坏 UID占用超过缓冲区 12.5% 且超过丢弃量的 10 倍 3. 如果是 AID_SYSTEM进一步找到最坏 PID 4. 从 mLastWorstUid/mLastWorstPidOfSystem 水位线开始遍历 - 跳过前导 dropped 条目删除 - 合并相邻的 dropped 条目chatty 合并 - 黑名单条目直接删除 - 最坏 UID/PID 的条目设置为 dropped不删除保留占位符chatty 合并在此阶段发生不是插入时检测重复而是裁剪时发现连续的 dropped 条目后通过LogBufferElementLast::coalesce()合并计数。阶段二从最旧条目开始过期 白名单保护1. 从 mLast[id] 水位线开始遍历 2. 遇到 reader range lock 保护的条目 → 停止或触发 reader 跳过/释放 3. 白名单条目mPrune.nice()→ 跳过 4. 其他条目 → 直接删除阶段三白名单回退仅在阶段二因白名单保护而提前结束时触发如果阶段二中白名单保护导致 pruneRows 未用完 从 mLast[id] 重新开始遍历这次连白名单条目也删除裁剪算法总结优先级条件处理方式1黑名单naughtyUID/PID直接删除2最坏 UID占用 12.5% 容量设置为 dropped保留占位符3系统的最坏 PID设置为 dropped4普通旧条目直接删除5白名单niceUID/PID最后才删除—被 reader 锁定的条目跳过必要时触发 reader 丢弃或释放关键设计最坏 UID 的条目不是直接删除而是通过setDropped(1)将mMsg释放、mMsgLen改为mDroppedunion 机制。条目本身保留在链表中作为占位符当 logcat 读取时调用populateDroppedMessage()动态生成 “chatty” 消息。八、chatty 机制 — 裁剪时合并chatty 不是插入时检测重复日志而是在裁剪时合并连续的被丢弃条目。核心数据结构classLogBufferElementLast{// 按 (uid, pid, tid) 三元组为 key记录最近看到的 LogBufferElementstd::unordered_mapuint64_t,LogBufferElement*map;boolcoalesce(LogBufferElement*element,unsignedshortdropped){// 查找同一 (uid, pid, tid) 的前一个条目// 如果存在且 dropped 计数可以合并 → 合并计数返回 true// 否则返回 false}};chatty 消息生成当 logcat 读取到一个mMsg NULL的条目时调用populateDroppedMessage()动态生成消息// 动态生成格式// chatty: uid12345(com.app) 0x1234 expire 5 linessize_tLogBufferElement::populateDroppedMessage(char*buffer,LogBuffer*parent){staticconstchartag[]chatty;staticconstcharformat_uid[]uid%u%s%s expire %u line%s;// 格式ANDROID_LOG_INFO chatty \0 消息体}实际效果日志写入 D/MyTag(12345): hello ← seq100 D/MyTag(12345): hello ← seq101 ... (100 条相同日志) D/MyTag(12345): world ← seq200 裁剪后假设 MyTag 的 UID 成为最坏 UID 条目 seq100 被 dropped → mMsgNULL, mDropped1 条目 seq101 被 coalesce 合并 → 删除mDropped 加到 seq100 ... 最终链表seq100 (mDropped100) → seq200 (mMsgworld) logcat 读取时输出 D/MyTag(12345): hello I chatty: uid12345(com.app) 0x3039 expire 99 lines D/MyTag(12345): world100 条重复日志只占2 个链表节点的空间1 个 dropped 占位 1 个下一条不同消息。九、PruneList — 白名单与黑名单源码路径system/core/logd/LogWhiteBlackList.hclassPruneList{PruneCollection mNaughty;// 黑名单优先裁剪PruneCollection mNice;// 白名单尽量保留boolmWorstUidEnabled;// 是否启用最坏 UID 裁剪boolmWorstPidOfSystemEnabled;// 是否启用最坏 PID 裁剪boolnaughty(LogBufferElement*element);// 检查是否在黑名单boolnice(LogBufferElement*element);// 检查是否在白名单};每个Prune条目可以指定 UID 或 UID/PID 组合classPrune{constuid_t mUid;constpid_t mPid;// mPid pid_all(-1) 时匹配该 UID 下的所有 PID// mUid uid_all(-1) 时匹配所有 UID 的该 PID};白名单/黑名单通过-P参数设置或从/data/misc/logd/下的配置文件加载特殊语法~!表示自动将当前统计中的最吵 UID 加入黑名单白名单条目在裁剪时最后才被删除黑名单条目优先删除十、读者保护机制替代引用计数原文描述的引用计数机制在实际代码中不存在。实际的保护机制是LogTimeEntry的range lock// LogTimeEntry 记录每个 logcat 客户端当前读取到的 sequence 位置// prune() 在裁剪前会查找所有 reader 的最小 mStart 位置// 如果裁剪遇到该位置停止裁剪或者触发 reader 跳过/释放// 慢速 reader 的处理策略if(stats.sizes(id)(2*log_buffer_size(id))){// 日志积压超过 2 倍容量 → 释放杀掉慢速 readeroldest-release_Locked();}elseif(oldest-mTimeout.tv_sec||oldest-mTimeout.tv_nsec){// 有超时 → 唤醒 reader 让它赶快读oldest-triggerReader_Locked();}else{// 没有超时 → 让 reader 跳过指定数量的条目oldest-triggerSkip_Locked(id,pruneRows);}十一、LogStatistics — 多维度统计源码路径system/core/logd/LogStatistics.cppclassLogStatistics{boolenable;// 详细统计开关// 每 log_id 维度的容量统计size_t mSizes[LOG_ID_MAX];// 当前字节数size_t mElements[LOG_ID_MAX];// 当前条目数size_t mDroppedElements[LOG_ID_MAX];// 当前 dropped 条目数size_t mSizesTotal[LOG_ID_MAX];// 历史总字节数size_t mElementsTotal[LOG_ID_MAX];// 历史总条目数// 按 UID 统计每个 log_id 独立uidTable_t uidTable[LOG_ID_MAX];// 按 PID 统计仅 AID_SYSTEM 的 PIDpidTable_t pidSystemTable[LOG_ID_MAX];// 详细统计enabletrue 时pidTable_t pidTable;// 全部 PIDtidTable_t tidTable;// 全部 TIDtagTable_t tagTable;// events TAGtagTable_t securityTagTable;// security TAG};统计更新时机操作调用的统计方法插入条目stats.add(elem)删除条目stats.subtract(elem)条目被 droppedstats.drop(elem)→stats.subtract(elem)mDroppedElements条目被 erase含 coalescestats.erase(elem)或stats.subtract(elem)logcat -S 输出示例size/num main system radio events crash Total 123456/890 45678/234 12345/67 98765/432 1234/5 Now 45678/234 12345/67 3456/12 23456/78 567/2 Chattiest UIDs in main log buffer: UID PACKAGE BYTES /- Pruned NUM 1000 system 45678 1.2% 234 10123 com.example.app 12345 -0.5% 12 89 ... Chattiest PIDs: PID/UID COMMAND LINE BYTES NUM 1234/1000 system_server 23456 78 ... Chattiest TIDs: TID/UID COMM BYTES NUM 5678/1000 Binder:1234_5 12345 45 ...十二、完整写入与裁剪时序T1: 进程 A 写入日志 → LogBuffer::log(main, realtime, uid1000, ...) → 创建 LogBufferElement (seq500) → 找到时间戳位置插入链表 → stats.add(elem) → maybePrune(LOG_ID_MAIN) → sizes(MAIN) 260KB maxSize(MAIN) 256KB → 计算 pruneRows → prune(LOG_ID_MAIN, pruneRows) T2: prune() 阶段一 → stats.sort() 找出最坏 UID 10123占用 48KB 256KB/8 → 遍历链表跳过非 MAIN 条目 → 遇到 uid10123 的条目 → setDropped(1)释放 mMsg保留占位符 → 连续多条 uid10123 → coalesce 合并 dropped 计数 → 直到 pruneRows 用完或 worst_sizes second_worst_sizes T3: prune() 阶段二如果 pruneRows 还没用完 → 从最旧条目开始遍历 → 遇到 reader locked 条目 → 停止 → 白名单条目 → 跳过 → 其他条目 → 直接删除 T4: 裁剪完成 → 被 dropped 的条目 mMsgNULL, mDroppedN → logcat 读取时flushTo() 检测到 mMsgNULL → 调用 populateDroppedMessage() 生成 I chatty: uid10123 ... expire N lines十三、缓冲区大小配置默认值#defineLOG_BUFFER_SIZE(256*1024)// 256KB#defineLOG_BUFFER_MIN_SIZE(64*1024)// 64KB低内存设备#defineLOG_BUFFER_MAX_SIZE(256*1024*1024)// 256MB系统属性控制persist.logd.size // 全局默认大小Settings 可修改 persist.logd.size.main // 每个 log_id 独立配置 persist.logd.size.system persist.logd.size.events persist.logd.size.radio ro.logd.size // BoardConfig.mk 编译时配置persist 为空时使用 ro.config.low_ram // 低内存设备使用 LOG_BUFFER_MIN_SIZE (64KB)十四、本篇总结机制实际实现原文错误条目存储单个std::list所有 log_id 混合按 sequence 排序原文无此描述插入位置按时间戳找到正确位置插入保持时间有序原文说插入头部容量限制mMaxSize[LOG_ID_MAX]每个 log_id 独立原文说单个mMaxSizechatty 合并裁剪时通过LogBufferElementLast::coalesce()合并原文说插入时检测chatty 消息uid%u%s%s expire %u line%s 进程名/线程名原文格式错误裁剪算法三段式黑名单最坏UID → 最旧过期白名单 → 白名单回退原文描述过于简化读者保护LogTimeEntryrange lock 慢速 reader 触发跳过/释放原文说引用计数不存在最坏 UID 阈值超过maxSize/812.5%且超过 dropped 的 10 倍阈值正确但逻辑描述不完整裁剪条目数每次 4~256 条10% 阈值原文无此描述dropped 占位符mMsg/mDroppedunion 机制保留占位符在链表原文无此描述白名单/黑名单PruneList类mNice/mNaughty支持~!通配原文无此描述日志过滤log()中调用__android_log_is_loggable()过滤不可记录日志原文无此描述理解这些后就可以解释的常见现象旧日志消失 → 裁剪机制按 UID 和过期时间淘汰最旧条目相同的日志只出现一次 → chatty 在裁剪时合并连续 dropped 条目系统服务日志不会被应用日志冲掉 → 白名单nice保护 最坏 UID 裁剪先裁应用logcat -S能定位日志大户 →LogStatistics多维度统计模块日志缓冲区大小可单独配置 →mMaxSize[LOG_ID_MAX] 系统属性persist.logd.size.namelogcat 读得太慢会被踢掉 →LogTimeEntry超时/跳过/释放机制