🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
当进程调用read函数从日志设备读取日志记录时,Logger日志驱动程序中的函数logger_read就会被调用从相应的日志缓冲区中读取日志记录。 **kernel/goldfish/drivers/staging/android/logger.c** ~~~ /* * 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; break; } if (signal_pending(current)) { ret = -EINTR; break; } schedule(); } finish_wait(&log->wq, &wait); if (ret) return ret; mutex_lock(&log->mutex); /* is there still something to read or did we race? */ if (unlikely(log->w_off == reader->r_off)) { mutex_unlock(&log->mutex); goto start; } /* get the size of the next entry */ ret = get_entry_len(log, reader->r_off); if (count < ret) { ret = -EINVAL; goto out; } /* get exactly one entry from the log */ ret = do_read_log_to_user(log, reader, buf, ret); out: mutex_unlock(&log->mutex); return ret; } ~~~ 函数第16行得到日志记录读取进程结构体reader,而第17行得到日志缓冲区结构体log。在前面的4.2.3小节中提到,当进程以读模式打开相应的日志设备时,Logger日志驱动程序就会将上述两个结构体信息保存在打开文件结构体参数file的成员变量private_data中,因此,函数第16行和第17行就可以安全地将它们获取回来。 函数第22行到第42行的while循环用来检查日志缓冲区结构体log中是否有日志记录可读取,这主要是通过第26行代码来判断的。在进入第22行到第42行的while循环时,第23行首先调用函数prepare_to_wait将等待队列项wait加入日志记录读取进程的等待队列log->wq中。等到确认日志缓冲区结构体log有日志记录可读或者其他原因跳出该while循环时,第44行就会调用函数finish_wait将等待队列项wait从日志记录读取进程的等待队列log->wq中移除。 日志缓冲区结构体log的成员变量w_off用来描述下一条新写入的日志记录在日志缓冲区中的位置,而日志记录读取进程结构体reader的成员变量r_off用来描述当前进程下一条要读取的日志记录在日志缓冲区中的位置。当这两者相等时,就说明日志缓冲区结构体log中没有新的日志记录可读,因此,Logger日志驱动程序就会继续执行第22行到第42行的while循环来等待写入新的日志记录。如果日志缓冲区结构体log中有新的日志记录可读,那么第26行得到的变量ret的值就为false,因此,第29行就会跳出第22行到第42行的while循环,准备读取日志缓冲区结构体log中的日志记录。 如果日志缓冲区结构体log中没有日志记录可读,即第26行得到的变量ret为true,那么当前进程就有可能需要进入睡眠状态,直到日志缓冲区结构体log中有新的日志记录可读为止。当前进程是通过调用第41行的函数schedule来请求内核进行一次进程调度,从而进入睡眠状态的。但是有一种特殊情况——当前进程是以非阻塞模式来打开日志设备文件,即第31行的if语句为true时,当前进程就不会因为日志缓冲区结构体log中没有日志记录可读而进入睡眠状态,它直接返回到用户空间中。一般来说,当驱动程序决定使当前进程进入睡眠状态之前,要先通过调用函数signal_pending来检查当前进程是否有信号正在等待处理。如果有的话,即第36行的if语句为true,那么这时候驱动程序就不能使当前进程进入睡眠状态,而必须要让它结束当前系统调用,以便它可以立即返回去处理那些待处理信号。 如果日志缓冲区结构体log中有日志记录可读,那么Logger日志驱动程序就会跳出第22行到第42行的while循环。但是执行到第51行时,又会再次重新判断日志缓冲区结构体log的成员变量w_off 和日志读取进程结构体reader的成员变量r_off是否相等。这是因为在执行第48行代码来获取互斥量log->mutex时,可能会失败。在这种情况下,当前进程就会进入睡眠状态,直到成功获取互斥量log->mutex为止。在当前进程的睡眠过程中,日志缓冲区结构体log中的日志记录可能已经被其他进程访问过了,即log->w_off的值可能已经发生了变化。因此,第51行需要重新判断日志缓冲区结构体log的成员变量w_off 和日志读取进程结构体reader的成员变量r_off是否相等。 第51行再次确认日志缓冲区结构体log中有新的日志记录可读之后,接着第57行就调用函数get_entry_len来得到下一条要读取的日志记录的长度。 **kernel/goldfish/drivers/staging/android/logger.c** ~~~ /* * 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; } ~~~ 在4.2.1小节中介绍日志记录结构体logger_entry时提到,每一条日志记录都是由两部分内容组成的,其中一部分内容用来描述日志记录,即结构体logger_entry本身所占据的内容;另一部分内容是日志记录的有效负载,即真正的日志记录内容。由于结构体logger_entry的大小是固定的,因此只要知道它的有效负载长度,就可以通过计算得到一条日志记录的长度。日志记录结构体logger_entry的有效负载长度记录在它的成员变量len中。由于成员变量len是日志记录结构体logger_entry的第一个成员变量,因此,它们的起始地址是相同的。又由于日志记录结构体logger_entry的成员变量len的类型为__u16,因此,只要读取该结构体变量地址的前两个字节的内容就可以知道对应的日志记录的长度。我们知道,日志缓冲区是循环使用的,因此,一条日志记录的前两个字节有可能分别保存在日志缓冲区的首尾字节中。因此,在计算一条日志记录的长度时,需要分两种情况来考虑。第一种情况是该日志记录的前两个字节是连在一起的;第二种情况就是前两个字节分别保存在日志缓冲区的首字节和尾字节中。 在函数get_entry_len中,第11行将日志缓冲区结构体log的总长度size减去下一条要读取的日志记录的位置off。如果得到的结果等于1,那么就说明该日志记录的长度值分别保存在日志缓冲区的首尾字节中,因此,第13行和第14行就将它的前后两个字节从日志缓冲区的尾字节和首字节中读取出来,并且将它们的内容组合在变量val中。如果得到的结果不等于1,那么就说明该日志记录的长度值保存在两个连在一起的字节中,即保存在地址log->buffer + off中,因此,第17行就直接将日志记录的长度值读取出来,并且保存在变量val中。最后,第20行将变量val的值加上结构体logger_entry的大小,就得到下一条要读取的日志记录的总长度了。 回到logger_read函数中,第64行调用函数do_read_log_to_user执行真正的日志记录读取操作。 **kernel/goldfish/drivers/staging/android/logger.c** ~~~ /* * do_read_log_to_user - reads exactly 'count' bytes from 'log' into the * user-space buffer 'buf'. Returns 'count' on success. * * Caller must hold log->mutex. */ 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; } ~~~ 由于一条日志记录的内容有可能同时位于日志缓冲区的末尾和开头处,即它在地址空间上是不连续的,因此,函数do_read_log_to_user就有可能需要分两次来读取它。第19行计算第一次要读取的日志记录的长度,接着第20行就将这一部分的日志记录拷贝到用户空间缓冲区buf中。第27行检查上一次是否已经将日志记录的内容读取完毕,如果未读取完毕,即第27行的if语句为true,那么第28行就会继续将第二部分的日志记录拷贝到用户空间缓冲区buf中,这样就完成了一条日志记录的读取。 > 日志记录结构体log的成员变量buffer指向的是一个内核缓冲区。在将一个内核缓冲区的内容拷贝到一个用户空间缓冲区时,必须要调用函数copy_to_user来进行,因为用户空间缓冲区的地址有可能是无效的,而函数copy_to_user在拷贝内容之前会对它进行检查,避免访问非法地址。 当前进程从日志缓冲区结构体log中读取了一条日志记录之后,就要修改它的成员变量r_off的值,表示下次要读取的是日志缓冲区结构体log中的下一条日志记录。第31行首先将日志读取进程结构体reader的成员变量r_off的值加上前面已经读取的日志记录的长度count,然后再使用宏logger_offset来对计算结果进行调整。 **kernel/goldfish/drivers/staging/android/logger.c** ~~~ /* logger_offset - returns index 'n' into the log via (optimized) modulus */ #define logger_offset(n) ((n) & (log->size - 1)) ~~~ 这是由于日志缓冲区是循环使用的,如果计算得到的下一条日志记录的位置大于日志缓冲区的长度,那么就需要将它绕回到日志缓冲区的前面。 > 日志缓冲区的长度是2的N次方,因此,只要它的值减1之后,再与参数n执行按位与运算,就可以得到参数n在日志缓冲区中的正确位置。 至此,日志记录的读取过程就介绍完了。接下来,我们继续分析日志记录的写入过程。