seq_file.txt 翻译

发布时间 2023-12-29 18:03:43作者: Hello-World3

1. seq_file 接口

版权所有 2003 Jonathan Corbet <corbet@lwn.net> 该文件最初来自 LWN.net 驱动程序移植系列,网址为:http://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并得到正确的结果。 是的,这是一个完全无用的模块,但重点是展示该机制如何工作,而不会迷失在其他细节中。 (那些想要查看该模块完整源代码的人可以在 http://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);


2. 迭代器接口

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

例如,从表中格式化防火墙规则的 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 结构的 private 字段可用于保存会话之间的状态。 还有一个特殊值可以由 start() 函数返回,称为 SEQ_START_TOKEN; 如果您希望指示 show() 函数(如下所述)在输出顶部打印标题,则可以使用它。但是,仅当偏移量为零时才应使用 SEQ_START_TOKEN。

令人惊讶的是,下一个要实现的函数被称为 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;
}

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,这会导致当前项被跳过; 如果 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 代码还将避免在迭代器处于active状态时获取任何其他锁。

start() 或 next() 返回的迭代器值保证传递给后续的 next() 或 stop() 调用。 这使得诸如锁之类的资源能够被可靠地释放。*不*保证迭代器将被传递给 show(),尽管在实践中经常会这样。


3. 格式化输出

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() 一起使用。如果结果表明无法从根到达路径,则 seq_path_root() 返回 SEQ_SKIP。

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

bool seq_has_overflowed(struct seq_file *m);

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

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


4. 让一切顺利进行

到目前为止,我们有一组很好的函数可以在 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()。 它 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;
}

相应的关闭函数 seq_release_private() 可用,它释放相应打开中分配的内存。

其他感兴趣的操作 - 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);

差不多就是这样了。


5. 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_* 帮助器。


6. 超简单版本

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

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

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