天天看點

Android日志系統驅動程式Logger源代碼分析

       kernel/common/drivers/staging/android/logger.h

       kernel/common/drivers/staging/android/logger.c

       接下來,我們将分别介紹Logger驅動程式的相關資料結構,然後對Logger驅動程式源代碼進行情景分析,分别日志系統初始化情景、日志讀取情景和日志寫入情景。

       一. Logger驅動程式的相關資料結構。

      我們首先來看logger.h頭檔案的内容:

#ifndef _LINUX_LOGGER_H  

#define _LINUX_LOGGER_H  

#include <linux/types.h>  

#include <linux/ioctl.h>  

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_LOG_RADIO    "log_radio" /* radio-related messages */  

#define LOGGER_LOG_EVENTS   "log_events"    /* system/hardware events */  

#define LOGGER_LOG_MAIN     "log_main"  /* everything else */  

#define LOGGER_ENTRY_MAX_LEN        (4*1024)  

#define LOGGER_ENTRY_MAX_PAYLOAD    \  

    (LOGGER_ENTRY_MAX_LEN - sizeof(struct logger_entry))  

#define __LOGGERIO  0xAE  

#define LOGGER_GET_LOG_BUF_SIZE     _IO(__LOGGERIO, 1) /* size of log */  

#define LOGGER_GET_LOG_LEN      _IO(__LOGGERIO, 2) /* used log len */  

#define LOGGER_GET_NEXT_ENTRY_LEN   _IO(__LOGGERIO, 3) /* next entry len */  

#define LOGGER_FLUSH_LOG        _IO(__LOGGERIO, 4) /* flush log */  

#endif /* _LINUX_LOGGER_H */  

        struct logger_entry是一個用于描述一條Log記錄的結構體。len成員變量記錄了這條記錄的有效負載的長度,有效負載指定的日志記錄本身的長度,但是不包括用于描述這個記錄的struct logger_entry結構體。回憶一下我們調用android.util.Log接口來使用日志系統時,會指定日志的優先級别Priority、Tag字元串以及Msg字元串,Priority

+ Tag + Msg三者内容的長度加起來就是記錄的有效負載長度了。__pad成員變量是用來對齊結構體的。pid和tid成員變量分别用來記錄是哪條程序寫入了這條記錄。sec和nsec成員變量記錄日志寫的時間。msg成員變量記錄的就有效負載的内容了,它的大小由len成員變量來确定。

       接着定義兩個宏:

       #define LOGGER_ENTRY_MAX_LEN             (4*1024)

       #define LOGGER_ENTRY_MAX_PAYLOAD   \

                         (LOGGER_ENTRY_MAX_LEN - sizeof(struct logger_entry))

      從這兩個宏可以看出,每條日志記錄的有效負載長度加上結構體logger_entry的長度不能超過4K個位元組。

      logger.h檔案中還定義了其它宏,讀者可以自己分析,在下面的分析中,碰到時,我們也會詳細解釋。

      再來看logger.c檔案中,其它相關資料結構的定義:

/* 

 * struct logger_log - represents a specific log, such as 'main' or 'radio' 

 * 

 * This structure lives from module insertion until module removal, so it does 

 * not need additional reference counting. The structure is protected by the 

 * mutex 'mutex'. 

 */  

struct logger_log {  

    unsigned char *     buffer; /* the ring buffer itself */  

    struct miscdevice   misc;   /* misc device representing the log */  

    wait_queue_head_t   wq; /* wait queue for readers */  

    struct list_head    readers; /* this log's readers */  

    struct mutex        mutex;  /* mutex protecting buffer */  

    size_t          w_off;  /* current write head offset */  

    size_t          head;   /* new readers start here */  

    size_t          size;   /* size of the log */  

 * struct logger_reader - a logging device open for reading 

 * This object lives from open to release, so we don't need additional 

 * reference counting. The structure is protected by log->mutex. 

struct logger_reader {  

    struct logger_log * log;    /* associated log */  

    struct list_head    list;   /* entry in logger_log's list */  

