3
这个话题真的是非常简单的一个问题,以至于网上很难找到正确的解读。

最近做 Android 系统开发,系统开发通常要建立自己的 Log 系统,抓取 Android Log、Kernel Log 和其他一些特定的 Log。对于 Kernel Log 的需求也很简单,

  • 抓取 Kernel 打印的 Log,存储到文件中。
  • Log 使用墙上时钟(wall clock)显示时间戳,这样方便问题分析。

看似简单的需求却得到五花八门的实现,可见码农的创造力无限。一个方案是抓取 "/proc/kmsg" 信息,但它不是以墙上时钟显示的,又在 Kernel 中定时打印墙上时间用于对照。另一个方案是使用 “dmesg -T -c” 来抓取信息,这样是能得到墙上时间的 Log,但它清空了 Kernel Log buffer,等于是造成了破坏。还有网络上描述的各种解决方案。看到这些方法时,我就迷惑了,一个简单正确的获取 Kernel Log 的方法难道不是通过 Logcat 获取吗?发生了什么导致很多人并不知道这个方法,并且网上也很少提及。

正确的读取 Kernel Log

先说结论,偷懒的就省得看后面了。从 Android 6.0开始,收集 Kernel Log 只需两步,任何需求都满足了。

  1. 设置 ”ro.logd.kernel=true“
  2. 通过 ”logcat -b kernel“ 抓取。

接下来说一下具体的影响因素。本来在 debuggable 版本的系统,可以下面的命令读取 Kernel Log,

# logcat -b kernel

但是~~~,你会发现在许多系统中上面的命令并不能读到信息,因为它还受一些 property 的限制。在源码中的一个文档记录了与 logd 相关的 property,摘取 Kernel Log 相关的描述如下。

file: /system/core/logd/README.property

The properties that logd and friends react to are:

name                       type default  description

ro.logd.kernel             bool+ svelte+ Enable klogd daemon
......
ro.debuggable              number        if not "1", logd.statistics &
                                         ro.logd.kernel default false.
......
ro.config.low_ram          bool   false  if true, logd.statistics,
                                         ro.logd.kernel default false,
                                         logd.size 64K instead of 256K.
......
NB:
- auto - managed by /init
- bool+ - "true", "false" and comma separated list of "eng" (forced false if
  ro.debuggable is not "1") or "svelte" (forced false if ro.config.low_ram is
  true).
- svelte - see ro.config.low_ram for details.
- svelte+ - see ro.config.low_ram and ro.debuggable for details.
......

上面描述表明,ro.logd.kernel 用于控制是否使能 klogd 模块,klogd 就是用来采集 Kernel Log 的进程。ro.logd.kernel 的值为 bool+,可以设置为 "true"、"false" 或者通过逗号分隔符连接 "eng" 或 "svelte"。设置为 "eng" 时,如果 ro.debuggable 不为 "1",则 ro.logd.kernel 为 false。 设置为 "svelte" 时,如果 ro.config.low_ram 为 "true",则 ro.logd.kernel 为 false。

上面这段真的很乱,我看了半天+撸代码总算稍微看懂了,我们用下面的表格来说明一下。

Valuesvelte+Return
ro.logd.kernel=true,xxxN/Atrue
ro.logd.kernel=false,xxxN/Afalse
ro.logd.kernel=xxx,eng (xxx != true or false)ro.debuggable=1true
ro.logd.kernel=xxx,eng (xxx != true or false)ro.debuggable!=1false
ro.logd.kernel=xxx,svelte (xxx != true or false)ro.config.low_ram=truefalse
ro.logd.kernel=xxx,svelte (xxx != true or false)ro.config.low_ram=falsetrue

默认 ro.logd.kernel 是没有被设置的,这时会根据ro.debuggablero.config.low_ram来决定缺省值。

svelte+ro.logd.kernel default value
ro.config.low_ram=truefalse
ro.config.low_ram!=true && ro.debuggable!=1false
otherstrue

所以,当 logcat 无法抓取 Kernel Log 时,需要检查三个 property :ro.logd.kernelro.config.low_ramro.debuggable 的状态。而需要强制打开 Kernel Log 时,只要设置 ”ro.logd.kernel=true“。

为什么无人提及

首先,这个方法实在太简单了,简单的都不值得去认真分析它。但为何还有很多人不知道呢?我遇到的情况多数都是由于历史继承性引起的。在 Android 6.0 之前,抓取 Kernel Log 只能自己想办法,于是码农们实现了各种各样的方式来获取 Log,并加上时间戳。但是当系统版本升级时,这部分抓取 Log 日志的系统做为可贵的自研代码保留了下来,之后一直继承着。部分系统从来没重新审视这部分代码的合理性,因为它一致能用,就接着用。类似的问题在许多代码中都存在,很难有人跳出来重新整理。

代码分析

简单分析一下 Kernel Log 相关的代码,要不这个文档就更没有价值了。Android 的 Log 都由 logd 来管理,Kernel Log 当然也时一样,只是多开辟了一个 kernel buffer。

file: /system/core/logd/main.cpp
    
