Android_8.1 Log 系统源码分析_liblog.so 源码包-程序员宅基地

技术标签: android 8.1  android源码学习  log  源码  Android  

0x01 Android Log框架推荐

1、 logger

提供了一些格式化输出、美观

// 添加依赖
implementation 'com.orhanobut:logger:2.2.0'

// 初始化
Logger.addLogAdapter(new AndroidLogAdapter());
// 使用
Logger.d("hello,Android");
2、timber

基于原生Log类的小型可扩展的log框架

3、Hugo

使用注解形式的调试版本log框架

4、xLog

可扩展,支持多种数据格式,支持线程和调用栈信息

5、 LogUtils

支持多种数据结构,支持系统对象,支持高性能写入文件(mmap)

6、Log4a

基于mmap内存映射,最大化保证日志完整性

0x02 Android 原生 Log 系统

1、Java 层逻辑

Android 系统 Java 层 Log 定义在 /frameworks/base/core/java/android/util/Log.java

此外还提供了 EventLogSLog【todo】

Log 提供了六种日志级别,并定义了一系列静态方法:

public static int v/d/i/w/e/wtf(String tag, String msg) {
  return println_native(LOG_ID_MAIN, VERBOSE, tag, msg);
}

public static int v/d/i/w/e/wtf(String tag, String msg, Throwable tr) {
  return printlns(LOG_ID_MAIN, VERBOSE, tag, msg, tr);
}

下面这种调用带异常记录,会调用 Log 类内部类 ImediateLogWriter 来写入日志消息,最终也会调用 println_native

说明:Android 中不同的 log 会指定不同的缓冲区然后被写入到不同的设备中,包括 system(系统相关)、radio(无线/电话相关)、event(事件相关)、main(主缓冲区,默认)

本地实现定义在 /frameworks/base/core/jni/android_util_Log.cpp

static const JNINativeMethod gMethods[] = {
   
    
  {
   
     "isLoggable", "(Ljava/lang/String;I)Z", (void*) android_util_Log_isLoggable },
  {
   
     "println_native", "(IILjava/lang/String;Ljava/lang/String;)I", (void*) android_util_Log_println_native },
  {
   
     "logger_entry_max_payload_native", "()I", (void*) android_util_Log_logger_entry_max_payload_native },
};

android_util_Log_isLoggable

  • 检查 tag,取tag,调用 isLoggable 判断

android_util_Log_println_native

2、Native 层逻辑

Native 层通过定义一系列宏的方式提供 log 功能,全部是调用了 __android_log_print

LIBLOG_ABI_PUBLIC int __android_log_print(int prio, const char* tag,
                                          const char* fmt, ...) {
   
    
  va_list ap;
  char buf[LOG_BUF_SIZE];
  va_start(ap, fmt);
  vsnprintf(buf, LOG_BUF_SIZE, fmt, ap);
  va_end(ap);

  return __android_log_write(prio, tag, buf);
}

这个过程使用了可变参数,va 就是 variable-argument,相关宏定义在 stdarg.h

  • 首先定义 va_list 类型变量,是一个指向参数的指针
  • va_start 初始化变量,此函数的第一个参数是指针,第二个参数是固定参数
  • vsnprintf 将可变参数格式化输出到一个字符数组
  • 另外还有 va_arg 获取可变的参数,此函数的第一个参数是指针,第二个参数是可变参数类型
  • va_end 结束可变参数的获取

最后函数会调用 __android_log_write

LIBLOG_ABI_PUBLIC int __android_log_write(int prio, const char* tag,
                                          const char* msg) {
   
    
  return __android_log_buf_write(LOG_ID_MAIN, prio, tag, msg);
}

这里 Java/C++ 层就走到同一个函数,在这个函数中会实现写设备文件

3、liblog.so 逻辑

liblog.so 会被所有需要日志操作的进程加载,负责处理打印和读取日志的流程

主要代码及逻辑都在 /system/core/liblog/logger_write.c 中,__android_log_buf_write 里面做了下面这几件事:

  1. 定义 iovec 结构数组 vec[3],三个元素分别存放 prio、tag、msg

    struct iovec {
         
          
      void* iov_base;
      size_t iov_len;
    };
    

    iovec 结构体包含一个指向缓冲区的指针和读/写的长度

  2. 判断 bufID 修改 tag(bufID 这个变量表示不同的 log 缓冲区,或者理解成写到不同的文件里)

  3. 最后调用 write_to_log

