24、logd与日志系统:logd守护进程、logcat原理、日志缓冲区与dmesg

日志系统,说白了就是Android的“黑匣子”。

我当年刚接触Android底层时,最头疼的就是系统突然卡死或者重启,完全不知道发生了什么。后来我花了整整两周时间,把logd、logcat、dmesg这些东西彻底啃了一遍。嗯,从那以后,排查系统问题就顺手多了。

今天我们就来聊聊这套日志系统到底是怎么工作的。

24.1 logd守护进程:日志系统的“大管家”

logd,全称是log daemon。它是Android 4.2之后引入的一个核心守护进程。

你想想看,系统里那么多进程都在写日志,如果没人统一管理,那不乱套了?logd就是干这个的——它负责接收、缓存、分发所有日志消息。

核心职责:

  • 接收来自liblog的日志写入请求
  • 管理多个日志缓冲区(main、system、crash、events、kernel)
  • 响应logcat客户端的读取请求
  • 控制日志的权限和访问策略

logd的启动时机非常早。在init.rc中,它被标记为critical服务。什么意思?就是如果它挂了,系统会直接重启。我见过不少厂商定制ROM时不小心把logd搞崩了,结果手机无限重启——那场面,啧啧。

// init.rc 中的 logd 配置
service logd /system/bin/logd
    class core
    socket logd stream 0666 logd logd
    socket logdr stream 0666 logd logd
    socket logdw stream 0222 logd logd
    critical
    onrestart restart traced
    onrestart restart traced_probes

注意看这三个socket:logdlogdrlogdw。它们分别对应控制、读取、写入三个通道。这种设计很巧妙——读写分离,权限控制也更灵活。

24.2 日志缓冲区:数据到底存哪儿了?

logd管理着几个环形缓冲区。环形缓冲区,说白了就是一个固定大小的数组,写满了就从头覆盖。

我习惯把缓冲区比作一个“旋转木马”——数据一直在转,新的覆盖旧的。这样设计的好处是:内存占用固定,不会因为日志爆炸而撑死系统。

缓冲区名称 用途 默认大小 访问权限
main 应用日志(Log.d/v/i) 256KB 所有应用
system 系统服务日志 256KB 系统应用
crash 崩溃日志 256KB 所有应用
events 二进制事件日志 256KB 系统应用
kernel 内核日志(dmesg) 128KB root

这里有个坑,我踩过。曾经有个项目,客户反馈手机用几天就变慢。我查了半天,发现是某个三方应用疯狂写日志,把main缓冲区撑满了。虽然环形缓冲区不会爆内存,但频繁的写入操作会触发logd的CPU占用飙升。

避坑指南:我曾经在生产环境中遇到过日志缓冲区被恶意刷爆的情况。建议在user版本中通过persist.logd.size属性合理控制缓冲区大小,或者干脆关闭冗余日志。

24.3 logcat原理:日志是怎么被读出来的?

logcat,每个Android开发者都用过。但你知道它底层是怎么工作的吗?

logcat本质上是一个客户端,它通过socket连接到logd,然后请求读取日志数据。整个过程分为三步:

  1. 建立连接:logcat打开/dev/socket/logdr,发送读取请求
  2. 数据流传输:logd将缓冲区数据通过socket推送给logcat
  3. 格式化输出:logcat解析二进制数据,按指定格式打印

我个人觉得,logcat最巧妙的地方在于它的“尾随”模式。你执行logcat -s时,logcat会先dump当前缓冲区的内容,然后保持连接,等待新日志到来。这有点像tail -f的效果。

// logcat 的核心读取逻辑(简化版)
int main() {
    // 1. 连接 logd
    int fd = socket_local_client("logdr", 
                                 ANDROID_SOCKET_NAMESPACE_RESERVED,
                                 SOCK_STREAM);
    
    // 2. 发送读取请求(指定缓冲区、过滤条件)
    struct logger_list *list = android_logger_list_open(
        LOG_ID_MAIN,        // 缓冲区ID
        ANDROID_LOG_RDONLY, // 只读模式
        0,                  // 尾随模式
        PID_ANY             // 不过滤PID
    );
    
    // 3. 循环读取日志条目
    while (1) {
        struct log_msg msg;
        int ret = read(fd, &msg, sizeof(msg));
        if (ret <= 0) break;
        
        // 4. 解析并打印
        print_log_entry(&msg);
    }
}

