关注了就能看到更多这么棒的文章哦~
Printbuf rebuffed for now
By Jonathan Corbet
April 28, 2022
DeepL assisted translation
https://lwn.net/Articles/892611/
从内核中获取信息的方法有很多,而且在不断增加,但实际上 print 语句仍然是首选工具。尽管内核的 printk() 函数提供了一系列内核特有的功能,但往往还是不够的,所以近期人们一直想看到能更好地进行内核文本方式输出的 API。Kent Overstreet 提出的 "printbuf" 建议就是朝着这个方向迈出的一步,但是还需要做一些修改,才能让它与内核已有的功能很好地配合起来。
当内核代码需要输出一行普通文本信息时,使用 printk() 就可以了。但是在需要进行复杂的格式化、或者必须产生多行输出时,它就不那么方便了。就像用户空间中的 printf()一样,可以对一行文本进行多次 printk()调用,但是有一个问题:内核是一个高度并发的环境,在连续的 printk()调用之间可能发生很多其他事情,包括来自其他位置的 printk() 调用。这导致产生了混合的输出,通常被人们形容为 "乱码(garbled)",这种情况给人带来不少麻烦。
Printbuf
解决这个问题的方法是将复杂的输出先在内存中组装好,然后在一次调用中就打印完毕。这就是 printbuf 的方式了。一个 printbuf 就是一个简单的 structure,包含一个指向 char 类型 buffer 的指针以及一些管理相关的信息,包括 buffer 的 length 以及其中有多少有效数据。内核代码可以通过以下方式建立一个 printbuf:
#include <linux/printbuf.h>
struct printbuf buf = PRINTBUF;
PRINTBUF 是一个简单的 structure initializer,会将整个结构都清零。然后有一系列的函数用来将文本信息追加到 buffer 里面,包括:
void pr_buf(struct printbuf *buf, const char *fmt, ...);
void pr_char(struct printbuf *buf, char c);
void pr_newline(struct printbuf *buf);
void pr_human_readable_u64(struct printbuf *buf, u64 v);
void pr_human_readable_s64(struct printbuf *buf, s64 v);
void pr_time(struct printbuf *buf, u64);
/* ... */
pr_buf() 的工作原理与 printk()类似,只是所产生的文本最终会在 buf 里面,而不是直接输出进入系统日志(system log)。还有许多其他函数可以用来向 buffer 里添加特定类型的数据,其中一些函数在上面列出来了。buf.buf 中总是包含着那些积累下来的文本信息,可以将其传递给 printk(),从而在一次调用中就把整个 buffer 输出出去。当不再需要 printbuf 时,应将其作为参数传递给 printbuf_exit() 来释放资源。
到目前为止,在这个讨论中还没有提到内存管理的问题。printbuf 代码需要处理这个问题:它会分配字符串 buffer,并在可能会溢出时重新分配更大的 size。这些分配是在 GFP_KERNEL 方式下优先进行的,不过如果 structure 里面的 atomic 成员是 true 的话,printbuf 就会使用 GFP_ATOMIC。分配失败之后,代码会做一下记录,但会继续往下走。也就是说放弃一些输出,但把能保留的东西保留下来。
verstreet 在 4 月中旬首次发布这段代码时,最早的回应之一是 Christoph Hellwig 的一句话,他问道:"这个使用场景与 lib/seq_buf.c 的使用场景有什么不同?" 似乎 Overstreet 没有意识到 seq_buf 机制,因此重新实现了它的大部分内容。他后来回复说建议用他的新的实现代码来完全替代掉 seq_buf。
Seq_buf
Seq_buf 是在 2014 年的 3.19 版本中首次加入内核的。它本质是是希望解决类似的问题,尽管采取的方法有点不同。一个 seq_buf 会使用一个由调用者所分配的静态缓冲区(static buffer),初始化代码看起来像这样:
#include <linux/seq_buf.h>
char buf[MY_BUFFER_SIZE];
struct seq_buf seq;
seq_buf_init(&seq, buf, MY_BUFFER_SIZE);
在 seq_buf 中生成输出内容的过程与 printbuf 中使用的方法惊人地相似,有一系列看起来很眼熟的函数,包括:
int seq_buf_printf(struct seq_buf *s, const char *fmt, ...);
extern int seq_buf_puts(struct seq_buf *s, const char *str);
extern int seq_buf_putc(struct seq_buf *s, unsigned char c);
extern int seq_buf_putmem(struct seq_buf *s, const void *mem,
unsigned int len);
/* ... */
将 seq_buf 的内容发送到 log 里面是很简单的,即用预先分配好的 buffer 来调用 printk()。这个 API 也包括像 seq_buf_to_user()这样的函数,用来将 seq_buf 的内容复制到用户空间。另一方面,它缺乏 printbuf 机制所提供的一系列更复杂的格式化功能。但可以说这两个接口之间最大的区别是 printbuf 具有的自动内存管理功能。一个 seq_buf 可能会耗尽空间,但是在没有分配失败的情况下,printbuf 永远不会耗尽。
Reconciling the two
大家似乎一致认为 printbuf 这组改动带来了一些有用的功能,但是都不希望在内核中拥有两个做同样工作的子系统。因此,人们不出所料地建议 Overstreet 放弃 printbuf,而是把需要的功能添加到 seq_buf 中。写了最初的 seq_buf 代码的 Steve Rostedt 提出可以帮助来完成这项任务。
不过,Overstreet 对这个想法并不感兴趣:
Printbuf 是更先进、更广泛使用的实现,而你要求我抛弃它,以便内核能够坚持使用其更原始、使用得没那么广泛的实现。
"更广泛使用" 的说法引起了一些人的注意,因为 printbuf 不在内核中,因此,就 mainline 而言,根本就没有使用过。他似乎是算上了他自己的未合入 mainline 的 bcachefs 代码的使用数据。但是这种统计方法在内核社区中往往没有多少人认可。
同时,一个在内存管理子系统中增加 printbuf 调用的 patch 引起了 Michal Hocko 的质疑,他不相信它产生的新的输出内容有什么价值。后来他还提出了对内存管理子系统再使用动态内存分配来进行日志记录的担忧。每当试图记录跟内存不足相关情况的信息时,如果试图去分配更多的内存,往往不会有好结果。最好的情况就是它用上了最后的内存储备,而这些内存储备应该是专门用来进行内存释放任务的。
目前在几个不同的讨论分支上继续进行探讨,并在其中的几个分支中变得有些对抗性了。Overstreet 明确采用的说法是 "not-invented-here syndrome (也就是不在这里发明的就不喜欢用的这种偏见)",他对他的代码所受到的对待感到很不满意。看起来这些讨论有点像那种会导致相关开发者完全离开内核社区的那种感觉了。
希望这个讨论不会以这种方式结束。在即将举行的 Linux Storage, Filesystem, and Memory-Management Summit 峰会上会有一个 memory-management logging 主题会议。同时,Overstreet 最终同意在现有的 seq_buf 代码上实现他的功能的话,也是有可能发生的。假设这个方向是可行的,那么最终会得到内核社区通常在追求的那种解决方案:在不重复内核已经支持的机制的情况下来纳入有用的新功能。如果能看到新的 patch 发布出来的话,就能证明他们已经达成了一致。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~