Android日志系统Logcat源代码简要分析

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介:

   在前面两篇文章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工具的相关数据结构。

        这些数据结构是用来保存从日志设备文件读出来的日志记录:

 

 
 
  1. struct queued_entry_t {  
  2.     union {  
  3.         unsigned char buf[LOGGER_ENTRY_MAX_LEN + 1] __attribute__((aligned(4)));  
  4.         struct logger_entry entry __attribute__((aligned(4)));  
  5.     };  
  6.     queued_entry_t* next;  
  7.  
  8.     queued_entry_t() {  
  9.         next = NULL;  
  10.     }  
  11. };  
  12.  
  13. struct log_device_t {  
  14.     char* device;  
  15.     bool binary;  
  16.     int fd;  
  17.     bool printed;  
  18.     char label;  
  19.  
  20.     queued_entry_t* queue;  
  21.     log_device_t* next;  
  22.  
  23.     log_device_t(char* d, bool b, char l) {  
  24.         device = d;  
  25.         binary = b;  
  26.         label = l;  
  27.         queue = NULL;  
  28.         next = NULL;  
  29.         printed = false;  
  30.     }  
  31.  
  32.     void enqueue(queued_entry_t* entry) {  
  33.         if (this->queue == NULL) {  
  34.             this->queue = entry;  
  35.         } else {  
  36.             queued_entry_t** e = &this->queue;  
  37.             while (*e && cmp(entry, *e) >= 0) {  
  38.                 e = &((*e)->next);  
  39.             }  
  40.             entry->next = *e;  
  41.             *e = entry;  
  42.         }  
  43.     }  
  44. }; 

    其中,宏LOGGER_ENTRY_MAX_LEN和struct logger_entry定义在system/core/include/cutils/logger.h文件中,在Android应用程序框架层和系统运行库层日志系统源代码分析一文有提到,为了方便描述,这里列出这个宏和结构体的定义:

 
 
  1. struct logger_entry {  
  2.     __u16       len;    /* length of the payload */  
  3.     __u16       __pad;  /* no matter what, we get 2 bytes of padding */  
  4.     __s32       pid;    /* generating process's pid */  
  5.     __s32       tid;    /* generating process's tid */  
  6.     __s32       sec;    /* seconds since Epoch */  
  7.     __s32       nsec;   /* nanoseconds */  
  8.     char        msg[0]; /* the entry's payload */  
  9. };  
  10.  
  11. #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的定义如下:

 

 
 
  1. static int cmp(queued_entry_t* a, queued_entry_t* b) {  
  2.     int n = a->entry.sec - b->entry.sec;  
  3.     if (n != 0) {  
  4.         return n;  
  5.     }  
  6.     return a->entry.nsec - b->entry.nsec;  

 为什么日志记录要按照时间戳从小到大排序呢?原来,Logcat在使用时,可以指定一个参数-t <count>,可以指定只显示最新count条记录,超过count的记录将被丢弃,在这里的实现中,就是要把排在队列前面的多余日记记录丢弃了,因为排在前面的日志记录是最旧的,默认是显示所有的日志记录。在下面的代码中,我们还会继续分析这个过程。
        二. 打开日志设备文件。

 

        Logcat工具的入口函数main,打开日志设备文件和一些初始化的工作也是在这里进行。main函数的内容也比较多,前面的逻辑都是解析命令行参数。这里假设我们使用logcat工具时,不带任何参数。这不会影响我们分析logcat读取日志的主线,有兴趣的读取可以自行分析解析命令行参数的逻辑。

        分析完命令行参数以后,就开始要创建日志设备文件上下文结构体struct log_device_t了:

 

 
 
  1. if (!devices) {  
  2.     devices = new log_device_t(strdup("/dev/"LOGGER_LOG_MAIN), false'm');  
  3.     android::g_devCount = 1;  
  4.     int accessmode =  
  5.               (mode & O_RDONLY) ? R_OK : 0  
  6.             | (mode & O_WRONLY) ? W_OK : 0;  
  7.     // only add this if it's available  
  8.     if (0 == access("/dev/"LOGGER_LOG_SYSTEM, accessmode)) {  
  9.         devices->next = new log_device_t(strdup("/dev/"LOGGER_LOG_SYSTEM), false's');  
  10.         android::g_devCount++;  
  11.     }  

     由于我们假设使用logcat时,不带任何命令行参数,这里的devices变量为NULL,因此,就会默认创建/dev/log/main设备上下文结构体,如果存在/dev/log/system设备文件,也会一并创建。宏LOGGER_LOG_MAIN和LOGGER_LOG_SYSTEM也是定义在system/core/include/cutils/logger.h文件中:

 

 
 
  1. #define LOGGER_LOG_MAIN     "log/main" 
  2. #define LOGGER_LOG_SYSTEM   "log/system" 

   我们在Android日志系统驱动程序Logger源代码分析一文中看到,在Android日志系统驱动程序Logger中,默认是不创建/dev/log/system设备文件的。

        往下看,调用setupOutput()函数来初始化输出文件:

 

 
 
  1. android::setupOutput(); 

   setupOutput()函数定义如下:
 

 
 
  1. static void setupOutput()  
  2. {  
  3.  
  4.     if (g_outputFileName == NULL) {  
  5.         g_outFD = STDOUT_FILENO;  
  6.  
  7.     } else {  
  8.         struct stat statbuf;  
  9.  
  10.         g_outFD = openLogFile (g_outputFileName);  
  11.  
  12.         if (g_outFD < 0) {  
  13.             perror ("couldn't open output file");  
  14.             exit(-1);  
  15.         }  
  16.  
  17.         fstat(g_outFD, &statbuf);  
  18.  
  19.         g_outByteCount = statbuf.st_size;  
  20.     }  

   如果我们在执行logcat命令时,指定了-f  <filename>选项,日志内容就输出到filename文件中,否则,就输出到标准输出控制台去了。

        再接下来,就是打开日志设备文件了:

 

 
 
  1. dev = devices;  
  2. while (dev) {  
  3.     dev->fd = open(dev->device, mode);  
  4.     if (dev->fd < 0) {  
  5.         fprintf(stderr, "Unable to open log device '%s': %s\n",  
  6.             dev->device, strerror(errno));  
  7.         exit(EXIT_FAILURE);  
  8.     }  
  9.  
  10.     if (clearLog) {  
  11.         int ret;  
  12.         ret = android::clearLog(dev->fd);  
  13.         if (ret) {  
  14.             perror("ioctl");  
  15.             exit(EXIT_FAILURE);  
  16.         }  
  17.     }  
  18.  
  19.     if (getLogSize) {  
  20.         int size, readable;  
  21.  
  22.         size = android::getLogSize(dev->fd);  
  23.         if (size < 0) {  
  24.             perror("ioctl");  
  25.             exit(EXIT_FAILURE);  
  26.         }  
  27.  
  28.         readable = android::getLogReadableSize(dev->fd);  
  29.         if (readable < 0) {  
  30.             perror("ioctl");  
  31.             exit(EXIT_FAILURE);  
  32.         }  
  33.  
  34.         printf("%s: ring buffer is %dKb (%dKb consumed), " 
  35.                "max entry is %db, max payload is %db\n", dev->device,  
  36.                size / 1024, readable / 1024,  
  37.                (int) LOGGER_ENTRY_MAX_LEN, (int) LOGGER_ENTRY_MAX_PAYLOAD);  
  38.     }  
  39.  
  40.     dev = dev->next;  

     如果执行logcat命令的目的是清空日志,即clearLog为true,则调用android::clearLog函数来执行清空日志操作:

 

 
 
  1. static int clearLog(int logfd)  
  2. {  
  3.     return ioctl(logfd, LOGGER_FLUSH_LOG);  

      这里是通过标准的文件函数ioctl函数来执行日志清空操作,具体可以参考logger驱动程序的实现。

 

        如果执行logcat命令的目的是获取日志内存缓冲区的大小,即getLogSize为true,通过调用android::getLogSize函数实现:

 

 
 
  1. /* returns the total size of the log's ring buffer */  
  2. static int getLogSize(int logfd)  
  3. {  
  4.     return ioctl(logfd, LOGGER_GET_LOG_BUF_SIZE);  

   如果为负数,即size < 0,就表示出错了,退出程序。

 

        接着验证日志缓冲区可读内容的大小,即调用android::getLogReadableSize函数:

 

 
 
  1. /* returns the readable size of the log's ring buffer (that is, amount of the log consumed) */  
  2. static int getLogReadableSize(int logfd)  
  3. {  
  4.     return ioctl(logfd, LOGGER_GET_LOG_LEN);  

如果返回负数,即readable < 0,也表示出错了,退出程序。

 

        接下去的printf语句,就是输出日志缓冲区的大小以及可读日志的大小到控制台去了。

        继续看下看代码,如果执行logcat命令的目的是清空日志或者获取日志的大小信息,则现在就完成使命了,可以退出程序了:

 

 
 
  1. if (getLogSize) {  
  2.     return 0;  
  3. }  
  4. if (clearLog) {  
  5.     return 0;  

   否则,就要开始读取设备文件的日志记录了:
 

 
 
  1. android::readLogLines(devices); 

至此日志设备文件就打开并且初始化好了,下面,我们继续分析从日志设备文件读取日志记录的操作,即readLogLines函数。

 

       三. 读取日志设备文件。

       读取日志设备文件内容的函数是readLogLines函数:

 

 
 
  1. static void readLogLines(log_device_t* devices)  
  2. {  
  3.     log_device_t* dev;  
  4.     int max = 0;  
  5.     int ret;  
  6.     int queued_lines = 0;  
  7.     bool sleep = true;  
  8.  
  9.     int result;  
  10.     fd_set readset;  
  11.  
  12.     for (dev=devices; dev; dev = dev->next) {  
  13.         if (dev->fd > max) {  
  14.             max = dev->fd;  
  15.         }  
  16.     }  
  17.  
  18.     while (1) {  
  19.         do {  
  20.             timeval timeout = { 0, 5000 /* 5ms */ }; // If we oversleep it's ok, i.e. ignore EINTR.  
  21.             FD_ZERO(&readset);  
  22.             for (dev=devices; dev; dev = dev->next) {  
  23.                 FD_SET(dev->fd, &readset);  
  24.             }  
  25.             result = select(max + 1, &readset, NULLNULL, sleep ? NULL : &timeout);  
  26.         } while (result == -1 && errno == EINTR);  
  27.  
  28.         if (result >= 0) {  
  29.             for (dev=devices; dev; dev = dev->next) {  
  30.                 if (FD_ISSET(dev->fd, &readset)) {  
  31.                     queued_entry_t* entry = new queued_entry_t();  
  32.                     /* NOTE: driver guarantees we read exactly one full entry */  
  33.                     ret = read(dev->fd, entry->buf, LOGGER_ENTRY_MAX_LEN);  
  34.                     if (ret < 0) {  
  35.                         if (errno == EINTR) {  
  36.                             delete entry;  
  37.                             goto next;  
  38.                         }  
  39.                         if (errno == EAGAIN) {  
  40.                             delete entry;  
  41.                             break;  
  42.                         }  
  43.                         perror("logcat read");  
  44.                         exit(EXIT_FAILURE);  
  45.                     }  
  46.                     else if (!ret) {  
  47.                         fprintf(stderr, "read: Unexpected EOF!\n");  
  48.                         exit(EXIT_FAILURE);  
  49.                     }  
  50.  
  51.                     entry->entry.msg[entry->entry.len] = '\0';  
  52.  
  53.                     dev->enqueue(entry);  
  54.                     ++queued_lines;  
  55.                 }  
  56.             }  
  57.  
  58.             if (result == 0) {  
  59.                 // we did our short timeout trick and there's nothing new  
  60.                 // print everything we have and wait for more data  
  61.                 sleep = true;  
  62.                 while (true) {  
  63.                     chooseFirst(devices, &dev);  
  64.                     if (dev == NULL) {  
  65.                         break;  
  66.                     }  
  67.                     if (g_tail_lines == 0 || queued_lines <= g_tail_lines) {  
  68.                         printNextEntry(dev);  
  69.                     } else {  
  70.                         skipNextEntry(dev);  
  71.                     }  
  72.                     --queued_lines;  
  73.                 }  
  74.  
  75.                 // the caller requested to just dump the log and exit  
  76.                 if (g_nonblock) {  
  77.                     exit(0);  
  78.                 }  
  79.             } else {  
  80.                 // print all that aren't the last in their list  
  81.                 sleep = false;  
  82.                 while (g_tail_lines == 0 || queued_lines > g_tail_lines) {  
  83.                     chooseFirst(devices, &dev);  
  84.                     if (dev == NULL || dev->queue->next == NULL) {  
  85.                         break;  
  86.                     }  
  87.                     if (g_tail_lines == 0) {  
  88.                         printNextEntry(dev);  
  89.                     } else {  
  90.                         skipNextEntry(dev);  
  91.                     }  
  92.                     --queued_lines;  
  93.                 }  
  94.             }  
  95.         }  
  96. next:  
  97.         ;  
  98.     }  

  由于可能同时打开了多个日志设备文件,这里使用select函数来同时监控哪个文件当前可读:
 

 
 
  1. do {  
  2.     timeval timeout = { 0, 5000 /* 5ms */ }; // If we oversleep it's ok, i.e. ignore EINTR.  
  3.     FD_ZERO(&readset);  
  4.     for (dev=devices; dev; dev = dev->next) {  
  5.         FD_SET(dev->fd, &readset);  
  6.     }  
  7.     result = select(max + 1, &readset, NULLNULL, sleep ? NULL : &timeout);  
  8. } while (result == -1 && errno == EINTR); 

      如果result >= 0,就表示有日志设备文件可读或者超时。接着,用一个for语句检查哪个设备文件可读,即FD_ISSET(dev->fd, &readset)是否为true,如果为true,表明可读,就要进一步通过read函数将日志读出,注意,每次只读出一条日志记录:
 

 
 
  1. for (dev=devices; dev; dev = dev->next) {  
  2.            if (FD_ISSET(dev->fd, &readset)) {  
  3.                queued_entry_t* entry = new queued_entry_t();  
  4.                /* NOTE: driver guarantees we read exactly one full entry */  
  5.                ret = read(dev->fd, entry->buf, LOGGER_ENTRY_MAX_LEN);  
  6.                if (ret < 0) {  
  7.                    if (errno == EINTR) {  
  8.                        delete entry;  
  9.                        goto next;  
  10.                    }  
  11.                    if (errno == EAGAIN) {  
  12.                        delete entry;  
  13.                        break;  
  14.                    }  
  15.                    perror("logcat read");  
  16.                    exit(EXIT_FAILURE);  
  17.                }  
  18.                else if (!ret) {  
  19.                    fprintf(stderr, "read: Unexpected EOF!\n");  
  20.                    exit(EXIT_FAILURE);  
  21.                }  
  22.  
  23.                entry->entry.msg[entry->entry.len] = '\0';  
  24.  
  25.                dev->enqueue(entry);  
  26.                ++queued_lines;  
  27.            }  
  28.        } 

 调用read函数之前,先创建一个日志记录项entry,接着调用read函数将日志读到entry->buf中,最后调用dev->enqueue(entry)将日志记录加入到日志队例中去。同时,把当前的日志记录数保存在queued_lines变量中。
 

        继续进一步处理日志:

 

 
 
  1. if (result == 0) {  
  2.     // we did our short timeout trick and there's nothing new  
  3.     // print everything we have and wait for more data  
  4.     sleep = true;  
  5.     while (true) {  
  6.         chooseFirst(devices, &dev);  
  7.         if (dev == NULL) {  
  8.             break;  
  9.         }  
  10.         if (g_tail_lines == 0 || queued_lines <= g_tail_lines) {  
  11.             printNextEntry(dev);  
  12.         } else {  
  13.             skipNextEntry(dev);  
  14.         }  
  15.         --queued_lines;  
  16.     }  
  17.  
  18.     // the caller requested to just dump the log and exit  
  19.     if (g_nonblock) {  
  20.         exit(0);  
  21.     }  
  22. else {  
  23.     // print all that aren't the last in their list  
  24.     sleep = false;  
  25.     while (g_tail_lines == 0 || queued_lines > g_tail_lines) {  
  26.         chooseFirst(devices, &dev);  
  27.         if (dev == NULL || dev->queue->next == NULL) {  
  28.             break;  
  29.         }  
  30.         if (g_tail_lines == 0) {  
  31.             printNextEntry(dev);  
  32.         } else {  
  33.             skipNextEntry(dev);  
  34.         }  
  35.         --queued_lines;  
  36.     }  

  如果result == 0,表明是等待超时了,目前没有新的日志可读,这时候就要先处理之前已经读出来的日志。调用chooseFirst选择日志队列不为空,且日志队列中的第一个日志记录的时间戳为最小的设备,即先输出最旧的日志:
 

 
 
  1. static void chooseFirst(log_device_t* dev, log_device_t** firstdev) {  
  2.     for (*firstdev = NULL; dev != NULL; dev = dev->next) {  
  3.         if (dev->queue != NULL && (*firstdev == NULL || cmp(dev->queue, (*firstdev)->queue) < 0)) {  
  4.             *firstdev = dev;  
  5.         }  
  6.     }  

    如果存在这样的日志设备,接着判断日志记录是应该丢弃还是输出。前面我们说过,如果执行logcat命令时,指定了参数-t <count>,那么就会只显示最新的count条记录,其它的旧记录将被丢弃:
 

 
 
  1. if (g_tail_lines == 0 || queued_lines <= g_tail_lines) {  
  2.      printNextEntry(dev);  
  3. else {  
  4.      skipNextEntry(dev);  

  g_tail_lines表示显示最新记录的条数,如果为0,就表示全部显示。如果g_tail_lines == 0或者queued_lines <= g_tail_lines,就表示这条日志记录应该输出,否则就要丢弃了。每处理完一条日志记录,queued_lines就减1,这样,最新的g_tail_lines就可以输出出来了。
 

        如果result > 0,表明有新的日志可读,这时候的处理方式与result == 0的情况不同,因为这时候还有新的日志可读,所以就不能先急着处理之前已经读出来的日志。这里,分两种情况考虑,如果能设置了只显示最新的g_tail_lines条记录,并且当前已经读出来的日志记录条数已经超过g_tail_lines,就要丢弃,剩下的先不处理,等到下次再来处理;如果没有设备显示最新的g_tail_lines条记录,即g_tail_lines == 0,这种情况就和result  == 0的情况处理方式一样,先处理所有已经读出的日志记录,再进入下一次循环。希望读者可以好好体会这段代码:

 

 
 
  1. while (g_tail_lines == 0 || queued_lines > g_tail_lines) {  
  2.      chooseFirst(devices, &dev);  
  3.      if (dev == NULL || dev->queue->next == NULL) {  
  4.           break;  
  5.      }  
  6.      if (g_tail_lines == 0) {  
  7.           printNextEntry(dev);  
  8.      } else {  
  9.           skipNextEntry(dev);  
  10.      }  
  11.      --queued_lines;  

  丢弃日志记录的函数skipNextEntry实现如下:
 

 
 
  1. static void skipNextEntry(log_device_t* dev) {  
  2.     maybePrintStart(dev);  
  3.     queued_entry_t* entry = dev->queue;  
  4.     dev->queue = entry->next;  
  5.     delete entry;  

 这里只是简单地跳过日志队列头,这样就把最旧的日志丢弃了。

        printNextEntry函数处理日志输出,下一节中继续分析。

        四. 输出日志设备文件的内容。

        从前面的分析中看出,最终日志设备文件内容的输出是通过printNextEntry函数进行的:

 

 
 
  1. static void printNextEntry(log_device_t* dev) {  
  2.     maybePrintStart(dev);  
  3.     if (g_printBinary) {  
  4.         printBinary(&dev->queue->entry);  
  5.     } else {  
  6.         processBuffer(dev, &dev->queue->entry);  
  7.     }  
  8.     skipNextEntry(dev);  

    g_printBinary为true时,以二进制方式输出日志内容到指定的文件中:
 

 
 
  1. void printBinary(struct logger_entry *buf)  
  2. {  
  3.     size_t size = sizeof(logger_entry) + buf->len;  
  4.     int ret;  
  5.       
  6.     do {  
  7.         ret = write(g_outFD, buf, size);  
  8.     } while (ret < 0 && errno == EINTR);  

     我们关注g_printBinary为false的情况,调用processBuffer进一步处理:
 

 
 
  1. static void processBuffer(log_device_t* dev, struct logger_entry *buf)  
  2. {  
  3.     int bytesWritten = 0;  
  4.     int err;  
  5.     AndroidLogEntry entry;  
  6.     char binaryMsgBuf[1024];  
  7.  
  8.     if (dev->binary) {  
  9.         err = android_log_processBinaryLogBuffer(buf, &entry, g_eventTagMap,  
  10.                 binaryMsgBuf, sizeof(binaryMsgBuf));  
  11.         //printf(">>> pri=%d len=%d msg='%s'\n",  
  12.         //    entry.priority, entry.messageLen, entry.message);  
  13.     } else {  
  14.         err = android_log_processLogBuffer(buf, &entry);  
  15.     }  
  16.     if (err < 0) {  
  17.         goto error;  
  18.     }  
  19.  
  20.     if (android_log_shouldPrintLine(g_logformat, entry.tag, entry.priority)) {  
  21.         if (false && g_devCount > 1) {  
  22.             binaryMsgBuf[0] = dev->label;  
  23.             binaryMsgBuf[1] = ' ';  
  24.             bytesWritten = write(g_outFD, binaryMsgBuf, 2);  
  25.             if (bytesWritten < 0) {  
  26.                 perror("output error");  
  27.                 exit(-1);  
  28.             }  
  29.         }  
  30.  
  31.         bytesWritten = android_log_printLogLine(g_logformat, g_outFD, &entry);  
  32.  
  33.         if (bytesWritten < 0) {  
  34.             perror("output error");  
  35.             exit(-1);  
  36.         }  
  37.     }  
  38.  
  39.     g_outByteCount += bytesWritten;  
  40.  
  41.     if (g_logRotateSizeKBytes > 0   
  42.         && (g_outByteCount / 1024) >= g_logRotateSizeKBytes  
  43.     ) {  
  44.         rotateLogs();  
  45.     }  
  46.  
  47. error:  
  48.     //fprintf (stderr, "Error processing record\n");  
  49.     return;  

   当dev->binary为true,日志记录项是二进制形式,不同于我们在Android日志系统驱动程序Logger源代码分析一文中提到的常规格式:

        struct logger_entry | priority | tag | msg
        这里我们不关注这种情况,有兴趣的读者可以自已分析,android_log_processBinaryLogBuffer函数定义在system/core/liblog/logprint.c文件中,它的作用是将一条二进制形式的日志记录转换为ASCII形式,并保存在entry参数中,它的原型为:

 

 
 
  1. /**  
  2.  * Convert a binary log entry to ASCII form.  
  3.  *  
  4.  * For convenience we mimic the processLogBuffer API.  There is no 
  5.  * pre-defined output length for the binary data, since we're free to format  
  6.  * it however we choose, which means we can't really use a fixed-size buffer  
  7.  * here.  
  8.  */  
  9. int android_log_processBinaryLogBuffer(struct logger_entry *buf,  
  10.     AndroidLogEntry *entry, const EventTagMap* map, char* messageBuf,  
  11.     int messageBufLen); 

  通常情况下,dev->binary为false,调用android_log_processLogBuffer函数将日志记录由logger_entry格式转换为AndroidLogEntry格式。logger_entry格式在在Android日志系统驱动程序Logger源代码分析一文中已经有详细描述,这里不述;AndroidLogEntry结构体定义在system/core/include/cutils/logprint.h中:

 
 
  1. typedef struct AndroidLogEntry_t {  
  2.     time_t tv_sec;  
  3.     long tv_nsec;  
  4.     android_LogPriority priority;  
  5.     pid_t pid;  
  6.     pthread_t tid;  
  7.     const char * tag;  
  8.     size_t messageLen;  
  9.     const char * message;  
  10. } AndroidLogEntry; 

  android_LogPriority是一个枚举类型,定义在system/core/include/android/log.h文件中:
 

 
 
  1. /*  
  2.  * Android log priority valuesin ascending priority order.  
  3.  */  
  4. typedef enum android_LogPriority {  
  5.     ANDROID_LOG_UNKNOWN = 0,  
  6.     ANDROID_LOG_DEFAULT,    /* only for SetMinPriority() */  
  7.     ANDROID_LOG_VERBOSE,  
  8.     ANDROID_LOG_DEBUG,  
  9.     ANDROID_LOG_INFO,  
  10.     ANDROID_LOG_WARN,  
  11.     ANDROID_LOG_ERROR,  
  12.     ANDROID_LOG_FATAL,  
  13.     ANDROID_LOG_SILENT,     /* only for SetMinPriority(); must be last */  
  14. } android_LogPriority; 

    android_log_processLogBuffer定义在system/core/liblog/logprint.c文件中:
 

 
 
  1. /**  
  2.  * Splits a wire-format buffer into an AndroidLogEntry  
  3.  * entry allocated by caller. Pointers will point directly into buf  
  4.  *  
  5.  * Returns 0 on success and -1 on invalid wire format (entry will be  
  6.  * in unspecified state)  
  7.  */  
  8. int android_log_processLogBuffer(struct logger_entry *buf,  
  9.                                  AndroidLogEntry *entry)  
  10. {  
  11.     size_t tag_len;  
  12.  
  13.     entry->tv_sec = buf->sec;  
  14.     entry->tv_nsec = buf->nsec;  
  15.     entry->priority = buf->msg[0];  
  16.     entry->pid = buf->pid;  
  17.     entry->tid = buf->tid;  
  18.     entry->tag = buf->msg + 1;  
  19.     tag_len = strlen(entry->tag);  
  20.     entry->messageLen = buf->len - tag_len - 3;  
  21.     entry->message = entry->tag + tag_len + 1;  
  22.  
  23.     return 0;  

     结合logger_entry结构体中日志项的格式定义(struct logger_entry | priority | tag | msg),这个函数很直观,不再累述。
 

        调用完android_log_processLogBuffer函数后,日志记录的具体信息就保存在本地变量entry中了,接着调用android_log_shouldPrintLine函数来判断这条日志记录是否应该输出。

        在分析android_log_shouldPrintLine函数之前,我们先了解数据结构AndroidLogFormat,这个结构体定义在system/core/liblog/logprint.c文件中:

 

 
 
  1. struct AndroidLogFormat_t {  
  2.     android_LogPriority global_pri;  
  3.     FilterInfo *filters;  
  4.     AndroidLogPrintFormat format;  
  5. }; 

    AndroidLogPrintFormat也是定义在system/core/liblog/logprint.c文件中:
 

 
 
  1. typedef struct FilterInfo_t {  
  2.     char *mTag;  
  3.     android_LogPriority mPri;  
  4.     struct FilterInfo_t *p_next;  
  5. } FilterInfo; 

    因此,可以看出,AndroidLogFormat结构体定义了日志过滤规范。在logcat.c文件中,定义了变量
 

 
 
  1. static AndroidLogFormat * g_logformat; 

   这个变量是在main函数里面进行分配的:
 

 
 
  1. g_logformat = android_log_format_new(); 

  在main函数里面,在分析logcat命令行参数时,会将g_logformat进行初始化,有兴趣的读者可以自行分析。
 

       回到android_log_shouldPrintLine函数中,它定义在system/core/liblog/logprint.c文件中:

 

 
 
  1. /**  
  2.  * returns 1 if this log line should be printed based on its priority  
  3.  * and tag, and 0 if it should not 
  4.  */  
  5. int android_log_shouldPrintLine (  
  6.         AndroidLogFormat *p_format, const char *tag, android_LogPriority pri)  
  7. {  
  8.     return pri >= filterPriForTag(p_format, tag);  

   这个函数判断在p_format中根据tag值,找到对应的pri值,如果返回来的pri值小于等于参数传进来的pri值,那么就表示这条日志记录可以输出。我们来看filterPriForTag函数的实现:
 

 
 
  1. static android_LogPriority filterPriForTag(  
  2.         AndroidLogFormat *p_format, const char *tag)  
  3. {  
  4.     FilterInfo *p_curFilter;  
  5.  
  6.     for (p_curFilter = p_format->filters  
  7.             ; p_curFilter != NULL 
  8.             ; p_curFilter = p_curFilter->p_next  
  9.     ) {  
  10.         if (0 == strcmp(tag, p_curFilter->mTag)) {  
  11.             if (p_curFilter->mPri == ANDROID_LOG_DEFAULT) {  
  12.                 return p_format->global_pri;  
  13.             } else {  
  14.                 return p_curFilter->mPri;  
  15.             }  
  16.         }  
  17.     }  
  18.  
  19.     return p_format->global_pri;  

如果在p_format中找到与tag值对应的filter,并且该filter的mPri不等于ANDROID_LOG_DEFAULT,那么就返回该filter的成员变量mPri的值;其它情况下,返回p_format->global_pri的值。
 

        回到processBuffer函数中,如果执行完android_log_shouldPrintLine函数后,表明当前日志记录应当输出,则调用android_log_printLogLine函数来输出日志记录到文件fd中, 这个函数也是定义在system/core/liblog/logprint.c文件中:

 

 
 
  1. int android_log_printLogLine(  
  2.     AndroidLogFormat *p_format,  
  3.     int fd,  
  4.     const AndroidLogEntry *entry)  
  5. {  
  6.     int ret;  
  7.     char defaultBuffer[512];  
  8.     char *outBuffer = NULL;  
  9.     size_t totalLen;  
  10.  
  11.     outBuffer = android_log_formatLogLine(p_format, defaultBuffer,  
  12.             sizeof(defaultBuffer), entry, &totalLen);  
  13.  
  14.     if (!outBuffer)  
  15.         return -1;  
  16.  
  17.     do {  
  18.         ret = write(fd, outBuffer, totalLen);  
  19.     } while (ret < 0 && errno == EINTR);  
  20.  
  21.     if (ret < 0) {  
  22.         fprintf(stderr, "+++ LOG: write failed (errno=%d)\n", errno);  
  23.         ret = 0;  
  24.         goto done;  
  25.     }  
  26.  
  27.     if (((size_t)ret) < totalLen) {  
  28.         fprintf(stderr, "+++ LOG: write partial (%d of %d)\n", ret,  
  29.                 (int)totalLen);  
  30.         goto done;  
  31.     }  
  32.  
  33. done:  
  34.     if (outBuffer != defaultBuffer) {  
  35.         free(outBuffer);  
  36.     }  
  37.  
  38.     return ret;  

 这个函数的作用就是把AndroidLogEntry格式的日志记录按照指定的格式AndroidLogFormat进行输出了,这里,不再进一步分析这个函数。

 

        processBuffer函数的最后,还有一个rotateLogs的操作:

 

 
 
  1. static void rotateLogs()  
  2. {  
  3.     int err;  
  4.  
  5.     // Can't rotate logs if we're not outputting to a file  
  6.     if (g_outputFileName == NULL) {  
  7.         return;  
  8.     }  
  9.  
  10.     close(g_outFD);  
  11.  
  12.     for (int i = g_maxRotatedLogs ; i > 0 ; i--) {  
  13.         char *file0, *file1;  
  14.  
  15.         asprintf(&file1, "%s.%d", g_outputFileName, i);  
  16.  
  17.         if (i - 1 == 0) {  
  18.             asprintf(&file0, "%s", g_outputFileName);  
  19.         } else {  
  20.             asprintf(&file0, "%s.%d", g_outputFileName, i - 1);  
  21.         }  
  22.  
  23.         err = rename (file0, file1);  
  24.  
  25.         if (err < 0 && errno != ENOENT) {  
  26.             perror("while rotating log files");  
  27.         }  
  28.  
  29.         free(file1);  
  30.         free(file0);  
  31.     }  
  32.  
  33.     g_outFD = openLogFile (g_outputFileName);  
  34.  
  35.     if (g_outFD < 0) {  
  36.         perror ("couldn't open output file");  
  37.         exit(-1);  
  38.     }  
  39.  
  40.     g_outByteCount = 0;  
  41.  

   这个函数只有在执行logcat命令时,指定了-f <filename>参数时,即g_outputFileName不为NULL时才起作用。它的作用是在将日志记录循环输出到一组文件中。例如,指定-f参数为logfile,g_maxRotatedLogs为3,则这组文件分别为:

 

        logfile,logfile.1,logfile.2,logfile.3

        当当前输入到logfile文件的日志记录大小g_outByteCount大于等于g_logRotateSizeKBytes时,就要将logfile.2的内容移至logfile.3中,同时将logfile.1的内容移至logfile.2中,同时logfle的内容移至logfile.1中,再重新打开logfile文件进入后续输入。这样做的作用是不至于使得日志文件变得越来越来大,以至于占用过多的磁盘空间,而是只在磁盘上保存一定量的最新的日志记录。这样,旧的日志记录就会可能被新的日志记录所覆盖。

        至此,关于Android日志系统源代码,我们就完整地分析完了,其中包括位于内核空间的驱动程序Logger源代码分析,还有位于应用程序框架层和系统运行库层的日志写入操作接口源代码分析和用于日志读取的工具Logcat源代码分析,希望能够帮助读者对Android的日志系统有一个清晰的认识。





本文转自 Luoshengyang 51CTO博客,原文链接:http://blog.51cto.com/shyluo/964523,如需转载请自行联系原作者

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
20天前
|
监控 应用服务中间件 定位技术
要统计Nginx的客户端IP,可以通过分析Nginx的访问日志文件来实现
要统计Nginx的客户端IP,可以通过分析Nginx的访问日志文件来实现
|
21天前
|
存储 监控 安全
什么是事件日志管理系统?事件日志管理系统有哪些用处?
事件日志管理系统是IT安全的重要工具,用于集中收集、分析和解释来自组织IT基础设施各组件的事件日志,如防火墙、路由器、交换机等,帮助提升网络安全、实现主动威胁检测和促进合规性。系统支持多种日志类型,包括Windows事件日志、Syslog日志和应用程序日志,通过实时监测、告警及可视化分析,为企业提供强大的安全保障。然而,实施过程中也面临数据量大、日志管理和分析复杂等挑战。EventLog Analyzer作为一款高效工具,不仅提供实时监测与告警、可视化分析和报告功能,还支持多种合规性报告,帮助企业克服挑战,提升网络安全水平。
|
1月前
|
存储 SQL 监控
|
1月前
|
运维 监控 安全
|
1月前
|
存储 Linux Docker
centos系统清理docker日志文件
通过以上方法,可以有效清理和管理CentOS系统中的Docker日志文件,防止日志文件占用过多磁盘空间。选择合适的方法取决于具体的应用场景和需求,可以结合手动清理、logrotate和调整日志驱动等多种方式,确保系统的高效运行。
57 2
|
1月前
|
监控 关系型数据库 MySQL
分析慢查询日志
【10月更文挑战第29天】分析慢查询日志
43 3
|
1月前
|
监控 关系型数据库 数据库
怎样分析慢查询日志?
【10月更文挑战第29天】怎样分析慢查询日志?
43 2
|
2月前
|
存储 缓存 关系型数据库
MySQL事务日志-Redo Log工作原理分析
事务的隔离性和原子性分别通过锁和事务日志实现,而持久性则依赖于事务日志中的`Redo Log`。在MySQL中,`Redo Log`确保已提交事务的数据能持久保存,即使系统崩溃也能通过重做日志恢复数据。其工作原理是记录数据在内存中的更改,待事务提交时写入磁盘。此外,`Redo Log`采用简单的物理日志格式和高效的顺序IO,确保快速提交。通过不同的落盘策略,可在性能和安全性之间做出权衡。
1700 14
|
2月前
|
XML JSON 监控
告别简陋:Java日志系统的最佳实践
【10月更文挑战第19天】 在Java开发中,`System.out.println()` 是最基本的输出方法,但它在实际项目中往往被认为是不专业和不足够的。本文将探讨为什么在现代Java应用中应该避免使用 `System.out.println()`,并介绍几种更先进的日志解决方案。
61 1
|
2月前
|
监控 网络协议 安全
Linux系统日志管理
Linux系统日志管理
62 3