write_to_log 是一个函数指针,初始设置指向 __write_to_log_init,进入 __write_to_log_init 后,首先会去调用 __write_to_log_initialize,然后将 write_to_log 设置为指向 __write_to_log_daemon,然后又调用一次 write_to_log

static int (*write_to_log)(log_id_t, struct iovec* vec, size_t nr) = __write_to_log_init;


static int __write_to_log_init(log_id_t log_id, struct iovec* vec, size_t nr) {
   
    
  __android_log_lock();
  if (write_to_log == __write_to_log_init) {
   
    
    int ret;
    ret = __write_to_log_initialize();
    if (ret < 0) {
   
    
      __android_log_unlock();
      if (!list_empty(&__android_log_persist_write)) {
   
    
        __write_to_log_daemon(log_id, vec, nr);
      }
      return ret;
    }
    write_to_log = __write_to_log_daemon;
  }
  __android_log_unlock();
  return write_to_log(log_id, vec, nr);
}

__write_to_log_initialize__write_to_log_daemon 实现太复杂了,总结下来就是 initialize 基于默认配置构造结构体链表,daemon 从链表中取出节点,就是 android_log_transport_write,节点的结构体的定义在liblog/logger.h

struct android_log_transport_write {
   
    
  struct listnode node;
  const char* name;                  /* human name to describe the transport */
  unsigned logMask;                  /* mask cache of available() success */
  union android_log_context context; /* Initialized by static allocation */

  int (*available)(log_id_t logId); /* Does not cause resources to be taken */
  int (*open)();   /* can be called multiple times, reusing current resources */
  void (*close)(); /* free up resources */
  int (*write)(log_id_t logId, struct timespec* ts, struct iovec* vec, size_t nr); /* write log to transport, returns number of bytes propagated, or -errno */
};

初始化 的时候会调用 __android_log_config_write 会基于不同的场景定义不同的结构去写日志,包括 localLoggerWritelogdLoggerWritepmsgLoggerWritefakeLoggerWritestderrLoggerWrite,调用 retval = (*node->write)(log_id, &ts, vec, nr) 进行 write 操作

LIBLOG_HIDDEN void __android_log_config_write() {
   
    
  if (__android_log_transport & LOGGER_LOCAL) {
   
    
    extern struct android_log_transport_write localLoggerWrite;
    __android_log_add_transport(&__android_log_transport_write,
                                &localLoggerWrite);
  }

  if ((__android_log_transport == LOGGER_DEFAULT) || (__android_log_transport & LOGGER_LOGD)) {
   
    
#if (FAKE_LOG_DEVICE == 0)
    extern struct android_log_transport_write logdLoggerWrite;
    extern struct android_log_transport_write pmsgLoggerWrite;
    __android_log_add_transport(&__android_log_transport_write, &logdLoggerWrite);
    __android_log_add_transport(&__android_log_persist_write, &pmsgLoggerWrite);
#else
    extern struct android_log_transport_write fakeLoggerWrite;
    __android_log_add_transport(&__android_log_transport_write, &fakeLoggerWrite);
#endif
  }

  if (__android_log_transport & LOGGER_STDERR) {
   
    
    extern struct android_log_transport_write stderrLoggerWrite;
    if (list_empty(&__android_log_transport_write)) {
   
    
      __android_log_add_transport(&__android_log_transport_write, &stderrLoggerWrite);
    } else {
   
    
      struct android_log_transport_write* transp;
      write_transport_for_each(transp, &__android_log_transport_write) {
   
    
        if (transp == &stderrLoggerWrite) {
   
    
          return;
        }
      }
      __android_log_add_transport(&__android_log_persist_write, &stderrLoggerWrite);
    }
  }
}

看其中比较重要的几个,logdLoggerWrite 的定义在 /system/core/liblog/logd_writer.c

LIBLOG_HIDDEN struct android_log_transport_write logdLoggerWrite = {
   
    
  .node = {
   
     &logdLoggerWrite.node, &logdLoggerWrite.node },
  .context.sock = -EBADF,
  .name = "logd",
  .available = logdAvailable,
  .open = logdOpen,
  .close = logdClose,
  .write = logdWrite,
};

其中,logdOpen 方法创建 sockaddr_un 结构体并将 “/dev/socket/logdw” 写入 sun_path 成员变量中,然后调用 connect 去建立连接,并将套接字标识放到 logdLoggerWrite.context.sock 中;write 函数指针指向 logdWrite 方法,会调用 writev 非阻塞地写入日志信息

同理,pmsgLoggerWrite 打开的是 “/dev/pmsg0” 的 socket

