Android的log机制小结
本文是对Android的log机制学习的小结,内容包括对log框架的理解、写log、读log和log的驱动这几个部分。
一、 log框架
log机制包括三部分:底层驱动、读和写。关于写log,我们可以在Java文件中,或者jni层的C/C++代码中添加类似log.d()这样的代码来实现写log,通过logcat命令来输出我们想要查看的log,驱动的任务则是真正地帮助我们实现读和写。
http://blog.csdn.net/mickeyfirst/article/details/6233885
引用这张图来说明log的框架。我们在JavaProgram或者Native Program中通过android.util.Log或者System.out或者log.h为我们提供的一下方法如Log.d或者ALOGD,就能把log添加进去,最终这些log都被底层驱动写到了如下四个设备中:
/dev/log/main
/dev/log/radio
/dev/log/event
/dev/log/system
之后我们可以通过logcat命令或者Eclipse中的logcat来查看和过滤log,不过这些最本质上是调用logcat命令,Eclipse中的logcat或者adblogcat命令都是对通过adbd进程远程调用logcat命令罢了。这就是基本的框架,下面将详细分析各个部分。
二、写log
这里我们把写log的方式简单分为两类:Javaprogram和Native Program。首先来看Java层的log写入。以最简单的android.util.Log为例。
public staticfinal int VERBOSE = 2; public static final int DEBUG = 3; public static final int INFO = 4; public static final int WARN = 5; public static final int ERROR = 6; publicstatic final int ASSERT = 7;log分级别,对应的数字越大,则级别越高,这在过滤log时会用到。这个文件提供了许多公共的静态方法,我们可以直接通过类名调用。下面是Log.d()方法:
publicstatic int d(String tag, String msg) {
return println_native(LOG_ID_MAIN,DEBUG, tag, msg);
}
这个方法大家都很熟悉,不再叙述,只请注意一点:LOG_ID_MAIN
这个类中的其它方法最终也会调用这个本地方法:
publicstatic native int println_native(int bufID, int priority, String tag, Stringmsg);
它的实现则在jni层:android_util_Log.cpp文件中,有如下定义:
staticJNINativeMethod 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 }, };从这里可以看出,println_native这个方法对应的是该文件中的android_util_Log_println_native方法。继续看这个方法,发现它最终调用logd_write.c文件中__android_log_buf_write方法,调用层次如下:
__android_log_buf_write
write_to_log
__write_to_log_init
__write_to_log_kernel
log_writev
writev
log_fds[LOG_ID_MAIN]= log_open("/dev/"LOGGER_LOG_MAIN, O_WRONLY); log_fds[LOG_ID_RADIO]= log_open("/dev/"LOGGER_LOG_RADIO, O_WRONLY); log_fds[LOG_ID_EVENTS]= log_open("/dev/"LOGGER_LOG_EVENTS, O_WRONLY); log_fds[LOG_ID_SYSTEM]= log_open("/dev/"LOGGER_LOG_SYSTEM, O_WRONLY); #defineLOGGER_LOG_MAIN "log/main" #defineLOGGER_LOG_RADIO "log/radio" #defineLOGGER_LOG_EVENTS "log/events" #defineLOGGER_LOG_SYSTEM "log/system"这些调用过程比较简单,在不断调用的过程中也对数据进行了一些判断和处理。这里需要说的是,一般我们使用Log.d方法输出的log,都会写入到/dev/log/main中,因为这个函数定义中传入了 LOG_ID_MAIN这个参数。而对于以slog.d这种方式添加的log,一般都写到了/dev/log/system,因为它的定义如下:
publicstatic int d(String tag, String msg) {
return Log.println_native(Log.LOG_ID_SYSTEM,Log.DEBUG, tag, msg);
}
而/dev/log/radio中一般都是些和通讯相关的log会保存在这里,/dev/log/event则保存了一些事件相关的log,再具体的我不是很清楚。
我们通过一步步跟踪方法调用,最终找到了writev这个方法。这里先留个疑问,接下来再看C/C++文件中使用log。以ALOGD方法为例:
system/core/include/cutils/log.h
ALOGD
(void)ALOG(LOG_DEBUG,LOG_TAG, __VA_ARGS__)
LOG_PRI(ANDROID_##priority,tag, __VA_ARGS__)
android_printLog(priority,tag, __VA_ARGS__)
__android_log_print(prio,tag, fmt)
__android_log_print
__android_log_write
write_to_log
……
writev
最终也会调用writev这个方法(在开始写log之前还调用了open这个方法去打开设备),但是再向下就不能继续跟踪了,之后就会执行驱动层的相关代码。在调用驱动层的代码时,会根据传入的filedes这个参数找到对应设备的驱动,这里这个值为/dev/log/main对应的那个设备。
三、log的底层驱动
相关文件位置:
kernel/drivers/staging/android/logger.c
kernel/drivers/staging/android/logger.h
首先,前面已多次提到,被驱动的设备有四个,位置在dev/log下。
yutao@yutao:~$adb shell
root@android:/ #ll dev/log
crw-rw-rw-root log 10, 46 2013-01-01 08:00 events
crw-rw-rw-root log 10, 47 2013-01-01 08:00 main
crw-rw-rw-root log 10, 45 2013-01-01 08:00 radio
crw-rw-rw-root log 10, 44 2013-01-01 08:00 system
其中的c 标示这是一个字符设备文件characterdevice ,也表示硬件设备,但是数据是以字节流发送的,这些设备包括终端设备和串口设备。我们不能利用adb pull命令将其取出来。这里的10是主设备号,表明它是misc设备。我们可以通过logcat–g –b main这样的命令来查看各个设备的大小信息
root@android:/ #logcat -g -b main
/dev/log/main:ring buffer is 256Kb (255Kb consumed), max entry is 5120b, max payload is 4076b
这里显示/dev/log/main这个设备的buffer大小为256Kb,没条log包含的内容最大为4076b。我们不断地向这个设备中写log,当它的buffer不够用时,它就会从头开始写,也意味着之前的log就会被覆盖掉,这些可以在代码中看到。
然后,我们来看设备驱动的主要工作。我对驱动的理解比较肤浅,就是应该要做一些初始化的工作,然后提供读和写的借口给上层,所以我也只关注这些内容。关于初始化工作,请看如下代码:
static conststruct file_operations logger_fops = { .owner = THIS_MODULE, .read = logger_read, .aio_write = logger_aio_write, .poll = logger_poll, .unlocked_ioctl = logger_ioctl, .compat_ioctl = logger_ioctl, .open = logger_open, .release = logger_release, };这里声明了一个static结构体变量logger_fops,它是file_operations结构体,这个file_operations结构体似乎在linux系统中很常见,这里我们只需要知道它为上层调用提供了接口。举例:上层想要往这个设备文件里写内容的话,就可以调用read方法,当然在使用read方法时还传入了一个与设备名称绑定的值,可能是 LOG_ID_MAIN这样的值,它指向了/dev/log/main这个设备,因此就会调用我们这个驱动的与read关联的方法,就是logger_read。而如果用户调用了aio_write方法时,就会执行这里的logger_aio_write方法。不过logger_aio_write方法的注释中有提到,当用户调用aio_write、write或者writev方法时,都会执行logger_aio_write方法。这首先解释了我们在之前的疑问:在logd_write.c文件中最终走到了writev之后就再看不到更底层的代码了,现在知道是到了驱动层。至于为何使用哪个write方法都会走这里,我也不清楚,猜测是不是因为它只提供了一个write方法,所以只能走这里。
下面的代码是四个设备的创建(个人理解):
#defineDEFINE_LOGGER_DEVICE(VAR, NAME, SIZE) \ static unsignedchar _buf_ ##VAR[SIZE]; \ static structlogger_log VAR = { \ .buffer = _buf_ ## VAR, \ .misc = { \ .minor = MISC_DYNAMIC_MINOR, \ .name = NAME, \ .fops = &logger_fops, \ .parent = NULL, \ }, \ .wq = __WAIT_QUEUE_HEAD_INITIALIZER(VAR .wq), \ .readers = LIST_HEAD_INIT(VAR .readers), \ .mutex = __MUTEX_INITIALIZER(VAR .mutex), \ .w_off = 0, \ .head = 0, \ .size = SIZE, \ }; DEFINE_LOGGER_DEVICE(log_main,LOGGER_LOG_MAIN,256*1024) DEFINE_LOGGER_DEVICE(log_events,LOGGER_LOG_EVENTS,256*1024) DEFINE_LOGGER_DEVICE(log_radio,LOGGER_LOG_RADIO,256*1024) DEFINE_LOGGER_DEVICE(log_system,LOGGER_LOG_SYSTEM,256*1024)这里每个设备的buffer大小都是256Kb,它们都是misc设备,且在.fops= &logger_fops中指明了上层调用这个设备的关联的方法或接口为刚才提到的logger_fops静态结构体变量。
device_initcall(logger_init);这里指明初始化方法为logger_init,之后调用init_log方法,通过misc_register这个方法来注册misc设备,这里的细节我也不清楚。
这些应该就是初始化工作了,稍微总结一下就能得到如下关心的结果:当上层调用read、writev和open方法时,就会执行这里的logger_read、logger_aio_write和logger_open方法。当然,在调用时一定也在参数中指明了操作的设备是哪个,这可以通过类似LOG_ID_MAIN这样的量来知名。了解这些就够了。
为了更深入地了解对log的操作,我们来简单分析下logger_aio_write中的关键代码。
log的读取和写入都是以一条记录为单位进行的,每条记录包含的内容都在logger_entry这个结构体中定义,包含进程和线程ID,时间,target和输出信息等内容。可是我们知道,我们在添加log时只是指定了优先级、target和输出信息这些内容,并未给出进程和线程id、时间等信息,这些都是在这个方法中添加的:
structlogger_entry header;
struct timespecnow;
now =current_kernel_time();
header.pid = current->tgid; //进程id
header.tid = current->pid; //线程id
header.sec = now.tv_sec; //时间s
header.nsec = now.tv_nsec; //时间ns
header.euid = current_euid();
header.len = min_t(size_t, iocb->ki_left,LOGGER_ENTRY_MAX_PAYLOAD);
header.hdr_size = sizeof(struct logger_entry);
target和输出内容等信息已经被包装在了structiovec *iov中,所以只要将这里的信息和header中的内容写入到设备的buf中即可:log->buffer。关于这些结构体各项代表的内容有兴趣的可以仔细研究。这里的buffer就是之前在创建设备时申请的具有256Kb大小的数组。
真正写log的方法是do_write_log,
do_write_log(log,&header, sizeof(struct logger_entry));
nr =do_write_log_from_user(log, iov->iov_base, len);
多次调用这个方法将header和iov中的信息写入到设备的buffer中。
static ssize_tdo_write_log_from_user(struct logger_log *log, const void __user *buf, size_tcount) //log就代表了设备,我们需要将信息写入到设备的buffer中:log->buffer //buf则存储了log信息 //count为log信息的大小 { size_t len; len = min(count, log->size - log->w_off); //log->size为buffer的大小 //log->w_off为当前缓冲区偏移量 //此处比较count(log大小)与缓冲区剩余字节(log->size- log->w_off),取其小者首先复制 if (len &©_from_user(log->buffer + log->w_off, buf, len)) return -EFAULT; //如果这里不相等,则意味着缓冲区剩余空间比log的字节数小,因此剩余的log信息则需要写到缓冲区的最前面 if (count != len) if (copy_from_user(log->buffer, buf +len, count - len)) return -EFAULT; //重新定向buffer的偏移量 log->w_off = logger_offset(log,log->w_off + count); return count; }这部分分析我们可以得出如下结论:每个设备有256Kb的缓冲区域,新的log信息会一条条地依次写入到缓冲区,当缓冲区满后,则又从缓冲区的头开始继续写,因此,最前面的log就会被覆盖。但是每条log的大小也有限制:
#defineLOGGER_ENTRY_MAX_PAYLOAD 4076
#define LOGGER_ENTRY_MAX_LEN(5*1024)
四、读log
log的读取则由logcat命令来完成。相关文件为system/core/logcat/logcat.cpp。logcat命令可以过滤和查看log信息,关于它的使用这里不提。logcat命令的入口函数当然是该文件的main方法。实际读取log的方法是readLogLines方法,当然最终还是要调用我们前面提到的read方法:
ret =read(dev->fd, entry->buf, LOGGER_ENTRY_MAX_LEN);
一般地当我们输出log时,输出的是/dev/log/main设备中的log信息,如果想输出其它三个设备的log,可以使用-bradio参数来输出radio设备中的信息。
对于adblogcat命令输出log和Eclipse中的logcat输出log,它实际也是使用logcat命令,只是那是通过一些其它方式来间接地调用而已。
这部分只能分享到这里,更多的细节请大家自己学习。
五、小结
根据个人片面的理解,Android的log机制主要包括三部分:读、写和驱动,而驱动是核心。但是我们真正需要熟练掌握的应该是读和写。由于log机制贯穿了应用层、framework层和驱动层,学习它对于我们深入学习android还是有帮助的。
相关文档可以参考如下链接:
http://blog.csdn.net/luoshengyang/article/details/6598703
http://blog.csdn.net/thl789/article/details/6629905