seq_file 接口

Copyright 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 有三个方面:

  • 一个迭代器接口,允许虚拟文件实现逐步执行其呈现的对象。

  • 一些实用程序函数,用于格式化对象以进行输出,而无需担心输出缓冲区之类的事情。

  • 一组 canned 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(),令人惊讶的是;它的工作是将迭代器前进到序列中的下一个位置。示例模块可以简单地将位置递增 1;更有用的模块将执行所需的操作以逐步执行某些数据结构。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() 必须释放该锁。在 stop() 之前,上次 next() 调用设置为 *pos 的值将被记住,并用于下一个会话的第一次 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,这将导致跳过当前项;如果在返回 SEQ_SKIP 之前,show() 函数已生成输出,则该输出将被删除。

我们稍后会看到 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(),尽管在实践中它经常会传递。

格式化输出

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 接口提供了一组 canned 操作,这些操作完成了大部分工作。但是,虚拟文件作者仍然必须实现 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 结构的私有字段中;该值随后可以由迭代器函数检索。

还有一个包装函数 seq_open(),称为 seq_open_private()。它 kmallocs 一个零填充的内存块,并将指向该内存块的指针存储在 seq_file 结构的私有字段中,成功时返回 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 私有字段的内容传递给 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() 函数一次。提供给 single_open() 的数据值可以在 seq_file 结构的私有字段中找到。使用 single_open() 时,程序员应使用 single_release() 代替 file_operations 结构中的 seq_release(),以避免内存泄漏。