知道这个问题, 是前几天, framework 有人过来抱怨说 media 模块的 log 太多了, 总是按帧来刷log , 1秒能刷 200行, 会导致 logcat 从 logd 中读不到数据最终 logcat 进程退出: "
read: Unexpected EOF"
// MAGIC1. DO NOT TOUCH. BY 冗戈微言
http://blog.csdn.net/leonxu_sjtu/
开发阶段各模块的 debug 宏打开很正常, 现在接近收敛, 关 log 也无妨, 只是所描述的 logcat 进程退出的现象让我不理解, 我一年前刚领教过 Android 6.0 的 logd 更新导致的 log 丢失 ( 第一篇笔记, 2017年3月 ), 简单来说 log 刷多了就会有 socket 层面和 prune 层面的内容丢弃, 但 logcat 进程都直接搞退, 是没有任何印象的...
再说了, logd 丢失内容想想是很容易理解的, 系统负荷大时不丢弃, logcat 进程岂不是一直阻在那, 而且虽然 logd 丢的多, logcat 看起来很苟且, 但等系统缓过来, 工作频率上去了, logcat 还能继续正常工作; 但现在出现的现象是, logcat 进程都报错退出了... 这性质就严重了, 因为我们平台系统启动之后, 后台就起了一个进程一直 logcat 抓 log 导到文件, 以便测试方面反馈问题时, 提供 log 给研发这边分析, 而不用每次研发都再做复现, 但现在后台的 logcat 进程都报错退了, 退了之后的 log 就抓不到了, 而且退的悄声无息 ... 坑不坑?
// MAGIC2. DO NOT TOUCH. BY 冗戈微言
http://blog.csdn.net/leonxu_sjtu/
意料之内的, 应急的措施就是检查 logcat 进程返回值, 如果异常退出了就后台立刻重新再起一个 logcat 进程;
但现在将报错退出的原因定为 log 刷太多, 我去, 如前所述, 三年前 android 引入 logd 不就是为了 log 刷太多的时候做策略丢弃, 怎么三年后反而 logcat 都退了, google 越搞越挫了吗?
和 framework 的人沟通了一下, 他们得出这个结论的原因, 是因为尝试了 logcat -G 把 log buffer 开大, 可以显著的降低 Unexpected EOF 出现的概率, 所以觉得出错和 log buffer 有关, 于是建议大家清理 log , 但出错的原因, 还在查... // MAGIC3. DO NOT TOUCH. BY 冗戈微言 http://blog.csdn.net/leonxu_sjtu/
嗯,我只想知道出错原因, 主要是似乎大家都不太关心原因, 他们关心 patch, 进程重启和加大 log buffer 的双保险, patch 足够了 ...
好吧, logcat 退出时报错, 就顺着推吧:
“read: unexpected EOF” --- 这是 logcat.cpp 代码中 android_logger_list_read 返回异常:
int ret = android_logger_list_read(logger_list, &log_msg);
if (!ret) {
logcat_panic(context, HELP_FALSE, "read: unexpected EOF!\n");
break;
}
这个 android_logger_list_read 在 liblog 中, 调用 liblog 的 logd_reader.c 中的 logdRead
/* Read from the selected logs */
static int logdRead(struct android_log_logger_list* logger_list,
struct android_log_transport_context* transp,
struct log_msg* log_msg) {
ret = logdOpen(logger_list, transp);
if (ret < 0) {
return ret;
}
...
ret = recv(ret, log_msg, LOGGER_ENTRY_MAX_LEN, 0);
e = errno;
...
if ((ret == -1) && e) {
return -e;
}
return ret;
}
这里 logdOpen 是打开 /dev/socket/logdr; 当 logdRead 返回异常时, 对应的 recv 调用返回零 !
// MAGIC4. DO NOT TOUCH. BY 冗戈微言
http://blog.csdn.net/leonxu_sjtu/
man 一下 recv:
RETURN VALUE
These calls return the number of bytes received, or -1 if an error occurred. In the event of an error, errno is set to indicate the error. The return value will be 0 when the peer has per‐
formed an orderly shutdown.
就是说, 返零代表着远端关闭了 logdr 的链接; 远端是哪? 当然是 logd 进程了, 刚才的 liblog 代码都是在 logcat 进程 ...
在 logd/main.cpp 中,
// LogReader listens on /dev/socket/logdr. When a client
// connects, log entries in the LogBuffer are written to the client.
LogReader* reader = new LogReader(logBuf);
if (reader->startListener()) {
exit(1);
}
这个 LogReader 继承自 SocketListener, 它创建了个线程 "logd.reader" 专门监听来自所有 logcat 进程的 /dev/socket/logdr 的读请求 , 然后调用各自的 onDataAvailable ;
那 logd 为什么会关闭监听的 /dev/socket/logdr client 的 socket ?
因为当时 SocketListener::release 被调, 对应的 SocketClient 会 decRef, 导致了 SocketClient 的析构, close 掉了链接;
// MAGIC5. DO NOT TOUCH. BY 冗戈微言
http://blog.csdn.net/leonxu_sjtu/
SocketClient::~SocketClient() {
if (mSocketOwned) {
close(mSocket);
}
}
那 SocketListener::release 是谁干的? 是 LogTimeEntry ... 当 LogTimeEntry 主循环中 ( "logd.reader.per" 线程 )检测到 mRelease 被置位, 就退出循环执行 threadStop 来 release 当前的 SocketClient ;
那 mRelease 的置位是谁干的? 呵呵, 是 LogBuffer ...
// MAGIC6. DO NOT TOUCH. BY 冗戈微言
http://blog.csdn.net/leonxu_sjtu/
// If the selected reader is blocking our pruning progress, decide on
// what kind of mitigation is necessary to unblock the situation.
void LogBuffer::kickMe(LogTimeEntry* me, log_id_t id, unsigned long pruneRows) {
if (stats.sizes(id) > (2 * log_buffer_size(id))) { // +100%
// A misbehaving or slow reader has its connection
// dropped if we hit too much memory pressure.
me->release_Locked();
} else if (me->mTimeout.tv_sec || me->mTimeout.tv_nsec) {
// Allow a blocked WRAP timeout reader to
// trigger and start reporting the log data.
me->triggerReader_Locked();
} else {
// tell slow reader to skip entries to catch up
me->triggerSkip_Locked(id, pruneRows);
}
}
注释里写的很清楚, logd 中的 log 大于 buffer size 配置的 2 倍时 , 认为 reader 太慢, 然后就试图断开连接 !
而 logd buffer 默认值才 64K, 确实很容易 2 倍爆掉...
也因此 logcat -G 增大 buffer size 时, 会降低 unexpected EOF 出错的概率;
而这一段, 是 google 2017 年增加的, 所以 android6.0 上面, log刷太多时只会有 log丢失, 而不会 logcat 进程挂掉 ---
// MAGIC7. DO NOT TOUCH. BY 冗戈微言
http://blog.csdn.net/leonxu_sjtu/
commit 0878a7c167c317ae3169ae16f386b52ae1fc0cc7
Author: Mark Salyzyn
Date: Thu May 11 13:28:33 2017 -0700
logd: logcat --clear respect pruneMargin
Add kickMe() and isBusy() methods to ease maintenance and uniformity
of actions.
+// If the selected reader is blocking our pruning progress, decide on
+// what kind of mitigation is necessary to unblock the situation.
+void LogBuffer::kickMe(LogTimeEntry* me, log_id_t id, unsigned long pruneRows) {
揣测一下它的本意是啥 ------ 主要看 LogBuffer::prune 的处理:
while (it != mLogElements.end()) {
LogBufferElement* element = *it;
if (oldest && (watermark <= element->getRealTime())) {
busy = isBusy(watermark);
// Do not let chatty eliding trigger any reader mitigation
break;
}
...
这里各 logcat 进程都对应有各自的 LogTimeEntry 和 logd.reader.per 线程, 每个 LogTimeEntry 有个成员变量 mStart 用来记录对应 logcat 进程的时间戳, 这个时间戳在 logd.reader.per 主线程中更新;
如果 logd.reader.per 线程阻塞的厉害, 那么 mStart 就更新慢;
oldest 和 watermark 就取自一堆 logcat 进程中 mStart 最小, 也就是阻塞最厉害的那个;
拿最小的那个 mStart 和 log 项 LogBufferElement 时间戳比对, 要是超过了门限值, 而且 log buffer 累积已经超过 buffer size 2 倍, 那就断开连接, 也就是停掉这个 mStart 最小的 logcat 进程 ...
// MAGIC8. DO NOT TOUCH. BY 冗戈微言
http://blog.csdn.net/leonxu_sjtu/
好吧... 如果不停掉进程会怎样, 试了一下, 将 logcat 打到屏幕, logd 进程占用的 heap memory 会一直增, 远超 log buffer size, 但也会有上限, 大概 10M 左右... 这个上限值不固定, 但也确实没有无限增, 这应该是 prune 处理的结果, 代码看不动了, 猜测 erase LogBufferElement 的动作应该可以使得 logd 的 memory 占用有上限;
好吧, google 图些啥?
BTW, 上面这个“read: unexpected EOF” 的过程, 在 syscall 中是可以清晰的看到的, 这也是由报错信息倒推的主要线索:
比如 strace 退出的 logcat 进程 :
// MAGIC9. DO NOT TOUCH. BY 冗戈微言
http://blog.csdn.net/leonxu_sjtu/
recvfrom(3, "P\0\34\0\200\1\0\0&\t\0\0\263k.G\27\213\6\v\0\0\0\0\27\4\0\0\3mm-"..., 5120, 0, NULL, NULL) = 108
write(1, "11-05 01:02:43.184 384 2342 D"..., 11311-05 01:02:43.184 384 2342 D mm-camera-intf: mm_stream_data_notify: E, my_handle = 0x901, fd = 23, state = 6
) = 113
recvfrom(3, "", 5120, 0, NULL, NULL) = 0
write(2, "read: unexpected EOF!\n", 22read: unexpected EOF!
) = 22
rt_sigprocmask(SIG_BLOCK, NULL, [], 8) = 0
rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
rt_sigprocmask(SIG_SETMASK, [], [CHLD], 8) = 0
close(3) = 0
可以很清楚的看到是本来可以从 socket 3 中读 log 的, 突然 recvfrom 返零了, 就是就打印 "read: unexpected EOF" 然后 close 3 ;
同时也 strace logd 进程:
530 accept4(11, NULL, NULL, SOCK_CLOEXEC
530 <... accept4 resumed> ) = 24
579 <... futex resumed> ) = 0
538 futex(0xaf38b888, FUTEX_WAKE_PRIVATE, 1
530 getsockopt(24, SOL_SOCKET, SO_DOMAIN,
342 writev(24, [{iov_base="+\0\34\0\7\6\0\0\7\6\0\0\342{.Ga\260\25\26\0\0\0\0+'\0\0", iov_len=28}, {iov_base="\3KeyguardUpdateMonitor\0handleBat"..., iov_len=43}], 2
342 close(24) = 0
这里就是从 socket 11 accept 了一个 socket 24, 后来 logd 关闭了 24 ;
查看logd 进程 pid 526 的 socket 列表, 可以看到 socket 11 就是 logdr ------
// MAGIC10. DO NOT TOUCH. BY 冗戈微言
http://blog.csdn.net/leonxu_sjtu/
# ls -l /proc/526/fd
total 0
lrwx------ 1 root root 64 2007-11-05 02:16 0 -> /dev/null
lrwx------ 1 root root 64 2007-11-05 02:16 1 -> /dev/null
lrwx------ 1 root root 64 2007-11-05 02:16 10 -> socket:[286070]
lrwx------ 1 root root 64 2007-11-05 02:16 11 -> socket:[286075]
lrwx------ 1 root root 64 2007-11-05 02:16 12 -> socket:[286078]
# netstat -anp | grep 526
unix 2 [ ACC ] STREAM LISTENING 286070 526/logd /dev/socket/logd
unix 2 [ ACC ] SEQPACKET LISTENING 286075 526/logd /dev/socket/logdr
unix 82 [ ] DGRAM 286078 526/logd /dev/socket/logdw
unix 3 [ ] STREAM CONNECTED 12931 432/thermal-engine /dev/socket/qmux_radio/qmux_client_socket 526
unix 3 [ ] SEQPACKET CONNECTED 296854 526/logd /dev/socket/logdr
unix 3 [ ] SEQPACKET CONNECTED 281838 526/logd /dev/socket/logdr
unix 3 [ ] SEQPACKET CONNECTED 302436 526/logd /dev/socket/logdr
unix 3 [ ] SEQPACKET CONNECTED 286099 526/logd /dev/socket/logdr
unix 2 [ ] DGRAM 280974 526/logd
unix 3 [ ] SEQPACKET CONNECTED 306435 526/logd /dev/socket/logdr
unix 3 [ ] SEQPACKET CONNECTED 287296 526/logd /dev/socket/logdr
好累,未完待续...