目录
- 1 介绍
- 2 ADB的原理
- 2.1 adbd服务进程
- 2.2 ADB 服务端
- 2.2.1 USB方式
- 2.2.2 网络方式
- 2.2.3串口方式
- 2.3 ADB 客户端
- 3 安卓框架中日志的收集和存储
- 3.1 Android API level L之前的log处理
- 3.1.1 日志的初始化和创建MISC设备节点
- 3.1.2 日志写操作
- 3.1.3 日志读操作
- 3.2 Android API level L之后的log处理
- 3.2.1 Android 日志打印的流程
- 3.2.2 logd进程的初始化
- 3.2.3 以LogListener为例分析logd对log写入的处理
- 3.2.4 logd读取log浅释
- 3.2.5 对这部分的说明
- 4 logcat的使用
- 4.1 log的组成
- 4.2 logcat的不同选项
- 4.3 logcat的过滤项
- 5 引用文章
1 介绍
本篇文章介绍一下安卓的日志系统,包括以下几个要点:
- ADB的原理
- 安卓框架中日志的收集和存储
- logcat的使用
2 ADB的原理
安卓ADB(Android Debug Bridge),包含以下三部分内容:
- adbd服务进程 是运行在安卓手机内部一个服务进程,等待来自调试PC机的连接
- ADB 服务端 一般是运行在调试安卓的PC机中,作为客户端连接到安卓手机的adbd服务上
- ADB 客户端 可执行程序放在SDK目录的platform-tools文件夹下,作为客户端连接到adb服务进程上
他们之间的关系如下图所示
2.1 adbd服务进程
通过以下命令开启关闭adbd服务
start adbd//开启
stop adbd//关闭
2.2 ADB 服务端
通过以下方式开启或关闭adb服务端,windows系统的adb server会默认使用5037端口创建socket与客户端通信
开启关闭adb服务端通过以下命令
adb start-srver//开启
adb kill-server//关闭
PC调试机连接安卓手机adbd可以通过三种方式
- USB
- 网络
- 串口
2.2.1 USB方式
当然adbd会为PC机的连接分配一个端口,当在USB模式下,端口号<=-1,手机开启USB调试即可,PC端键入命令:
adb devices
可以查看当前连接到PC的手机或模拟器,显示的第一列是设备串号,第二列是设备的状态,有device(连接),offline(掉线)和no device三个状态,
当目前只有一个设备连接的时候,直接输入
adb shell
就可以进入该设备的shell控制台;
当有一个是模拟器一个是真机的时候,可以使用
adb -e shell//进入模拟器
adb -d shell//进入真机
进行区分,而当有多个设备连接时,可以使用-s选项通过序列号区别不同的设备,命令如下
adb -s <序列号> shell
2.2.2 网络方式
从adb v1.0.25开始支持通过网络方式(TCP)进行连接调试,让手机与电脑在一个局域网中,在PC端输入
adb connect <ip>:<port>
连接到指定的手机,端口号可以省略,也可以使用 adb 的shell命令
adb -s <ip>:<port> shell
ADB的说明文档节选
“An ADB transport models a connection between the ADB server and one device
or emulator. There are currently two kinds of transports:
- USB transports, for physical devices through USB
- Local transports, for emulators running on the host, connected to
the server through TCP”
以上内容的大致意思是: 模拟器一般就是通过tcp连接到电脑adb服务端的,adbd在启动时,首先会尝试获取系统属性service.adb.tcp.port,如果获取到就会使用tcp来进行连接,否则查看是否有USB连接,如果有就是要USB了,否则还是使用tcp方式。
port默认5555,此端口可以更改, 通过以下代码
stop adbd
setprop service.adb.tcp.port <端口号>
start adbd
或者这样也可以
adb tcpip <port>
2.2.3串口方式
串口方式需要使用到一个叫做busybox的工具,可以到这里下载对应的平台包,想办法获取到/system目录的写入权限,把busybox放入发到/system/bin目录下,然后使用如下命令开启串口:
busybox microcom -t 15000 -s 115200 /dev/ttyS0
-t 单位时毫秒 表示无操作的自动退出的时间
-s表示波特率
/dev/ttyS0 串口设备文件
这样就创建了一个串口设备文件,可以可以另开一个终端,输入
adb shell cat /dev/ttyS0
查看串口接收的消息,可以通过
adb shell echo <text> >/dev/ttyS0
向串口发送消息
2.3 ADB 客户端
adb客户端完成的任务是通过向服务端发送各种命令来实现安卓应用程序的调试,例如:
- adb shell 打开一个shell交互程序实现像控制linux一样控制安卓手机,但是该shell功能不全
- adb push 实现安卓手机和电脑的文件传输
- adb devices 列出连接到adb服务端的手机列表
- adb install/uninstall 安装/卸载应用
adb 中有一个命令是今天的重点,就是adb logcat命令,也可以进入到adb shell中直接执行logcat命令,adb logcat命令是由adb shell logcat映射过来的,所以使用
- adb shell logcat
- adb logcat
- 进入shell后再执行logcat命令
这三者的效果是一样的。
在看logcat的使用之前,我们先来看看安卓的日志系统结构概貌
3 安卓框架中日志的收集和存储
3.1 Android API level L之前的log处理
安卓提供了一个轻量级的日志框架,是以驱动程序的方式实现在内核空间的,这个驱动程序为Java层和C++提供了写log的接口,在Android L 之前,日志系统是使用linux kernel的几个环形缓冲区Ring Buffer
来管理实现的,下面我们先来看一张结构图
解释一下:图中可以看出中间的虚线上面的进程运行在用户空间,下边是内核空间。我们用java语言编写安卓APP的时候,使用安卓的log工具android.util.Log
打印出来的log,都会通过JNI的方式调用liblog里面的函数,liblog是一个动态链接库,而在liblog里面的函数最终会陷入内核运行,通过misc设备驱动程序完成log的写入。进而缓冲区中的log会被logcat,adbd等读取进程读取,进而输出到标准输出或通过adbd调试服务到达adb shell。
现在我们通过安卓的源代码来简要看看这个日志处理的过程。源代码位置在安卓源代码目录的driver/staging/android下面,里面有两个文件logger.c和logger.h实现了log的处理。
3.1.1 日志的初始化和创建MISC设备节点
在logger.c中,初始化函数是logger_init函数,代码片段如下:
static int __init logger_init( void )
{
/*
* ...省略
* 创建四个四个log缓冲区,也就是RING BUFFER,
* 分别是main(主缓冲区),events(事件相关),radio(无线通信相关),system(系统相关)
* 大小都是256KB
*/
ret = create_log( LOGGER_LOG_MAIN, 256 * 1024 );
ret = create_log( LOGGER_LOG_EVENTS, 256 * 1024 );
ret = create_log( LOGGER_LOG_RADIO, 256 * 1024 );
ret = create_log( LOGGER_LOG_SYSTEM, 256 * 1024 );
/* ...省略 */
}
LOGGER_LOG_**是预定义宏,他们的定义如下:
#define LOGGER_LOG_RADIO "log_radio" /* radio-related messages */
#define LOGGER_LOG_EVENTS "log_events" /* system/hardware events */
#define LOGGER_LOG_SYSTEM "log_system" /* system/framework messages */
#define LOGGER_LOG_MAIN "log_main" /* everything else */
我们先给出不同类别log实体的定义,然后在分析create_log函数都做了什么,log消息结构体定义如下:
struct logger_log {
unsigned char *buffer; /* 指向四个环形缓冲区的首地址 */
struct miscdevice misc; /* linux把无法归类的设备都叫做混杂设备,用于管理log的设备也是*/
/*混杂设备,这个保存的是描述日志的混杂设备 */
wait_queue_head_t wq; /* 日志读取进程的等待队列 */
struct list_head readers; /* 读取进程的链表 */
struct mutex mutex; /* 用于进程间资源互斥的信量 */
size_t w_off; /* 当前写入的位置与缓冲区首地址的偏移量 */
size_t head; /* 应该是读取进程当前读取的位置与缓冲区首地址的偏移量 */
size_t size; /* 当前的日志长度 */
struct list_head logs; /*不知道这个含义 */
};
这里给出这个定义的官方注释,如下:
/**
*struct logger_log - represents a specific log, such as ‘main’ or ‘radio’
*@buffer: The actual ring buffer
*@misc: The “misc” device representing the log
*@wq: The wait queue for @readers
*@readers: This log’s readers
*@mutex: The mutex that protects the @buffer
*@w_off: The current write head offset
*@head: The head, or location that readers start reading at.
*@size: The size of the log
*@logs: The list of log channels
*This structure lives from module insertion until module removal, so it does
*not need additional reference counting. The structure is protected by the
*mutex ‘mutex’.
*/
create_log函数的实现如下:
static int __init create_log( char *log_name, int size )
{
... /* 省略 */
log->buffer = buffer; /* 分配缓冲区,这里的log就是上面的logger_log类型 */
ret = misc_register( &log->misc ); /* 为log注册misc设备文件 */
... /* 省略 */
}
可见,logger_init函数通过create_log函数创建了四个logger_log 结构体实体,并为其中的属性赋值,并创建了四个混杂设备用于与其读写进程通信。在/dev/log目录下,有四个设备文件,分别就是在logger_init创建的:
root@U:/dev/log # ls
events
main
radio
system
这里简要介绍下这几个设备文件是如何生成的,misc_register(&log->misc)
的调用,调用层次如下:
- misc_register(…)
- device_create(…)
- device_add(…)
- kobject_uevent(&dev->kobj, KOBJ_ADD)
kobject_uevent函数就会发送一个KOBJ_ADD的UEVENT给到上层,这个UEVENT事件最终会由init进程处理,处理过程在devices.cpp文件中,代码片段如下:
...//省略
} else if(!strncmp(uevent->subsystem, "misc", 4) &&!strncmp(name, "log_", 4)) {
//判断UEVENT的子系统名称是否为misc开头,和name(其实就是create_log的参数log_name)是否为log_开头
base = "/dev/log/";//父目录
make_dir(base, 0755);//权限0755
name += 4;//名称指针向前跳,用于判断后面的内容,根据后面的名称,创建相应的设备文件
...//省略
用户进程通过操作这四个设备文件,实现日志的读取和写入,在linux中所有打开的文件通过一个叫做文件描述符的数据结构来表示,其中有一个f_op字段指向一个叫做file_operations
的结构体,这个结构体中含有操作该文件的一系列函数,用于操作日志的file_operations实体定义如下:
static const struct file_operations logger_fops = {
.owner = THIS_MODULE, /* 拥有者,表示属于那个模块 */
.read = logger_read, /* read函数的函数指针 */
/*下面的函数不太清楚 */
.aio_write = logger_aio_write, /* 写入日志设备文件的函数指针 */
.poll = logger_poll,
.unlocked_ioctl = logger_ioctl,
.compat_ioctl = logger_ioctl,
.open = logger_open,
.release = logger_release,
};
3.1.2 日志写操作
一次写操作需要写入的内容如下,struct logger_entry | priority | tag | msg
对应于logcat打印出的日志格式12-06 16:12:23.849 3700 3788 | D | WifiStateMachine | : handleMessage: X
可以看出他们的对应关系,其中
struct logger_entry----------12-06 16:12:23.849 3700 3788
在前面提到写入日志的函数是logger_aio_write,现在我们来看看他的实现:
/*
* logger_aio_write - our write method, implementing support for write(),
* writev(), and aio_write(). Writes are our fast path, and we try to optimize
* them above all else.
* iocb表示io上下文
* iov表示要写入的内容的首地址
* nr_segs表示要写入的长度
*/
ssize_t logger_aio_write(struct kiocb *iocb, const struct iovec *iov,
unsigned long nr_segs, loff_t ppos) {
//这里的代码运行在内核空间
struct logger_log *log = file_get_log(iocb->ki_filp);
size_t orig = log->w_off;
struct logger_entry header; //在这里构造logger_entry 结构体,也就是日志的头部,
struct timespec now;
ssize_t ret = 0;
now = current_kernel_time();
header.pid = current->tgid;
header.tid = current->pid;
header.sec = now.tv_sec;
header.nsec = now.tv_nsec;
header.len = min_t(size_t, iocb->ki_left, LOGGER_ENTRY_MAX_PAYLOAD);
/* null writes succeed, return zero */
if (unlikely(!header.len))
return 0;
mutex_lock(&log->mutex);
/*
* Fix up any readers, pulling them forward to the first readable
* entry after (what will be) the new write offset. We do this now
* because if we partially fail, we can end up with clobbered log
* entries that encroach on readable buffer.
* 由于缓冲区是环形的,循环使用的,当缓冲区满的时候,写指针会走到读指针的
* 前面,从而覆盖了读进程正在读取的内容,所以这里要做调整,会暂时丢弃这条读
* 使得读指针到一个合适的位置
*/
fix_up_readers(log, sizeof(struct logger_entry) + header.len);
//写入日志头部
do_write_log(log, &header, sizeof(struct logger_entry));
//循环nr_segs次
while (nr_segs-- > 0) {
size_t len;
ssize_t nr;
/* figure out how much of this vector we can keep */
/**
*在这里会比较剩余的信息长度是否超过了MAX_PAYLOAD(4KB),如果超过
信息会被截断,一般是不会超过
*/
len = min_t(size_t, iov->iov_len, header.len - ret);
/* write out this segment's payload */
//写入日志的剩余部分,由于iov是从用户空间传过来的,所以调用
//do_write_log_from_user函数,而头部logger_entry header;
//是在内核空间构造的,所以调用do_write_log函数,该函数仅仅使用
//memcpy函数即可
nr = do_write_log_from_user(log, iov->iov_base, len);
if (unlikely(nr < 0)) {
log->w_off = orig;
mutex_unlock(&log->mutex);
return nr;
}
iov++;
ret += nr;
}
mutex_unlock(&log->mutex);
/* 唤醒等待的读取进程 */
wake_up_interruptible(&log->wq);
return ret;
}
日志头部的写入 do_write_log函数
/*
* do_write_log - writes 'len' bytes from 'buf' to 'log'
*
* The caller needs to hold log->mutex.
* log 要被写入的log结构体
* buf包含要写入内容的缓冲区,就是log_entry结构体
* count 写入的长度
*/
static void do_write_log(struct logger_log *log, const void *buf, size_t count)
{
size_t len;
//这里是取count和当前log环形缓冲区剩余长度的二者的最小值
len = min(count, log->size - log->w_off);
//仅仅调用memcpy函数,因为这是内核空间中构造的
//log->buffer + log->w_off:首地址+偏移量=要写入的地址
memcpy(log->buffer + log->w_off, buf, len);
if (count != len)//写入的长度不是count,说明没写完,就写入剩下的
memcpy(log->buffer, buf + len, count - len);
//更新写入指针
log->w_off = logger_offset(log->w_off + count);
}
日志剩余部分的写入:do_write_log_from_user函数
/*
* log 要被写入的log结构体
* buf包含要写入内容的缓冲区,就是log_entry结构体
* count 写入的长度
*/
static ssize_t do_write_log_from_user(struct logger_log *log,
const void __user *buf, size_t count)
{
size_t len;
//方法和思路与do_write_log差不多,只不过由于剩余部分是用户空间传过来
//这里需要调用copy_from_user函数而已
len = min(count, log->size - log->w_off);
if (len && copy_from_user(log->buffer + log->w_off, buf, len))
return -EFAULT;
if (count != len)
if (copy_from_user(log->buffer, buf + len, count - len))
return -EFAULT;
log->w_off = logger_offset(log->w_off + count);
return count;
}
前面有一个函数是fix_up_readers,用于调整当本次写操作会覆盖正在读取的内容的时候,对读取的指针进行调整:我们看看他如何实现:
static void fix_up_readers(struct logger_log *log, size_t len) {
//log 操作的log结构体
//len 当前要写入的日志长度
size_t old = log->w_off;//获取上次写入之后的偏移量
size_t new = logger_offset(old + len); //计算得到本次log写入之后的偏移量
struct logger_reader *reader;
if (clock_interval(old, new, log->head))//判断当前的读取指针是否会被覆盖
log->head = get_next_entry(log, log->head, len);//调整
list_for_each_entry(reader, &log->readers, list)//对于所有读取进程
if (clock_interval(old, new, reader->r_off))
reader->r_off = get_next_entry(log, reader->r_off, len);
}
以上代码会判断当前的log->head和所有读取进程的读取指针是否会被覆盖,如果会覆盖,就会调用get_next_entry来调整。
clock_interval函数的思路是,判断当前读取的偏移量是否在上次写入之后的偏移量和本次log写入之后的偏移量之间,实现可以很明显看出来:
static inline int clock_interval(size_t a, size_t b, size_t c)
{
if (b < a) {
if (a < c || b >= c)
return 1;
} else {
if (a < c && b >= c)
return 1;
}
return 0;
}
为了便于理解,请看下图,分析了b<a的情况
调整函数get_next_entry的思路是,如果发生了覆盖,就沿着偏移增大的方向一直跑,一直找到第一个不被覆盖的那一条log首地址即可,代码也是很好理解的:
static size_t get_next_entry(struct logger_log *log, size_t off, size_t len)
{
size_t count = 0;
do {
size_t nr = get_entry_len(log, off);
off = logger_offset(off + nr);
count += nr;
} while (count < len);//一直跑到超过len即可
return off;
}
3.1.3 日志读操作
日志读操作在logger_read函数中完成:
static ssize_t logger_read( struct file *file, char __user *buf,size_t count, loff_t *pos ){
while ( 1 ){
prepare_to_wait( &log->wq, &wait, TASK_INTERRUPTIBLE ); /* 在这里进行等待 */
/*
* logger_write函数中在写之后会调用wake_up_interruptible(&log->wq);把正在等待的读取进程唤醒
* 如果读的偏移r_off与写的偏移w_off相等,则循环阻塞,直到write最后唤醒
*/
ret = (log->w_off == reader->r_off);
mutex_unlock( &log->mutex );
if ( !ret )
break; /* 如果不相等就跳出等待的过程。 */
schedule(); /* 进行一次进程调度,等待自己被唤醒 */
}
if ( !reader->r_all ) /* 这里目前不知道是什么作用 */
reader->r_off = get_next_entry_by_uid( log, reader->r_off, current_euid() );
/* 2,获取要读的整个一条日志的长度,同样由两部分组成,一个head长度+信息的长度。 */
ret = get_user_hdr_len( reader->r_ver ) +
get_entry_msg_len( log, reader->r_off );
/* 读ret长度的信息到buf中。 */
ret = do_read_log_to_user( log, reader, buf, ret );
mutex_unlock( &log->mutex );
return(ret);
}
do_read_log_to_user函数的实现:
static ssize_t do_read_log_to_user( struct logger_log *log, struct logger_reader *reader, char __user *buf,size_t count )
{
entry = get_entry_header( log, reader->r_off, &scratch );
/* 拷贝日志头部到用户空间 */
if ( copy_header_to_user( reader->r_ver, entry, buf ) )
return(-EFAULT);
/* 减少读取长度 */
count -= get_user_hdr_len( reader->r_ver );
/* 读取到的buf指针向前移 */
buf += get_user_hdr_len( reader->r_ver );
msg_start = logger_offset( log,
reader->r_off + sizeof(struct logger_entry) );
/* 判断读取是否越过了缓冲区首地址 */
len = min( count, log->size - msg_start );
if ( copy_to_user( buf, log->buffer + msg_start, len ) )
return(-EFAULT);
/*
* ...
* 如果超过了,需要再读取一次,从缓冲区首地址开始,这里体现了环形缓冲区的特性
*/
if ( count != len )
if ( copy_to_user( buf + len, log->buffer, count - len ) )
return(-EFAULT);
/* 移动reader的off偏移 */
reader->r_off = logger_offset( log, reader->r_off + sizeof(struct logger_entry) + count );
return(count + get_user_hdr_len( reader->r_ver ) );
}
3.2 Android API level L之后的log处理
在Android L以及之后,log的处理发生了变化:
- 日志环形缓冲区从内核空间移动到用户空间
- log的读写原来是通过内核misc设备驱动程序执行,现在是由logd进程来管理。
也就是说现在用户进程是通过logd进程向log缓冲区写入读取log的,缓冲区在用户空间。下面我们来看看APP中java程序打印log的一些流程:
3.2.1 Android 日志打印的流程
我们通过安卓SDK里面的日志工具Log.v() Log.d() Log.i() Log.w() and Log.e()等方法打印日志,最终会调用到JNI的接口函数android_util_Log_println_native,我们看一下他的代码片段:
/*
* 此函数是声明在java中的println_native函数的JNI实现
* 此函数的参数
* env:JNI环境上下文
* clazz:调用函数的当前对象,即java里的this
* bufID:缓冲区ID 前面提到的四个缓冲区之一
* priority:日志等级,优先级
* tagObj:日志tag
* msgObj :日志的内容
* 这个JNI函数最终会调用到动态链接库里面的函数
*/
static jint android_util_Log_println_native( JNIEnv* env, jobject clazz, jint bufID, jint priority, jstring tagObj, jstring msgObj )
{
/*
* ...省略
* 先判断ID是否合法
*/
if ( bufID < 0 || bufID >= LOG_ID_MAX )
{
jniThrowNullPointerException( env, "bad bufID" );
return(-1);
}
/* 取出来TAG */
if ( tagObj != NULL )
tag = env->GetStringUTFChars( tagObj, NULL );
/* 取出要写入的Message */
msg = env->GetStringUTFChars( msgObj, NULL );
/* 调用__android_log_buf_write来写入到buffer 这个函数实现在liblog.so中 */
int res = __android_log_buf_write( bufID, (android_LogPriority) priority, tag, msg );
if ( tag != NULL )
env->ReleaseStringUTFChars( tagObj, tag );
env->ReleaseStringUTFChars( msgObj, msg );
return(res);
}
__android_log_buf_write会通过socket请求logd服务,把log交给logd处理,请看下图:
在上面的调用过程中,__write_to_log_initialize函数去请求连接,__write_to_log_daemon函数把数据传给logdw,logdw是logd进程中负责log写入的socket,下面我们来看看logd进程是如何做的。
3.2.2 logd进程的初始化
在系统启动的时候,会执行到init进程,init进程解析init.rc资源文件,执行下面动作:
onload_persist_props_action //加载持久化属性动作列表
load_persist_props
start logd
start logd-reinit
init进程在这里会开启logd服务和logd-reinit服务,在开启这两个服务之前会解析system/core/logd
中的logd.rc
文件,里面配置了启动服务的动作:
service logd /system/bin/logd //开启三个socket
socket logd stream 0666 logd logd //logd socket 用于接收命令
socket logdr seqpacket 0666 logd logd //logdr socket 用于处理读取请求
socket logdw dgram 0222 logd logd //logdw socket 用于处理写请求
group root system readproc
writepid /dev/cpuset/system-background/tasks
启动logd-reinit服务的动作列表;
service logd-reinit /system/bin/logd --reinit
oneshot//只启动一次
disabled
writepid/dev/cpuset/system-background/tasks//记录pid
init进程如何解析这个动作列表,并开启socket呢?
service_start(struct service *svc, const char *dynamic_args){
for (si = svc->sockets; si; si = si->next) {
//读取socket类型,stream或者dgram
int socket_type = (!strcmp(si->type, "stream") ? SOCK_STREAM :(!strcmp(si->type, "dgram") ? SOCK_DGRAM : SOCK_SEQPACKET));
//创建socket 传入名称 类型 权限 uid 组id
int s = create_socket(si->name, socket_type,si->perm, si->uid, si->gid, si->socketcon ?: scon);
if (s >= 0) {
//发布socket,把创建的socket文件描述符写到环境变量,让其它Sokect的Server端通过 android_get_control_socket(mSocketName)来获得socket文件描述符.
publish_socket(si->name, s);
}
}
create_socket函数的代码如下:
int create_socket( const char *name, int type, mode_t perm, uid_t uid, gid_t gid, const char *socketcon )
{
struct sockaddr_un addr; /* 声明一个socket地址结构体 */
int fd, ret;
char *filecon;
/* 调用系统调用socket来创建一个PF_UNIX的socket */
fd = socket( PF_UNIX, type, 0 ); /* 系统调用 */
addr.sun_family = AF_UNIX;
/* addr.sun_path的内容是/dev/socket/{name} */
snprintf( addr.sun_path, sizeof(addr.sun_path), ANDROID_SOCKET_DIR "/%s", name );
/* 把这个socket绑定到addr上,这个addr就与socket设备文件关联 */
ret = bind( fd, (struct sockaddr *) &addr, sizeof(addr) );
/* ... */
}
logd-reinit进程会通过socket向logd进程的logd socket发送reinit命令就会退出,用于logd中log缓冲区的初始化。logd进程开启的时候会创建四个对象用于处理请求。
- LogBuffer log的缓冲区,用于log的存储
- LogReader 用于处理读取进程发来的请求
- LogListener 监听log的写入
- CommandListener 监听发送到logd socket的命令
3.2.3 以LogListener为例分析logd对log写入的处理
这里我们介绍下LogListener 对象,这个对象的目的是监听logdw socket的写入,该对象的会在logd启动的时候创建,请看system/core/logd/main.cpp
中的有关内容如下:
// LogListener listens on /dev/socket/logdw for client
// initiated log messages. New log entries are added to LogBuffer
// and LogReader is notified to send updates to connected clients.
//LogListener监听/dev/socket/logdw 上的客户端初始化log消息,当有一个新的log entry加入到
//缓冲区时,会唤醒LogReader 向其客户端发送更新
LogListener *swl = new LogListener(logBuf, reader);//reader就是LogReader 对象
if (swl->startListener(300)) {//开启监听
exit(1);
}
实际上LogListener继承自SocketListener,在LogListener的构造方法中会构造一个SocketListener的对象,并把参数logBuf
,reader
保存到自己的属性中,LogListener的构造函数如下:
LogListener::LogListener(LogBuffer *buf, LogReader *reader) :
//同时会构造一个父类SocketListener,getLogSocket()是通过logdw这个名字返回一个Socket文件描述符
SocketListener(getLogSocket(), false),
//把两个结构体传过来
logbuf(buf),
reader(reader) {}
再来看SocketListener的构造函数:
SocketListener::SocketListener( int socketFd, bool listen )
{
init( NULL, socketFd, listen, false ); /* 调用init函数,把socketFd和listen保存起来 */
}
void SocketListener::init( const char *socketName, int socketFd, bool listen, bool useCmdNum )
{
mListen = listen;
mSocketName = socketName;
mSock = socketFd;
mUseCmdNum = useCmdNum;
mClients = new SocketClientCollection(); /* 创建一个集合存储到来的请求SocketClient对象 */
/* ... */
}
通过SocketListener的构造函数看到,SocketListener拿到了logdw socket的引用,SocketListener中有一个方法,startListener
,在上面的LogListener构造完之后,就调用的startListener
方法其实是其父类的方法,我们看他的实现:
int SocketListener::startListener(int backlog) {
//...
//创建PID为mThread的线程,线程执行函数是thradStart,并启动 。
if (pthread_create(&mThread, NULL, SocketListener::threadStart, this)) {
SLOGE("pthread_create (%s)", strerror(errno));// 打印log
return -1;
}
return 0;
}
threadStart线程执行函数:
void *SocketListener::threadStart( void *obj )
{
SocketListener *me = reinterpret_cast<SocketListener *>(obj);
me->runListener(); /* 进行监听 */
pthread_exit( NULL );
return(NULL);
}
runListener函数中通过listen
系统调用开启监听请求的到来,然后会调用accept
函数接收请求,为请求分配SocketClient对象:
void SocketListener::runListener() {
...
//listen..
c = accept(mSock, &addr, &alen);//此方法在没有请求的时候阻塞
while (!pendingList.empty()) {//pendingList等待处理的请求队列
it = pendingList.begin(); //依次取出处理
SocketClient* c = *it;
pendingList.erase(it);
if (!onDataAvailable(c))//onDataAvailable这个函数是虚函数,由子类实现,这里就是LogListener来实现
c->decRef();
}
...
}
Linux的Socket Server一般要经过socket,bind,listen,accept,然后处理的过程
对于LogListener来说,socket的创建和bind在init创建logd服务的时候就完成了,listen,accept过程在其父类中完成,数据的处理由自己完成。可以通过下图对上边logd处理日志写入请求的过程进行梳理
在介绍onDataAvailable函数之前,我们先来看两个数据结构:
msghdr ,是一个结构体,是用于Socket在两个进程之间通讯定义的消息头,定义如下:
struct msghdr {
void *msg_name;
socklen_t msg_namelen;
struct iovec *msg_iov; /* scatter/gather array */
size_t msg_iovlen; /* # msg_iov中的元素个数 */
void *msg_control; /*填充数据 */
size_t msg_controllen; /* 填充数据长度 */
int msg_flags; /* flags on received message */
};
msg_control填充数据,是一个指向cmsghdr 结构体的指针,用于存放一些额外的数据,在这里就是存放打印日志进程的pid,uid,gid,msghdr和cmsghdr的关系,如下图所示;
msg_controllen填充数据长度,由于cmsghdr不止一个,所以msg_controllen的计算如下图所示:
CMSG_SPACE是一个计算个附属数据对象的所必需的空白的预定义宏,包括了cmsghdr对象及其填充的数据。
现在我们来看看onDataAvailable函数里都做了什么:
/* ... */
char buffer[sizeof_log_id_t + sizeof(uint16_t) + sizeof(log_time) + LOGGER_ENTRY_MAX_PAYLOAD];
/* 定义一个缓冲区 */
/* 定义iov用于接收Client的writerv的内容。即一条LOG会在在buffer中 */
struct iovec iov = { buffer, sizeof(buffer) };
memset( buffer, 0, sizeof(buffer) );
/* 存放Client的进程信息 */
char control[CMSG_SPACE( sizeof(struct ucred) )];
/* 调用CMSG_SPACE宏计算存放ucred结构体所需要的空间*/
/* ucred是一个结构体,存放了进程的pid,uid和gid */
struct msghdr hdr = {
NULL,
0,
&iov, /* 真正存放LOG message */
1, /* 一个元素 */
control,
sizeof(control),
0,
};
int socket = cli->getSocket(); /* 获取到连接的socket */
ssize_t n = recvmsg( socket, &hdr, 0 ); /* 从socket中接收数据放到hdr中 */
/* ... */
然后就是对接收到的数据进行检查和处理了,一把就是检查打印log进程的权限,如果没有问题的话,就会去保存log日志到list容器中,相关代码片段:
android_log_header_t *header = reinterpret_cast<android_log_header_t *>(buffer); /* buffer就是上面定义的接收日志的缓冲区 */
if ( header->id >= LOG_ID_MAX || header->id == LOG_ID_KERNEL ) /* 校验logid的 */
{
return(false);
}
char *msg = ( (char *) buffer) + sizeof(android_log_header_t);
n -= sizeof(android_log_header_t);
/* truncated message to the logs. */
if ( logbuf->log( /* logbuf->log函数构造一个log */
(log_id_t) header->id,
header->realtime,
cred->uid,
cred->pid,
header->tid,
msg,
( (size_t) n <= USHRT_MAX) ? (unsigned short) n : USHRT_MAX ) >= 0
)
{
reader->notifyNewLog(); /* 把log保存到缓冲区,并唤醒相应正在阻塞的读取进程 */
}
return(true);
3.2.4 logd读取log浅释
logcat可以作为一个客户端从logd中读取日志,logcat的源代码在源码文件夹的/system/core/logcat
文件夹中,其中的logcat.cpp文件涵盖了大部分logcat的函数,例如其中__logcat就完成了logcat命令的参数的解析以及log的相关处理,logcat是通过liblog.so库中的logdRead函数实现的,读取方式是通过打开logdr设备文件,使用socket进行读取。
3.2.5 对这部分的说明
本篇博客属于整理类型的文章,希望那些想了解安卓log系统整体结构的人通过对文章的阅读,能够对安卓的log的框架有一个整体的认识。这部分我们从安卓框架的层面,深入到linux设备驱动程序,大致了解了安卓中log的处理方法,其中有些不乏对源代码的分析,笔者也是在网络上搜索相关博客,帖子了解的,笔者才疏学浅,对搜集到的材料,有时不加思索,甚至囫囵吞枣,分析的不是很到位,代码里的有些内容我也是不太了解,在注释代码的时候,有些也是自己的猜测,我会加上有可能
等不确定词语,还希望大佬看到能不吝赐教,也欢迎所有的朋友提出宝贵的意见, 我的博客会根据大家意见更新。
4 logcat的使用
这部分聊一聊logcat的简单使用。logcat的命令格式 logcat [选项] [过滤项]
选项和过滤项是可选的
4.1 log的组成
log一般含有以下内容
- 日期和时间 格式 MM-dd HH:mm:ss.毫秒
- log等级,包括以下内容:
- V:Verbose(明细,所有的log),等级最低
- D:Debug(调试信息)
- I :Info (信息)
- W :Warn (警告);
- E :Error (错误);
- F:Fatal (严重错误);
- S :Silent(最高的等级, 可能不会记载东西);
- 日志标签 tag
- 进程号 pid
- 线程号
- 日志信息
4.2 logcat的不同选项
-f <filepath> 把log写入文件,默认是写入标准输出stdout
-c 清空日志缓冲区 然后退出
-d 把缓存的log输出到屏幕上,输出完就会退出,不会一直读取阻塞
-t <num> 输出最近的num条日志,不阻塞
-g 输出缓冲区的信息
-b <buf type> 加载指定缓冲区的log 默认是main
-B 二进制形式输出log
-v <format type> 以指定格式输出log
有关log的格式,默认情况下是:
10-25 22:15:40.045 841 26192 D DlbDap2Process: process() called [4740500] times
10-25 22:15:40.502 10917 15665 I SocketReaderNew: SocketReader(889) continue
10-25 22:15:40.503 10917 15665 D MSF.C.NetConnTag: MsfCoreSocketReaderNew closeConn readError
10-25 22:15:40.503 10917 15665 D MSF.C.NetConnTag: conn is already closed on readError
格式对应:日期 时:分:秒.毫秒 进程id 线程id 日志等级 日志tag:消息
format type有以下类型:
- time
- thread
- brief
- process
- tag
- raw
- threadtime
- long
time格式:
xxx:/ # logcat -v time
--------- beginning of system
10-25 21:04:51.883 E/storaged( 1266): getDiskStats failed with result NOT_SUPPORTED and size 0
10-25 21:05:17.504 I/chatty ( 1685): uid=1000(system) watchdog expire 8 lines
10-25 21:05:29.492 I/chatty ( 1685): uid=1000(system) Binder:1685_C expire 6 lines
10-25 21:05:49.542 I/chatty ( 1685): uid=1000(system) PowerManagerSer expire 14 lines
10-25 21:05:51.891 E/storaged( 1266): getDiskStats failed with result NOT_SUPPORTED and size 0
格式对应:日期 时:分:秒.毫秒 日志等级/日志tag(进程id):进程名称:消息
进程名称有时候没有
thread格式:
xxx:/ # logcat -v thread
--------- beginning of system
E( 1266: 1334) getDiskStats failed with result NOT_SUPPORTED and size 0
I( 1685: 2725) uid=1000(system) watchdog expire 8 lines
I( 1685: 3303) uid=1000(system) Binder:1685_C expire 6 lines
I( 1685: 1898) uid=1000(system) PowerManagerSer expire 14 lines
E( 1266: 1334) getDiskStats failed with result NOT_SUPPORTED and size 0
格式对应:日志等级 ( 进程ID : 线程ID) 标签 : 消息
brief格式:
xxx:/ # logcat -v brief
--------- beginning of system
E/storaged( 1266): getDiskStats failed with result NOT_SUPPORTED and size 0
I/chatty ( 1685): uid=1000(system) watchdog expire 8 lines
I/chatty ( 1685): uid=1000(system) Binder:1685_C expire 6 lines
I/chatty ( 1685): uid=1000(system) PowerManagerSer expire 14 lines
E/storaged( 1266): getDiskStats failed with result NOT_SUPPORTED and size 0
格式对应:日志等级/日志tag(进程ID) : 消息
process 格式:
xxx:/ # logcat -v process
--------- beginning of system
E( 1266) getDiskStats failed with result NOT_SUPPORTED and size 0 (storaged)
I( 1685) uid=1000(system) watchdog expire 8 lines (chatty)
I( 1685) uid=1000(system) Binder:1685_C expire 6 lines (chatty)
I( 1685) uid=1000(system) PowerManagerSer expire 14 lines (chatty)
格式对应:日志等级(进程ID) : 消息
tag 格式:
xxx:/ # logcat -v tag
--------- beginning of system
E/storaged: getDiskStats failed with result NOT_SUPPORTED and size 0
I/chatty : uid=1000(system) watchdog expire 8 lines
I/chatty : uid=1000(system) Binder:1685_C expire 6 lines
格式对应:日志等级/日志tag: 消息
raw 格式:
xxx:/ # logcat -v raw
--------- beginning of system
getDiskStats failed with result NOT_SUPPORTED and size 0
uid=1000(system) watchdog expire 8 lines
uid=1000(system) Binder:1685_C expire 6 lines
格式对应:消息
只会输出日志的消息,没有其他附加信息
threadtime 格式:
xxx:/ # logcat -v threadtime
--------- beginning of system
10-25 21:04:51.883 1266 1334 E storaged: getDiskStats failed with result NOT_SUPPORTED and size 0
10-25 21:05:17.504 1685 2725 I chatty : uid=1000(system) watchdog expire 8 lines
10-25 21:05:29.492 1685 3303 I chatty : uid=1000(system) Binder:1685_C expire 6 lines
格式对应:日期 时:分:秒.毫秒 进程id 线程id 日志等级 日志tag:消息
这个好像是默认格式
long格式:
xxx:/ # logcat -v long
--------- beginning of system
[ 10-25 21:04:51.883 1266: 1334 E/storaged ]
getDiskStats failed with result NOT_SUPPORTED and size 0
[ 10-25 21:05:17.504 1685: 2725 I/chatty ]
uid=1000(system) watchdog expire 8 lines
[ 10-25 21:05:29.492 1685: 3303 I/chatty ]
uid=1000(system) Binder:1685_C expire 6 lines
格式对应:[日期 时:分:秒.毫秒 进程id: 线程id 日志等级/日志tag] \n 消息 \n
4.3 logcat的过滤项
过滤项的写法格式:<日志tag> [:日志等级]
tag是必要的,等级是可选的,如果没有等级,就显示所有等级的信息。过滤选项的含义是,显示指定tag中等级为指定等级以上的log,对于其他的tag日志,也是会显示的,所以当要只显示某一tag的指定等级以上的日志时,对于其他tag的日志进行屏蔽,需要再加上加上一个过滤选项 *:S
,如果不想这么搞可以使用logcat的-s
选项,仅仅使用-s表示什么都不显示,(就是设置过滤*:S
),后面也可以加上自己的过滤选项,这个时候过滤出来的结果与在最后加上*:S
是一样的。
如下表示只查看tag为ActivityManager的日志:
xxx:/sdcard/android_log # logcat ActivityManager *:S
--------- beginning of system
--------- beginning of crash
10-26 14:51:20.504 1685 4245 V ActivityManager: mIgnoreSleepCheckLater, do not care.
10-26 14:51:24.551 1685 1898 V ActivityManager: getAllTopPkgName 1 com.coloros.pictorial
10-26 14:51:25.477 1685 1818 D ActivityManager: reset mIgnoreSleepCheckLater