relay interface (formerly relayfs) 【ChatGPT】

发布时间 2023-12-11 19:19:53作者: 摩斯电码

Relay Interface (formerly relayfs)

介绍

Relay接口提供了一种方式,让内核应用能够通过用户定义的“中继通道”高效地将大量数据从内核传输到用户空间。

一个“中继通道”是一种内核到用户空间数据中继机制,它由一组每个CPU的内核缓冲区(“通道缓冲区”)实现,每个缓冲区在用户空间被表示为一个常规文件(“中继文件”)。内核客户端使用高效的写入函数将数据写入通道缓冲区;这些函数会自动记录到当前CPU的通道缓冲区中。用户空间应用程序可以通过mmap()或read()从中继文件中读取数据,并在数据变得可用时检索数据。中继文件本身是在主机文件系统(例如debugfs)中创建的文件,并且使用下面描述的API与通道缓冲区相关联。

被记录到通道缓冲区的数据的格式完全由内核客户端决定;但是,中继接口提供了钩子,允许内核客户端对缓冲区数据施加一些结构。中继接口不实现任何形式的数据过滤 - 这也留给内核客户端。其目的是尽可能地保持简单。

本文概述了中继接口的API。函数参数的详细信息已经在中继接口代码中记录 - 请参阅那里获取详细信息。

语义

每个中继通道每个CPU有一个缓冲区,每个缓冲区有一个或多个子缓冲区。消息被写入第一个子缓冲区,直到它已经装不下新消息为止,然后会被写入下一个(如果有的话)。消息永远不会被分割成多个子缓冲区。此时,用户空间可以被通知,以便它清空第一个子缓冲区,而内核继续向下一个子缓冲区写入。

当通知某个子缓冲区已满时,内核知道其中有多少字节是填充的,即由于完整消息无法适应子缓冲区而产生的未使用空间。用户空间可以利用这一知识仅复制有效数据。

复制完成后,用户空间可以通知内核某个子缓冲区已被消耗。

中继通道可以以一种模式运行,即它将覆盖尚未被用户空间收集的数据,并且不等待其被消耗。

中继通道本身不提供用户空间和内核之间的这些数据通信,从而使内核端保持简单,不对用户空间施加单一接口。但它提供了一组示例和一个单独的辅助程序,如下所述。

read()接口既删除填充,又内部消耗读子缓冲区;因此,在使用read(2)来排空通道缓冲区的情况下,基本操作并不需要内核和用户之间的特殊通信。

中继接口的主要目标之一是提供一种低开销的机制,将内核数据传递到用户空间。虽然read()接口易于使用,但它不如mmap()方法高效;示例代码试图使这两种方法之间的权衡尽可能小。

klog和relay-apps示例代码

中继接口本身已经准备就绪,但为了使事情更容易,提供了一些简单的实用函数和一组示例。

中继应用示例压缩包,可在relay sourceforge网站上找到,包含一组独立的示例,每个示例由一对.c文件组成,其中包含中继应用程序的用户和内核两侧的样板代码。将这两组样板代码组合在一起,就可以轻松地将数据流式传输到磁盘,而无需费心进行琐碎的日常工作。

“klog调试函数”补丁(中继应用示例压缩包中的klog.patch)提供了一些高级日志函数,允许内核将格式化文本或原始数据写入通道,而不管是否存在要写入的通道,甚至不管中继接口是否编译到内核中。这些函数允许您在内核或内核模块的任何地方放置无条件的“跟踪”语句;只有在注册了“klog处理程序”时,数据才会被记录(有关详细信息,请参阅klog和kleak示例)。

当然,也可以从头开始使用中继接口,即不使用任何中继应用示例代码或klog,但是您将不得不实现用户空间和内核之间的通信,使两者都能传达缓冲区的状态(满、空、填充量)。read()接口既删除填充,又内部消耗读子缓冲区;因此,在使用read(2)来排空通道缓冲区的情况下,基本操作并不需要内核和用户之间的特殊通信。但是,诸如缓冲区满条件之类的事情仍然需要通过某个通道进行通信。

klog和中继应用示例可以在http://relayfs.sourceforge.net的中继应用示例压缩包中找到。

中继接口用户空间API

