这个话题真的是非常简单的一个问题,以至于网上很难找到正确的解读。
最近做 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 只需两步,任何需求都满足了。
- 设置 ”
ro.logd.kernel
=true“ - 通过 ”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。
上面这段真的很乱,我看了半天+撸代码总算稍微看懂了,我们用下面的表格来说明一下。
Value | svelte+ | Return |
---|---|---|
ro.logd.kernel =true,xxx |
N/A | true |
ro.logd.kernel =false,xxx |
N/A | false |
ro.logd.kernel =xxx,eng (xxx != true or false) |
ro.debuggable =1 |
true |
ro.logd.kernel =xxx,eng (xxx != true or false) |
ro.debuggable !=1 |
false |
ro.logd.kernel =xxx,svelte (xxx != true or false) |
ro.config.low_ram =true |
false |
ro.logd.kernel =xxx,svelte (xxx != true or false) |
ro.config.low_ram =false |
true |
默认 ro.logd.kernel
是没有被设置的,这时会根据ro.debuggable
和ro.config.low_ram
来决定缺省值。
svelte+ | ro.logd.kernel default value |
---|---|
ro.config.low_ram =true |
false |
ro.config.low_ram !=true && ro.debuggable !=1 |
false |
others | true |
所以,当 logcat 无法抓取 Kernel Log 时,需要检查三个 property :ro.logd.kernel
、ro.config.low_ram
、ro.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(1 << LOG_ID_KERNEL));
}
return rc;
}