    size_t          r_off;  /* current read head offset */  

/* logger_offset - returns index 'n' into the log via (optimized) modulus */  

#define logger_offset(n)    ((n) & (log->size - 1))  

Driver Development一書。wq成員變量是一個等待隊列,用于儲存正在等待讀取日志的程序。readers成員變量用來儲存目前正在讀取日志的程序,正在讀取日志的程序由結構體logger_reader來描述。mutex成員變量是一個互斥量,用來保護log的并發通路。可以看出,這裡的日志系統的讀寫問題,其實是一個生産者-消費者的問題,是以,需要互斥量來保護log的并發通路。 w_off成員變量用來記錄下一條日志應該從哪裡開始寫。head成員變量用來表示打開日志檔案中,應該從哪一個位置開始讀取日志。

       結構體struct logger_reader用來表示一個讀取日志的程序,log成員變量指向要讀取的日志緩沖區。list成員變量用來連接配接其它讀者程序。r_off成員變量表示目前要讀取的日志在緩沖區中的位置。

       struct logger_log結構體中用于儲存日志資訊的記憶體緩沖區buffer是一個循環使用的環形緩沖區,緩沖區中儲存的内容是以struct logger_entry為機關的,每個機關的組成為:

       struct logger_entry | priority | tag | msg

       由于是記憶體緩沖區buffer是一個循環使用的環形緩沖區,給定一個偏移值,它在buffer中的位置由下logger_offset來确定:

       #define logger_offset(n)          ((n) & (log->size - 1))

       二. Logger驅動程式子產品的初始化過程分析。

       繼續看logger.c檔案,定義了三個日志裝置:

 * Defines a log structure with name 'NAME' and a size of 'SIZE' bytes, which 

 * must be a power of two, greater than LOGGER_ENTRY_MAX_LEN, and less than 

 * LONG_MAX minus LOGGER_ENTRY_MAX_LEN. 

#define DEFINE_LOGGER_DEVICE(VAR, NAME, SIZE) \  

static unsigned char _buf_ ## VAR[SIZE]; \  

static struct logger_log VAR = { \  

    .buffer = _buf_ ## VAR, \  

    .misc = { \  

        .minor = MISC_DYNAMIC_MINOR, \  

        .name = NAME, \  

        .fops = &logger_fops, \  

        .parent = NULL, \  

    }, \  

    .wq = __WAIT_QUEUE_HEAD_INITIALIZER(VAR .wq), \  

    .readers = LIST_HEAD_INIT(VAR .readers), \  

    .mutex = __MUTEX_INITIALIZER(VAR .mutex), \  

    .w_off = 0, \  

    .head = 0, \  

    .size = SIZE, \  

DEFINE_LOGGER_DEVICE(log_main, LOGGER_LOG_MAIN, 64*1024)  

DEFINE_LOGGER_DEVICE(log_events, LOGGER_LOG_EVENTS, 256*1024)  

DEFINE_LOGGER_DEVICE(log_radio, LOGGER_LOG_RADIO, 64*1024)  

       分别是log_main、log_events和log_radio,名稱分别LOGGER_LOG_MAIN、LOGGER_LOG_EVENTS和LOGGER_LOG_RADIO,它們的次裝置号為MISC_DYNAMIC_MINOR,即為在注冊時動态配置設定。在logger.h檔案中,有這三個宏的定義:

       #define LOGGER_LOG_RADIO "log_radio"

/* radio-related messages */

       #define LOGGER_LOG_EVENTS "log_events"

/* system/hardware events */

       #define LOGGER_LOG_MAIN "log_main"

/* everything else */

       注釋說明了這三個日志裝置的用途。注冊的日志裝置檔案操作方法為logger_fops:

static struct file_operations logger_fops = {  

    .owner = THIS_MODULE,  

    .read = logger_read,  

    .aio_write = logger_aio_write,  

    .poll = logger_poll,  

    .unlocked_ioctl = logger_ioctl,  

    .compat_ioctl = logger_ioctl,  

    .open = logger_open,  

    .release = logger_release,  

       日志驅動程式子產品的初始化函數為logger_init:

static int __init logger_init(void)  

{  

    int ret;  

    ret = init_log(&log_main);  

    if (unlikely(ret))  

        goto out;  

    ret = init_log(&log_events);  

    ret = init_log(&log_radio);  

out:  

    return ret;  

}  

device_initcall(logger_init);  

        logger_init函數通過調用init_log函數來初始化了上述提到的三個日志裝置:

static int __init init_log(struct logger_log *log)  

