前言
該篇文章是我的讀書和實踐筆記。參考的是《Android系統源代碼情景分析》。
運行時庫層日志庫——liblog
Android系統在運行時庫層提供了一個用來和Logger日志驅動程序進行交互的日志庫liblog。通過日志庫liblog提供的接口,應用程序就可以方便地往Logger日志驅動程序中寫入日志記錄。
位於運行時庫層的C/C++日志寫入接口和位於應用程序框架層的Java日志寫入接口都是通過liblog庫提供的日志寫入接口來往Logger日志驅動程序中寫入日志記錄的。
源碼分析
日志庫liblog提供的日志記錄寫入接口實現在logd_write.c文件中,它的源碼位置為:/system/core/liblog/logd_write.c。
根據寫入的日志記錄的類型不同,這些函數可以划分為三個類別,其中:
函數__android_log_assert、__android_log_vprint和__android_log_print用來寫入類型為main的日志記錄。
函數__android_log_btwrite和__android_log_bwrite用來寫入類型為events的日志記錄。
函數__android_log_buf_print可以寫入任意一種類型的日志記錄。
無論寫入的是什么類型的日志記錄,它們最終都是通過調用函數write_to_log寫入到Logger日志驅動程序中的。write_to_log是一個函數指針,它開始時指向函數__write_to_log_init。因此,當函數write_to_log第一次被調用時,實際上執行的是函數__write_to_log_init。函數__write_to_log_init主要是進行一些日志庫初始化操作,接着函數指針write_to_log重定向到函數__write_to_log_kernel或者__write_to_log_null中,這取決於能否成功地將日志設備文件打開。
源碼分析如上,源碼實現如下:
// 先聲明,后引用 static int __write_to_log_init(log_id_t, struct iovec *vec, size_t nr); int (*write_to_log)(log_id_t, struct iovec *vec, size_t nr) = __write_to_log_init; // 一些定義在system/core/include/cutils/log.h中的宏 typedef enum { LOG_ID_MAIN = 0, LOG_ID_RADIO = 1, LOG_ID_EVENTS = 2, LOG_ID_SYSTEM = 3, LOG_ID_MAX } log_id_t; #define LOGGER_LOG_MAIN "log/main" #define LOGGER_LOG_RADIO "log/radio" #define LOGGER_LOG_EVENTS "log/events" #define LOGGER_LOG_SYSTEM "log/system" // 真正函數執行的地方 static int __write_to_log_init(log_id_t log_id, struct iovec *vec, size_t nr) { if (write_to_log == __write_to_log_init) { 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); // 修改write_to_log函數指針 write_to_log = __write_to_log_kernel; if (log_fds[LOG_ID_MAIN] < 0 || log_fds[LOG_ID_RADIO] < 0 || log_fds[LOG_ID_EVENTS] < 0) { log_close(log_fds[LOG_ID_MAIN]); log_close(log_fds[LOG_ID_RADIO]); log_close(log_fds[LOG_ID_EVENTS]); log_fds[LOG_ID_MAIN] = -1; log_fds[LOG_ID_RADIO] = -1; log_fds[LOG_ID_EVENTS] = -1; write_to_log = __write_to_log_null; } if (log_fds[LOG_ID_SYSTEM] < 0) { log_fds[LOG_ID_SYSTEM] = log_fds[LOG_ID_MAIN]; } } return write_to_log(log_id, vec, nr); }
通過上述代碼,我們在替換宏定義之后,是可以知道調用log_open打開的分別是/dev/log/main、/dev/log/radio、/dev/log/events、/dev/log/system四個日志設備文件。而宏log_open定義在system/core/liblog/logd_write.c中:
#if FAKE_LOG_DEVICE // 不需要care這里,真正編譯的時候FAKE_LOG_DEVICE為0 #else #define log_open(pathname, flags) open(pathname, (flags) | O_CLOEXEC) #define log_writev(filedes, vector, count) writev(filedes, vector, count) #define log_close(filedes) close(filedes) #endif
從上面代碼可以看出,log_open的真正實現是open函數。
回到最開始的地方,如果log_open的文件都是ok的,那接下來會調用__write_to_log_kernel函數,源碼實現如下:
static int __write_to_log_kernel(log_id_t log_id, struct iovec *vec, size_t nr) { ssize_t ret; int log_fd; if ((int)log_id < (int)LOG_ID_MAX) { log_fd = log_fds[(int)log_id]; } else { return EBADF; } do { ret = log_writev(log_fd, vec, nr); } while (ret < 0 && errno == EINTR); return ret; }
函數__write_to_log_kernel會根據參數log_id在全局數組log_fds中找到對應的日志設備文件描述符,然后調用宏log_writev,即函數writev,把日志記錄寫入到Logger日志驅動程序中。
如果設備文件打開失敗的話,write_to_log函數指針會被賦值為__write_to_log_kernel,這個函數其實什么都沒有做,只是返回了個-1。所以就不貼源碼了。
最后,我們在分析一下__android_log_buf_write函數。因為C/C++日志寫入接口和Java日志寫入接口最終都是調用了這個函數完成了日志的寫入。源碼如下:
int __android_log_buf_write(int bufID, int prio, const char *tag, const char *msg) { struct iovec vec[3]; char tmp_tag[32]; if (! tag) tag = ""; if ((bufID != LOG_ID_RADIO) && (!strcmp(tag, "HTC_RIL") || (!strncmp(tag, "RIL", 3)) || (!strncmp(tag, "IMS", 3)) || !strcmp(tag, "AT") || !strcmp(tag, "GSM") || !strcmp(tag, "STK") || !strcmp(tag, "CDMA") || !strcmp(tag, "PHONE") || !strcmp(tag, "SMS"))) { bufID = LOG_ID_RADIO; snprintf(tmp_tag, sizeof(tmp_tag), "use-Rlog/RLOG-%s", tag); tag = tmp_tag; } vec[0].iov_base = (unsigned char *) &prio; vec[0].iov_len = 1; vec[1].iov_base = (void *) tag; vec[1].iov_len = strlen(tag) + 1; vec[2].iov_base = (void *) msg; vec[2].iov_len = strlen(msg) + 1; return write_to_log(log_id, vec, 3); }
在默認情況下,函數__android_log_write寫入的日志記錄類型為main。然后,如果傳進來的日志記錄的標請以”RIL”等標志開頭,那么它就會被認為是類型是radio的日志記錄。
C/C++日志寫入接口
Android系統提供了三組常用的C/C++宏來封裝日志寫入接口。之所以這樣做,是為了方便開發同學進行日志的開關控制,例如不在發布版本中打開日志。
三組宏定義分別為:
ALOGV,ALOGD,ALOGI,ALOGW和ALOGE。用來記錄類型為main的日志。
SLOGV,SLOGD,SLOGI,SLOGW和SLOGE,用來寫入類型為system的日志。
LOG_EVENT_INT,LOG_EVENT_LONG和LOG_EVENT_STRING,它們用來寫入類型為events的日志記錄。
這些宏定義在system/core/include/log/log.h中,並且使用了一個LOG_NDEBUG的宏來作為日志開關。
具體源碼如下:
// 日志開關 #ifndef LOG_NDEBUG #ifdef NDEBUG #define LOG_NDEBUG 1 #else #define LOG_NDEBUG 0 #endif #endif // 以ALOGE為例子 #ifnded ALOGE #define ALOGE(...) ((void)ALOG(LOG_WARN, LOG_TAG, __VA_ARGS__)) #endif #ifndef ALOG #define ALOG(priority, tag, ...) \ LOG_PRI(ANDROID_##priority, tag, __VA_ARGS__) #endif #ifndef LOG_PRI #define LOG_PRI(priority, tag, ...) \ android_printLog(priority, tag, __VA_ARGS__) #endif # 回到了我們熟悉的__android_log_print函數 #define android_printLog(prio, tag, fmt...)\ __android_log_print(prio, tag, fmt)
Java日志寫入接口
Android系統在應用程序框架中定義了三個Java日志寫入接口,它們分別是android.util.Log、android.util.Slog和android.util.EventLog,寫入的日志記錄類型分別為main、system和events。
這里主要分析android.util.log的實現。源碼如下:
public final class Log { /** * Priority constant for the println method; use Log.v. */ public static final int VERBOSE = 2; /** * Priority constant for the println method; use Log.d. */ public static final int DEBUG = 3; /** * Priority constant for the println method; use Log.i. */ public static final int INFO = 4; /** * Priority constant for the println method; use Log.w. */ public static final int WARN = 5; /** * Priority constant for the println method; use Log.e. */ public static final int ERROR = 6; /** * Priority constant for the println method. */ public static final int ASSERT = 7; private Log() { } /** * Send a {@link #VERBOSE} log message. * @param tag Used to identify the source of a log message. It usually identifies * the class or activity where the log call occurs. * @param msg The message you would like logged. */ public static int v(String tag, String msg) { return println_native(LOG_ID_MAIN, VERBOSE, tag, msg); } /** * Send a {@link #DEBUG} log message. * @param tag Used to identify the source of a log message. It usually identifies * the class or activity where the log call occurs. * @param msg The message you would like logged. */ public static int d(String tag, String msg) { return println_native(LOG_ID_MAIN, DEBUG, tag, msg); } /** * Send an {@link #INFO} log message. * @param tag Used to identify the source of a log message. It usually identifies * the class or activity where the log call occurs. * @param msg The message you would like logged. */ public static int i(String tag, String msg) { return println_native(LOG_ID_MAIN, INFO, tag, msg); } /** * Send a {@link #WARN} log message. * @param tag Used to identify the source of a log message. It usually identifies * the class or activity where the log call occurs. * @param msg The message you would like logged. */ public static int w(String tag, String msg) { return println_native(LOG_ID_MAIN, WARN, tag, msg); } /** * Send an {@link #ERROR} log message. * @param tag Used to identify the source of a log message. It usually identifies * the class or activity where the log call occurs. * @param msg The message you would like logged. */ public static int e(String tag, String msg) { return println_native(LOG_ID_MAIN, ERROR, tag, msg); } /** @hide */ public static final int LOG_ID_MAIN = 0; /** @hide */ public static final int LOG_ID_RADIO = 1; /** @hide */ public static final int LOG_ID_EVENTS = 2; /** @hide */ public static final int LOG_ID_SYSTEM = 3; /** @hide */ public static native int println_native(int bufID, int priority, String tag, String msg); }
可以看到,JAVA應用層logger代碼是調用了JNI層的android_util_Log.cpp,源碼如下:
static jint android_util_Log_println_native(JNIEnv* env, jobject clazz, jint bufID, jint priority, jstring tagObj, jstring msgObj) { const char* tag = NULL; const char* msg = NULL; if (msgObj == NULL) { jniThrowNullPointerException(env, "println needs a message"); } if (bufID < 0 || bufID >= LOG_ID_MAX) { jniThrowNullPointerException(env, "bad bufID"); return -1; } if (tagObj != NULL) { tag = env->GetStringUTFChars(tagObj, NULL); } msg = env->GetStringUTFChars(msgObj, NULL); int res = -1; // 真正日志寫入的函數(liblog.so中的函數) res = __android_log_buf_write(bufID, (android_LogPriority), tag, msg); return res; }
logcat工具分析
前面分析的將日志記錄寫入到Logger日志中的目的就是通過logcat工具將它們讀出來,然后給開發人員進行分析。
Logcat的用法很多,但是這里主要從源碼的角度出發,分析Logcat的四個部分:
基礎數據結構。
初始化過程。
日志記錄的讀取過程。
日志記錄的輸出過程。
logcat的源碼位於:system/core/logcat.cpp中。
基礎數據結構
首先是定義在system/core/include/log/logger.h中的logger_entry,定義如下:
struct logger_entry { uint16_t len; uint16_t __pad; int32_t pid; int32_t tid; int32_t sec; int32_t nsec; char msg[0]; };
結構體logger_entry用來描述一條日志記錄。其中,char msg[0]指針用來記錄消息實體內容。
然后,在看一下queued_entry_t結構體,源碼如下:
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; } };
結構體queued_entry_t用來描述一個日志記錄隊列。每一種類型的日志記錄都對應有一個日志記錄隊列。
接下來,再來看一下日志設備的結構體log_device_t。源碼如下:
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; } } };
結構體log_device_t用來描述一個日志設備。其中:
成員變量device保存的是日志設備文件名稱。Logger日志驅動程序初始化時,會創建四個設備文件/dev/log/main、/dev/log/system、/dev/log/radio和/dev/log/events分別代表四個日志設備。
成員變量label用來描述日志設備的標號,其中,日志設備/dev/log/main、/dev/log/system、/dev/log/radio、/dev/log/events對應的標號分別為m、s、r、e。
成員binary是一個布爾值,表示日志記錄的內容是否是二進制格式的。目前只有/dev/log/events記錄的是二進制格式。
成員變量fd是一個文件描述符,它是調用函數open來打開相應的日志設備文件得到的,用來從logger日志驅動程序中讀取日志記錄。
成員變量printed是一個布爾值,用來表示一個日志設備是否已經處於輸出狀態。
成員變量queue用來保存日志設備中的日志記錄。
成員變量next用來連接下一個日志設備。
成員函數enqueue用來將一條日志記錄添加到內部的日志記錄隊列中。每次往隊列中加入一條日志記錄時,都會根據它的寫入時間來找到它在隊列中的位置,然后將它插入隊列中。寫入的時間比較是通過cmp函數實現的。
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; }
其實,我覺得cpm函數沒什么好解釋的,真正有意思的是enqueue函數的實現。這里使用了一個技巧,通過二級指針來減少變量聲明(ps:通常我們在做鏈表插入操作的時候,一般會維護兩個指針)。
二級指針的精髓在於,可以讓我們修改指針的值。(ps:想要修改指針的值,就需要修改指針的指針)。
真正日志打印的時候,需要轉換成AndroidLogEntry結構體,相關的結構體定義如下:
typedef struct AndroidLogEntry_t { time_t tv_sec; long tv_nsec; android_LogPriority priority; int msg_type; int32_t pid; int32_t tid; const char *tag; size_t messageLen; const char *message; } AndroidLogEntry; typedef enum android_LogPriority { ANDROID_LOG_UNKNOWN = 0, ANDROID_LOG_DEFAULT, ANDROID_LOG_VERBOSE, ANDROID_LOG_DEBUG, ANDROID_LOG_INFO, ANDROID_LOG_WARN, ANDROID_LOG_ERROR, ANDROID_LOG_FATAL, ANDROID_LOG_SILENT, } android_LogPriority;
同時,還有一個結構體FilterInfo_t用來描述日志記錄輸出過濾器。源碼如下:
typedef struct FilterInfo_t { char *mTag; android_LogPriority mPri; struct FilterInfo_t *p_next; } FilterInfo;
成員變量mTag和mPri分別表示要過濾的日志記錄的標簽和優先級。當一條日志記錄的標簽等於mTag時,如果它的優先級大於等於mPri,那么它就會被輸出,否則就會被忽略。成員變量p_next用來連接下一個日志輸出過濾器。
最后,再介紹AndroidLogFormat_t結構體。
struct AndroidLogFormat_t { android_LogPriority global_pri; FilterInfo *filters; AndroidLogPrintFormat format; };
從結構體定義上就可以知道,這個結構體是用來保存日志記錄的輸出格式以及輸出過濾器的。
初始化過程
Logcat工具的初始化過程是從文件logcat.cpp中的main函數開始的,它會打開日志設備和解析命令行參數。由於函數較長,需要分段解釋一下。
// 數據結構定義 struct AndroidLogFormat_t { android_LogPriority global_pri; FilterInfo *filters; AndroidLogPrintFormat format; }; typedef struct AndroidLogFormat_t AndroidLogFormat; // 變量聲明 static AndroidLogFormat *g_logformat; // 函數定義 AndroidlogFormat *android_log_format_new() { AndroidLogFormat *p_ret; p_ret = calloc(1, sizeof(AndroidLogFormat)); p_ret->global_pri = ANDROID_LOG_VERBOSE; p_ret->format = FORMAT_BRIEF; return p_ret; } // main函數 int main(int argc, char **argv) { int err = 0; int hasSetLogFormat = 0; int clearLog = 0; int getLogSize = 0; int mode = O_RDONLY; const char *forceFilters = NULL; log_device_t *devices = NULL; log_device_t *dev; bool needBinary = false; g_logformat = android_log_format_new(); }
從函數android_log_format_new的實現可以看出,全局變量g_logformat指定了日志的默認輸出格式為FOTAMT_BRIEF,指定了日志記錄過濾優先級為ANDROID_LOG_VERBOSE.
回到main函數,我們繼續分析main函數對傳入參數的解析過程源碼如下:
#define LOG_FILE_DIR "/dev/log/" for (;;) { int ret; ret = getopt(argc, argv, "cdt:gsQf:r::n:v:b:B"); if (ret < 0) break; switch(ret) { case 'd': // logcat日志驅動程序中沒有日志記錄可讀時,logcat工具直接退出 g_nonblock = true; break; case 't': // 同d選項,並且指定每次日志輸出的條數為g_tail_lines g_nonblock = true; g_tail_lines = atoi(optarg); break; case 'b': { // 通過-b參數指定需要打開的日志文件(main,system,radio,events),並將其設備數據結構添加到鏈表devices中 char* buf = (char*) malloc(strlen(LOG_FILE_DIR) + strlen(optarg) + 1); strcpy(buf, LOG_FILE_DIR); strcat(buf, optarg); bool binary = strcmp(optarg, "events") == 0; if (binary) { needBinary = true; } if (devices) { dev = devices; while (dev->next) { dev = dev->next; } dev->next = new log_device_t(buf, binary, optarg[0]); } else { devices = new log_device_t(buf, binary, optarg[0]); } android::g_devCount ++; } break; case 'B': // 表示以二進制格式輸出日志 android::g_printBinary = 1; break; case 'f': // 日志記錄輸出文件的名稱 android::g_outputFileName = optarg; break; case 'r': // 記錄每一個日志記錄輸出文件的最大容量 if (optarg == NULL) { android::g_logRotateSizeKBytes = DEFAULT_LOG_ROTATE_SIZE_KBYTES; } else { long logRotateSize; char *lastDigit; if (!isdigit(optarg[0])) { fprintf(stderr, "Invalid parameter to -r\n"); exit(-1); } android::g_logRotateSizeKBytes = atoi(optarg); } break; case 'n': if (!isdigit(optarg[0])) { // 這里有個Android源碼的Bug,源碼里寫的是-r(太粗心了吧!!) fprintf(stderr, "Invalid parameter to -n\n"); exit(-1); } android:g_maxRotatedLogs = atoi(optarg); break; case 'v': // 設置日志輸出格式 err = setLogFormat(optarg); if (err < 0) { fprintf(stderr, "Invalid parameter to -v\n"); exit(-1); } hasSetLogFormat = 1; break; }
這段代碼主要是解析參數,每一個參數的含義我已經通過注釋寫到代碼里去了。
解析完命令行參數后,代碼繼續往后執行:
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; // 如果/dev/log/system文件存在,默認也讀取system日志 if (0 == access("/dev/"LOGGER_LOG_SYSTEM, accessmode)) { devices->next = new log_device_t(strdup("/dev/"LOGGER_LOG_SYSTEM), false, 's'); android::g_devCount ++; } }
這段代碼的主要作用是:當用戶沒有指定-b參數時,默認將main和system的log輸出到logcat中。
接下來,繼續分析main函數源碼。
android:setupOutput(); static void setupOutput() { if (g_outputFileName == NULL) { // logcat的默認輸出為標准輸出 g_outFD = STDOUT_FILENO; } else { // 使用-f選項指定了輸出 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; } }
回到main函數,繼續向下閱讀源碼。
if (hasSetLogFormat == 0) { const char* logFormat = getenv("ANDROID_PRINTF_LOG"); if (logFormat != NULL) { err = setLogFormat(logFormat); if (err < 0) { fprintf(stderr, "invalid format in ANDROID_PRINTF_LOG '%s'\n", logFormat); } } }
這塊代碼的主要作用是:當用戶沒有指定特定的輸出格式時,logcat會查一下ANDROID_PRINTF_LOG的值,如果這個值設置的話,就將日志格式改為這個值。
設置好logcat日志輸出格式后,logcat會繼續向下執行。
if (forceFilters) { // 不需要管這里 } else if (argc == optind){ // 不需要care } else { // 增加logcat過濾器 for (int i = optind; i < argc; i ++) { err = android_log_addFilterString(g_logformat, argv[i]); if (err < 0) { fprintf(stderr, "Invalid filter expression '%s'\n", argv[i]); exit(-1); } } } int android_log_addFilterString(AndroidLogFormat *p_format, const char *filterString) { char *filterStringCopy = strdup(filterString); char *p_cur = filterStringCopy; char *p_ret; int err; while (NULL != (pret = strsep(&p_cur, " \t,"))) { if (p_ret[0] != '\0') { err = android_log_addFilterRule(p_format, p_ret); if (err < 0) { goto err; } } } free(filterStringCopy); return 0; error: free(filterStringCopy); return -1; } int android_log_addFilterRule(AndroidLogFormat *p_format, const char *filterExpression) { size_t i = 0; size_t tagNameLength; android_LogPriority pri = ANDROID_LOG_DEFAULT; // 獲取tag的長度 tagNameLength = strcspn(filterExpression, ":"); if (tagNameLength == 0) { goto err; } // 獲取tag對應的日志權限pri if (filterExpression[tagNameLength] == ':') { pri = filterCharToPri(filterExpresion[tagNameLength + 1]); if (pri == ANDROID_LOG_UNKNOWN) { goto err; } } if (0 == strncmp("*", filterExpression, tagNameLength)) { // *默認是打印當前tag的所有級別的log if (pri == ANDROID_LOG_DEFAULT) { pri = ANDROID_LOG_DEBUG; } p_format->global_pri = pri; } else { if (pri == ANDROID_LOG_DEFAULT) { pri = ANDROID_LOG_VERBOSE; } char *tagName; tagName = strdup(filterExpression); tagName[tagNameLength] = '\0'; FilterInfo *p_fi = filterinfo_new(tagName, pri); free(tagName); // 頭插法將過濾條件插入 p_fi->p_next = p_format->filters; p_format->filters = p_fi; } return 0; error: return -1; }
logcat日志過濾格式為:[:priority]。其中,tag為任意字符串,代表一個日志記錄標簽。priority是一個字符,表示一個日志記錄優先級。增加了過濾后,也是代表日志記錄tag-過濾tag時,只有priority大於過濾priority的日志才會被輸出。
日志記錄的讀取過程
Logcat工具是從源代碼文件logcat.cpp中的函數readLogLines開始讀取日志記錄的,我們來分段閱讀這個函數的實現。
android::readLogLines(devices); 1 函數實現如下: static void readLogLines(log_device_t* devices) { log_device_t* dev; int max = 0; int ret; int queued_lines = 0; bool sleep = false; 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}; 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工具有可能打開了多個日志設備,因此,while循環中使用了select函數來同時監控他們是否有內容可讀,即是否有新日志需要讀取。調用select函數時,需要設定用來查找這些打開的日志設備中的最大文件描述符,並保存在變量max中。
當代碼跳出select時,是存在兩種可能性的。
當前logcat有新日志可讀。
select選擇超時,當前無新日志可讀。
首先分析當前日志設備有新的日志記錄可讀的情況,如下所示:
if (result >= 0) { for (dev = devices; dev; dev = dev->next) { if (FD_ISSET(dev->fd, &readset)) { queued_entry_t* entry = new queued_entry_t(); ret = read(dev->fd, entry->buf, LOGGER_ENTRY_MAX_LEN); if (ret < 0) { exit(EXIT_FAILURE); } else if (!ret) { exit(EXIT_FAILURE); } else if (entry->entry.len != ret - sizeof(struct logger_entry)) { exit(EXIT_FAILURE); entry->entry.msg[entry->entry.len] = '\0'; dev->enqueue(entry); ++queued_lines; } } }
每當設備有新數據可讀時,就取出新數據構造queued_entry_t結構體,並插入到隊列entry中,並且queued_lines全局變量+1。
if (result == 0) { 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; } if (g_nonblock) { return; } } else { sleep = false; while (g_tail_lines == 0 || queued_lines > g_tail_lines) { chooseFirst(devices, &dev); if (dev == NULL || dev->queue->next == NULL) { if (entry_too_match) { trigger_log(dev); } else { break; } } if (g_tail_lines == 0) { printnextEntry(dev); } else { skipNextEntry(dev); } --queued_lines; } }
由於Logcat工具是按照寫入時間的先后順序來輸出日志記錄的,因此,在輸出已經讀取的日志記錄之前,Logcat工具會首先調用chooseFirst找到包含有最早的未輸出日志記錄的日志設備,源碼實現如下:
static void chooseFirst(log_device_t* dev, log_device_t** firstdev) { if (*firstdev = NULL; dev != NULL; dev = dev->next) { if (dev->queue != NULL && (*firstdev == NULL || cmp(dev->queue, (*firstdev)->queue) < 0)) { *firstdev = dev; } } }
chooseFirst函數只是用來找到最早的日志記錄,而日志真正的輸出是通過函數printNextEntry實現的。源碼實現如下:
static void printNextEntry(log_device_t* dev) { maybePrintStart(dev); processBuffer(dev, &dev->queue->entry); skipNextEntry(dev); }
其中,maybePrintStart是用例判斷當前日志設備dev中的日志記錄是否是第一次輸出,如果是第一次輸出,會增加一些特定標志信息。
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) { exit(-1); } } } }
日志輸出后,就需要將日志從隊列中刪除,這是通過調用函數skipNextEntry實現的,源碼如下:
static void skipNextEntry(log_device_t* dev) { maybePrintStart(dev); queued_entry_t* entry = dev->queue; dev->queue = entry->next; delete entry; entry_num --; }
---------------------
原文:https://blog.csdn.net/wzy_1988/article/details/47341121