上篇文章从开始的app中打印一条log开始一直讲到进入到运行库中调用设备的write方法,现在开始要进入内核来写日志了。下面开始先介绍下内核中和日志有关的类中相关的变量和方法。
```c
// 文件路径 drivers/staging/android/logger.c
#define LOGGER_LOG_RADIO "log_radio"
#define LOGGER_LOG_EVENTS "log_events"
#define LOGGER_LOG_MAIN "log_main"
#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(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)
```
最上面开始的是一个宏定义结构体,最下面3行,就是利用宏来初始化3个变量,我们用第一个来翻译一下就清楚了,把代码展开后如下:
```c
static unsigned char _buf_log_main[64*1024];
static struct logger_log log_main = { \
.buffer = _buf_log_main, \ // 缓冲区
.misc = { \
.minor = MISC_DYNAMIC_MINOR, \ // MISC_DYNAMIC_MINOR 255表示设备号系统动态分配
.name = log_main, \ // 名字
.fops = &logger_fops, \ // 文件操作列表
.parent = NULL, \
}, \
.wq = __WAIT_QUEUE_HEAD_INITIALIZER(log_main .wq), \ // 等待读此设备的队列
.readers = LIST_HEAD_INIT(log_main .readers), \ // 正在读此设备的队列
.mutex = __MUTEX_INITIALIZER(log_main .mutex), \ // 互斥锁
.w_off = 0, \
.head = 0, \
.size = SIZE, \
};
```
可以看到这个结构体就是一个设备,它有一个缓冲区大小是64K,然后就是初始化一些等待队列之类的,值得注意的是这个比较关键的是misc里面的fops字段,这个是操作本设备的所有的方法,比如读写等,它指向了一个地址&logger_fops,我们去看一下:
```c
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,
};
```
这个中间还是有不少方法,我们比较关注的比如写方法aio_write = logger_aio_write,可以看到他指向了一个地址,不用猜也大概可以知道具体写设备的方法应该就是他了,其他比如还有读方法,打开方法,都是在这里定义的。好了,既然之前说到了写方法,我们现在就过去看看这个方法:
```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.
*/
// iocb可以看做当前上下文,这里可以简单看做是当前进程的一个代表,这里即内核的代表。iov是保存数据的参数,第三个参数nr_segs表示前面这个iov数组前几个有效,因为日志是有优先级,标签,内容组成的,所以每个内容代表一维
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;
// 一条日志有效数据最大长度LOGGER_ENTRY_MAX_PAYLOAD,iocb->ki_left是一个IO操作对象可以还允许有多少空间,取2者小的值,也就是说一条日志
最大不能超过LOGGER_ENTRY_MAX_PAYLOAD字节
header.len = min_t(size_t, iocb->ki_left, LOGGER_ENTRY_MAX_PAYLOAD);
/* null writes succeed, return zero */
if (unlikely(!header.len)) // 如果len为0,表示没有需要写入的数据,return
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)); // 先把logger_entry写入日志设备缓冲区
while (nr_segs-- > 0) { // nr_segs表示一共有几个数组的数据需要取出
size_t len;
ssize_t nr;
/* figure out how much of this vector we can keep */ // iov->iov_len是每个数组的有效数据长度
len = min_t(size_t, iov->iov_len, header.len - ret); // iov->iov_len是当前需要写入的有效数据的总长度,header.len - ret是还剩余有效数据有多少,取他们小的值
/* write out this segment's payload */
nr = do_write_log_from_user(log, iov->iov_base, len); // 把用户态中缓冲区iov->iov_base的len个字节写入设备缓冲区log中
if (unlikely(nr < 0)) { // 如果写人数据小于0,return
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;
}
```
这个方法就是具体写入的方法了,第一行file_get_log(iocb->ki_filp)这个方法表示的是获取日志设备的缓冲区,即日志设备保存数据的地方,iocb可以看做是当前这个设备,ki_filp就是代表了这个设备的文件,还记得linux所有都是文件吗,这个就代表了一个真实的日志设备,然后我们看下file_get_log这个方法
```c
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大小
};
static inline struct logger_log * file_get_log(struct file *file)
{
if (file->f_mode & FMODE_READ) {
// 获取private_data字段,可以理解成当前正在写这个设备的一个读者,也就是一个进程
struct logger_reader *reader = file->private_data;
return reader->log; // 返回读者保留的缓冲区,也就是该设备的缓冲区
} else
return file->private_data; // 如果是写进程的话,private_data就是设备的缓冲区
}
```
上面这个方法结果都是返回该设备的缓冲区,但是如果读和写的话,设备文件private_data的字段保存的值是不同的,简单而言,如果是写进程,保存的是这个代表这个进程的一个对象,当前设备的缓冲区也会被保存在这个对象里面,而如果是写者,直接保存的是设备的缓冲区。为什么要这样做呢?因为如果是写进程的话,无论哪个进程写缓冲区的开始位置都是一样的(当然肯定是在线程安全的前提下),写任务肯定是一个接着一个的,只要设备保留了开始写入的偏移点,那么进程只要根据这个写入点写就可以了。而读进程不一样,每个读进程读取数据的开始点可能都不一样,所以必须给每个进程把开始读入的位置保存在进程自己里面,这样在读的时候就知道从哪里读了。
好了, 我们回到logger_aio_write方法继续往下看,接着有一个变量struct logger_entry header,我们先看一下这个类定义:
```c
struct logger_entry { // 一条日志的结构体,用来描述一条日志
__u16 len; // 有效数据长度,每条日志都有个logger_entry,即去掉这个头文件后的长度
__u16 __pad; // 用来4字节对齐的,没实际作用
__s32 pid; // 进程ID
__s32 tid; // 线程组ID
__s32 sec; // 秒
__s32 nsec; // 纳秒
char msg[0]; // 日志记录内容,长度由len决定
};
```
这个类就是代表了一个具体的日志,下面写数据的时候要操作这个类。对于日志的缓冲区是可以循环写的,即如果已经写满了,是可以把最早的数据覆盖的,所以我们在写数据前需要判断下是否会覆盖以前的数据,还记得前面说的读进程每个开始的读便宜位置是不一样吗,所以我就需要再有数据覆盖的情况下,修改这些读进程的偏移点,具体方法是fix_up_readers:
```c
static void fix_up_readers(struct logger_log *log, size_t len) // 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); // 需要修正
}
```
首先获取老的和新的写入偏移点,然后通过clock_interval来判断,方法如下:
```c
static inline int clock_interval(size_t a, size_t b, size_t c) // a是老的写入位置,b是新的写入位置,c是日志记录的读取位置
{
if (b < a) {
if (a < c || b >= c)
return 1;
} else {
if (a < c && b >= c)
return 1;
}
// 上面2种情况读取位置c会被新写入的位置覆盖,所以返回1,如果不覆盖返回0
return 0;
}
```
老的写入,新的写入,和当前日志读取的偏移都知道了,只要判断下他们之间相互的关系就可以知道会不会覆盖了,如果会覆盖返回1,不会返回0。
继续前面fix_up_readers的方法,如果会覆盖,首先我们要修改日志本身的读取偏移点,即og->head变量,具体方法是get_next_entry:
```c
// 获取下一个不被覆盖的点,off当前读偏移点,len需要写入长度
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开始的一个日志的长度
off = logger_offset(off + nr); // 下一个日志数据的位置
count += nr; // 因为写入最长也就len,所以只要count > len就OK了
} while (count < len);
return off;
}
```
这个方法是往前走n个日志,只要走到长度超过了需要写入的长度就可以了,然后返会往前走的偏移点。具体方法是logger_offset,我们看下:
```c
static __u32 get_entry_len(struct logger_log *log, size_t off) // 获取头文件 + 有效数据长度,数据有2部分组成,logger_entry + 数据,logger_entry中的len有实际数据的长度,返回的是头+有效数据的总长度
{
__u16 val;
// 每个读入点都对应一条日志,即logger_entry,logger_entry的前2个字节表示该日志去掉头后的大小
switch (log->size - off) { // 缓冲区是循环的,所以要判断第一个字节,有没有在最后。缓冲区大小 - 第一个读数据的偏移量
case 1: // 如果等于1,说明第一个字节正好在缓冲区最后一字节上
memcpy(&val, log->buffer + off, 1); //获取第一个字节
memcpy(((char *) &val) + 1, log->buffer, 1); // 获取第二个字节
break;
default:
memcpy(&val, log->buffer + off, 2); // 直接获取2个字节
}
return sizeof(struct logger_entry) + val; // 返回头+数据的总长度
}
```
logger_log类的第一个字段buffer是指向缓冲区的,即指向一个日志,而一个日志就是logger_entry,而他前2个字节就是指不包括日志头后的实际数据的大小,我们需要取出这2个字节然后计算当前这个日志有多大,从跳过后看写入的长度还会不会覆盖。如果还是会覆盖则继续跳过一个日志,直到不会覆盖为止。这样就把缓冲区本身读的偏移点更新了,由于每个进程都有自己的读位置偏移点,还要更新每个读进程的这个变量:
```c
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); // 需要修正
```
其实新老写入点和设备缓冲区是一样的,就是读偏移点不一样,reader->r_off是每个读进程的偏移点,其余逻辑都是一样,好了,这样就把所有读位置都更新好了,回到logger_aio_write方法,接下去do_write_log(log, &header, sizeof(struct logger_entry))这个方法先把日志头给写进去:
```c
static void do_write_log(struct logger_log *log, const void *buf, size_t count) // 把buf开始的count个字节写入log的缓冲区中
{
size_t len;
len = min(count, log->size - log->w_off); // count是需要写入的字节数,log->size - log->w_off是从写入点到末尾还有多少字节,去他们中小的值,保证不会超出末尾
memcpy(log->buffer + log->w_off, buf, len); // 把buf开始的len个字节写入log->buffer + log->w_off开始的缓冲区
if (count != len) // 如果不相等,说明还需要从头写起
memcpy(log->buffer, buf + len, count - len); // 把buf + len开始的(count - len)个字节写入log->buffer
log->w_off = logger_offset(log->w_off + count); // 写入点更新
}
```
这个方法先算出从日志设备当前写入点到结尾有多大,然后和需要写入的长度比较,取他们中较小的,这样保证写入不会超过缓冲区的尾巴,然后就复制数据,如果还是剩余的,需要从头部开始写起,这样一个日志头就写完了。接下去就是写入具体的数据内容了:
```c
while (nr_segs-- > 0) { // nr_segs表示一共有几个数组的数据需要取出
size_t len;
ssize_t nr;
/* figure out how much of this vector we can keep */ // iov->iov_len是每个数组的有效数据长度
len = min_t(size_t, iov->iov_len, header.len - ret); // iov->iov_len是当前需要写入的有效数据的总长度,header.len - ret是还剩余有效数据有多少,取他们小的值
/* write out this segment's payload */
nr = do_write_log_from_user(log, iov->iov_base, len); // 把用户态中缓冲区iov->iov_base的len个字节写入设备缓冲区log中
if (unlikely(nr < 0)) { // 如果写人数据小于0,return
log->w_off = orig; // 写入点还原
mutex_unlock(&log->mutex);
return nr;
}
iov++; // 下一个数组
ret += nr; // 已经写了多少个字节数据
}
```
这里的写入逻辑和上面写入日志头是一样的,注释中也写得比较清楚了,相信大家如果理解了上面写入头的逻辑,这里应该都能看懂。比较值得注意的是上面写入都用的方法是do_write_log,而这里用的是do_write_log_from_user:
```c
static ssize_t do_write_log_from_user(struct logger_log *log,
const void __user *buf, size_t count) // 从用户态buf缓冲区写count个字节数据到log中
{
size_t len;
len = min(count, log->size - log->w_off); // 先算出写入点w_off到末尾有多少字节,和需要写入的count比较,取小的值
if (len && copy_from_user(log->buffer + log->w_off, buf, len)) // 如果len大于0,并且copy_from_user返回0,就算成功了
return -EFAULT;
if (count != len) // 说明还有需要写入,并且需要从头写
if (copy_from_user(log->buffer, buf + len, count - len)) // copy_from_user返回0表示成功
return -EFAULT;
log->w_off = logger_offset(log->w_off + count); // 更新写入点
return count;
}
```
这里其他都没什么好说的,和前面介绍的do_write_log是一样的,主要还是copy_from_user替代了前面的memcpy方法。copy_from_user方法的原理就比较复杂了,前面之所以可以用do_write_log是因为,这些都是在内核之中的数据,前面是复制的header就是在这个方法里面创建的,当前是在内核中,所以这些数据都是在内核中操作的,这是没有问题的。但是记得我们的数据是从哪里来的吗?我们是在app中调用Log方法来打印的,这些数据都是在app中的,和内核是两个不同的进程,app是属于用户态的进程,他们的地址空间都是互相独立不同,所以即使内核有权限去获得用户进程地址空间中的数据,但是首先需要获得用户的地址,才能拿到数据。
copy_from_user这个方法的就是获取数据在用户进程空间的地址,然后再进程复制。这个是涉及到的操作系统内核态和用户态之间的通信问题,这里简单说下大致原理,就是用户态的app会通过一种叫系统调用的机制去执行在内核中的方法,这里日志写的过程其实就是app调用的内核态中的写方法,从而从用户态进入了内核态,在进入内核态的时候,操作系统会把用户进程的信息压入栈,这些信息中就包括了可以寻找到用户进程物理地址的信息,在内核态的执行中如果需要获取用户进程的数据地址,就会从栈中取出相关的信息,最终获得用户进程的地址,从而拿到用户的数据,copy_from_user方法的原理基本也是这样的,这里我们只要知道这个过程就可以了,这个方法虽然实现的过程复杂了些,但是最终的目的就是把用户数据复制给日志设备。
这些执行完毕后所有的数据也就写完了,最后唤醒日志设备中还在等待写的进程。好了,整个写进程的流程基本也就这样了,是不是从上层到内核,一个完整的流程走下来,虽然感到很累,但是一旦走通后,感觉豁然开朗的感觉,总算是解开了他们神秘的面纱。写日志就说到这了。后面我们在看下读日志的过程。
Android日志模块解析二