    ret = misc_register(&log->misc);  

    if (unlikely(ret)) {  

        printk(KERN_ERR "logger: failed to register misc "  

               "device for log '%s'!\n", log->misc.name);  

        return ret;  

    }  

    printk(KERN_INFO "logger: created %luK log '%s'\n",  

           (unsigned long) log->size >> 10, log->misc.name);  

    return 0;  

        init_log函數主要調用了misc_register函數來注冊misc裝置,misc_register函數定義在kernel/common/drivers/char/misc.c檔案中:

/** 

 *      misc_register   -       register a miscellaneous device 

 *      @misc: device structure 

 *      Register a miscellaneous device with the kernel. If the minor 

 *      number is set to %MISC_DYNAMIC_MINOR a minor number is assigned 

 *      and placed in the minor field of the structure. For other cases 

 *      the minor number requested is used. 

 *      The structure passed is linked into the kernel and may not be 

 *      destroyed until it has been unregistered. 

 *      A zero is returned on success and a negative errno code for 

 *      failure. 

int misc_register(struct miscdevice * misc)  

        struct miscdevice *c;  

        dev_t dev;  

        int err = 0;  

        INIT_LIST_HEAD(&misc->list);  

        mutex_lock(&misc_mtx);  

        list_for_each_entry(c, &misc_list, list) {  

                if (c->minor == misc->minor) {  

                        mutex_unlock(&misc_mtx);  

                        return -EBUSY;  

                }  

        }  

        if (misc->minor == MISC_DYNAMIC_MINOR) {  

                int i = DYNAMIC_MINORS;  

                while (--i >= 0)  

                        if ( (misc_minors[i>>3] & (1 << (i&7))) == 0)  

                                break;  

                if (i<0) {  

                misc->minor = i;  

        if (misc->minor < DYNAMIC_MINORS)  

                misc_minors[misc->minor >> 3] |= 1 << (misc->minor & 7);  

        dev = MKDEV(MISC_MAJOR, misc->minor);  

        misc->this_device = device_create(misc_class, misc->parent, dev, NULL,  

                                          "%s", misc->name);  

        if (IS_ERR(misc->this_device)) {  

                err = PTR_ERR(misc->this_device);  

                goto out;  

        /* 

         * Add it to the front, so that later devices can "override" 

         * earlier defaults 

         */  

        list_add(&misc->list, &misc_list);  

 out:  

        mutex_unlock(&misc_mtx);  

        return err;  

        注冊完成後,通過device_create建立裝置檔案節點。這裡,将建立/dev/log/main、/dev/log/events和/dev/log/radio三個裝置檔案,這樣,使用者空間就可以通過讀寫這三個檔案和驅動程式進行互動。

        三. Logger驅動程式的日志記錄讀取過程分析。

        繼續看logger.c 檔案,注冊的讀取日志裝置檔案的方法為logger_read:

 * logger_read - our log's read() method 

 * Behavior: 

 *  - O_NONBLOCK works 

 *  - If there are no log entries to read, blocks until log is written to 

 *  - Atomically reads exactly one log entry 

 * Optimal read size is LOGGER_ENTRY_MAX_LEN. Will set errno to EINVAL if read 

 * buffer is insufficient to hold next entry. 

static ssize_t logger_read(struct file *file, char __user *buf,  

               size_t count, loff_t *pos)  

    struct logger_reader *reader = file->private_data;  

    struct logger_log *log = reader->log;  

    ssize_t ret;  

    DEFINE_WAIT(wait);  

start:  