int main(int argc, char* argv[]) {
    ......
    static const char dev_kmsg[] = "/dev/kmsg"; //使用这个节点来输出logd中的打印
    fdDmesg = android_get_control_file(dev_kmsg);
    ......
    bool klogd = __android_logger_property_get_bool( //获取klogd状态
        "ro.logd.kernel",
        BOOL_DEFAULT_TRUE | BOOL_DEFAULT_FLAG_ENG | BOOL_DEFAULT_FLAG_SVELTE);
    if (klogd) {
        static const char proc_kmsg[] = "/proc/kmsg"; //用来读取Kernel Log的节点
        fdPmesg = android_get_control_file(proc_kmsg);
        ......
    }
    ......
    logBuf = new LogBuffer(times); //创建logBuffer,用于保存日志
    ......
    //LogReader监听/dev/socket/logdr,logcat通过它来获取日志
    LogReader* reader = new LogReader(logBuf); 
    if (reader->startListener()) {
        exit(1);
    }
    ......
    //LogListener监听/dev/socket/logdw,用于写入日志
    LogListener* swl = new LogListener(logBuf, reader);
    if (swl->startListener(600)) {
        exit(1);
    }
    // CommandListener监听/dev/socket/logd,用于命令传输
    CommandListener* cl = new CommandListener(logBuf, reader, swl);
    if (cl->startListener()) {
        exit(1);
    }
    ......
    LogKlog* kl = nullptr; //LogKlog用于存储Kernel Log
    if (klogd) {
        kl = new LogKlog(logBuf, reader, fdDmesg, fdPmesg, al != nullptr);
    }

    readDmesg(al, kl); //启动时读取kernel buffer中缓存的日志

    if (kl && kl->startListener()) { //启动Kernel log监听
        delete kl;
    }
    ......
}

重点的几个地方:

  • 打开 "/dev/kmsg" 用来输出 logd 中的一些日志。这个设计挺巧妙的,在 logd 准备好之前,日志是无法写入 Android Log buffer 中的。这里将日志写入 "/dev/kmsg" 中,等于写入了 Kernel Log buffer 中,通过 dmesg 或 "/proc/kmsg" 就可以获取相关日志。
  • 使用 "/proc/kmsg" 来获取 Kernel Log。"/proc/kmsg" 会记录上次读取的位置,与 dmesg 不同,读取时不会输出整个 buffer 内容。只要监听这个节点就可以实时获取 Kernel Log。
  • LogKlog 是一个 socket listener,用来监听 "/proc/kmsg",然后将 Kernel Log 写入到 Android Log buffer 中。

接下来简单看一下 Kernel Log 真正写入的代码,

file: /system/core/logd/LogKlog.cpp
    
// 当监听到有 Kernel Log 写入时会调用该方法    
bool LogKlog::onDataAvailable(SocketClient* cli) {
    ......
    char buffer[LOGGER_ENTRY_MAX_PAYLOAD]; //每次最多读取4068字节的日志
    ssize_t len = 0;

    for (;;) {
        ssize_t retval = 0;
        if (len < (ssize_t)(sizeof(buffer) - 1)) {
            retval = //读取 "/proc/kmsg" 中的日志
                read(cli->getSocket(), buffer + len, sizeof(buffer) - 1 - len);
        }
        ......
        for (char *ptr = nullptr, *tok = buffer;
             !!(tok = android::log_strntok_r(tok, len, ptr, sublen));
             tok = nullptr) {
            ......
            if ((sublen > 0) && *tok) {
                log(tok, sublen); // 一行行的将日志写入 buffer
            }
        }
    }

    return true;
}
......
// 写入日志,对 Kernel Log 做了大量解析,转换为 Android Log 格式。
// 可能 Kernel Log 太随意了,这段代码看起来都晕,大致理解一下。
int LogKlog::log(const char* buf, ssize_t len) {
    // 如果 auditd 在运行,它会记录相关日志,所以在 LogKlog 不会重复记录该日志。
    if (auditd && android::strnstr(buf, len, auditStr)) {
        return 0;
    }
    
    // 解析 Kernel Log 中的优先级信息
    const char* p = buf;
    int pri = parseKernelPrio(p, len);
    
    // 解析时间信息
    log_time now;
    sniffTime(now, p, len - (p - buf), false);
    ......
    // 解析 pid、tid、uid
    const pid_t pid = sniffPid(p, len - (p - buf));
    const pid_t tid = pid;
    uid_t uid = AID_ROOT;
    if (pid) {
        logbuf->wrlock();
        uid = logbuf->pidToUid(pid);
        logbuf->unlock();
    }
    
    // 解析 Kernel Log 中的 tag 信息
    while ((p < &buf[len]) && (isspace(*p) || !*p)) {
        ++p;
    }
    ......
    // 根据解析的信息创建 Android Log
    char newstr[n];
    char* np = newstr;
    
    // 将优先级转换为 Android Log 的优先级格式
    *np = convertKernelPrioToAndroidPrio(pri);
    ++np;
    
    // 复制解析的 tag 信息
    memcpy(np, tag, taglen);
    np += taglen;
    *np = '\0';
    ++np;

    // 复制主信息
    memcpy(np, p, b);
    np[b] = '\0';
    
    // 时间修正
    if (!isMonotonic()) {
        ......
    }

    // 将日志写入 LOG_ID_KERNEL buffer 中
    int rc = logbuf->log(LOG_ID_KERNEL, now, uid, pid, tid, newstr,
                         (unsigned short)n);

    // 通知 readers
    if (rc > 0) {
        reader->notifyNewLog(static_cast<log_mask_t>(1 << LOG_ID_KERNEL));
    }

    return rc;
}

戈壁老王
143 声望64 粉丝

做为一个不称职的老年码农,一直疏忽整理笔记,开博记录一下,用来丰富老年生活,