Android 7系统日志(六)日志缓冲区管理—容量、裁剪与统计机制

系列目录第一篇:全景图与架构概览 | 第二篇: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;            // 时间戳
};

最关键的设计mMsgLenmDroppedunion 。正常条目有消息体(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 泛滥、开机日志缺失等常见问题。