    while (1) {  

        prepare_to_wait(&log->wq, &wait, TASK_INTERRUPTIBLE);  

        mutex_lock(&log->mutex);  

        ret = (log->w_off == reader->r_off);  

        mutex_unlock(&log->mutex);  

        if (!ret)  

            break;  

        if (file->f_flags & O_NONBLOCK) {  

            ret = -EAGAIN;  

        if (signal_pending(current)) {  

            ret = -EINTR;  

        schedule();  

    finish_wait(&log->wq, &wait);  

    if (ret)  

    mutex_lock(&log->mutex);  

    /* is there still something to read or did we race? */  

    if (unlikely(log->w_off == reader->r_off)) {  

        goto start;  

    /* get the size of the next entry */  

    ret = get_entry_len(log, reader->r_off);  

    if (count < ret) {  

        ret = -EINVAL;  

    /* get exactly one entry from the log */  

    ret = do_read_log_to_user(log, reader, buf, ret);  

    mutex_unlock(&log->mutex);  

       注意,在函數開始的地方,表示讀取日志上下文的struct logger_reader是儲存在檔案指針的private_data成員變量裡面的,這是在打開裝置檔案時設定的,裝置檔案打開方法為logger_open:

 * logger_open - the log's open() file operation 

 * Note how near a no-op this is in the write-only case. Keep it that way! 

static int logger_open(struct inode *inode, struct file *file)  

    struct logger_log *log;  

    ret = nonseekable_open(inode, file);  

    log = get_log_from_minor(MINOR(inode->i_rdev));  

    if (!log)  

        return -ENODEV;  

    if (file->f_mode & FMODE_READ) {  

        struct logger_reader *reader;  

        reader = kmalloc(sizeof(struct logger_reader), GFP_KERNEL);  

        if (!reader)  

            return -ENOMEM;  

        reader->log = log;  

        INIT_LIST_HEAD(&reader->list);  

        reader->r_off = log->head;  

        list_add_tail(&reader->list, &log->readers);  

        file->private_data = reader;  

    } else  

        file->private_data = log;  

       新打開日志裝置檔案時,是從log->head位置開始讀取日志的,儲存在struct logger_reader的成員變量r_off中。

       start标号處的while循環是在等待日志可讀,如果已經沒有新的日志可讀了,那麼就要讀程序就要進入休眠狀态,等待新的日志寫入後再喚醒,這是通過prepare_wait和schedule兩個調用來實作的。如果沒有新的日志可讀,并且裝置檔案不是以非阻塞O_NONBLOCK的方式打開或者這時有信号要處理(signal_pending(current)),那麼就直接傳回,不再等待新的日志寫入。判斷目前是否有新的日志可讀的方法是:

       ret = (log->w_off == reader->r_off);

       即判斷目前緩沖區的寫入位置和目前讀程序的讀取位置是否相等,如果不相等,則說明有新的日志可讀。

       繼續向下看,如果有新的日志可讀,那麼就,首先通過get_entry_len來擷取下一條可讀的日志記錄的長度,從這裡可以看出,日志讀取程序是以日志記錄為機關進行讀取的,一次隻讀取一條記錄。get_entry_len的函數實作如下:

 * get_entry_len - Grabs the length of the payload of the next entry starting 

 * from 'off'. 

 * Caller needs to hold log->mutex. 

static __u32 get_entry_len(struct logger_log *log, size_t off)  

    __u16 val;  

    switch (log->size - off) {  

    case 1:  

        memcpy(&val, log->buffer + off, 1);  

        memcpy(((char *) &val) + 1, log->buffer, 1);  

        break;  

    default:  

        memcpy(&val, log->buffer + off, 2);  

    return sizeof(struct logger_entry) + val;  

        上面我們提到,每一條日志記錄是由兩大部分組成的,一個用于描述這條日志記錄的結構體struct logger_entry,另一個是記錄體本身,即有效負載。結構體struct logger_entry的長度是固定的,隻要知道有效負載的長度,就可以知道整條日志記錄的長度了。而有效負載的長度是記錄在結構體struct logger_entry的成員變量len中,而len成員變量的位址與struct

logger_entry的位址相同,是以,隻需要讀取記錄的開始位置的兩個位元組就可以了。又由于日志記錄緩沖區是循環使用的,這兩個節字有可能是第一個位元組存放在緩沖區最後一個位元組,而第二個位元組存放在緩沖區的第一個節,除此之外,這兩個位元組都是連在一起的。是以,分兩種情況來考慮,對于前者,分别通過讀取緩沖區最後一個位元組和第一個位元組來得到日志記錄的有效負載長度到本地變量val中,對于後者,直接讀取連續兩個位元組的值到本地變量val中。這兩種情況是通過判斷日志緩沖區的大小和要讀取的日志記錄在緩沖區中的位置的內插補點來差別的,如果相差1,就說明是前一種情況了。最後,把有效負載的長度val加上struct

logger_entry的長度就得到了要讀取的日志記錄的總長度了。

       接着往下看,得到了要讀取的記錄的長度,就調用do_read_log_to_user函數來執行真正的讀取動作:

static ssize_t do_read_log_to_user(struct logger_log *log,  

                   struct logger_reader *reader,  

                   char __user *buf,  

                   size_t count)  

    size_t len;  

    /* 

     * We read from the log in two disjoint operations. First, we read from 

     * the current read head offset up to 'count' bytes or to the end of 

     * the log, whichever comes first. 

     */  

    len = min(count, log->size - reader->r_off);  

    if (copy_to_user(buf, log->buffer + reader->r_off, len))  

        return -EFAULT;  

     * Second, we read any remaining bytes, starting back at the head of 

     * the log. 

    if (count != len)  

        if (copy_to_user(buf + len, log->buffer, count - len))  

            return -EFAULT;  

    reader->r_off = logger_offset(reader->r_off + count);  

    return count;  

        這個函數簡單地調用copy_to_user函數來把位于核心空間的日志緩沖區指定的内容拷貝到使用者空間的記憶體緩沖區就可以了,同時,把目前讀取日志程序的上下文資訊中的讀偏移r_off前進到下一條日志記錄的開始的位置上。

        四.  Logger驅動程式的日志記錄寫入過程分析。

        繼續看logger.c 檔案,注冊的寫入日志裝置檔案的方法為logger_aio_write:

 * logger_aio_write - our write method, implementing support for write(), 

 * writev(), and aio_write(). Writes are our fast path, and we try to optimize 

 * them above all else. 

ssize_t logger_aio_write(struct kiocb *iocb, const struct iovec *iov,  

             unsigned long nr_segs, loff_t ppos)  

    struct logger_log *log = file_get_log(iocb->ki_filp);  

    size_t orig = log->w_off;  

    struct logger_entry header;  

    struct timespec now;  

    ssize_t ret = 0;  

    now = current_kernel_time();  

    header.pid = current->tgid;  

    header.tid = current->pid;  

    header.sec = now.tv_sec;  

    header.nsec = now.tv_nsec;  

    header.len = min_t(size_t, iocb->ki_left, LOGGER_ENTRY_MAX_PAYLOAD);  

    /* null writes succeed, return zero */  

    if (unlikely(!header.len))  

        return 0;  

     * Fix up any readers, pulling them forward to the first readable 

     * entry after (what will be) the new write offset. We do this now 

     * because if we partially fail, we can end up with clobbered log 

     * entries that encroach on readable buffer. 

    fix_up_readers(log, sizeof(struct logger_entry) + header.len);  

    do_write_log(log, &header, sizeof(struct logger_entry));  

    while (nr_segs-- > 0) {  

        size_t len;  

        ssize_t nr;  

        /* figure out how much of this vector we can keep */  

        len = min_t(size_t, iov->iov_len, header.len - ret);  

        /* write out this segment's payload */  

        nr = do_write_log_from_user(log, iov->iov_base, len);  

        if (unlikely(nr < 0)) {  

            log->w_off = orig;  

            mutex_unlock(&log->mutex);  

            return nr;  

        iov++;  

        ret += nr;  

    /* wake up any blocked readers */  

    wake_up_interruptible(&log->wq);  

        輸入的參數iocb表示io上下文,iov表示要寫入的内容,長度為nr_segs,表示有nr_segs個段的内容要寫入。我們知道,每個要寫入的日志的結構形式為:

        struct logger_entry | priority | tag | msg

        其中, priority、tag和msg這三個段的内容是由iov參數從使用者空間傳遞下來的,分别對應iov裡面的三個元素。而logger_entry是由核心空間來構造的:

        struct logger_entry header;

struct timespec now;

now = current_kernel_time();

header.pid = current->tgid;

header.tid = current->pid;

header.sec = now.tv_sec;

header.nsec = now.tv_nsec;

header.len = min_t(size_t, iocb->ki_left, LOGGER_ENTRY_MAX_PAYLOAD);

        然後調用do_write_log首先把logger_entry結構體寫入到日志緩沖區中:

 * do_write_log - writes 'len' bytes from 'buf' to 'log' 

 * The caller needs to hold log->mutex. 

static void do_write_log(struct logger_log *log, const void *buf, size_t count)  

    len = min(count, log->size - log->w_off);  

    memcpy(log->buffer + log->w_off, buf, len);  

        memcpy(log->buffer, buf + len, count - len);  

    log->w_off = logger_offset(log->w_off + count);  

       由于logger_entry是核心堆棧空間配置設定的,直接用memcpy拷貝就可以了。

       接着,通過一個while循環把iov的内容寫入到日志緩沖區中,也就是日志的優先級别priority、日志Tag和日志主體Msg:

while (nr_segs-- > 0) {  

         由于iov的内容是由使用者空間傳下來的,需要調用do_write_log_from_user來寫入:

static ssize_t do_write_log_from_user(struct logger_log *log,  

                      const void __user *buf, size_t count)  

    if (len && copy_from_user(log->buffer + log->w_off, buf, len))  

        if (copy_from_user(log->buffer, buf + len, count - len))  

        這裡,我們還漏了一個重要的步驟:

 /* 

  * Fix up any readers, pulling them forward to the first readable 

  * entry after (what will be) the new write offset. We do this now 

  * because if we partially fail, we can end up with clobbered log 

  * entries that encroach on readable buffer. 

  */  

fix_up_readers(log, sizeof(struct logger_entry) + header.len);  

        為什麼要調用fix_up_reader這個函數呢?這個函數又是作什麼用的呢?是這樣的,由于日志緩沖區是循環使用的,即舊的日志記錄如果沒有及時讀取,而緩沖區的内容又已經用完時,就需要覆寫舊的記錄來容納新的記錄。而這部分将要被覆寫的内容,有可能是某些reader的下一次要讀取的日志所在的位置,以及為新的reader準備的日志開始讀取位置head所在的位置。是以,需要調整這些位置,使它們能夠指向一個新的有效的位置。我們來看一下fix_up_reader函數的實作:

 * fix_up_readers - walk the list of all readers and "fix up" any who were 

 * lapped by the writer; also do the same for the default "start head". 

 * We do this by "pulling forward" the readers and start head to the first 

 * entry after the new write head. 

static void fix_up_readers(struct logger_log *log, size_t len)  

    size_t old = log->w_off;  

    size_t new = logger_offset(old + len);  

    struct logger_reader *reader;  

    if (clock_interval(old, new, log->head))  

        log->head = get_next_entry(log, log->head, len);  

    list_for_each_entry(reader, &log->readers, list)  

        if (clock_interval(old, new, reader->r_off))  

            reader->r_off = get_next_entry(log, reader->r_off, len);  

        判斷log->head和所有讀者reader的目前讀偏移reader->r_off是否在被覆寫的區域内,如果是,就需要調用get_next_entry來取得下一個有效的記錄的起始位置來調整目前位置:

 * get_next_entry - return the offset of the first valid entry at least 'len' 

 * bytes after 'off'. 

 * Caller must hold log->mutex. 

static size_t get_next_entry(struct logger_log *log, size_t off, size_t len)  

    size_t count = 0;  

    do {  

        size_t nr = get_entry_len(log, off);  

        off = logger_offset(off + nr);  

        count += nr;  

    } while (count < len);  

    return off;  

        而判斷log->head和所有讀者reader的目前讀偏移reader->r_off是否在被覆寫的區域内,是通過clock_interval函數來實作的:

 * clock_interval - is a < c < b in mod-space? Put another way, does the line 

 * from a to b cross c? 

static inline int clock_interval(size_t a, size_t b, size_t c)  

    if (b < a) {  

        if (a < c || b >= c)  

            return 1;  

    } else {  

        if (a < c && b >= c)  

        最後,日志寫入完畢,還需要喚醒正在等待新日志的reader程序:

        /* wake up any blocked readers */

wake_up_interruptible(&log->wq);

        至此, Logger驅動程式的主要邏輯就分析完成了,還有其它的一些接口,如logger_poll、 logger_ioctl和logger_release函數,比較簡單,讀取可以自行分析。這裡還需要提到的一點是,由于Logger驅動程式子產品在退出系統時,是不會解除安裝的,是以這個子產品沒有module_exit函數,而對于子產品裡面定義的對象,也沒有用對引用計數技術。

      這篇文章着重介紹了Android日志系統在核心空間的實作,在下一篇文章中,我們将接着介紹在使用者空間中,提供給Android應用程式使用的Java和C/C++ LOG調用接口的實作過程,敬請關注。