关于 android 8.0 的 logcat 进程 Unexpected EOF 退出问题

知道这个问题, 是前几天, 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/
关于 android 8.0 的 logcat 进程 Unexpected EOF 退出问题_第1张图片

意料之内的, 应急的措施就是检查 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  图些啥?   

关于 android 8.0 的 logcat 进程 Unexpected EOF 退出问题_第2张图片
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




好累,未完待续...

关于 android 8.0 的 logcat 进程 Unexpected EOF 退出问题_第3张图片

你可能感兴趣的:(Android)