在前面两篇文章android日志系统驱动程序Logger源代码分析和Android应用程序框架层和系统运行库层日志系统源代码中,介绍了Android内核空间层、系统运行库层和应用程序框架层日志系统相关的源代码,其中,后一篇文章着重介绍了日志的写入操作。为了描述完整性,这篇文章着重介绍日志的读取操作,这就是我们在开发Android应用程序时,经常要用到日志查看工具Logcat了。
Logcat工具内置在Android系统中,可以在主机上通过adb logcat命令来查看模拟机上日志信息。Logcat工具的用法很丰富,因此,源代码也比较多,本文并不打算完整地介绍整个Logcat工具的源代码,主要是介绍Logcat读取日志的主线,即从打开日志设备文件到读取日志设备文件的日志记录到输出日志记录的主要过程,希望能起到一个抛砖引玉的作用。
Logcat工具源代码位于system/core/logcat目录下,只有一个源代码文件logcat.cpp,编译后生成的可执行文件位于out/target/product/generic/system/bin目录下,在模拟机中,可以在/system/bin目录下看到logcat工具。下面我们就分段来阅读logcat.cpp源代码文件。
一. Logcat工具的相关数据结构。
这些数据结构是用来保存从日志设备文件读出来的日志记录:
[color=amily:Consolas,'Courier]
[cpp] view plaincopy
struct queued_entry_t {
union {
unsigned char buf[LOGGER_ENTRY_MAX_LEN + 1] __attribute__((aligned(4)));
struct logger_entry entry __attribute__((aligned(4)));
};
queued_entry_t* next;
queued_entry_t() {
next = NULL;
}
};
struct log_device_t {
char* device;
bool binary;
int fd;
bool printed;
char label;
queued_entry_t* queue;
log_device_t* next;
log_device_t(char* d, bool b, char l) {
device = d;
binary = b;
label = l;
queue = NULL;
next = NULL;
printed = false;
}
void enqueue(queued_entry_t* entry) {
if (this->queue == NULL) {
this->queue = entry;
} else {
queued_entry_t** e = &this->queue;
while (*e && cmp(entry, *e) >= 0) {
e = &((*e)->next);
}
entry->next = *e;
*e = entry;
}
}
};
其中,宏LOGGER_ENTRY_MAX_LEN和struct logger_entry定义在system/core/include/cutils/logger.h文件中,在 Android应用程序框架层和系统运行库层日志系统源代码分析一文有提到,为了方便描述,这里列出这个宏和结构体的定义:
[color=amily:Consolas,'Courier]
[cpp] view plaincopy
struct logger_entry {
__u16 len; /* length of the payload */
__u16 __pad; /* no matter what, we get 2 bytes of padding */
__s32 pid; /* generating process's pid */
__s32 tid; /* generating process's tid */
__s32 sec; /* seconds since Epoch */
__s32 nsec; /* nanoseconds */
char msg[0]; /* the entry's payload */
};
#define LOGGER_ENTRY_MAX_LEN (4*1024)
从结构体struct queued_entry_t和struct log_device_t的定义可以看出,每一个log_device_t都包含有一个queued_entry_t队列,queued_entry_t就是对应从日志设备文件读取出来的一条日志记录了,而log_device_t则是对应一个日志设备文件上下文。在 Android日志系统驱动程序Logger源代码分析一文中,我们曾提到,Android日志系统有三个日志设备文件,分别是/dev/log/main、/dev/log/events和/dev/log/radio。
每个日志设备上下文通过其next成员指针连接起来,每个设备文件上下文的日志记录也是通过next指针连接起来。日志记录队例是按时间戳从小到大排列的,这个log_device_t::enqueue函数可以看出,当要插入一条日志记录的时候,先队列头开始查找,直到找到一个时间戳比当前要插入的日志记录的时间戳大的日志记录的位置,然后插入当前日志记录。比较函数cmp的定义如下:
[color=amily:Consolas,'Courier]
[cpp] view plaincopy
static int cmp(queued_entry_t* a, queued_entry_t* b) {
int n = a->entry.sec - b->entry.sec;
if (n != 0) {
return n;
}
return a->entry.nsec - b->entry.nsec;
}
为什么日志记录要按照时间戳从小到大排序呢?原来,Logcat在使用时,可以指定一个参数-t <count>,可以指定只显示最新count条记录,超过count的记录将被丢弃,在这里的实现中,就是要把排在队列前面的多余日记记录丢弃了,因为排在前面的日志记录是最旧的,默认是显示所有的日志记录。在下面的代码中,我们还会继续分析这个过程。
二. 打开日志设备文件。
Logcat工具的入口函数main,打开日志设备文件和一些初始化的工作也是在这里进行。main函数的内容也比较多,前面的逻辑都是解析命令行参数。这里假设我们使用logcat工具时,不带任何参数。这不会影响我们分析logcat读取日志的主线,有兴趣的读取可以自行分析解析命令行参数的逻辑。
分析完命令行参数以后,就开始要创建日志设备文件上下文结构体struct log_device_t了:
[color=amily:Consolas,'Courier]
[cpp] view plaincopy
if (!devices) {
devices = new log_device_t(strdup("/dev/"LOGGER_LOG_MAIN), false, 'm');
android::g_devCount = 1;
int accessmode =
(mode & O_RDONLY) ? R_OK : 0
| (mode & O_WRONLY) ? W_OK : 0;
// only add this if it's available
if (0 == access("/dev/"LOGGER_LOG_SYSTEM, accessmode)) {
devices->next = new log_device_t(strdup("/dev/"LOGGER_LOG_SYSTEM), false, 's');
android::g_devCount++;
}
}
由于我们假设使用logcat时,不带任何命令行参数,这里的devices变量为NULL,因此,就会默认创建/dev/log/main设备上下文结构体,如果存在/dev/log/system设备文件,也会一并创建。宏LOGGER_LOG_MAIN和LOGGER_LOG_SYSTEM也是定义在system/core/include/cutils/logger.h文件中:
[color=amily:Consolas,'Courier]
[cpp] view plaincopy
#define LOGGER_LOG_MAIN "log/main"
#define LOGGER_LOG_SYSTEM "log/system"
我们在 Android日志系统驱动程序Logger源代码分析一文中看到,在Android日志系统驱动程序Logger中,默认是不创建/dev/log/system设备文件的。
往下看,调用setupOutput()函数来初始化输出文件:
[color=amily:Consolas,'Courier]
[cpp] view plaincopy
android::setupOutput();
setupOutput()函数定义如下:
[color=amily:Consolas,'Courier]
[cpp] view plaincopy
static void setupOutput()
{
if (g_outputFileName == NULL) {
g_outFD = STDOUT_FILENO;
} else {
struct stat statbuf;
g_outFD = openLogFile (g_outputFileName);
if (g_outFD < 0) {
perror ("couldn't open output file");
exit(-1);
}
fstat(g_outFD, &statbuf);
g_outByteCount = statbuf.st_size;
}
}
如果我们在执行logcat命令时,指定了-f <filename>选项,日志内容就输出到filename文件中,否则,就输出到标准输出控制台去了。
再接下来,就是打开日志设备文件了:
[color=amily:Consolas,'Courier]
[cpp] view plaincopy
dev = devices;
while (dev) {
dev->fd = open(dev->device, mode);
if (dev->fd < 0) {
fprintf(stderr, "Unable to open log device '%s': %s\n",
dev->device, strerror(errno));
exit(EXIT_FAILURE);
}
if (clearLog) {
int ret;
ret = android::clearLog(dev->fd);
if (ret) {
perror("ioctl");
exit(EXIT_FAILURE);
}
}
if (getLogSize) {
int size, readable;
size = android::getLogSize(dev->fd);
if (size < 0) {
perror("ioctl"
); exit(EXIT_FAILURE); } readable = android::getLogReadableSize(dev->fd); if (readable < 0) { perror("ioctl"); exit(EXIT_FAILURE); } printf("%s: ring buffer is %dKb (%dKb consumed), " "max entry is %db, max payload is %db\n", dev->device, size / 1024, readable / 1024, (int) LOGGER_ENTRY_MAX_LEN, (int) LOGGER_ENTRY_MAX_PAYLOAD); } dev = dev->next; 盛世游戏http://www.shengshiyouxi.com } 如果执行logcat命令的目的是清空日志,即clearLog为true,则调用android::clearLog函数来执行清空日志操作: [color=amily:Consolas,'Courier] [cpp] view plaincopy static int clearLog(int logfd) { return ioctl(logfd, LOGGER_FLUSH_LOG); } 这里是通过标准的文件函数ioctl函数来执行日志清空操作,具体可以参考logger驱动程序的实现。 如果执行logcat命令的目的是获取日志内存缓冲区的大小,即getLogSize为true,通过调用android::getLogSize函数实现: [color=amily:Consolas,'Courier] [cpp] view plaincopy /* returns the total size of the log's ring buffer */ static int getLogSize(int logfd) { return ioctl(logfd, LOGGER_GET_LOG_BUF_SIZE); } 如果为负数,即size < 0,就表示出错了,退出程序。 接着验证日志缓冲区可读内容的大小,即调用android::getLogReadableSize函数: [color=amily:Consolas,'Courier] [cpp] view plaincopy /* returns the readable size of the log's ring buffer (that is, amount of the log consumed) */ static int getLogReadableSize(int logfd) { return ioctl(logfd, LOGGER_GET_LOG_LEN); } 如果返回负数,即readable < 0,也表示出错了,退出程序。 接下去的printf语句,就是输出日志缓冲区的大小以及可读日志的大小到控制台去了。 继续看下看代码,如果执行logcat命令的目的是清空日志或者获取日志的大小信息,则现在就完成使命了,可以退出程序了: [color=amily:Consolas,'Courier] [cpp] view plaincopy if (getLogSize) { return 0; } if (clearLog) { return 0; } 否则,就要开始读取设备文件的日志记录了: [color=amily:Consolas,'Courier] [html] view plaincopy android::readLogLines(devices); 至此日志设备文件就打开并且初始化好了,下面,我们继续分析从日志设备文件读取日志记录的操作,即readLogLines函数。 三. 读取日志设备文件。 读取日志设备文件内容的函数是readLogLines函数: [color=amily:Consolas,'Courier] [cpp] view plaincopy static void readLogLines(log_device_t* devices) { log_device_t* dev; int max = 0; int ret; int queued_lines = 0; bool sleep = true; int result; fd_set readset; for (dev=devices; dev; dev = dev->next) { if (dev->fd > max) { max = dev->fd; } } while (1) { do { timeval timeout = { 0, 5000 /* 5ms */ }; // If we oversleep it's ok, i.e. ignore EINTR. FD_ZERO(&readset); for (dev=devices; dev; dev = dev->next) { FD_SET(dev->fd, &readset); } result = select(max + 1, &readset, NULL, NULL, sleep ? NULL : &timeout); } while (result == -1 && errno == EINTR); if (result >= 0) { for (dev=devices; dev; dev = dev->next) { if (FD_ISSET(dev->fd, &readset)) { queued_entry_t* entry = new queued_entry_t(); /* NOTE: driver guarantees we read exactly one full entry */ ret = read(dev->fd, entry->buf, LOGGER_ENTRY_MAX_LEN); if (ret < 0) { if (errno == EINTR) { delete entry; goto next; } if (errno == EAGAIN) { delete entry; break; } perror("logcat read"); exit(EXIT_FAILURE); } else if (!ret) { fprintf(stderr, "read: Unexpected EOF!\n"); exit(EXIT_FAILURE); } entry->entry.msg[entry->entry.len] = '\0'; dev->enqueue(entry); ++queued_lines; } } if (result == 0) { // we did our short timeout trick and there's nothing new // print everything we have and wait for more data sleep = true; while (true) { chooseFirst(devices, &dev); if (dev == NULL) { break; } if (g_tail_lines == 0 || queued_lines <= g_tail_lines) { printNextEntry(dev); } else { skipNextEntry(dev); } --queued_lines; } // the caller requested to just dump the log and exit if (g_nonblock) { exit(0); } } else { // print all that aren't the last in their list sleep = false; while (g_tail_lines == 0 || queued_lines > g_tail_lines) { chooseFirst(devices, &dev); if (dev == NULL || dev->queue->next == NULL) { break; } if (g_tail_lines == 0) { printNextEntry(dev); } else { skipNextEntry(dev); } --queued_lines; } } } next: ; } } 由于可能同时打开了多个日志设备文件,这里使用select函数来同时监控哪个文件当前可读: [color=amily:Consolas,'Courier] [cpp] view plaincopy do { timeval timeout = { 0, 5000 /* 5ms */ }; // If we oversleep it's ok, i.e. ignore EINTR. FD_ZERO(&readset); for (dev=devices; dev; dev = dev->next) { FD_SET(dev->fd, &readset); } result = select(max + 1, &readset, NULL, NULL, sleep ? NULL : &timeout); } while (result == -1 && errno == EINTR); 如果result >= 0,就表示有日志设备文件可读或者超时。接着,用一个for语句检查哪个设备文件可读,即FD_ISSET(dev->fd, &readset)是否为true,如果为true,表明可读,就要进一步通过read函数将日志读出,注意,每次只读出一条日志记录: [color=amily:Consolas,'Courier] [cpp] view plaincopy for (dev=devices; dev; dev = dev->next) { if (FD_ISSET(dev->fd, &readset)) { queued_entry_t* entry = new queued_entry_t(); /* NOTE: driver guarantees we read exactly one full entry */ ret = read(dev->fd, entry->buf, LOGGER_ENTRY_MAX_LEN); if (ret < 0) {
|