💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
当进程调用函数write、writev或者aio_write往日志设备写入日志记录时,Logger日志驱动程序中的函数logger_aio_write就会被调用来执行日志记录的写入操作。 **kernel/goldfish/drivers/staging/android/logger.c** ~~~ /* * 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; mutex_lock(&log->mutex); /* * 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; } mutex_unlock(&log->mutex); /* wake up any blocked readers */ wake_up_interruptible(&log->wq); return ret; } ~~~ 函数的第一个参数iocb表示一个IO上下文。第二个参数iov保存了要写入的日志记录的内容,它是一个数组向量,长度由第三个参数nr_segs决定。第四个参数ppos指定日志记录的写入位置。由于日志设备中保存了下一条日志记录的写入位置,因此,第四个参数ppos是不需要使用的。 对于类型为main、system和radio的日志记录来说,它们的内容是由优先级(priority)、标签(tag)和内容(msg)三个字段组成的,分别保存在iov[0]、iov[1]和iov[2]中,这时候第三个参数nr_segs的值就为3。对于类型为events的日志记录来说,它们的内容只有标签和内容两个字段,分别保存在iov[0]和iov[1]中,这时候第三个参数nr_segs的值就为2。在特殊情况下,当类型为events的日志记录的内容只有一个值时,第二个参数iov的大小也会等于3,这时候,iov[0]表示日志标签,iov[1]表示日志记录的内容值类型,iov[2]表示日志记录的内容值。在后面的4.4小节中分析日志的写入接口时,我们再详细介绍参数iov的使用方法。无论要写入的是什么类型的日志记录,它的总长度值都是保存在参数iocb的成员变量ki_left中的,单位是字节。 在前面的4.2.3小节提到,当进程以写模式打开日志设备时,Logger日志驱动程序会把对应的日志缓冲区结构体log保存在一个打开文件结构体file的成员变量private_data中。参数iocb的成员变量ki_filp正好指向该打开文件结构体,因此,第9行就调用函数file_get_log安全地将对应的日志缓冲区结构体log获取回来。 **kernel/goldfish/drivers/staging/android/logger.c** ~~~ static inline struct logger_log * file_get_log(struct file *file) { if (file->f_mode & FMODE_READ) { struct logger_reader *reader = file->private_data; return reader->log; } else return file->private_data; } ~~~ 有了这个日志缓冲区结构体log之后,就可以执行日志记录的写入操作了。 回到函数logger_aio_write中,第11行首先定义一个日志记录结构体header,接着第17行到第21行对它进行初始化,即记录日志记录写入进程的TGID和PID,以及日志记录的写入时间和长度。由于一条日志记录的最大长度为LOGGER_ENTRY_MAX_PAYLOAD,因此,第21行在初始化日志记录的长度时,要取参数iocb的成员变量ki_left和宏LOGGER_ENTRY_MAX_PAYLOAD的较小值。 在将日志记录写入到日志缓冲区之前,还有一件重要的事情要做,就是修正那些正在读取该日志缓冲区的进程的当前日志记录的读取位置,即日志读取进程结构体logger_reader的成员变量r_off的值,以及修正新的日志读取进程的日志记录开始读取位置,即日志缓冲区结构体logger_log的成员变量head的值。我们知道,日志缓冲区是循环使用的,即当日志缓冲区写满之后,新的日志记录会覆盖旧的日志记录,而这些将要被覆盖的日志记录有可能正好是某些进程的下一条日志记录的读取位置。由于这些位置即将被新的日志记录覆盖,因此,我们就需要对它们进行修正,以便这些受影响的进程下次能够读取到正确的日志记录。这个修正工作要在日志记录写入前去做,这是为了保证在日志记录写入的过程中出现部分失败时,不会破坏整个日志缓冲区的内容。 修正日志记录读取进程的读取位置的工作是通过调用函数fix_up_readers来完成的,它的实现如下所示。 **kernel/goldfish/drivers/staging/android/logger.c** ~~~ /* * 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. * * The caller needs to hold log->mutex. */ 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); } ~~~ 第二个参数len表示即将要写入的日志记录的总长度,它等于日志记录结构体logger_entry的长度加上日志记录的有效负载长度。第11行得到日志缓冲区结构体log的下一条日志记录的写入位置,保存在变量old中。第12行计算将新的日志记录写入到日志缓冲区结构体log之后,下一条日志记录的写入位置,保存在变量new中。位于old值和new值之间的那些日志记录读取位置是无效的,因为它们即将被新写入的日志记录覆盖。 给定一个日志记录读取位置c,判断它是否位于a和b之间是通过调用函数clock_interval来实现的,如下所示。 **kernel/goldfish/drivers/staging/android/logger.c** ~~~ /* * 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) return 1; } return 0; } ~~~ 函数clock_interval分两种情况来判断c是否位于a和b之间:第一种情况是b小于a;第二种情况是b大于等于a。在这两种情况下,如果c位于a和b之间,那么函数的返回值为1,否则为0。 回到函数fix_up_readers中,第16行修正的是新的日志读取进程在日志缓冲区结构体log中的开始读取位置,即日志缓冲区结构体log的成员变量head的值。第18行到第20行的循环语句修正的是那些正在读取日志缓冲区结构体log中的日志记录的进程的下一条日志记录的位置,即日志读取进程结构体reader的成员变量r_off的值。如果这些位置正好位于变量old和new的值之间,那么就调用函数get_next_entry将它们的值修改为下一条日志记录的位置。 **kernel/goldfish/drivers/staging/android/logger.c** ~~~ /* * 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; } ~~~ 参数off表示要修正的位置,而参数len表示即将要写入的日志记录的长度。第11行到第15行的while循环首先调用函数get_entry_len来获得在位置off的日志记录的长度,然后将它增加到位置off中去。如果累计增加的位置大于或者等于参数len的值,那么就完成了对位置off的修正;否则,就要继续将位置off移到再下一条日志记录的位置。 回到函数logger_aio_write中,第37行到第56行代码用来将参数iov中的日志记录内容写入到日志缓冲区结构体log中。由于一条日志记录在日志缓冲区中是由一个日志记录结构体和一个有效负载组成的,因此,函数logger_aio_write就分两步将它们写入到日志缓冲区中。 函数前面已经为即将要写入的日志记录准备好了一个日志记录结构体header,因此,第37行就调用函数do_write_log将它的内容写入到日志缓冲区结构体log中。 **kernel/goldfish/drivers/staging/android/logger.c** ~~~ /* * 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) { size_t len; len = min(count, log->size - log->w_off); memcpy(log->buffer + log->w_off, buf, len); if (count != len) memcpy(log->buffer, buf + len, count - len); log->w_off = logger_offset(log->w_off + count); } ~~~ 由于一个日志记录结构体的内容可能会被分别写在日志缓冲区的尾部和头部,因此,函数do_write_log就分两次来写入它的内容。第10行计算写入到日志缓冲区尾部的内容的长度,接着第11行就直接将对应的日志记录结构体的内容拷贝到日志缓冲区中。第13行判断前面是否已经将日志记录结构体的内容全部写入到日志缓冲区中了。如果不是,第14行就会将剩余的内容写入到日志缓冲区的头部。 > 参数buf指向的内容即为要写入的日志记录结构体的内容,它位于内核空间中,因此,第11行和第14行将它的内容写入到日志缓冲区中时,直接调用函数memcpy来拷贝就可以了。将日志记录结构体的内容写入到日志缓冲区中之后,第16行就要更新它的成员变量w_off的值,因为它始终指向下一条日志记录的写入位置。 回到函数logger_aio_write中,第39行到第56行的while循环把参数iov的内容写入到日志缓冲区结构体log中。参数iov里面所保存的内容对应于要写入的日志记录的有效负载,它们是从用户空间传进来的,因此,函数logger_aio_write不能直接调用函数memcpy将它们拷贝到日志缓冲区结构体log中,而是调用函数do_write_log_from_user来执行拷贝操作。 **kernel/goldfish/drivers/staging/android/logger.c** ~~~ /* * do_write_log_user - writes 'len' bytes from the user-space buffer 'buf' to * the log 'log' * * The caller needs to hold log->mutex. * * Returns 'count' on success, negative error code on failure. */ static ssize_t do_write_log_from_user(struct logger_log *log, const void __user *buf, size_t count) { size_t len; len = min(count, log->size - log->w_off); if (len && copy_from_user(log->buffer + log->w_off, buf, len)) return -EFAULT; if (count != len) if (copy_from_user(log->buffer, buf + len, count - len)) return -EFAULT; log->w_off = logger_offset(log->w_off + count); return count; } ~~~ 与日志记录结构体的写入过程类似,日志记录的有效负载也有可能会被分别写入到日志缓冲区的尾部和头部,因此,函数do_write_log_from_user就分两次来写入它的内容。第14行计算写入到日志缓冲区尾部的内容的长度,接着第15行就调用函数copy_from_user将对应的日志记录的有效负载拷贝到日志缓冲区中。第18行判断前面是否已经将日志记录的有效负载全部写入到日志缓冲区了。如果不是,第19行就会将剩余的内容写入到日志缓冲区的头部。 > 参数buf指向的内容是保存在用户空间中的,因此,第15行和第19行将它的内容写入到日志缓冲区中时,需要调用函数copy_from_user来执行拷贝操作。将日志记录的有效负载写入到日志缓冲区中之后,接下来第22行就要更新它的成员变量w_off的值,因为它始终指向下一条日志记录的写入位置。 在前面的4.2.4小节中,我们在分析日志记录的读取过程时提到,如果日志缓冲区当前没有新的日志记录可读,那么日志读取进程就会进入到日志缓冲区的等待队列wq中去睡眠等写入新的日志记录。现在既然已经往日志缓冲区中写入新的日志记录了,函数logger_aio_write在第61行就会调用函数wake_up_interruptible来唤醒那些睡眠在日志缓冲区的等待队列wq中的进程,目的是通知它们有新的日志记录可读了。