android Logger 一二三


我們在開發Android應用的過程中可以很方便地使用Log信息來調試程序,這都歸功於Android的Logger驅動為用戶層提供的Log支持。無論是底層的源代碼還是上層的應用,我們都可以使用Logger這個日志設備來進行調試。Logger一共包括三個設備節點,它們分別是:

  /dev/log/main

  /dev/log/event

  /dev/log/radio

  其驅動程序的實現源文件位於:

  include/linux/logger.h

  include/linux/logger.c

  下面將對該驅動的實現進行分析,首先打開logger.h文件,我們可以看到如下所示的一個結構體logger_entry,它定義了每一條日志信息的屬性。

struct logger_entry {
    __u16        len;    
    __u16        __pad;    
    __s32        pid;    
    __s32        tid;    
    __s32        sec;    
    __s32        nsec;    
    char            msg[0];    
};

 其中,len表示日志信息的有效長度;__pad目前沒有什么實質作用,但是需要使用兩個字節來占位;pid表示生成該日志信息的進程的pid;tid表示生成該日志信息的進程的tid;sec表示生成該日志的時間,單位是秒;nsec表示當生成該日志的時間不足1秒時,用納秒來計算;msg儲存着該日志的有效信息,即我們前面說的長度為len的日志信息屬於有效信息。

  此外,還定義了代表不同設備事件的宏,分別對應於Logger的三個不同的設備節點,如下所示:

  #define LOGGER_LOG_RADIO "log_radio" /* 無線相關消息 */

  #define LOGGER_LOG_EVENTS "log_events" /* 系統硬件事件 */

  #define LOGGER_LOG_MAIN "log_main" /* 任何事件 */

  接下來在logger.c中還定義了logger_log結構體,它定義每一個日志設備的相關信息。我們上面所說的radio、events和main都將使用logger_log結構體來表示,定義如下:

struct logger_log {
    unsigned char *        buffer;    
    struct miscdevice        misc;    
    wait_queue_head_t        wq;    
    struct list_head        readers; 
    struct mutex            mutex;    
    size_t                w_off;    
    size_t                head;    
    size_t                size;    
};

其中,buffer表示該設備儲存日志的環形緩沖區,(為什么是環形緩沖區,后面將給大家解釋);misc代表日志設備的miscdevice,在注冊設備的時候需要使用;wq表示一個等待隊列,等待在該設備上讀取日志的進程readers;readers表示讀取日志的readers鏈表;mutex則是用於多線程同步和保護該結構體的mutex;w_off代表當前寫入日志的位置,即在環形緩沖區中(buffer)的偏移量;head是一個讀取日志的新的readers,表示從這里開始讀取,同樣指在環形緩沖區中(buffer)的偏移量;size則代表該日志的大小,即環形緩沖區中(buffer)的大小。

  根據上面這個日志設備結構logger_log可以得知,要讀取日志還需要一個用於讀取日志的readers。下面我們來分析一下readers的定義,其結構體位於logger.c中的logger_reader結構體中,代碼如下:

struct logger_reader {
    struct logger_log *    log;    
    struct list_head        list;    
    size_t                r_off;    
};

  logger_reader結構體的實現就很簡單,其中log代表相關的日志設備,即當前將要讀取數據的日志設備(logger_log);list用於指向日志設備的讀取進程(readers);r_off則表示開始讀取日志的一個偏移量,即日志設備中將要被讀取的buffer的偏移量。

  了解了這些數據結構之后,我們來分析一下該驅動是如何工作的,即該驅動的工作流程。

1.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);
    if (unlikely(ret))
        goto out;
    ret = init_log(&log_radio);
    if (unlikely(ret))
        goto out;
out:
    return ret;
}
device_initcall(logger_init);

 當系統內核啟動后,在init過程中就會調用device_initcall所指向的logger_init來初始化日志設備。我們可以看到,在logger_init函數中正好調用了init_log函數來初始化前面所提到的日志系統的三個設備節點。下面我們來看看init_log函數中究竟是如何初始化這些設備節點的。init_log的實現如下

static int __init init_log(struct logger_log *log)
{
    int ret;
    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;
}

非常簡單,通過調用misc_register來初始化每個日志設備的miscdevice(logger_log->misc)。我們並沒有看到具體的初始化日志設備的操作,那是因為這些工作都由DEFINE_LOGGER_ DEVICE宏來完成了,DEFINE_LOGGER_DEVICE的實現如下