中继接口实现了用户空间访问中继通道缓冲区数据的基本文件操作。以下是可用的文件操作以及关于它们行为的一些注释:

  • open():允许用户打开一个_已存在的_通道缓冲区。
  • mmap():导致通道缓冲区被映射到调用者的内存空间。请注意,您不能进行部分mmap - 您必须映射整个文件,即NRBUF * SUBBUFSIZE。
  • read():读取通道缓冲区的内容。读取的字节被读取者“消耗”,即它们不会再次对后续读取可用。如果通道正在使用不覆盖模式(默认情况下),即使有活动的内核写入者,也可以随时进行读取。如果通道正在使用覆盖模式,并且有活动的通道写入者,则结果可能是不可预测的 - 用户应确保在使用覆盖模式的read()之前,所有对通道的记录都已结束。子缓冲区的填充会被自动删除,并且读取者将看不到它。
  • sendfile():将数据从通道缓冲区传输到输出文件描述符。子缓冲区的填充会被自动删除,并且读取者将看不到它。
  • poll():支持POLLIN/POLLRDNORM/POLLERR。当子缓冲区边界被跨越时,用户应用程序会收到通知。
  • close():减少通道缓冲区的引用计数。当引用计数达到0时,即没有进程或内核客户端打开缓冲区时,通道缓冲区将被释放。

为了让用户应用程序使用中继文件,主机文件系统必须被挂载。例如:

mount -t debugfs debugfs /sys/kernel/debug

注意:主机文件系统不需要被挂载,以便内核客户端创建或使用通道 - 只有当用户空间应用程序需要访问缓冲区数据时,才需要挂载。

中继接口内核API

以下是中继接口提供给内核客户端的API的摘要:

  • 通道管理函数:
    • relay_open(base_filename, parent, subbuf_size, n_subbufs, callbacks, private_data)
    • relay_close(chan)
    • relay_flush(chan)
    • relay_reset(chan)
  • 通常在用户空间发起时调用的通道管理函数:
    • relay_subbufs_consumed(chan, cpu, subbufs_consumed)
  • 写入函数:
    • relay_write(chan, data, length)
    • __relay_write(chan, data, length)
    • relay_reserve(chan, length)
  • 回调函数:
    • subbuf_start(buf, subbuf, prev_subbuf, prev_padding)
    • buf_mapped(buf, filp)
    • buf_unmapped(buf, filp)
    • create_buf_file(filename, parent, mode, buf, is_global)
    • remove_buf_file(dentry)
  • 辅助函数:
    • relay_buf_full(buf)
    • subbuf_start_reserve(buf, length)

创建通道

relay_open() 用于创建一个通道,以及它的每个 CPU 通道缓冲区。每个通道缓冲区都将在主机文件系统中创建一个关联的文件,可以在用户空间中进行内存映射或读取。这些文件的命名方式为 basename0...basenameN-1,其中 N 是在线 CPU 的数量,并且默认情况下将在文件系统的根目录中创建(如果 parent 参数为 NULL)。如果您希望创建一个目录结构来包含中继文件,您应该使用主机文件系统的目录创建函数(例如 debugfs_create_dir())来创建它,并将父目录传递给 relay_open()。用户负责在关闭通道时清理他们创建的任何目录结构 - 同样应该使用主机文件系统的目录删除函数进行清理,例如 debugfs_remove()。

为了创建通道并将主机文件系统的文件与其通道缓冲区关联,用户必须为两个回调函数提供定义,即 create_buf_file()remove_buf_file()create_buf_file() 在从 relay_open() 中为每个 CPU 缓冲区调用一次,允许用户创建用于表示相应通道缓冲区的文件。回调应返回创建的文件的 dentry 以表示通道缓冲区。remove_buf_file() 也必须被定义;它负责删除在 create_buf_file() 中创建的文件,并在 relay_close() 期间被调用。

以下是这些回调的一些典型定义,本例中使用了 debugfs:

/*
* create_buf_file() 回调。在 debugfs 中创建中继文件。
*/
static struct dentry *create_buf_file_handler(const char *filename,
                                            struct dentry *parent,
                                            umode_t mode,
                                            struct rchan_buf *buf,
                                            int *is_global)
{
        return debugfs_create_file(filename, mode, parent, buf,
                                &relay_file_operations);
}

/*
* remove_buf_file() 回调。从 debugfs 中删除中继文件。
*/
static int remove_buf_file_handler(struct dentry *dentry)
{
        debugfs_remove(dentry);

        return 0;
}

/*
* 中继接口回调
*/
static struct rchan_callbacks relay_callbacks =
{
        .create_buf_file = create_buf_file_handler,
        .remove_buf_file = remove_buf_file_handler,
};

以及使用它们的 relay_open() 调用示例:

chan = relay_open("cpu", NULL, SUBBUF_SIZE, N_SUBBUFS, &relay_callbacks, NULL);

如果 create_buf_file() 回调失败或未定义,通道创建以及 relay_open() 将失败。

