LWN:代码tag功能的新框架!
关注了就能看到更多这么棒的文章哦~
A framework for code tagging
By Jonathan Corbet
September 1, 2022
DeepL assisted translation
https://lwn.net/Articles/906660/
内核代码有时很需要进行内省,因为它经常需要对自己进行引用。为了实现这种内省(introspection),内核已经开发了几种机制来识别代码中的特定位置,并执行与这些位置相关的操作。由 Suren Baghdasaryan 和 Kent Overstreet 发布的代码标记框架(code-tagging framework)这组 patch,试图用一个框架来取代之前各种临时(ad hot)实现,并增加一些新的应用。
内核有很多场合都需要确认出代码中的特定位置。例如,内核代码通常不允许产生 page fault,但是访问用户空间内存的函数往往会出现这种情况。为了在这种情况下能采取正确行为,内核 build 的过程会记下每个用户空间的访问操作的位置;当发生 page fault 时,就会检查这个列表,如果发现 page fault 发生在预期的位置,就会正常处理。内核的动态调试机制(dynamic debugging mechanism)则是另一个例子;每个用来调试的 print 语句都被 track 起来,并且可以独立启用。
实现这种机制的通常方法是在内核二进制文件中创建一个特殊的 ELF section;然后在这个 section 里面填写那些记录了内核中感兴趣的位置信息的 struct 结构。在运行时,内核可以找到这个 section,从中查找到包含了它所需信息的 struct 的数组。tagging framework 的核心是一组用来使创建和访问这个特殊 section 更加容易的函数和宏。
一个 code tag 表示代码本身的一个位置;该位置用一个新增的 struct 来表示:
struct codetag {
unsigned int flags;
unsigned int lineno;
const char *modname;
const char *function;
const char *filename;
};
这个结构中记录了一个位置,但没有其他信息;它是为了嵌入到另一个跟特定的 tagging application 相关的结构中的。例如,patch set 中的很大一部分代码是专门用来创建一个追踪内存分配的机制的;它可以记录每个调用位置分配和释放了多少内存,从而用来追踪内存泄漏。为了做到这一点,它将在每个内存分配的位置创建一个 tag,这个 structure 如下:
struct alloc_tag {
struct codetag ct;
unsigned long last_wrap;
struct raw_lazy_percpu_counter call_count;
struct raw_lazy_percpu_counter bytes_allocated;
};
raw_lazy_percpu_counter 是一个此 patch set 中新增的 counter 类型。这样我们就有了一个 structure,可以将这些 counter 与存储在 codetag struct 中的位置联系起来。
这些结构中的一个会被放置在特殊的 alloc_tags ELF section,并使用了宏定义的技巧:
#define DEFINE_ALLOC_TAG(_alloc_tag) \
static struct alloc_tag _alloc_tag __used __aligned(8) \
__section("alloc_tags") = { .ct = CODE_TAG_INIT }
然后再继续使用一些宏定义的技巧,替换掉当前的 alloc_pages() 函数,来增加放置 tag 和记住这个 allocation 调用的功能:
#define alloc_tag_add(_ref, _bytes) \
do { \
DEFINE_ALLOC_TAG(_alloc_tag); \
if (_ref && !WARN_ONCE(_ref->ct, "alloc_tag was not cleared")) \
__alloc_tag_add(&_alloc_tag, _ref, _bytes); \
} while (0)
#define pgtag_alloc_pages(gfp, order) \
({ \
struct page *_page = _alloc_pages((gfp), (order)); \
\
if (_page) \
alloc_tag_add(get_page_tag_ref(_page), PAGE_SIZE << (order)); \
_page; \
})
#define alloc_pages(gfp, order) pgtag_alloc_pages(gfp, order)
最终的结果是,对 alloc_pages()的每次调用都会改为同时也创建了一个静态的 alloc_tag 结构,记录了所有调用的位置;这个结构被放在 alloc_tags 部分中。当进行内存分配的调用时,该结构中的两个 counter 会相应地增加(相关实现在__alloc_tag_add()函数中,本文未展示)。此外,代码中还在所分配 page 的 page_ext 结构中记录了内存分配调用位置的 tag 的位置;这让内核得以跟踪哪个调用位置分配了每个 page。当分配的 page 今后被释放时,这些信息就可以用来减少该调用位置的 counter 计数。
所有这些工作的最终成果,就是一个数组,其中放置了所有 alloc_pages()调用位置,每个调用位置都记录了这里所分配的、尚未被释放的内存数量。该 framework 还包括用于遍历这个数组以及在 debugfs 文件系统中显示其内容的相关功能的支持。不难看出,这些信息对于试图追踪内存泄漏的开发者来说是非常有用的。这组 patch 的其他一些改动包括为 slab 分配器增加了类似的跟踪功能,并且能够存储每次分配的调用栈,从而提供更多关于内存泄漏的真正来源的信息。
这个框架可以用在另一个完全不同的场景,那就是动态故障注入(dynamic fault injection)。例如,驱动程序代码可以包括这样的代码:
if (dynamic_fault("foo-driver-init"))
return -EIO; /* Simulate a failure */
这个 dynamic_fault()函数,也同样会在调用的位置放置一个 code tag。它通常会返回 false,所以下面一行模拟出现失败的代码就不会被运行。不过在 /sys/kernel/debug/dynamic_faults 下会出现一个新的开关,可以用来启用这个 fault 点,从而测试驱动程序的错误处理是否可以正常工作。
这组 patch 中还有更多内容,包括 latency 跟踪机制,以及重新实现的 dynamic debug 的功能。目前看来,code-tagging framework 使得开发者可以更容易地采用对性能影响最小的方式来给内核添加这类功能。
围绕这个 patch set 的早期讨论大部分是由 Peter Zijlstra 的提问所引发的,他询问这个新的机制比起内核的 tracepoint 机制提供了什么新的功能。Overstreet 回答说,代码标记机制有很多的优点。其中包括从 boot 一开始就捕捉所有的活动,而不仅仅是在 tracing 功能启动之后才能开始追踪,并且性能更好,更容易使用,而且不会出现 event dropped 的问题。他说,这个问题应该反过来问:tracing 功能的支持者应该说明如何使用该子系统来提供类似的能力,并具有相当水平的性能和使用方便性。
作为回应,Zijlstra 指出,使用 ftrace 就没有必要 attach 到 tracepoint 上;将自定义处理程序(custom handler)attach 到 tracepoint 上,就可以解决对性能和 dropped event 的担忧。Mel Gorman 补充说,tracepoint 的方法更灵活,适用于旧内核,而且更广泛。他还指出了 Oscar Salvador 的 patch set,其中实现了另一种内存泄漏检测方法。Michal Hocko 担心 review 和维护如此规模的 patch set 比较有困难。
这是一个新的、大型的 patch set;它可能会被讨论一段时间。code-tagging 部分本身看起来应该是一个相对没有争议的代码清理工作;理论上,它可以用一个框架取代内核中的多种独立实现。不过,每一个额外的改动都可能需要额外的讨论;我们不能简单地冲到内存管理子系统里面修改掉核心分配器的代码而不需要回答人们的质疑。这组 patch 有可能最终被分割成多个部分,从而让每个部分都能独立地根据自己特有的优势来考虑是否合入。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~