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 <utils/Log.h>             
#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系统学习笔记

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容