每个 CPU 缓冲区的总大小通过将传递给 relay_open() 的子缓冲区大小乘以子缓冲区的数量来计算。子缓冲区的概念基本上是将双缓冲扩展到 N 个缓冲区,并且它们还允许应用程序轻松实现基于缓冲区边界的随机访问方案,这对于一些高容量应用程序可能很重要。子缓冲区的数量和大小完全取决于应用程序,即使对于同一个应用程序,不同的条件也会在不同时间需要这些参数的不同值。通常情况下,最佳值是在一些实验之后决定的;总的来说,可以安全地假设只有 1 个子缓冲区是一个坏主意 - 您肯定会要么覆盖数据,要么丢失事件,这取决于所使用的通道模式。

create_buf_file() 实现也可以被定义为以允许创建单个“全局”缓冲区而不是默认的每 CPU 集合。这对于主要关注查看系统范围事件的应用程序可能很有用,而无需为了合并/排序后处理步骤而麻烦地保存显式时间戳。

要让 relay_open() 创建一个全局缓冲区,create_buf_file() 实现应该将 is_global 输出参数的值设置为非零值,除了创建用于表示单个缓冲区的文件之外。对于全局缓冲区,create_buf_file()remove_buf_file() 将只被调用一次。仍然可以使用正常的通道写入函数,例如 relay_write() - 来自任何 CPU 的写入将透明地最终进入全局缓冲区 - 但由于它是全局缓冲区,调用者应确保他们对这样的缓冲区使用适当的锁定,要么通过在写入中使用自旋锁,要么通过从 relay.h 复制写函数并创建一个内部执行适当锁定的本地版本。

传递给 relay_open() 的 private_data 允许客户端将用户定义的数据与通道关联,并且可以立即通过 chan->private_data 或 buf->chan->private_data 进行访问,包括在 create_buf_file() 中。

仅缓冲区通道

这些通道没有关联的文件,可以使用 relay_open(NULL, NULL, ...) 来创建。这种通道在内核进行早期跟踪时很有用,在 VFS 准备就绪之前。在这些情况下,可以打开一个仅缓冲区通道,然后在内核准备处理文件时调用 relay_late_setup_files(),以将缓冲的数据暴露给用户空间。

通道模式

relay 通道可以在“覆盖”或“不覆盖”两种模式下使用。模式完全由 subbuf_start() 回调的实现确定,如下所述。如果未定义 subbuf_start() 回调,则默认模式为“不覆盖”。如果默认模式适合您的需求,并且您计划使用 read() 接口来检索通道数据,您可以忽略本节的细节,因为这主要与 mmap() 实现有关。

在“覆盖”模式中,也称为“飞行记录器”模式,写入将持续循环在缓冲区中,并且永远不会失败,但将无条件地覆盖旧数据,而不管它是否实际被消耗。在“不覆盖”模式中,如果未消耗的子缓冲区数量等于通道中的总子缓冲区数量,则写入将失败,即数据将丢失。应清楚地指出,如果没有消费者或者如果消费者无法快速消耗子缓冲区,数据将在任何情况下都会丢失;唯一的区别是数据是从缓冲区的开始还是结束丢失。

如上所述,relay 通道由当前每个 CPU 的通道缓冲区组成,每个通道缓冲区都实现为一个分成一个或多个子缓冲区的循环缓冲区。消息通过下面描述的写函数写入到通道的当前每个 CPU 缓冲区的当前子缓冲区中。每当消息无法适应当前子缓冲区时,因为没有足够的空间,客户端将通过 subbuf_start() 回调被通知即将发生对新子缓冲区的切换。客户端使用此回调来:1)初始化下一个子缓冲区(如果适用);2)完成前一个子缓冲区(如果适用);3)返回一个布尔值,指示是否实际上转移到下一个子缓冲区。

要实现“不覆盖”模式,用户空间客户端将提供类似以下内容的 subbuf_start() 回调的实现:

static int subbuf_start(struct rchan_buf *buf,
                        void *subbuf,
                        void *prev_subbuf,
                        unsigned int prev_padding)
{
        if (prev_subbuf)
                *((unsigned *)prev_subbuf) = prev_padding;

        if (relay_buf_full(buf))
                return 0;

        subbuf_start_reserve(buf, sizeof(unsigned int));

        return 1;
}

如果当前缓冲区已满,即所有子缓冲区仍未被消耗,回调将返回 0,表示不应立即进行缓冲区切换,即直到消费者有机会读取当前一组准备好的子缓冲区。对于 relay_buf_full() 函数来说,消费者负责通过 relay_subbufs_consumed() 通知 relay 接口何时子缓冲区已被消耗。任何后续尝试写入缓冲区将再次使用相同的参数调用 subbuf_start() 回调;只有当消费者已经消耗了一个或多个准备好的子缓冲区时,relay_buf_full() 才会返回 0,此时缓冲区切换可以继续。

