seq_file 接口

版权所有 2003 Jonathan Corbet <corbet@lwn.net>

此文件最初来自 LWN.net 驱动移植系列,网址为 https://lwn.net/Articles/driver-porting/

设备驱动程序(或其他内核组件)有多种方法可以向用户或系统管理员提供信息。一种有用的技术是在 debugfs、/proc 或其他位置创建虚拟文件。虚拟文件可以提供易于访问且无需任何特殊实用程序程序的人工可读输出;它们还可以使脚本编写人员的生活更加轻松。多年来,虚拟文件的使用不断增长,这不足为奇。

然而,正确地创建这些文件一直是一个挑战。创建一个返回字符串的虚拟文件并不难。但是,如果输出很长(大于应用程序可能在单个操作中读取的内容),情况就会变得棘手。处理多个读取(和查找)需要仔细关注读取器在虚拟文件中的位置——该位置很可能位于输出行的中间。传统上,内核有许多实现错误地处理了这一点。

2.6 内核包含一组函数(由 Alexander Viro 实现),旨在使虚拟文件创建者可以轻松地正确处理这种情况。

seq_file 接口可通过 <linux/seq_file.h> 获得。seq_file 有三个方面

  • 一个迭代器接口,允许虚拟文件实现逐步遍历它正在呈现的对象。

  • 一些用于格式化对象的实用函数,无需担心输出缓冲区之类的问题。

  • 一组预定义的 file_operations,它们实现了虚拟文件上的大多数操作。

我们将通过一个极其简单的示例来了解 seq_file 接口:一个可加载的模块,它创建一个名为 /proc/sequence 的文件。读取该文件时,它只会生成一组递增的整数值,每行一个。该序列将一直持续到用户失去耐心并找到更好的事情为止。该文件是可查找的,也就是说,可以执行以下操作

dd if=/proc/sequence of=out1 count=1
dd if=/proc/sequence skip=1 of=out2 count=1