你可能会问:为什么logcat能读到所有进程的日志?因为logd的socket权限是0666,任何进程都能连接。但读取events和kernel缓冲区需要系统权限——这是通过SELinux策略控制的。

24.4 dmesg:内核日志的“直通车”

dmesg,全称是display message。它读取的是内核的ring buffer,也就是kernel缓冲区。

内核日志和用户空间日志最大的区别是什么?内核日志是C语言写的,通过printk函数输出。而用户空间日志是通过__android_log_print写入的。

我记得有一次,系统启动时WiFi死活打不开。logcat里什么错误都没有,但dmesg里清清楚楚地写着:

[   12.345678] wlan: firmware load failed, ret = -110
[   12.345679] wlan: failed to load calibration data

看到没?硬件初始化失败这种底层问题,logcat根本抓不到。只有dmesg能告诉你真相。

实用技巧:我习惯在调试驱动问题时同时开三个窗口:一个logcat看上层,一个dmesg看内核,还有一个strace看系统调用。三管齐下,问题定位效率翻倍。

dmesg的缓冲区大小可以在内核配置中调整:

// 内核配置选项
CONFIG_LOG_BUF_SHIFT=18  // 2^18 = 256KB
// 运行时查看
cat /proc/sys/kernel/printk
// 输出: 7       4       1       7
//      控制台 默认 最小 最大 日志级别

这里printk的四个数字分别代表:控制台日志级别、默认日志级别、最小日志级别、最大日志级别。数值越小越重要。7是DEBUG级别,1是ALERT级别。

24.5 日志系统的整体架构

好了,我们把前面讲的串起来,画一张图看看整体架构。

Android日志系统架构图 用户空间 (Userspace) App进程 Log.d() / Log.e() 系统服务 Slog.i() / Slog.w() liblog __android_log_print() logcat 读取日志 内核空间 (Kernel Space) 内核驱动 printk() dmesg 读取内核ring buffer logd 守护进程 日志接收 / 缓存 / 分发 日志缓冲区 (环形缓冲区) main system crash events kernel

从这张图可以看得很清楚:

  • 上层应用通过liblog写入日志,liblog通过socket发给logd
  • logd将日志存入对应的环形缓冲区
  • logcat通过socket从logd读取日志
  • 内核日志走的是另一条路——printk直接写入内核ring buffer,dmesg直接读取

这里有个细节值得注意:logd本身也运行在用户空间,但它通过socket和内核交互。说白了,logd就是一个“中间人”——它把用户空间的日志和内核空间的日志统一管理起来。

24.6 实战经验:日志系统的调优

最后分享几个我在项目中积累的调优经验:

1. 日志级别控制

在user版本中,我建议把默认日志级别调到INFO以上。这样可以减少90%以上的日志写入量,对性能有明显提升。

// 设置日志级别
setprop persist.log.tag V    // 所有日志
setprop persist.log.tag W    // 只显示WARNING以上
setprop log.tag.MyApp D      // 单独控制某个TAG

2. 缓冲区大小调整

我习惯在调试阶段把缓冲区调大,避免日志被覆盖:

// 设置单个缓冲区大小为4MB
setprop persist.logd.size.main 4194304
setprop persist.logd.size.system 4194304
// 重启logd生效
stop logd
start logd

3. 不要在生产环境开full log

我曾经在一个项目里,测试人员开了full log跑了一整天,结果logd占用了将近100MB内存。虽然环形缓冲区不会无限增长,但频繁的写入和读取操作会消耗大量CPU。记住:日志是调试工具,不是运行时的必需品。

好了,关于logd和日志系统,我们就聊到这里。这套机制看似简单,但里面涉及的设计思想——读写分离、环形缓冲区、权限控制——在很多系统组件中都能看到影子。理解了它,你就理解了Android系统日志的“魂”。


公众号:蓝海资料掘金营,微信deep3321