Kernel Log Buffer源码分析
1 为什么 printk 需要 Ring Buffer
有没有想过一个问题:为什么 printk() 不直接把字符串输出到串口,而是需要一个 ring buffer?
因为串口、console 等输出设备非常慢,CPU 执行代码的速度远远快于串口发送数据的速度。CPU 时钟大约是 GHz 级别,而串口的传输速度只有几 Mbps。如果 CPU 每次的日志都需要等串口等设备同步,那么就会造成大量的日志积累,拖慢系统的运行。
于是 printk() 会把产生的消息存入 ring buffer,实现生产者与消费者解耦的目的。
2 printk Ring Buffer 整体结构
源码很清晰,我们直接看一下源码:
1 | |
主要由两个环形缓冲区组成:
desc_ring:这条日志是什么text_data_ring:日志的具体内容
例如:
1 | |
上面这条日志就会产生两种信息,一种是日志的描述信息,包括日志等级、日志序号和时间戳等;另一种就是日志的具体内容,即 "temperature=25\n"。
通过全局变量访问,其定义如下:
1 | |
比如我们之前讲到的 prb_reserve(&e, prb, &r);,这里的 prb 就是整个 printk Ring Buffer。
我们接着讲解 desc_ring,其定义如下:
1 | |
主要就是两个核心数组 descs 和 infos,其中 descs 记录日志的状态,infos 记录日志信息,示意如下:

继续看 text_data_ring,简单理解就是一个 char data[] 存放真正的日志字符串。
我们举个例子来看,假设日志如下:
1 | |
则其被拆解为:

上面还有两个东西,一个是 head,一个是 tail。我们之前说过了,新的日志会覆盖旧的,就需要两个指针,指明新的日志写在哪里、旧的写在哪里。当空间不够时,tail 会向前移动,旧日志将被淘汰,具体代码我们在后面讨论。
上面这个流程的示意图如下:

3 一条日志如何写入 Ring Buffer
我们假设一条日志为:
1 | |
其写入的整体流程我们在上一章看过了,这里我们只关注它是怎么写入 ring buffer 的。
之前我们说过日志等级:
1 | |
上面的信息在编译时被拼接为 "\0016temperature=%d\n":
1 | |
其核心函数就是 vprintk_store(),printk 的参数经过解析后传入其中,如图所示。接着开始获取日志信息,包括产生时间和调用者(日志是谁产生的):
1 | |
保存日志前先计算日志所需的空间大小,计算完成后,把要写入的东西和大小记录下来。
接着就是最核心的一步:去 ring buffer 中拿空间。
1 | |
其中:
prb:指向 ring bufferr:希望写入的日志和大小
上面说过 ring buffer 由两类组成,所以需要完成 descriptor ring 和 text data ring 的空间预留,一个是记录槽位,一个是正文槽位。
然后填写 infos 里面的信息,最后提交即可,其流程图如下:

4 prb_reserve()
根据上面的流程我们知道,prb_reserve() 的核心作用就是预留空间,分两部分。为一条新日志预留一个 descriptor,并在 text data ring 中预留正文空间,然后把可写地址返回给调用者。
其定义为:
1 | |
未完待续……