#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需要我們傳入三個參數,其作用就是使用參數NAME作為名稱和使用SIZE作為尺寸來創建一個日志設備。這里需要注意:SIZE的大小必須為2的冪,並且要大於LOGGER_ENTRY_MAX_LEN,小於LONG_MAX-LOGGER_ENTRY_ MAX_ LEN。該宏的定義如下(源代碼在logger.h文件中),表示日志的最大長度,同時還定義了LOGGER_ ENTRY_MAX_PAYLOAD表示日志的最大有效長度。

  #define LOGGER_ENTRY_MAX_LEN (4*1024)

  #define LOGGER_ENTRY_MAX_PAYLOAD

  (LOGGER_ENTRY_MAX_LEN - sizeof(struct logger_entry))

  有了這些定義之后,現在要初始化一個日志設備就變得非常簡單,以下代碼初始化了三個不同的日志設備:

  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)

  在初始化過程中,我們為設備指定了對應的file_operations,其定義如下:

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_read、打開日志設備文件的logger_open讀取數據的logger_read,等等。下面,我們將分別對這些函數的實現進行分析。

 2.logger_open

  該方法為打開日志設備文件的方法,具體實現如下:

{
    struct logger_log *log;
    int ret;
    ret = nonseekable_open(inode, file);
    if (ret)
        return ret;
    //判斷類型
    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);
        //指定mutex
        mutex_lock(&log->mutex);
        //指定讀取偏移量
        reader->r_off = log->head;
        list_add_tail(&reader->list, &log->readers);
        mutex_unlock(&log->mutex);
        //保存數據到private_data
        file->private_data = reader;
    } else //讀寫模式
        file->private_data = log;

    return 0;
}

該函數首先調用get_log_from_minor函數來判斷需要打開的日志設備的類型,判斷方法非常簡單,直接判斷日志設備的misc.minor參數和minor參數即可,實現代碼如下

static struct logger_log * get_log_from_minor(int minor)
{
    if (log_main.misc.minor == minor)
        return &log_main;
    if (log_events.misc.minor == minor)
        return &log_events;
    if (log_radio.misc.minor == minor)
        return &log_radio;
    return NULL;
}

  再回過頭來看logger_open函數,在取得了日志設備的類型之后,我們需要判斷其讀寫模式。如果是只讀模式,則將創建一個logger_reader,然后對其所需的數據進行初始化(指定日志設備、mutex、讀取偏移量r_off),最后將該logger_reader保存到file->private_data中;如果是讀寫模式或者寫模式,則直接將日志設備log保存到file->private_data中,這樣做就方便我們在以后的讀寫過程中直接通過file->private_data來取得logger_reader和logger_log。

3.logger_release

  在分析了打開操作之后,我們再來看一下釋放操作,具體實現如下:

static int logger_release(struct inode *ignored, struct file *file)
{
    if (file->f_mode & FMODE_READ) {
        struct logger_reader *reader = file->private_data;
        list_del(&reader->list);
        kfree(reader);
    }
    return 0;
}

 首先判斷其是否為只讀模式,如果是只讀模式,則直接通過file->private_data取得其對應的logger_reader,然后刪除其隊列並釋放即可。寫操作則沒有額外分配空間,所以不需要處理。

4.logger_read

  接下來分析一下讀數據的操作方法,其實現代碼如下:

static ssize_t logger_read(struct file *file, char __user *buf,
               size_t count, loff_t *pos)
{
    //通過file->private_data獲取logger_reader及其日志設備logger_log
    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;
            break;
        }
        if (signal_pending(current)) {
            ret = -EINTR;
            break;
        }
        schedule();
    }
    finish_wait(&log->wq, &wait);
    if (ret) return ret;
    mutex_lock(&log->mutex);
    if (unlikely(log->w_off == reader->r_off)) {
        mutex_unlock(&log->mutex);
        goto start;
    }
    //讀取下一條日志
    ret = get_entry_len(log, reader->r_off);
    if (count < ret) {
        ret = -EINVAL;
        goto out;
    }
    //復制到用戶空間
    ret = do_read_log_to_user(log, reader, buf, ret);
out:
    mutex_unlock(&log->mutex);
    return ret;
}

整體過程比較簡單,但是這里需要注意:我們首先是通過prepare_to_wait函數將當前進程添加到等待隊列log->wq之中,通過偏移量來判斷當前日志的buffer是否為空。如果為空,則調度其他的進程運行,自己掛起;如果指定了非阻塞模式,則直接返回EAGAIN。然后,通過while循環來重復該過程,直到buffer中有可供讀取的日志為止。最后,通過get_entry_len函數讀取下一條日志,並通過do_read_log_to_user將其復制到用戶空間,讀取完畢。

