Kernel Log Buffer源码分析

1 为什么 printk 需要 Ring Buffer

有没有想过一个问题:为什么 printk() 不直接把字符串输出到串口,而是需要一个 ring buffer?

因为串口、console 等输出设备非常慢,CPU 执行代码的速度远远快于串口发送数据的速度。CPU 时钟大约是 GHz 级别,而串口的传输速度只有几 Mbps。如果 CPU 每次的日志都需要等串口等设备同步,那么就会造成大量的日志积累,拖慢系统的运行。

于是 printk() 会把产生的消息存入 ring buffer,实现生产者与消费者解耦的目的。

2 printk Ring Buffer 整体结构

源码很清晰,我们直接看一下源码:

1
2
3
4
5
struct printk_ringbuffer {
struct prb_desc_ring desc_ring;
struct prb_data_ring text_data_ring;
atomic_long_t fail;
};

主要由两个环形缓冲区组成:

  • desc_ring:这条日志是什么
  • text_data_ring:日志的具体内容

例如:

1
printk(KERN_INFO "temperature=%d\n", 25);

上面这条日志就会产生两种信息,一种是日志的描述信息,包括日志等级、日志序号和时间戳等;另一种就是日志的具体内容,即 "temperature=25\n"

通过全局变量访问,其定义如下:

1
2
3
static struct printk_ringbuffer printk_rb_dynamic;

struct printk_ringbuffer *prb = &printk_rb_static;

比如我们之前讲到的 prb_reserve(&e, prb, &r);,这里的 prb 就是整个 printk Ring Buffer。

我们接着讲解 desc_ring,其定义如下:

1
2
3
4
5
6
7
8
struct prb_desc_ring {
unsigned int count_bits;
struct prb_desc *descs;
struct printk_info *infos;
atomic_long_t head_id;
atomic_long_t tail_id;
atomic_long_t last_finalized_seq;
};

主要就是两个核心数组 descsinfos,其中 descs 记录日志的状态,infos 记录日志信息,示意如下:

继续看 text_data_ring,简单理解就是一个 char data[] 存放真正的日志字符串。

我们举个例子来看,假设日志如下:

1
printk(KERN_INFO "temperature=%d\n", 25);

则其被拆解为:

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

上面这个流程的示意图如下:

3 一条日志如何写入 Ring Buffer

我们假设一条日志为:

1
printk(KERN_INFO "temperature=%d\n", 25);

其写入的整体流程我们在上一章看过了,这里我们只关注它是怎么写入 ring buffer 的。

之前我们说过日志等级:

1
2
#define KERN_SOH  "\001"  // ASCII 码 1,即 SOH 控制字符
#define KERN_INFO KERN_SOH "6"

上面的信息在编译时被拼接为 "\0016temperature=%d\n"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
int vprintk_store(...)
{
struct prb_reserved_entry e;
struct printk_record r;
u16 reserve_size;
u16 text_len;

/* 1. 计算正文需要多少空间 */
reserve_size = vsnprintf(..., fmt, args) + 1;

/* 2. 解析日志等级 */
printk_parse_prefix(..., &level, &flags);

/* 3. 初始化记录 */
prb_rec_init_wr(&r, reserve_size);

/* 4. 预留descriptor和文本空间 */
if (!prb_reserve(&e, prb, &r))
...;

/* 5. 写入日志正文 */
text_len = printk_sprint(r.text_buf, ...,
fmt, args);

/* 6. 填写元数据 */
r.info->text_len = text_len;
r.info->level = level;
r.info->flags = flags;
r.info->ts_nsec = ts_nsec;
r.info->caller_id = caller_id;

/* 7. 提交记录 */
if (flags & LOG_NEWLINE)
prb_final_commit(&e);
else
prb_commit(&e);

return text_len;
}

其核心函数就是 vprintk_store(),printk 的参数经过解析后传入其中,如图所示。接着开始获取日志信息,包括产生时间和调用者(日志是谁产生的):

1
2
ts_nsec = local_clock();
caller_id = printk_caller_id();

保存日志前先计算日志所需的空间大小,计算完成后,把要写入的东西和大小记录下来。

接着就是最核心的一步:去 ring buffer 中拿空间。

1
2
3
4
5
6
7
8
9
prb_reserve(&e, prb, &r)

// r 定义
struct printk_record {
struct printk_info *info;
char *text_buf;
unsigned int text_buf_size;
};
struct printk_record r;

其中:

  • prb:指向 ring buffer
  • r:希望写入的日志和大小

上面说过 ring buffer 由两类组成,所以需要完成 descriptor ring 和 text data ring 的空间预留,一个是记录槽位,一个是正文槽位。

然后填写 infos 里面的信息,最后提交即可,其流程图如下:

4 prb_reserve()

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

其定义为:

1
2
3
bool prb_reserve(struct prb_reserved_entry *e,
struct printk_ringbuffer *rb,
struct printk_record *r);

未完待续……


Kernel Log Buffer源码分析
https://cj0510.github.io/2026/08/18/Linux Kernel/Logging subsystem/ring buffer/
作者
CJ1018
发布于
2026年8月18日
许可协议
CC BY-NC-SA 4.0