Android 平台日志系统整体框架

11/20/2023

这是一个介绍 Android 平台日志系统的系列文章:

  • Android 平台日志系统整体框架 (本文)
  • logd 守护进程初始化过程
  • 客户端写日志过程分析
  • logd 写日志过程分析
  • 冗余日志处理过程分析
  • logcat 读日志过程分析
  • logd 读日志过程分析
  • 内核日志处理分析
  • SeLinux 日志处理分析

本文基于 AOSP android-10.0.0_r41 版本讲解

# 1. 日志的类别与基本使用

在 Android 系统中,常见的日志有 5 类,main、radio、events、system、crash:

main 日志

main 日志是应用层 App 唯一可用的日志类型。

在 Java 层提供了 android.util.Log 接口来写 main 日志:

// 写 VERBOSE 等级的日志
Log.v("YOUR TAG", "Message body");
// 写 DEBUG 等级的日志
Log.d("YOUR TAG", "Message body");
// 写 INFO 等级的日志
Log.i("YOUR TAG", "Message body");
// 写 WARN 等级的日志
Log.w("YOUR TAG", "Message body");
// 写 ERROR 等级的日志
Log.e("YOUR TAG", "Message body");
1
2
3
4
5
6
7
8
9
10

android.util.Log 接口提供了 5 个方法来写 5 个级别(Level)的日志,日志级别分别为 VERBOSE、DEBUG、INFO、WARN、ERROR、ASSERT,日志级别依次提升:

  • VERBOSE 级别日志:开发调试中的一些详细信息,仅在开发中使用,不可在发布产品中输出,不是很常见,包含诸如方法名,变量值之类的信息
  • DEBUG 级别日志:用于调试的信息,可以在发布产品中关闭,比较常见,开发中经常选择输出此种级别的日志,有时在 beta 版应用中出现
  • INFO 级别日志:该等级日志显示运行状态信息,可在产品出现问题时提供帮助,从该级别开始的日志通常包含完整的文本信息和调试信息,是最常见的日志级别
  • WARN 级别日志:运行出现异常即将发生错误或表明已发生非致命性错误,该级别日志通常显示出执行过程中的意外情况,例如将 try-catch 语句块中的异常打印堆栈轨迹之后可输出此种级别日志
  • ERROR 级别日志:已经出现可影响运行的错误,比如应用 crash 时输出的日志

在 native 层提供了 __android_log_print 函数来写 main 日志。习惯上,会自定义一些宏来简化 __android_log_print 函数的使用:

#define ALOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
#define ALOGD(...) __android_log_print(ANDROID_LOG_DEBUG  , LOG_TAG, __VA_ARGS__)
#define ALOGI(...) __android_log_print(ANDROID_LOG_INFO   , LOG_TAG, __VA_ARGS__)
#define ALOGW(...) __android_log_print(ANDROID_LOG_WARN   , LOG_TAG, __VA_ARGS__)
#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR  , LOG_TAG, __VA_ARGS__)
1
2
3
4
5

与 Java 层类似,在 Natvie 层也有 5 个级别(Level)的日志输出函数。

radio 日志

radio 日志主要用于记录无线装置/电话相关系统应用的日志,可以调用 android.telephony.Rlog 打印日志。

events 日志

events 日志是用来诊断系统问题的。在应用框架提供了 android.util.EventLog 接口写日志,native 层提供了宏 LOG_EVENT_INT、LOG_EVENT_LONG、LOG_EVENT_FLOAT、LOG_EVENT_STRING 用来写入 events 类型日志。

system 日志

system 日志主要用于记录系统应用的日志信息,在 Java 层提供了 android.util.SLog 接口写入日志, native 层提供了 SLOGV、SLOGD、SLOGI、SLOGW、SLOGE 等宏用来写入 system 类型的日志。

crash 日志

  • crash 日志通常是程序 crash 时,记录的日志类型, Java 层使用 Log.println() (第一个参数设置为 LOG_ID_CRASH)打印 crash 类型的日志

了解了如何写入日志,接下来我们看看如何读取日志。

通常使用 logcat 命令查看日志:

logcat -b + 参数
1

可选的参数主要有:

  • all:查看所有缓冲区日志
  • default:查看 main、system、crash 三个类型日志信息
  • main:查看 main 类型日志
  • radio:查看 radio 类型日志
  • events:查看 events 类型日志
  • system:查看 system 类型日志
  • crash:查看 crash 类型日志

比如:

logcat -b main
# 用来查看 main 和 system 类型日志
logcat -b main,system
logcat -b all #查看所有日志
1
2
3
4

# 2.Kernel 层的日志

Kernel 层通常使用 printk 函数来记录日志,具体用法如下:

// KERN_INFO为日志级别,"Message body\n"则为日志信息
printk(KERN_INFO "Message body\n"); 
1
2

kernel 日志级别分别是:KERN_EMERG,KERN_ALERT,KERN_CRIT,KERN_ERR,KERN_WARNING,KERN_NOTICE,KERN_INFO,KERN_DEBUG

这些级别定义在内核源码中的 kern_levels.h 文件中:

#define KERN_EMERGKERN_SOH "0"/* system is unusable */

#define KERN_ALERTKERN_SOH "1"/* action must be taken immediately */

#define KERN_CRITKERN_SOH "2"/* critical conditions */

#define KERN_ERRKERN_SOH "3"/* error conditions */

#define KERN_WARNINGKERN_SOH "4"/* warning conditions */

#define KERN_NOTICEKERN_SOH "5"/* normal but significant condition */

#define KERN_INFOKERN_SOH "6"/* informational */

#define KERN_DEBUGKERN_SOH "7"/* debug-level messages */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Kernel 日志输出到 /proc/kmsg 文件,用户可以通过读取这个文件来获取 kernel 日志:

cat /proc/kmsg
1

# 3. 日志系统整体框架

图片来自Android logd日志简介及典型案例分析 (opens new window),稍作修改。

系统启动时,init 进程会启动 logd 进程,logd 进程会启动多个子线程,我们主要关注其中的三个:

  • LogListener,启动一个 Unix Domain Socket 服务端,socket 服务端对应的文件是 /dev/socket/logdw,用于接受 App 发送的 socket 远程请求,向 LogBuffer 写入日志
  • LogReader,启动一个 Unix Domain Socket 服务端,socket 服务端对应的文件是 /dev/socket/logdr,用于接受 logcat 命令发送的 socket 远程请求,从 LogBuffer 中读出日志
  • CommandListener,启动一个 Unix Domain Socket 服务端,socket 服务端对应的文件是 /dev/socket/logd,用于接受 logcat 命令发送的 socket 远程请求,向 LogBuffer 发送管理命令

App 端写入日志时,最终都是调用到 __android_log_write 函数,通过 socket 远程调用,向 LogListener 发送消息,LogListener 读出 socket 消息,在写入 LogBuffer中

调用 logcat 命令时,最终都是调用到 __android_logger_readSendLogdControlMessage 函数,前者通过 socket 远程调用,向 LogReader 发送消息,LogReader 读出 socket 消息,接着从 LogBuffer 中读出日志信息;后者通过 socket 远程调用,向 CommandListener 发送消息,LogReader 读出 socket 消息,接着向 LogBuffer 发送管理命令

# 参考资料