zoukankan      html  css  js  c++  java
  • 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'!
    ", log->misc.name);
            return ret;
        }
        printk(KERN_INFO "logger: created %luK log '%s'
    ",
               (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驱动的分析,大家可以对应源码来阅读,这样会更容易理解。

  • 相关阅读:
    Zend Framework 2.1.5 中根据服务器的环境配置调用数据库等的不同配置
    在基于 Eclipse 的 IDE 中安装和使用 Emmet(ZenCoding)
    【翻译】Emmet(Zen Coding)官方文档 之六 自定义 Emmet
    【翻译】Emmet(Zen Coding)官方文档 之二 缩写
    【翻译】Emmet(Zen Coding)官方文档 之七 一览表
    【翻译】Emmet(Zen Coding)官方文档 之三 CSS 缩写
    【翻译】Emmet(Zen Coding)官方文档 之四 动作
    【翻译】Emmet(Zen Coding)官方文档 之一 web 编程的必备工具
    Zend Framework 2 时区设置警告问题的解决
    【翻译】Emmet (Zen Coding) 元素类型
  • 原文地址:https://www.cnblogs.com/manuosex/p/3592636.html
Copyright © 2011-2022 走看看