• 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驱动的分析,大家可以对应源码来阅读,这样会更容易理解。

  • 相关阅读:
    Unity小地图Map
    DoTween扩展Transform
    Android开发笔记1.2
    Android开发笔记1.1.1
    Unity向量夹角
    使用vi
    MIPS 两个数的和(输入,计算,输出)
    python openpyxl 读取excel表操作
    javaI/O文件,读操作
    试用git遇见问题
  • 原文地址:https://www.cnblogs.com/manuosex/p/3592636.html
Copyright © 2020-2023  润新知