Android 日志系统分析(一):概述

一、前言

在实际项目中经常会打印关键日志信息来反馈程序运行状况。例如 App 中常使用的 Log.dLog.v 等,而在 Native 层会使用 ALOGD 打印日志。对于第三方添加的 C/C++ 应用程序来说,如果希望使用 Android 的日志系统,就需要添加 liblog 库。而笔者面临的问题是在定制化的系统中研究 liblog 库是否满足我们的需求,或者修改 logd 这样的的守护进程,通过配置实现独特的分流?在此之前从来没有了解过这一块,因此本系列文章可以说是在众多参考文献上的一次总结。

二、日志系统框架

首先我们来看一下 Android (基于9.0)日志系统框架图:

图片来源于参考文献[5]

2.1 相关模块源码位置

  • Log.d : android.jar
  • logcat:system/core/logcat
  • liblog : system/core/liblog
  • logd : system/core/logd

2.2 应用层接口

Android 应用层提供日志系统的 Java 接口:Log.java、Rlog.java、Slog.java、EventLog.java。其功能类似只是写入 logd 的日志节点不同。Java 接口封装在 android.jar 中,作为 SDK 提供给开发者使用,在运行时通过 libandroid_runtime.so 中的 JNI 接口调用系统 native api

2.3 liblog 库

liblog 封装了 logd 访问的 socket 接口; liblog 通过 socket 通信完成客户端日志写入 logd;

Native 层使用方式:

① Android.mk 文件

  LOCAL_PATH:= $(call my-dir)
  include $(CLEAR_VARS)
  LOCAL_MODULE:= logtest
  #程序源文件
  LOCAL_SRC_FILES:= \
      main.cpp
  #需要使用到的库文件
  LOCAL_SHARED_LIBRARIES := \
      liblog \
      libutils
  LOCAL_MODULE_TAGS := optional
  include $(BUILD_EXECUTABLE)

② C/C++ 程序

方式一:

#include              
#define LOG_TAG "my_log"          //日志tag
int main(int args,char** argv){
    ALOGE("......");               
    SLOGE("......");              
}

方式二:

// 定义debug信息
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
// 定义info信息
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
// 定义error信息
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)

2.4 logd

守护进程,开机时由 init 进程;logd 内部维护了一个 RAM buffer 用作日志的缓存。各个进程的日志都会写入此缓存中。如果日志大小超过缓存限定,就会删除最老的日志。

注意 logd 对外维护了 3 个 socket api :

  • dev/socket/logd : 传输控制指令
  • dev/socket/logw : 写日志
  • dev/socket/logr : 读日志

三、写日志流程

3.1 流程框图

参考文献 [1]

这里着重关注一下 _write_to_log_init 这个函数,其作用是初始化日志的一些参数,例如建立与 logd 之间的 socket ,流程如下:

__write_to_log_init

      ----> __write_to_log_initialize
            ----> __android_log_config_write (日志配置)
      ----> logd_writer.write
      ----> logd_writer.logdOpen()
      ----> {

              struct sockaddr_un un;
              memset(&un, 0, sizeof(struct sockaddr_un));
              un.sun_family = AF_UNIX;
              strcpy(un.sun_path, "/dev/socket/logdw");

              if (TEMP_FAILURE_RETRY(connect(sock, (struct sockaddr*)&un,
                                             sizeof(struct sockaddr_un))) < 0) {
                 ......

              } 

      }

当有日志写入 logdw 时,LogListeneronDataAvailable 生效,先把日志写入 LogBuffer 中再调用 LogReadernotifyNewLog 函数来通知有新的日志写入。

当收到一个新的日志条目可用时,通知正在监视此条目的日志 idSocket 表示可以进行读取日志了:

void LogReader::notifyNewLog(log_mask_t logMask) {
    // 创建一个FlushCommand 对象,传入LogReader的对象和logmask
    FlushCommand command(*this, logMask);
    //调用socket接口,最终进入 logd的runSocketCommand()
    runOnEachSocket(&command);
}
 