所以,liblog.so 的一个主要工作就是写入日志

4、logd 逻辑

使用 file 命令查看 /dev/socket/logdw 发现是一个 socket,这个 socket 是由 logd 创建的,见 /system/corelogd/logd.rc

service logd /system/bin/logd
    socket logd stream 0666 logd logd
    socket logdr seqpacket 0666 logd logd
    socket logdw dgram+passcred 0222 logd logd
    file /proc/kmsg r
    file /dev/kmsg w
    user logd
    group logd system package_info readproc
    writepid /dev/cpuset/system-background/tasks

logd 是 C 层的守护进程,由 init 进程创建(创建 servicemanager 时同时创建),可以看到启动 logd 后会创建三个 socket,分别为 logd 用来监听命令、logdr 用于读日志、logdw 用于写日志,还打开了两个文件,修改了 user id 和 group id,并把 pid 写文件

logd 启动后会从 main 函数开始执行,见 /system/core/logd/main.cpp

第一步、打开 /dev/kmsg 用于写内核 log 的,在 logd 还未启动或出错时,只能写到内核日志中

int main(int argc, char* argv[]) {
   
    

  	......
      
  	static const char dev_kmsg[] = "/dev/kmsg";
    fdDmesg = android_get_control_file(dev_kmsg);
    if (fdDmesg < 0) {
   
    
        fdDmesg = TEMP_FAILURE_RETRY(open(dev_kmsg, O_WRONLY | O_CLOEXEC));
    }

    ......

}

android_get_control_file 里面先调用 __android_get_control_from_env 拼接 Android 文件前缀 ANDROID_FILE_ 和路径 /dev/kmsg,然后做符号转换得到 ANDROID_FILE__dev_kmsg,接下来通过 getenv 获取这个环境变量的值,最后通过 strtol 将这个值转换成 long 类型就是文件描述符,还要通过 fcntl 去验证下文件是不是开着


这里用了三种方法去验证:

  1. 通过访问文件表获取 flag,最快;
  2. 通过文件表去拿 file 对象,然后获取 flag,其次;
  3. 通过文件表拿到 file 对象,然后进一步拿到 inode 节点数据,再获取 stat,最慢。

__android_get_control_from_env 返回文件描述符后,还会调用 /proc/self/fd/fd_num 验证一次

第二步、打开 /proc/kmsg 用于读内核日志

int main(int argc, char* argv[]) {
   
    

  	......
  	
  	bool klogd = __android_logger_property_get_bool(
        "logd.kernel", BOOL_DEFAULT_TRUE | BOOL_DEFAULT_FLAG_PERSIST |
                           BOOL_DEFAULT_FLAG_ENG | BOOL_DEFAULT_FLAG_SVELTE);
    if (klogd) {
   
    
        static const char proc_kmsg[] = "/proc/kmsg";
        fdPmesg = android_get_control_file(proc_kmsg);
        if (fdPmesg < 0) {
   
    
            fdPmesg = TEMP_FAILURE_RETRY(
                open(proc_kmsg, O_RDONLY | O_NDELAY | O_CLOEXEC));
        }
        if (fdPmesg < 0) android::prdebug("Failed to open %s\n", proc_kmsg);
    }
   
    ......

}

第三步、启动 reinit 线程,处理 --reinit 命令,此外这个线程还会完成 uid 转 name

第四步、设置运行时优先级和权限【略】

第五步、启动 log 监听

  • 创建 LogBuffer 对象,负责管理所有的 log entries,可以理解为存放 log,对应线程“logd.auditd”
  • 创建 LogReader 对象,监听 /dev/socket/logdr,有客户端接入时就把 logBuf 中的 log entry 给客户端,即管理客户端读取 log
  • 创建 LogListener 对象,监听 /dev/socket/logdw,负责将 log entry 写入 logBuf,即写入 log
  • 创建 CommandListener 对象,监听 /dev/socket/logd,接收各种命令
  • LogAudit 读取 selinux 的日志
  • LogKlog 读取 kernel 的日志,写到 logBuf 中

看一下 logd 的线程,可以看到 logd 进程 pid = 574,ppid = 1,即 init 进程孵化,reinit_thread_start 函数启动线程“logd.daemon”,LogReader 启动线程“logd.reader”监听 /dev/socket/logdr,LogListener 启动线程“logd.writer”监听 /dev/socket/logdw,CommandListener 启动线程“logd.control”监听 /dev/socket/logd,LogAudit 启动线程“logd.auditd”,LogKlog 启动线程“logd.klogd”,LogTimeEntry 启动线程“logd.reader.per”

