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:logd、logdr、logdw。它们分别对应控制、读取、写入三个通道。这种设计很巧妙——读写分离,权限控制也更灵活。
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,然后请求读取日志数据。整个过程分为三步:
- 建立连接:logcat打开
/dev/socket/logdr,发送读取请求 - 数据流传输:logd将缓冲区数据通过socket推送给logcat
- 格式化输出: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 日志系统的整体架构
好了,我们把前面讲的串起来,画一张图看看整体架构。
从这张图可以看得很清楚:
- 上层应用通过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