Android Log機制的原理學習

應用程序的運行與維護,離不開日誌。APP開發者們有很多選擇,例如微信的xlog(高可靠性高性能的運行期日誌組件)等,同樣也離不開原生的日誌機制支持。所以我們從原生Android Log機制開始學起:

一. Android Log機制(基於Android P原生代碼)

APP打印日誌,最簡單的是使用Log類(android.util.Log),如下例子:

import android.util.Log;

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
           super.onCreate(savedInstanceState);
           setContentView(R.layout.activity_main);
 
           Log.d("kevintest", "onCreate");
    }
    // ...
}

APP運行,進入頁面後,通過adb logcat命令,會看到有一行日誌打印出:

 

1. 日誌類型有5類

 

2. 上述類型中,LOG_ID_MAIN、LOG_ID_RADIO、LOG_ID_SYSTEM的優先級有6種

 

一般使用時,級別值越高,打印的日誌越重要,從變量字面意思也可理解。Google官方文檔中有這樣的建議:VERBOSE日誌只建議用於開發調試的版本,不能被編譯入其他版本,如release版本;DEBUG日誌可用於release版本產品,但默認不會打印;INFO、WARN、ERROR日誌建議在release版本中使用與保存。手機中一般默認的日誌打印級別是INFO(包括INFO、WARN、ERROR),可參考:開發者選項-選擇日誌級別

3. 原生代碼分析

首先我們重點看下android.util.Log類:
Log類定義較簡單,無父類,final修飾,不可繼承。構造函數是private,不可實例化。舉例,APP調用Log.v方法打印VERBOSE日誌時,其方法實現是:

public static int v(String tag, String msg) {
       return println_native(LOG_ID_MAIN, VERBOSE, tag, msg);
}

其中關鍵的方法是println_native,方法實現位於:frameworks/base/core/jni/android_util_Log.cpp,傳入的三個參數分別是:LOG_ID_MAIN(類型),VERBOSE(級別),tag(標籤),msg(日誌信息),實現如下:

static jint android_util_Log_println_native(JNIEnv* env, jobject clazz,
        jint bufID, jint priority, jstring tagObj, jstring msgObj) {
    // ...
    int res = __android_log_buf_write(bufID, (android_LogPriority)priority, tag, msg);
    // ...
    return res;
}

繼續調用__android_log_buf_write方法,方法定義:system/core/liblog/include/android/log.h,實現:system/core/liblog/logger_write.c,其中核心部分:

注意:其他一些系統模塊,例如debuggered等C++代碼都會直接封裝or調用__android_log_buf_write來打印日誌

LIBLOG_ABI_PUBLIC int __android_log_buf_write(int bufID, int prio,
                                              const char* tag, const char* msg) {
     struct iovec vec[3];
     // ......
     vec[0].iov_base = (unsigned char*)&prio;      // 例如通過Log.v調用過來,這裏是2(VERBOSE)
     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(bufID, vec, 3);
}

繼續調用write_to_log方法,實現:system/core/liblog/logger_write.c,其中核心部分:

注意:第一次真正執行的方法是:__write_to_log_init,初始化後write_to_log的的方法實現變爲:__write_to_log_daemon。詳細參見__write_to_log_init的方法實現

static int __write_to_log_init(log_id_t, struct iovec* vec, size_t nr);
static int (*write_to_log)(log_id_t, struct iovec* vec,
                           size_t nr) = __write_to_log_init;
// ...
static int __write_to_log_init(log_id_t log_id, struct iovec* vec, size_t nr) {
    int ret, save_errno = errno;
 
    __android_log_lock();        // 加鎖
 
    if (write_to_log == __write_to_log_init) {
        ret = __write_to_log_initialize();
        if (ret < 0) {
            __android_log_unlock();
            if (!list_empty(&__android_log_persist_write)) {
                __write_to_log_daemon(log_id, vec, nr);
            }
           errno = save_errno;
           return ret;
        }
 
        write_to_log = __write_to_log_daemon;
    }
 
    __android_log_unlock();  // 去鎖
 
    ret = write_to_log(log_id, vec, nr);
    errno = save_errno;
    return ret;
}