logd	574   574   1   32428   4912 SyS_rt_sigsuspend 753d7b1634 S logd
logd	574   577   1   32428   4912 futex_wait_queue_me 753d7644b0 S logd.daemon
logd	574   578   1   32428   4912 do_select  753d7b15a4 S logd.reader
logd	574   579   1   32428   4912 do_select  753d7b15a4 S logd.writer
logd	574   580   1   32428   4912 do_select  753d7b15a4 S logd.control
logd	574   582   1   32428   4912 do_select  753d7b15a4 S logd.auditd
logd	574 14402   1   32428   4912 futex_wait_queue_me 753d7644b0 S logd.reader.per
4.1、LogBuffer 逻辑

LogBuffer 继承自 LogBufferInterface,类内部定义了很多成员变量和一些函数,包括:

  • LogBufferElementCollection
  • LogBufferElement 指针变量:lastLoggedElements 和 droppedElements
  • LastLogTimes
  • 构造和析构方法,init/log 方法

LogBufferElementCollection 是一个 LogBufferElement 指针类型的 list

LogBufferElement 中存放 LogBuffer 元素的相关信息,包括 uint32_t 类型的 uid/pid/tid,还有 realTime、消息内容 msg,logId 等;

LastLogTimes 是一个 LogTimeEntry 指针类型的 list,两者都定义在 LogTime 中,LogTimeEntry 里面包

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_35016347/article/details/102687162

智能推荐

【转载】电脑店超级U盘装系统-设置U盘启动_电脑店u盘装系统有捆绑吗-程序员宅基地

文章浏览阅读1.5k次。电脑店超级U盘装系统-设置U盘启动(V5.0 UD+ISO二合一版 )2012-11-30 21:02 点击进入电脑店超级U盘装系统-程序下载和运行(V5.0 UD+ISO二合一版 ) 点击进入电脑店超级U盘装系统-个性化设置和升级安装(V5.0 UD+ISO二合一版 ) 点击进入电脑店超级U盘装系统-设置U盘启动(V5.0 UD_电脑店u盘装系统有捆绑吗

【Unity】NavMeshAgent与Rigidbody冲突问题_navmeshagent不能和物理效果同时生效吗-程序员宅基地

文章浏览阅读4.7k次,点赞9次,收藏25次。同一物体同时开启NavMeshAgent和Rigidbody时,经常会发生一些意想不到的受力问题,经实测,NavMeshAgent在自动导航时,并不是直接改变物体的位移,会赋予物体一定的速度。关于组件:参考:Rigidbody、CharacterController和NavMeshAgent的区别Rigidbody是用来模拟真实物理效果的,它可以设置重力,可以为对象施加外力。注意它和..._navmeshagent不能和物理效果同时生效吗

docker部署php+nginx_dockerfile 安装php+nginx-程序员宅基地

文章浏览阅读275次。临近国庆,又回过头来鼓捣docker,因为从事php开发,所以还是先从环境入手。本来考虑搭建php+mysql+nginx+redis全部,但是由于使用的都是公司的mysql和redis,故只搭建php+nginx,因为我的操作系统是win10,一下操作都是在win下完成的。首先先拉取镜像,当然你也可以自己编写dockerfile去构建自己的镜像。这里先拉取nginx镜像:docker pu..._dockerfile 安装php+nginx

计算机毕业设计吊打导师Hadoop+Hive+PySpark旅游景点推荐 旅游推荐系统 景区游客满意度预测与优化 Apriori算法 景区客流量预测 旅游大数据 景点规划 知识图谱 机器学习 深度学习-程序员宅基地

文章浏览阅读1.2k次,点赞41次,收藏16次。计算机毕业设计吊打导师Hadoop+Hive+PySpark旅游景点推荐 旅游推荐系统 景区游客满意度预测与优化 Apriori算法 景区客流量预测 旅游大数据 景点规划 知识图谱 机器学习 深度学习 人工智能

Google APK Crash 解决方案-程序员宅基地

文章浏览阅读1.2k次。阅读五分钟,每日十点,和您一起终身学习,这里是程序员Android本篇文章主要介绍开发中我们没有源码的GMS Crash崩溃后的解决方案,通过阅读本篇文章,您将收获以下内容:一、gms.ui Service not registered CrashGMS(GoogleMobile Service)包是出口国外手机中Google限制必须要预制的,如果不预置无法过Google CTS认证,会导致手...

Java多线程(超详细!)-程序员宅基地

文章浏览阅读10w+次,点赞880次,收藏3.9k次。1、什么是进程?什么是线程?进程是:一个应用程序(1个进程是一个软件)。线程是:一个进程中的执行场景/执行单元。注意:一个进程可以启动多个线程。eg.对于java程序来说,当在DOS命令窗口中输入:java HelloWorld 回车之后。会先启动JVM,而JVM就是一个进程。JVM再启动一个主线程调用main方法。同时再启动一个垃圾回收线程负责看护,回收垃圾。最起码,现在的java程序中至少有两个线程并发,一个是垃圾回收线程,一个是执行main方法的主线程。3、进程和线程是什么关系?_java多线程

随便推点

ibm邮箱连接不到服务器,IBM i 安全邮件配置和常见故障排除方法-程序员宅基地

文章浏览阅读2.8k次。BodyIBM i安全邮件配置和常见故障排除方法简介:电子邮件是现在普遍使用的一种通信方式,为了提高通信过程中的安全并且保护邮件内容不被泄露,IBM i SMTP增加了对TLS的支持, 通过此技术保障了邮件通信过程中的安全和数据的不被篡改。本文提供了IBM i V7R2及以上版本安全邮件的配置方法和常见故障排除方法。术语缩写:SMTP:Simple Mail Transfer ProtocolTL..._邮件dcm

VLAN(虚拟局域网)_2019vlan-程序员宅基地

文章浏览阅读402次。创建VLAN的指令:VLAN +ID号(取值范围1-4094,1是默认存在)用来连接终端设备的(pc,打印机,监控摄像头)片面认为,access接口下面接的一定是终端。交换网络本身的工作原理是什么?交换机是基于MAC地址表转发的。hybrid(华为特有)_2019vlan

静态手势识别总体方案_手势识别有哪些方案-程序员宅基地

文章浏览阅读4.8k次,点赞3次,收藏44次。静态手势识别总体方案0.说明1.实现目标2.实现步骤1)总体思路2)每部分效果基于高斯肤色模型和动态阈值的手势分割基于Canny算法的轮廓提取基于Hu矩的量化基于傅里叶描述子的量化分类融合特征分类其他尝试0.说明静态手势识别是2019年四五月份做的一次设计,实验平台是Matlab。主要针对静态手势,采用肤色模型分离手部区域,提取手势的轮廓信息,采用不同的描述方式进行量化,最后采用BP神经网络和..._手势识别有哪些方案