// 运行FlushCommand 的runSocketCommand()
void SocketListener::runOnEachSocket(SocketClientCommand *command) {
    SocketClientCollection safeList;
  ...
    while (!safeList.empty()) {
    ...
        command->runSocketCommand(c);
    ...
    }
}

3.2 writer

这里着重关注一下 __write_to_log_daemon 这个函数,其内部调用了 write_transport_for_each函数, 目的是循环调用所有 writerwrite 方法来传输日志,例如 logdLoggerWritewrite,对应的就是 logdWritepmsgLoggerWrite 对应的就是 pmsgWrite

 
static int __write_to_log_daemon(log_id_t log_id, struct iovec* vec, size_t nr) {
  ......
  write_transport_for_each(node, &__android_log_transport_write) {
    if (node->logMask & i) {
      ssize_t retval;
      //从logdLoggerWrite中拿到write操作,即logdWrite()进行日志的写入
      retval = (*node->write)(log_id, &ts, vec, nr);
      if (ret >= 0) {
        ret = retval;
      }
  }
 
  write_transport_for_each(node, &__android_log_persist_write) {
    if (node->logMask & i) {
       //从pmsgLoggerWrite中拿到write操作,即pmsgWrite()进行日志的写入
      (void)(*node->write)(log_id, &ts, vec, nr);
    }
}

......

这里以 pmsgWrite 为例,其具体的读写如下:

[pmsg_writer.c] pmsgOpen()

说明:打开"/dev/pmsg0"

static int pmsgOpen() {
  int fd = atomic_load(&pmsgLoggerWrite.context.fd);
  if (fd < 0) {
    int i;
 
    fd = TEMP_FAILURE_RETRY(open("/dev/pmsg0", O_WRONLY | O_CLOEXEC));
    i = atomic_exchange(&pmsgLoggerWrite.context.fd, fd);
    if ((i >= 0) && (i != fd)) {
      close(i);
    }
  }
 
  return fd;
}

[pmsg_writer.c] pmsgWrite()

说明:日志写入到"/dev/pmsg0"

 
static int pmsgWrite(log_id_t logId, struct timespec* ts, struct iovec* vec, size_t nr) {
   ......
 
  ret = TEMP_FAILURE_RETRY(writev(atomic_load(&pmsgLoggerWrite.context.fd), newVec, i));
  if (ret < 0) {
    ret = errno ? -errno : -ENOTCONN;
  }
 
  ......
  return ret;
}

四、读日志流程

4.1 流程框图

参考文献 [1]

读取日志操作时,先读入 logdr 中传入的客户端参数,然后把之前 LogBuffer 中的日志通过 flushto,最终通过 socketsendDatav() 写给 client,比如 logcat,所以我们可以开启多个 logcat 来获取日志

4.2 日志解析过程

不同于写日志的流程,日志在保存时是有特定的格式去存储。在读取时 logcat 作为读取的客户端是需要先对日志进行格式解析,并拼接为命令行可见的字符串,因此有了 processBuffer 的过程。同时 logcat 打印日志超过了文件大小限制,就需要调用 rotateLogs 函数去建立新的文件。

参考文献 [1]

五、总结

打印日志是非常消耗资源的!,原因可概括为:

① 跨进程通信的消耗:日志信息通过 socket 发送给 logd
② 内存消耗:logd 中维持对应的 buffer。RAM 的消耗
③ CPU 资源消耗:logd 中 ring buffer 会经常进行 pruneLogs 操作,删减日志,耗费CPU资源。
④ IO 消耗:在应用程序中 ,创建后台线程保存日志信息,这回导致应用或者整机卡顿

参考

[ 1 ] 深入理解安卓日志系统(logcat / liblog / logd)
[ 2 ] Android 10 根文件系统和编译系统(六):log系统和logcat命令
[ 3 ] Android日志系统分析
[ 4 ] Android LOG系统原理剖析
[ 5 ] Android10.0 日志系统分析(二)-logd、logcat架构分析及日志系统初始化
[ 6 ] Android10.0 日志系统分析(三)-logd、logcat读写日志源码分析
[ 7 ] 基于Android P Log系统学习笔记

你可能感兴趣的:(Android 日志系统分析(一):概述)