首先會執行初始化方法__write_to_log_initialize,作用是:爲集合__android_log_transport_write設置各類writer,例如logdLoggerWrite,pmsgLoggerWrite等,然後依次調用writer的open方法,例如logdLoggerWrite#logdOpen方法,如果打開失敗,則關閉。

logdLoggerWrite#logdOpen方法,代碼位置:system/core/liblog/logd_writer.c,其實現是:

/* log_init_lock assumed */
static int logdOpen() {
    // ...
    int sock = TEMP_FAILURE_RETRY(
        socket(PF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0));   // 注意:SOCK_DGRAM代表着UDP通信,SOCK_NONBLOCK非阻塞式(效率高)
    // ...
    strcpy(un.sun_path, "/dev/socket/logdw");
 
    if (TEMP_FAILURE_RETRY(connect(sock, (struct sockaddr*)&un,
                                     sizeof(struct sockaddr_un))) < 0) {
    // ...
}

繼續調用__write_to_log_daemon方法,實現:system/core/liblog/logger_write.c,其中核心部分:

static int __write_to_log_daemon(log_id_t log_id, struct iovec* vec, size_t nr) {
    struct android_log_transport_write* node;
    int ret, save_errno;
    struct timespec ts;
    size_t len, i;
    // ...
    clock_gettime(android_log_clockid(), &ts);         // 獲得日誌時間戳
   
    if (log_id == LOG_ID_SECURITY) {
         // ...
    } else if (log_id == LOG_ID_EVENTS || log_id == LOG_ID_STATS) {
         // ...
    } else {
        /* Validate the incoming tag, tag content can not split across iovec */
        char prio = ANDROID_LOG_VERBOSE;
        const char* tag = vec[0].iov_base;
 
        // ...
        // 變量prio存儲vec[0].iov_base,例如2(VERBOSE),tag存儲vec[1].iov_base
 
       if (!__android_log_is_loggable_len(prio, tag, len - 1, ANDROID_LOG_VERBOSE)) {     //如果當前打印的log級別低於系統設置的級別,會直接返回,不會打印。默認是:ANDROID_LOG_VERBOSE(2),系統設置的級別來自於屬性:persist.log.tag 或 log.tag
             errno = save_errno;
             return -EPERM;
        }
   }
 
   //....
   // 以下是核心方法實現
   write_transport_for_each(node, &__android_log_transport_write) {
       if (node->logMask & i) {
           ssize_t retval;
           retval = (*node->write)(log_id, &ts, vec, nr);
           if (ret >= 0) {
               ret = retval;
           }
       }
    }
 
    write_transport_for_each(node, &__android_log_persist_write) {
        if (node->logMask & i) {
            (void)(*node->write)(log_id, &ts, vec, nr);
        }
    }
 
    errno = save_errno;
    return ret;
}

在上面會看到將循環調用所有writer的write方法來傳輸日誌,舉個例子logdLoggerWrite的write方法(之前已調用其logdOpen方法:建立Socket連接,path:/dev/socket/logdw):

static int logdWrite(log_id_t logId, struct timespec* ts, struct iovec* vec,
                     size_t nr) {
 
 
    // ...
    /*
     * The write below could be lost, but will never block.
     *
     * ENOTCONN occurs if logd has died.
     * ENOENT occurs if logd is not running and socket is missing.
     * ECONNREFUSED occurs if we can not reconnect to logd.
     * EAGAIN occurs if logd is overloaded.
     */
    if (sock < 0) {
        ret = sock;
    } else {
        ret = TEMP_FAILURE_RETRY(writev(sock, newVec, i));    // 通過socket寫數據
        if (ret < 0) {
            ret = -errno;
        }
   }
   // ...
}

下一步是Logd的邏輯分析,即接收到socket通信傳輸後的數據,該如何處理:
Logd進程是開機時由init進程啓動,啓動代碼參考:system/core/rootdir/init.rc。Logd進程啓動時創建3個Socket通道通信,代碼實現:system/core/logd/logd.rc,如下:

service logd /system/bin/logd
    socket logd stream 0666 logd logd
    socket logdr seqpacket 0666 logd logd
    socket logdw dgram+passcred 0222 logd logd
    file /proc/kmsg r
    file /dev/kmsg w
    user logd
    group logd system package_info readproc
    writepid /dev/cpuset/system-background/tasks
//...

例如adb shell連接手機,通過ss -pl查看socket連接:

 

進程啓動後,入口方法:system/core/logd/main.cpp,其中入口的main方法實現不復雜,主要創建LogBuffer,然後啓動5個listener,一般重要的是前三個:LogReader,LogListener,CommandListener,全部繼承於SocketListener(system/core/libsysutils),另外還有2個listener:LogAudit(監聽NETLINK_AUDIT,與selinux有關),LogKlog,這裏不做深究。

int main(int argc, char* argv[]) {
    // ...
     
    // LogBuffer,作用:存儲所有的日誌信息
    logBuf = new LogBuffer(times);
     
    // LogReader監聽Socket(/dev/socket/logdr),作用:當客戶端連接logd後,LogReader將LogBuffer中的日誌寫給客戶端。線程名:logd.reader,通過prctl(PR_SET_NAME, "logd.reader");設定
    LogReader* reader = new LogReader(logBuf);
    if (reader->startListener()) {
        exit(1);
    }
 
    // LogListener監聽Socket(/dev/socket/logdw),作用:接收傳來的日誌信息,寫入LogBuffer;同時LogReader將新的日誌傳給已連接的客戶端。線程名:logd.writer
    LogListener* swl = new LogListener(logBuf, reader);
    if (swl->startListener(600)) {
        exit(1);
    }
 
    //CommandListener監聽Socket(/dev/socket/logd),作用:接收發來的命令。線程名:logd.control
    CommandListener* cl = new CommandListener(logBuf, reader, swl);
    if (cl->startListener()) {
        exit(1);
    }
    // ...
    exit(0);
}

首先看LogListener,當有對端進程通過Socket傳遞過來數據後,onDataAvailable方法被調用,其中主要是解析數據、調用LogBuffer->log方法存儲日誌信息,調用LogReader→notifyNewLog方法通知有新的日誌信息,以便發送給其客戶端。如下:

bool LogListener::onDataAvailable(SocketClient* cli) {
    // ...
    // 1. 調用LogBuffer->log方法存儲日誌信息
    int res = logbuf->log(
            logId, header->realtime, cred->uid, cred->pid, header->tid, msg,
            ((size_t)n <= USHRT_MAX) ? (unsigned short)n : USHRT_MAX);
 
    // 2. 調用LogReader→notifyNewLog方法通知有新的日誌信息,以便發送給其客戶端
    if (res > 0 && reader != nullptr) {
        reader->notifyNewLog(static_cast<log_mask_t>(1 << logId));
    }
    // ...
}

繼續看LogBuffer的log方法,代碼位置:system/core/logd/LogBuffer.cpp

int LogBuffer::log(log_id_t log_id, log_time realtime, uid_t uid, pid_t pid,
                   pid_t tid, const char* msg, unsigned short len) {
    // ...
    // 低於當前設定的日誌優先級,返回
    if (!__android_log_is_loggable_len(prio, tag, tag_len,
                                       ANDROID_LOG_VERBOSE)) {
        // Log traffic received to total
        wrlock();
        stats.addTotal(elem);
        unlock();
        delete elem;
        return -EACCES;
    }
 
    // 調用重載的log方法
    log(elem);
    unlock();
 
    return len;
}

繼續log方法,主要作用是通過比對新進日誌信息的時間,將其插入到正確的存儲位置。所有日誌存儲在mLogElements變量中,其類型是:typedef std::list<LogBufferElement*>

void LogBuffer::log(LogBufferElement* elem) {
      // 插入正確位置,邏輯相對複雜,摘取其中關鍵一段
      do {
            last = it;
            if (__predict_false(it == mLogElements.begin())) {
                  break;
            }
            --it;
      } while (((*it)->getRealTime() > elem->getRealTime()) && (!end_set || (end <= (*it)->getRealTime())));
            mLogElements.insert(last, elem);
      }
 
     // ...
     stats.add(elem);           // 初步看做一些統計工作,例如通過數組,統計不同類型日誌的打印次數,不同類型日誌的字符串總長度等,並且將日誌信息以uid, pid, tid, tag等爲單位,保存elem信息至不同的hashtable中
     maybePrune(elem->getLogId());
}