机器学习(Machine Learning)&深度学习(Deep Learning)资料-程序员宅基地

文章浏览阅读1.4w次。##机器学习(Machine Learning)&深度学习(Deep Learning)资料(Chapter 1)---#####注:机器学习资料[篇目一](https://github.com/ty4z2008/Qix/blob/master/dl.md)共500条,[篇目二](https://github.com/ty4z2008/Qix/blob/master/dl2.md)开始更新...

2021-05-03 GTX650刷入UEFI模块_gtx650 uefi-程序员宅基地

文章浏览阅读9.7k次。GTX650刷入UEFI模块gtx650黑苹果需要关闭csm模式,但是微软规定csm模式关闭必须得所有pcie设备开启uefi模式,但是老的gtx650中vbios没有uefi模块,所以需要先刷vbios,刷入方法在我的资源解压后的Inno3D UEFI Updater中的readme中,前提需要开启主板uefi模式,最好用gpuz备份好原来的bios。下面是简单的步骤,仅供参考,目前是实现了华硕GTX650-FMLII-1GD5的UEFI刷入,其他的显卡并没有实物卡测试。打开主板UEFI模式:._gtx650 uefi

【华为机试真题 Python实现】华为机试题整理(已更新211篇)_华为机试垂直矩阵-程序员宅基地

文章浏览阅读2.6w次,点赞26次,收藏269次。拆分输出字符串求n阶方阵里所有数的和合法的三角形个数整型数组求整数对最小和机器人走迷宫【2022 Q1 Q2 |200分】数格子两个超大整型数相加字符串格式化输出【2022 Q1 Q2 |100分】树形目录操作【2022 Q1 Q2 |200分】整型数组按个位值排序奥运会奖牌榜的排名【2022 Q1 | 100分】无重复字符的最长子串最长回文子串两个字符串的最长公共子串括号生成字符串处理一个正整数到 Excel 编号之间的转换字符串压缩搜索矩阵免单统计数组的转换藏宝_华为机试垂直矩阵