对于“覆盖”模式的 subbuf_start() 回调的实现将非常类似:

static int subbuf_start(struct rchan_buf *buf,
                        void *subbuf,
                        void *prev_subbuf,
                        size_t prev_padding)
{
        if (prev_subbuf)
                *((unsigned *)prev_subbuf) = prev_padding;

        subbuf_start_reserve(buf, sizeof(unsigned int));

        return 1;
}

在这种情况下,relay_buf_full() 检查是没有意义的,回调总是返回 1,导致缓冲区切换无条件发生。在这种模式下,客户端使用 relay_subbufs_consumed() 函数也是没有意义的,因为它从不被使用。

如果客户端未定义任何回调,或者未定义 subbuf_start() 回调,则将使用默认的 subbuf_start() 实现,即实现了最简单的“不覆盖”模式,即什么也不做,只返回 0。

可以通过在 subbuf_start() 回调中调用 subbuf_start_reserve() 辅助函数来在每个子缓冲区的开头保留头部信息。此保留区域可以用于存储客户端想要的任何信息。在上面的示例中,为每个子缓冲区保留了空间,用于存储该子缓冲区的填充计数。这将在 subbuf_start() 实现中填充前一个子缓冲区的填充值;在打开通道时,将调用 subbuf_start() 回调以让客户端有机会在其中保留空间。在这种情况下,传递给回调的前一个子缓冲区指针将为 NULL,因此客户端在写入前一个子缓冲区之前应检查 prev_subbuf 指针的值。

写入通道

内核客户端使用 relay_write()__relay_write() 将数据写入当前 CPU 的通道缓冲区。relay_write() 是主要的日志记录函数 - 它使用 local_irqsave() 来保护缓冲区,如果可能从中断上下文中进行日志记录,则应使用它。如果您知道您永远不会从中断上下文中进行日志记录,可以使用 __relay_write(),它只会禁用抢占。这些函数不返回值,因此您无法确定它们是否失败 - 假设是因为您不希望在快速日志记录路径中检查返回值,并且它们将始终成功,除非缓冲区已满并且正在使用“不覆盖”模式,在这种情况下,您可以通过在 subbuf_start() 回调中调用 relay_buf_full() 辅助函数来检测到失败的写入。

relay_reserve() 用于保留通道缓冲区中稍后可以写入的插槽。这通常用于需要直接写入通道缓冲区而无需预先在临时缓冲区中存储数据的应用程序。因为实际的写入可能不会立即发生,所以使用 relay_reserve() 的应用程序可以保持实际写入的字节数的计数,可以在子缓冲区本身中保留,也可以作为一个单独的数组。在 relay-apps tarball 的“reserve”示例中可以看到如何实现这一点。因为写入由客户端控制,并且与保留分开,所以 relay_reserve() 根本不保护缓冲区 - 它取决于客户端在使用 relay_reserve() 时提供适当的同步。

关闭通道

当客户端完成使用通道时,应调用 relay_close()。当不再有任何对通道缓冲区的引用时,通道及其关联的缓冲区将被销毁。relay_flush() 强制在所有通道缓冲区上进行子缓冲区切换,并且可以在关闭通道之前用于完成和处理最后的子缓冲区。

其他

一些应用程序可能希望保留通道并重复使用它,而不是为每次使用打开和关闭一个新的通道。relay_reset() 可用于此目的 - 它将通道重置为其初始状态,而不重新分配通道缓冲区内存或销毁现有映射。但是,只有在安全时才应调用它,即当通道当前未被写入时。

最后,还有一些实用的回调可用于不同的目的。当通道缓冲区从用户空间进行内存映射时,将调用 buf_mapped(),并且在取消映射时将调用 buf_unmapped()。客户端可以使用此通知来触发内核应用程序中的操作,例如启用/禁用对通道的日志记录。

资源

有关新闻、示例代码、邮件列表等,请参阅 relay 接口主页:

http://relayfs.sourceforge.net

鸣谢

中继接口的想法和规范是作为涉及以下内容的跟踪讨论的结果产生的:

Michel Dagenais michel.dagenais@polymtl.ca Richard Moore richardj_moore@uk.ibm.com Bob Wisniewski bob@watson.ibm.com Karim Yaghmour karim@opersys.com Tom Zanussi zanussi@us.ibm.com

还要感谢 Hubertus Franke 提供了许多有用的建议和错误报告。