其中maybePrune方法的作用很重要,當不同類型的log日誌size超過最大限制時,會觸發對已保存日誌信息的裁剪,一次裁剪量約爲10%:

void LogBuffer::maybePrune(log_id_t id) {
    size_t sizes = stats.sizes(id);                                         // 來自LogStatistics->mSizes[id]變量的值,統計不同日誌類型的當前日誌長度(msg)
    unsigned long maxSize = log_buffer_size(id);            // 取不同日誌類型的日誌長度最大值
    if (sizes > maxSize) {
        size_t sizeOver = sizes - ((maxSize * 9) / 10);
        size_t elements = stats.realElements(id);
        size_t minElements = elements / 100;
        if (minElements < minPrune) {                               // minPrune值是4
            minElements = minPrune;                                 // minElements默認是全部日誌元素數的百分之一,最小值是4
        }
        unsigned long pruneRows = elements * sizeOver / sizes;  // 需要裁剪的元素個數,最小值是4個,最大值是256個,正常是總元素的比例:1 - (maxSize/sizes)* 0.9 = 約等於10%
        if (pruneRows < minElements) {
            pruneRows = minElements;
        }
        if (pruneRows > maxPrune) {                               // maxPrune值是256
            pruneRows = maxPrune;
        }
        prune(id, pruneRows);                                           // 如果日誌存儲已越界,則最終走到prune裁剪函數中處理,pruneRows是需要裁剪的元素個數
    }
}