5.logger_aio_write

  分析了讀操作,下面登場的應該是寫操作了。在這里,我們終於可以清楚地向大家解釋之前的疑問——為什么緩沖區是環形的。在寫入日志時,當其日志緩沖區buffer被寫滿之后,我們就不能再執行寫入操作了嗎?答案是否定的,正因為buffer是環形的,在寫滿之后,新寫入的數據就會覆蓋最初的數據,所以我們需要采取一定的措施來避免原來的數據被覆蓋,以免造成數據丟失。寫操作的具體實現如下:

ssize_t logger_aio_write(struct kiocb *iocb, const struct iovec *iov,
             unsigned long nr_segs, loff_t ppos)
{
    //取得日志設備logger_log
    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();
    //初始化日志數據logger_entry
    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);
    if (unlikely(!header.len))
        return 0;
    mutex_lock(&log->mutex);
    //修正偏移量,避免被覆蓋
    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;
        len = min_t(size_t, iov->iov_len, header.len - ret);
        //從用戶空間寫入日志
        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;
    }
    mutex_unlock(&log->mutex);
    wake_up_interruptible(&log->wq);
    return ret;
}

 與讀操作一樣,首先,需要取得日志設備logger_log,這里我們是通過file_get_log函數來獲取日志設備;然后,對要寫入的日志執行初始化操作(包括進程的pid、tid和時間等)。因為我們的寫操作支持同步、異步以及scatter等方式(非常靈活),而且在進行寫操作時讀操作可能並沒有發生,這樣就會被覆蓋,所以通過在寫操作之前執行fix_up_readers函數來修正其偏移量(r_off),然后才執行真正的寫入操作。

  fix_up_readers函數真正能修正其偏移量而使其不被覆蓋嗎?下面我們先看看該函數的具體實現,如下所示:

static void fix_up_readers(struct logger_log *log, size_t len)
{
    //當前寫偏移量
    size_t old = log->w_off;
    //寫入長度為len的數據后的偏移量
    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);
    //遍歷reader鏈表
    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);
}

大家可以看到,在執行clock_interval進行new復制時,將會覆蓋log->head,所以我們使用get_next_entry來查詢下一個節點,並使其作為head節點。通常在執行查詢時,我們使用的都是要被寫入的整個數據的長度(len),因為是環形緩沖區,所以會出現覆蓋數據的情況,因此這里傳入的長度為最大長度(即要寫入的數據長度);然后遍歷reader鏈表,如果reader在覆蓋范圍內,那么調整當前reader位置到下一個log數據區。因此從這里我們可以看出,fix_up_readers函數只是起到一個緩解的作用,也不能最終解決數據覆蓋問題,所以寫入的數據如果不被及時讀取,則會造成數據丟失。

  6.logger_poll

  該函數用來判斷當前進程是否可以對日志設備進行操作,其具體實現代碼如下:

static unsigned int logger_poll(struct file *file, poll_table *wait)
{
    struct logger_reader *reader;
    struct logger_log *log;
    unsigned int ret = POLLOUT | POLLWRNORM;
    if (!(file->f_mode & FMODE_READ))
        return ret;
    reader = file->private_data;
    log = reader->log;
    poll_wait(file, &log->wq, wait);
    mutex_lock(&log->mutex);
    //判斷是否為空
    if (log->w_off != reader->r_off)
        ret |= POLLIN | POLLRDNORM;
    mutex_unlock(&log->mutex);
    return ret;
}

我們可以看出,POLLOUT總是成立的,即進程總是可以進行寫入操作;讀操作則不一樣了,如果只是以FMODE_READ模式打開日志設備的進程,那么就需要判斷當前日志緩沖區是否為空,只有不為空才能讀取日志。

7.logger_ioctl

  該函數主要用於對一些命令進行操作,它可以支持以下命令操作:

  LOGGER_GET_LOG_BUF_SIZE得到日志環形緩沖區的尺寸

  LOGGER_GET_LOG_LEN得到當前日志buffer中未被讀出的日志長度

  LOGGER_GET_NEXT_ENTRY_LEN得到下一條日志長度

  LOGGER_FLUSH_LOG清空日志

  它們分別對應於logger.h中所定義的下面這些宏:

  #define LOGGER_GET_LOG_BUF_SIZE_IO(__LOGGERIO, 1)

  #define LOGGER_GET_LOG_LEN_IO(__LOGGERIO, 2)

  #define LOGGER_GET_NEXT_ENTRY_LEN_IO(__LOGGERIO, 3)

  #define LOGGER_FLUSH_LOG_IO(__LOGGERIO, 4)

  這些操作的具體實現很簡單,大家可以參考logger.c中的logger_ioctl函數。以上就是我們對Logger驅動的分析,大家可以對應源碼來閱讀,這樣會更容易理解。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM