一、前言
在实际项目中经常会打印关键日志信息来反馈程序运行状况。例如 App 中常使用的 Log.d、Log.v 等,而在 Native 层会使用 ALOGD 打印日志。对于第三方添加的 C/C++ 应用程序来说,如果希望使用 Android 的日志系统,就需要添加 liblog 库。而笔者面临的问题是在定制化的系统中研究 liblog 库是否满足我们的需求,或者修改 logd 这样的的守护进程,通过配置实现独特的分流?在此之前从来没有了解过这一块,因此本系列文章可以说是在众多参考文献上的一次总结。
二、日志系统框架
首先我们来看一下 Android (基于9.0)日志系统框架图:
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 流程框图
这里着重关注一下 _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 时,LogListener 的 onDataAvailable 生效,先把日志写入 LogBuffer 中再调用 LogReader 的 notifyNewLog 函数来通知有新的日志写入。
当收到一个新的日志条目可用时,通知正在监视此条目的日志 id 的 Socket 表示可以进行读取日志了:
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函数, 目的是循环调用所有 writer 的 write 方法来传输日志,例如 logdLoggerWrite 的 write,对应的就是 logdWrite,pmsgLoggerWrite 对应的就是 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 流程框图
读取日志操作时,先读入 logdr 中传入的客户端参数,然后把之前 LogBuffer 中的日志通过 flushto,最终通过 socket 的 sendDatav() 写给 client,比如 logcat,所以我们可以开启多个 logcat 来获取日志
4.2 日志解析过程
不同于写日志的流程,日志在保存时是有特定的格式去存储。在读取时 logcat 作为读取的客户端是需要先对日志进行格式解析,并拼接为命令行可见的字符串,因此有了 processBuffer 的过程。同时 logcat 打印日志超过了文件大小限制,就需要调用 rotateLogs 函数去建立新的文件。
五、总结
打印日志是非常消耗资源的!,原因可概括为:
① 跨进程通信的消耗:日志信息通过 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系统学习笔记