重要備註:不同日誌類型的日誌長度最大值,由上到下取值順序:
persist.logd.size.* // 例如:persist.logd.size.main、persist.logd.size.radio、persist.logd.size.events、persist.logd.size.system、persist.logd.size.crash、persist.logd.size.stats、persist.logd.size.security、persist.logd.size.kernel
ro.logd.size.* // 例如:ro.logd.size.main、ro.logd.size.radio、ro.logd.size.events、ro.logd.size.system、ro.logd.size.crash、ro.logd.size.stats、ro.logd.size.security、ro.logd.size.kernel
persist.logd.size // 設置APP:開發者選項-日誌記錄器緩衝區大小,默認256K
ro.logd.size
LOG_BUFFER_MIN_SIZE // 64K,條件是如果ro.config.low_ram是true,表示低內存手機
LOG_BUFFER_SIZE // 256K

另外可以用adb logcat -g命令查看緩衝區大小
具體執行的prune裁剪方法這裏沒有深究,感興趣的同學可以看下system/core/logd/LogBuffer.cpp#prune方法,大致思路是:
a. 支持黑/白名單(詳見LogWhiteBlackList.cpp,uid + pid。注意:adb logcat -P可設置),白名單中不裁剪
b. 優先裁剪黑名單、打印日誌最多的uid,system uid中打印日誌最多的pid

至此一次完整的:APP調用Log.v方法打印VERBOSE日誌,調用執行過程完畢!

二. Logcat命令行工具

官方定義:

Logcat 是一個命令行工具,用於轉儲系統消息日誌,包括設備拋出錯誤時的堆棧軌跡,以及從您的應用中使用 Log 類寫入的消息。

常用命令:

 

代碼位置:system/core/logcat/,可執行文件位於:/system/bin/logcat,每次執行adb shell logcat命令後,系統會新起一個logcat進程,用來處理命令,父進程是adbd進程。adb shell logcat命令退出後,進程退出。

logcat進程啓動時入口在logcat_main.cpp#main()方法,其中核心android_logcat_run_command方法中調用__logcat方法來解析命令參數,最終通過Socket發送給logd處理等,例如clear命令會通過發送給logd的CommandListener類(logd.control線程)來處理。

最後

如果你看到了這裏,覺得文章寫得不錯就給個讚唄!歡迎大家評論討論!如果你覺得那裏值得改進的,請給我留言。一定會認真查詢,修正不足,定期免費分享技術乾貨。喜歡的小夥伴可以關注一下哦。謝謝!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章