然后连接输出文件 out1 和 out2,并获得正确的结果。是的,这是一个完全无用的模块,但重点是展示该机制的工作原理,而不会迷失在其他细节中。(想要查看此模块完整源代码的人可以在 https://lwn.net/Articles/22359/ 上找到它)。

已弃用的 create_proc_entry

请注意,上面的文章使用了 create_proc_entry,它在内核 3.10 中被删除。当前版本需要以下更新

-   entry = create_proc_entry("sequence", 0, NULL);
-   if (entry)
-           entry->proc_fops = &ct_file_ops;
+   entry = proc_create("sequence", 0, NULL, &ct_file_ops);

迭代器接口

使用 seq_file 实现虚拟文件的模块必须实现一个迭代器对象,该对象允许在“会话”期间(大致是 read() 系统调用一次)逐步遍历感兴趣的数据。如果迭代器能够移动到特定位置(就像它们实现的文件一样,但可以自由地以任何方便的方式将位置编号映射到序列位置),则迭代器只需要在会话期间瞬时存在。如果迭代器不容易找到数字位置,但与 first/next 接口配合良好,则迭代器可以存储在私有数据区域中,并从一个会话继续到下一个会话。

例如,一个格式化防火墙规则表中的规则的 seq_file 实现可以提供一个简单的迭代器,该迭代器将位置 N 解释为链中的第 N 个规则。一个呈现潜在易失的链表内容的 seq_file 实现可以记录指向该列表的指针,前提是这样做不会有当前位置被删除的风险。

因此,可以以对数据生成器最有意义的任何方式进行定位,该生成器无需知道位置如何转换为虚拟文件中的偏移量。一个明显的例外是,位置零应表示文件的开头。

/proc/sequence 迭代器仅使用它将输出的下一个数字的计数作为其位置。

必须实现四个函数才能使迭代器工作。第一个名为 start(),它启动一个会话并接受一个位置作为参数,返回一个将从该位置开始读取的迭代器。传递给 start() 的 pos 将始终为零,或上一个会话中使用的最新 pos。

对于我们的简单序列示例,start() 函数如下所示

static void *ct_seq_start(struct seq_file *s, loff_t *pos)
{
        loff_t *spos = kmalloc(sizeof(loff_t), GFP_KERNEL);
        if (! spos)
                return NULL;
        *spos = *pos;
        return spos;
}

此迭代器的整个数据结构是一个保存当前位置的单个 loff_t 值。序列迭代器没有上限,但这对于大多数其他 seq_file 实现而言并非如此;在大多数情况下,start() 函数应检查“超出文件末尾”条件,并在需要时返回 NULL。

对于更复杂的应用程序,seq_file 结构的私有字段可用于保存会话之间的状态。还有一个特殊值,可以由 start() 函数返回,称为 SEQ_START_TOKEN;如果您希望指示您的 show() 函数(如下所述)在输出顶部打印标题,可以使用它。但是,只有当偏移量为零时,才应使用 SEQ_START_TOKEN。SEQ_START_TOKEN 对核心 seq_file 代码没有特殊含义。它被提供为 start() 函数与 next() 和 show() 函数通信的便利方法。

要实现的下一个函数称为 next(),这令人惊讶;它的工作是将迭代器向前移动到序列中的下一个位置。示例模块可以简单地将位置加一;更有用的模块将执行所需的操作以逐步遍历某些数据结构。next() 函数返回一个新的迭代器,如果序列完成,则返回 NULL。这是示例版本

static void *ct_seq_next(struct seq_file *s, void *v, loff_t *pos)
{
        loff_t *spos = v;
        *pos = ++*spos;
        return spos;
}

next() 函数应将 *pos 设置为 start() 可以用来查找序列中新位置的值。当迭代器存储在私有数据区域中,而不是在每次 start() 上重新初始化时,似乎只需将 *pos 设置为任何非零值就足够了(零总是告诉 start() 重新启动序列)。由于历史问题,这还不够。

从历史上看,许多 next() 函数在文件末尾没有更新 *pos。如果该值随后被 start() 用于初始化迭代器,则可能会导致在文件中最后一次条目被报告两次的极端情况。为了防止此错误被重新出现,如果 next() 函数不更改 *pos 的值,则核心 seq_file 代码现在会生成警告。因此,next() 函数必须更改 *pos 的值,并且当然必须将其设置为非零值。

stop() 函数关闭会话;它的工作当然是清理。如果为迭代器分配了动态内存,则 stop() 是释放它的地方;如果 start() 获取了锁,则 stop() 必须释放该锁。*pos 的值由 stop() 之前的最后一次 next() 调用设置,该值会被记住,并用于下一个会话的第一次 start() 调用,除非在文件上调用了 lseek();在这种情况下,将要求下一个 start() 从位置零开始

static void ct_seq_stop(struct seq_file *s, void *v)
{
        kfree(v);
}

最后,show() 函数应格式化当前迭代器指向的对象以进行输出。示例模块的 show() 函数为

static int ct_seq_show(struct seq_file *s, void *v)
{
        loff_t *spos = v;
        seq_printf(s, "%lld\n", (long long)*spos);
        return 0;
}

如果一切正常,show() 函数应返回零。以通常方式返回负错误代码表示出现问题;它将传递回用户空间。此函数还可以返回 SEQ_SKIP,这会导致跳过当前项;如果 show() 函数在返回 SEQ_SKIP 之前已生成输出,则将删除该输出。

我们稍后将讨论 seq_printf()。但首先,通过创建一个包含我们刚刚定义的四个函数的 seq_operations 结构,完成 seq_file 迭代器的定义

static const struct seq_operations ct_seq_ops = {
        .start = ct_seq_start,
        .next  = ct_seq_next,
        .stop  = ct_seq_stop,
        .show  = ct_seq_show
};

这个结构稍后将用于将我们的迭代器与 /proc 文件绑定。

值得注意的是,start() 返回并由其他函数操作的迭代器值,对于 seq_file 代码来说被认为是完全不透明的。因此,它可以是任何有助于遍历要输出的数据的东西。计数器可能很有用,但也可能是指向数组或链表的直接指针。只要程序员意识到在对迭代器函数的调用之间可能会发生事情,任何东西都可以使用。但是,seq_file 代码(根据设计)在调用 start()stop() 之间不会休眠,因此在此期间持有锁是合理的。seq_file 代码在迭代器处于活动状态时也会避免获取任何其他锁。

start() 或 next() 返回的迭代器值保证会传递给后续的 next() 或 stop() 调用。 这允许可靠地释放已获取的锁等资源。保证迭代器会传递给 show(),尽管在实践中它通常会传递给 show()。

格式化输出

seq_file 代码管理迭代器创建的输出中的定位,并将其放入用户的缓冲区中。但是,为了使其工作,必须将该输出传递给 seq_file 代码。已经定义了一些实用函数,可以轻松完成此任务。

大多数代码将简单地使用 seq_printf(),它的工作方式与 printk() 非常相似,但它需要 seq_file 指针作为参数。

对于直接字符输出,可以使用以下函数

seq_putc(struct seq_file *m, char c);
seq_puts(struct seq_file *m, const char *s);
seq_escape(struct seq_file *m, const char *s, const char *esc);

前两个函数输出单个字符和字符串,正如人们所期望的那样。seq_escape() 类似于 seq_puts(),只是 s 中任何属于字符串 esc 的字符都将以八进制形式在输出中表示。

还有一对用于打印文件名的函数

int seq_path(struct seq_file *m, const struct path *path,
             const char *esc);
int seq_path_root(struct seq_file *m, const struct path *path,
                  const struct path *root, const char *esc)

这里,path 指示感兴趣的文件,而 esc 是一组应该在输出中转义的字符。 调用 seq_path() 将输出相对于当前进程文件系统根目录的路径。 如果需要不同的根目录,则可以与 seq_path_root() 一起使用。 如果发现无法从根目录访问 path,则 seq_path_root() 返回 SEQ_SKIP。

生成复杂输出的函数可能需要检查

bool seq_has_overflowed(struct seq_file *m);

如果返回 true,则避免进一步的 seq_<output> 调用。

从 seq_has_overflowed 返回 true 表示 seq_file 缓冲区将被丢弃,并且 seq_show 函数将尝试分配更大的缓冲区并重试打印。

使其全部工作

到目前为止,我们有一组可以在 seq_file 系统中生成输出的很好的函数,但我们尚未将它们变成用户可以看到的文件。当然,要在内核中创建文件,需要创建一组实现对该文件进行操作的 file_operations。seq_file 接口提供了一组罐装操作,可以完成大部分工作。但是,虚拟文件作者仍然必须实现 open() 方法才能将所有内容连接起来。open 函数通常是一行代码,如示例模块中所示

static int ct_open(struct inode *inode, struct file *file)
{
        return seq_open(file, &ct_seq_ops);
}

在此,对 seq_open() 的调用会获取我们之前创建的 seq_operations 结构,并设置为遍历虚拟文件。

成功打开后,seq_open() 将 struct seq_file 指针存储在 file->private_data 中。如果您的应用程序可以使用同一个迭代器处理多个文件,则可以在 seq_file 结构的 private 字段中存储任意指针;然后,迭代器函数可以检索该值。

还有一个 seq_open() 的包装函数,称为 seq_open_private()。 它会 kmalloc 一个零填充的内存块,并将指向该内存块的指针存储在 seq_file 结构的 private 字段中,成功时返回 0。 块大小在函数的第三个参数中指定,例如

static int ct_open(struct inode *inode, struct file *file)
{
        return seq_open_private(file, &ct_seq_ops,
                                sizeof(struct mystruct));
}

还有一个变体函数 __seq_open_private(),它的功能相同,只是如果成功,它会返回指向已分配内存块的指针,允许进一步初始化,例如

static int ct_open(struct inode *inode, struct file *file)
{
        struct mystruct *p =
                __seq_open_private(file, &ct_seq_ops, sizeof(*p));

        if (!p)
                return -ENOMEM;

        p->foo = bar; /* initialize my stuff */
                ...
        p->baz = true;

        return 0;
}

可以使用相应的 close 函数 seq_release_private() 来释放在相应的 open 中分配的内存。

感兴趣的其他操作 - read()、llseek() 和 release() - 都由 seq_file 代码本身实现。因此,虚拟文件的 file_operations 结构如下所示

static const struct file_operations ct_file_ops = {
        .owner   = THIS_MODULE,
        .open    = ct_open,
        .read    = seq_read,
        .llseek  = seq_lseek,
        .release = seq_release
};

还有一个 seq_release_private(),它在释放结构之前将 seq_file private 字段的内容传递给 kfree()

最后一步是创建 /proc 文件本身。在示例代码中,这是在初始化代码中以通常的方式完成的

static int ct_init(void)
{
        struct proc_dir_entry *entry;

        proc_create("sequence", 0, NULL, &ct_file_ops);
        return 0;
}

module_init(ct_init);

差不多就是这样了。

seq_list

如果您的文件将遍历链表,您可能会发现这些例程很有用

struct list_head *seq_list_start(struct list_head *head,
                                 loff_t pos);
struct list_head *seq_list_start_head(struct list_head *head,
                                      loff_t pos);
struct list_head *seq_list_next(void *v, struct list_head *head,
                                loff_t *ppos);

这些助手会将 pos 解释为列表中的位置并相应地进行迭代。您的 start() 和 next() 函数只需要使用指向适当的 list_head 结构的指针调用 seq_list_* 助手即可。

超简单版本

对于极其简单的虚拟文件,还有一个更简单的接口。一个模块只能定义 show() 函数,该函数应创建虚拟文件将包含的所有输出。然后,文件的 open() 方法调用

int single_open(struct file *file,
                int (*show)(struct seq_file *m, void *p),
                void *data);

当输出时间到来时,show() 函数将被调用一次。可以在 seq_file 结构的 private 字段中找到传递给 single_open() 的数据值。使用 single_open() 时,程序员应在 file_operations 结构中使用 single_release() 而不是 seq_release(),以避免内存泄漏。