企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
从前面4.6.2小节的内容可以知道,Logcat工具是从源代码文件logcat.cpp中的函数readLogLines开始读取日志记录的,我们分段来阅读这个函数的实现。 **system/core/logcat/logcat.cpp** ~~~ 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); ~~~ 由于Logcat工具有可能同时打开了多个日志设备,因此,第19行到第26行的while循环就使用函数select来同时监控它们是否有内容可读,即是否有新的日志记录需要读取。调用函数select时,需要指定所监控的日志设备文件描述符的最大值,因此,第12行到第16行的for循环就用来查找这些打开的日志设备中的最大文件描述符,并保存在变量max中。在调用函数select之前,第22行到第24行的for循环把所有打开的日志设备的文件描述符都保存到一个fd_set对象readset中,接着第25行就调用函数select来监控前面所打开的日志设备是否有新的日志记录可读,其中,指定的等待时间为5毫秒,即如果在5毫秒之内,所有打开的日志设备都没有新的日志记录可读,那么函数select就超时返回,即它的返回值为0;否则,函数select就会将fd_set对象readset中的相应位设置为1,表示该位所对应的日志设备有新的日志记录可读,这时候函数select的返回值是大于0的。如果在调用函数select的过程中,Logcat工具有信号需要处理,那么函数select的返回值就会等于-1,并且错误代码errno等于EINTR,表示Logcat工具需要重新调用函数select来检查打开的日志设备是否有新的日志记录可读。 当函数跳出第19行到26行的while循环之后,有可能是等待超时,也有可能是所监控的日志设备中有新的日志记录可读,因此,我们需要分两种情况来分析日志记录的读取过程。 首先分析日志设备中有新的日志记录可读的情况,如下所示。 **system/core/logcat/logcat.cpp** ~~~ 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; } } ~~~ 第28行到第55行的for循环依次处理有新的日志记录可读的日志设备。如果一个日志设备有新的日志记录可读,那么第29行的if语句就会为true,接着第30行就会分配一个queued_entry_t结构体entry,并且第32行调用函数read把该日志设备中的一条新的日志记录读到结构体entry内部的缓冲区buf中。如果在读取日志记录的过程中出现错误,那么Logcat工具就会调用函数exit直接退出。但是如果错误码等于EINTR或者EAGAIN,就需要特殊处理。如果错误代码errno等于EINTR,就说明Logcat工具在读取日志记录的过程中被信号打断,因此,Logcat工具会重新执行next标签处的代码,即重新执行第18行的while循环来监控所打开的日志设备中是否有新的日志记录可读。如果错误码等于EAGAIN,就说明该日志设备在打开时指定了O_NONBLOCK标志,即以非阻塞的模式来打开该日志设备,这时候Logcat工具就会跳出第28行的for循环,继续往下执行。 如果第32行成功地从相应的日志设备中读取到新的日志记录,那么第52行就会将它加入到相应的日志设备的日志记录队列中,并且第53行就会将队列中的日志记录计数queued_lines增加1,表示Logcat工具当前正在等待显示的日志记录条数。 第28行的for循环执行完成之后,就从每个有新的日志记录的日志设备中读出一条日志记录。 > 这些日志设备中的可读日志记录数可能不只一条,因此,接下来还需要执行第18行的while循环来继续读取这些日志设备中的其他日志记录。不过,在继续读取这些剩余的日志记录之前,Logcat工具先处理前面已经从日志设备中读取出来的日志记录,如下所示。 **system/core/logcat/logcat.cpp** ~~~ 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: ; } } ~~~ 第56行到第92行的if语句块是用来处理日志记录输出的,主要通过chooseFirst、printNextEntry和skipNextEntry三个函数来实现。 由于Logcat工具是按照写入时间的先后顺序来输出日志记录的,因此,在输出已经读取的日志记录之前,Logcat工具首先会调用函数chooseFirst找到包含有最早的未输出日志记录的日志设备,它的实现如下所示。 **system/core/logcat/logcat.cpp** ~~~ static void chooseFirst(log_device_t* dev, log_device_t** firstdev) { for (*firstdev = NULL; dev != NULL; dev = dev->next) { if (dev->queue != NULL && (*firstdev == NULL || cmp(dev->queue, (*firstdev)->queue) < 0)) { *firstdev = dev; } } } ~~~ 因为每一个日志设备的日志队列都是按照写入时间的先后顺序来排列日志记录的,因此,函数chooseFirst只要比较日志队列中的第一个日志记录的写入时间,就可以找到包含有最早的未输出日志记录的日志设备。 真正用来输出日志记录的函数是printNextEntry,它的实现如下所示。 **system/core/logcat/logcat.cpp** ~~~ static void printNextEntry(log_device_t* dev) { maybePrintStart(dev); if (g_printBinary) { printBinary(&dev->queue->entry); } else { processBuffer(dev, &dev->queue->entry); } skipNextEntry(dev); } ~~~ 第2行调用函数maybePrintStart来检查日志设备dev中的日志记录是否是第一次输出。如果是,就会首先输出一行提示性文字,如下所示。 **system/core/logcat/logcat.cpp** ~~~ static void maybePrintStart(log_device_t* dev) { if (!dev->printed) { dev->printed = true; if (g_devCount > 1 && !g_printBinary) { char buf[1024]; snprintf(buf, sizeof(buf), "--------- beginning of %s\n", dev->device); if (write(g_outFD, buf, strlen(buf)) < 0) { perror("output error"); exit(-1); } } } } ~~~ 回到函数printNextEntry中,如果在启动Logcat工具时,指定了B选项,那么全局变量g_printBinary的值就会被设置为1,表示Logcat工具要以二进制格式来输出读取到的日志记录,因此,第4行就会调用函数printBinary来输出已经读取到的日志记录;否则,第6行就会调用函数processBuffer来输出已经读取到的日志记录。在接下来的4.6.4小节中分析日志记录的输出过程时,我们再详细分析这两个函数的实现。 日志队列中的日志记录输出之后,就要将它从队列中删除,这是通过调用函数skipNextEntry来实现的,如下所示。 **system/core/logcat/logcat.cpp** ~~~ static void skipNextEntry(log_device_t* dev) { maybePrintStart(dev); queued_entry_t* entry = dev->queue; dev->queue = entry->next; delete entry; } ~~~ 回到函数readLogLines中,我们接着分析第56行到第92行的if语句块是如何处理那些已经从日志设备中读取出来的日志记录的。 如果变量result的值等于0,即第56行的if语句为true,就说明前面在调用函数select监控日志设备中是否有新的日志记录可读时超时了。既然所有打开的日志设备都没有新的日志记录可读,第60行到第71行的while循环就是时候输出之前已经读取出来的日志记录了。第61行首先调用函数chooseFirst来获得包含有最早的未输出日志记录的日志设备,然后再考虑是否要输出这条最早的日志记录。如果在启动Logcat工具时,指定了t选项,即限定了可以输出的最新日志记录的条数,那么就需要计算所有日志设备中的未输出日志记录的条数。如果未输出的日志记录条数大于可以输出的最大值,即全局变量g_tail_lines的值,那么就需要将最早的一部分日志记录丢弃;如果没有限定可以输出的最新日志记录的条数,即全局变量g_tail_lines的值为0,那么Logcat工具就会将所有未输出的日志记录输出。处理完成日志设备中的已读取日志记录之后,第74行检查Logcat工具是否以非阻塞的模式来打开日志设备。如果是,由于这时候函数select是超时返回,即日志设备中没有新的日志记录可读,那么Logcat工具就直接从第75行退出了。 如果变量result的值大于0,即第56行的if语句为false,那么Logcat工具就会执行第77行到第92行代码来处理日志设备中的已读取日志记录。在这种情况下,日志设备中可能还有新的日志记录等待读取,因此,它的处理方式就会与函数select超时的情况有所不同。这时候如果没有限定可以输出的最新日志记录的条数,即全局变量g_tail_lines的值为0,那么Logcat工具就不用考虑日志设备中是否还有剩余的日志记录未读取了,它可以立即输出那些已经读取的日志记录;如果限定了可以输出的最新日志记录的条数,即全局变量g_tail_lines的值大于0,那么就要把最早的一部分日志记录删除,直到它们的数量小于等于限定的可以输出的最新日志记录的条数为止。在这种情况下,还需要继续读取日志设备中的其余未读取日志记录,直到所有打开的日志设备都没有新的日志记录可读时,Logcat工具才会将已经读取的日志记录输出。 以上就是日志记录的读取过程。接下来,我们继续分析日志记录的输出过程。