[toc]

fs/seq_file.c 序列文件接口(Sequential File Interface) 构建大型虚拟文件的标准工具

历史与背景

这项技术是为了解决什么特定问题而诞生的?

这项技术以及它所实现的seq_file(Sequential File)接口,是为了从根本上解决Linux内核早期在创建虚拟文件(尤其是在/proc文件系统中)时遇到的几个严重问题:

  • 4KB(PAGE_SIZE)的输出限制:在seq_file出现之前,实现一个/proc文件的标准方法是read_proc回调。这个回调函数被要求一次性地将所有输出内容生成到一个大小固定的缓冲区中(通常为一个页,即4KB)。如果输出内容超过了这个大小,就会被无情地截断。这对于显示大量信息(如系统中的所有mount点、中断、或者大量的内核调试信息)的场景来说是一个致命的限制。
  • 复杂的实现和状态管理read_proc回调需要开发者手动管理文件位置(offset),以支持用户空间程序多次read()一个文件。这个过程非常繁琐且极易出错。
  • 并发不安全:在多核(SMP)系统上,read_proc的实现很难保证原子性。在生成输出的过程中,如果被读取的内核数据结构发生了变化,就可能导致输出不一致甚至系统崩溃。开发者需要自己实现复杂的锁机制来避免这种情况。

seq_file的诞生,就是为了提供一个简单的、高效的、无大小限制的、并且是SMP安全的框架,来取代老旧且问题重重的read_proc接口。

它的发展经历了哪些重要的里程碑或版本迭代?

seq_file的发展主要体现在其被内核社区广泛接受和采纳,成为标准。

  • 引入seq_file被引入内核,作为一个专门解决上述问题的优雅方案。它创造性地使用了**迭代器(Iterator)**的设计模式。
  • 标准化:它很快就成为了在procfs, debugfs, tracefs等所有伪文件系统中创建可读文件的事实标准和推荐方法。
  • 广泛重构:内核中大量原先使用read_proc/proc文件被逐步重构为使用seq_file。例如,/proc/mounts, /proc/partitions, /proc/interrupts等关键文件都迁移到了seq_file实现上。这一过程本身就是其成功的最重要证明。

目前该技术的社区活跃度和主流应用情况如何?

seq_file是内核中一个极其成熟、稳定且被普遍使用的核心API。

  • 社区活跃度:其核心API和实现非常稳定,几乎没有变化。它被认为是“已完成”的、解决了一类特定问题的标准工具。
  • 主流应用:它是内核开发者向用户空间导出格式化文本信息的首选工具。
    • Procfs:几乎所有/proc下内容可变且可能较长的文件,如meminfo, stat, vmstat, mounts等,都是通过seq_file实现的。
    • Debugfs / Tracefs:驱动程序和子系统开发者广泛使用seq_filedebugfs下创建文件,以导出内部状态、统计信息或调试日志。
    • Configfs等其他伪文件系统。

核心原理与设计

它的核心工作原理是什么?

seq_file的核心是一个**迭代器(Iterator)**模型。开发者不再需要一次性生成所有输出,而是提供一组简单的回调函数,让seq_file框架来“拉动”数据。
这组回调函数被定义在struct seq_operations中,主要包含四个部分:

  1. start():当一个read()操作开始时被调用。它的任务是找到序列中的第一个元素,并返回一个指向它的“迭代器”(通常是一个指针,void *v)。如果序列为空,返回NULL。它还会负责获取必要的锁,以确保在整个读取过程中数据的一致性。
  2. next():给定当前元素的迭代器,找到并返回下一个元素的迭代器。如果已经是最后一个元素,它就返回NULL
  3. show():这是核心的格式化函数。给定当前元素的迭代器,它负责将这个元素的信息格式化成文本,并输出到seq_file的内部缓冲区中(通过seq_printf, seq_putc等函数)。
  4. stop():当一个read()操作完成(或整个序列被读取完毕)时被调用。它的任务是进行清理,最重要的是释放start()中获取的锁。

工作流程
当用户空间的程序read()一个seq_file实现的文件时,内核会:

  1. 调用start()获取第一个迭代器。
  2. 如果start()成功,就调用show()来格式化第一个元素。
  3. 然后进入一个循环:调用next()获取下一个迭代器,如果成功,就调用show()格式化这个元素。
  4. 这个循环会一直进行,直到seq_file的内部缓冲区被填满,或者next()返回NULL(表示序列结束)。
  5. 内核将缓冲区中的数据拷贝到用户空间。
  6. 如果用户程序再次read()seq_file框架会智能地从上次结束的地方继续,再次调用next()show(),直到整个序列被遍历完毕。
  7. 最后,调用stop()进行清理。

它的主要优势体现在哪些方面?

  • 无大小限制:由于是迭代式生成,理论上可以产生无限大的输出,完美解决了4KB的限制。
  • 实现简单:开发者只需关注于如何遍历自己的数据结构(实现start/next)和如何格式化单个元素(实现show),而无需关心缓冲、分页和文件位置管理等复杂问题。
  • SMP安全seq_file框架鼓励一种安全的编程模式:在start()中加锁,在stop()中解锁。这确保了在一次完整的read()系统调用期间,用户看到的是一份一致的数据快照。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 只适用于文本seq_file是为生成格式化文本而设计的。它不适合用来导出原始的二进制数据。
  • 只读模型:其设计压倒性地偏向于只读文件。虽然可以实现可写的seq_file,但这非常罕见且复杂。
  • 长时锁的风险:如果遍历一个非常非常大的数据集,并且在start()/stop()中持有一个重要的锁,可能会导致该锁被长时间占用,影响系统其他部分的性能。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。

当一个内核模块或驱动需要向用户空间导出一个只读的、基于文本的、内容长度可变(可能很长)的虚拟文件时,seq_file绝对的首选和标准解决方案。

  • 列出内核数据结构:一个网络驱动想要在debugfs中显示所有活动的网络连接。它可以实现一套seq_operations,其中迭代器就是指向其内部连接列表的一个指针。show()函数负责将每个连接的源/目的IP、端口等信息格式化输出。
  • cat /proc/meminfo:这个命令的背后就是一套seq_operations。它的实现中没有真正的“迭代”,start()返回一个非NULL的哨兵值,next()在第一次调用后就返回NULL,而show()函数则一口气调用多个seq_printf来打印出所有内存信息。
  • cat /proc/partitions:这个命令的seq_operations会遍历内核中所有已知的块设备和分区,迭代器就是分区的索引号。show()函数负责打印出每个分区的设备号、大小和名称。

是否有不推荐使用该技术的场景?为什么?

  • 导出单个值:如果只需要导出一个简单的整数或字符串,使用seq_file就有点小题大做了。procfsdebugfs都提供了更简单的API,如proc_create_singledebugfs_create_u32/u64等,它们更易于使用。
  • 导出二进制数据:如果要导出一个固件的二进制dump或一批原始的寄存器值,应该使用debugfs_create_blob,它专门用于处理二进制数据。
  • 需要用户写入配置:对于需要接收用户输入的配置文件,应该使用proc_create并提供一个自定义的.write.proc_write回调函数。

对比分析

请将其 与 其他相似技术 进行详细对比。

对比 seq_file vs. read_proc (旧的procfs方法)

特性 seq_file read_proc (已废弃)
输出大小限制 无限制 严格限制于一个页的大小(通常是4KB)。
实现复杂度 。开发者只需实现简单的迭代器和格式化函数。 。开发者需要手动管理缓冲区、偏移量和分页。
并发安全性 (SMP) 。框架本身鼓励安全的加锁/解锁模式。 。需要开发者手动实现复杂的锁,极易出错。
状态管理 由框架自动处理 由开发者手动处理
适用场景 现代内核中所有可变长度的文本虚拟文件的标准实现。 仅存在于非常古老的、尚未被重构的内核代码中。新代码中严禁使用

fs/seq_file.c

Seq_file Read Iterator:驱动虚拟文件内容生成的状态机

本代码片段是seq_file框架的“引擎室”——seq_read_iter函数。这是seq_file所有read操作的最终执行点。其核心功能是实现一个复杂的状态机,该状态机负责驱动seq_operations中定义的回调函数(.start, .next, .show),将动态生成的内容填充到内部缓冲区,然后安全地将这些内容复制到用户空间,同时精确地管理迭代器位置、文件偏移和缓冲区状态。它完美地封装了按需生成、缓冲区管理、溢出处理和lseek同步等所有复杂性。

实现原理分析

seq_read_iter的实现原理是一个精心设计的状态驱动循环,它在一次read调用中可能会经历多个阶段。

  1. 锁定与状态同步:

    • 函数首先获取m->lock互斥锁,确保在整个读操作期间,seq_file的内部状态是独占的。
    • 它会检查用户请求的读取位置iocb->ki_pos是否与seq_file内部缓存的位置m->read_pos一致。如果不一致(意味着之前有lseek操作),它会调用traverse函数来“快进”迭代器到正确的位置。这是lseekread能够协同工作的关键。
  2. 处理剩余数据:

    • 读操作首先检查内部缓冲区m->buf中是否还有上一次read操作未完全复制走的数据(由m->count > 0判断)。如果有,它会优先将这些剩余数据复制到用户空间。
  3. 核心生成循环 (第一部分):

    • 如果缓冲区已空,它会进入第一个内容生成循环。其目标是至少生成一个非空的记录到缓冲区中。
    • 它调用.start.next来获取数据项。
    • 调用.show来生成文本。
    • 溢出处理: 如果.show的输出超出了缓冲区大小,它会释放旧缓冲区,分配一个双倍大小的新缓冲区,然后从头开始重新调用.start来填充这个更大的缓冲区。这个机制保证了即使单个记录也非常大,seq_file也能处理。
  4. 填充循环 (第二部分):

    • 在缓冲区中至少有一个记录后,它会进入第二个循环,尝试继续填充缓冲区,直到缓冲区满或者用户请求的字节数(iov_iter_count(iter))已经满足。
    • 它不断调用.next.show,将新生成的内容附加到缓冲区末尾。
    • 这个循环会在缓冲区空间不足、遇到迭代结束或发生错误时停止。
  5. 数据拷贝与状态更新:

    • 在内容生成完成后,它调用copy_to_iter将缓冲区中的数据安全地复制到用户空间的I/O向量中。
    • 根据实际复制的字节数,它会更新m->count(缓冲区中剩余的字节数)和m->from(下一次复制的起始点),以及iocb->ki_posm->read_pos(文件位置)。
  6. 解锁与返回:

    • 最后,函数释放互斥锁,并返回实际复制到用户空间的字节数。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
// seq_read_iter: seq_file的 ->read_iter() 方法,是读操作的核心实现。
ssize_t seq_read_iter(struct kiocb *iocb, struct iov_iter *iter)
{
struct seq_file *m = iocb->ki_filp->private_data;
size_t copied = 0;
size_t n;
void *p;
int err = 0;

if (!iov_iter_count(iter)) // 如果请求读取0字节,直接返回。
return 0;

mutex_lock(&m->lock); // 锁定seq_file状态机。

// 优化:如果从文件头开始读,重置迭代器状态。
if (iocb->ki_pos == 0) {
m->index = 0;
m->count = 0;
}

// 同步状态:如果文件位置与内部缓存位置不符(通常由lseek导致)。
if (unlikely(iocb->ki_pos != m->read_pos)) {
// 调用traverse来快进到正确的位置。
while ((err = traverse(m, iocb->ki_pos)) == -EAGAIN)
;
if (err) { // 如果traverse失败,重置所有状态。
m->read_pos = 0;
m->index = 0;
m->count = 0;
goto Done;
} else {
m->read_pos = iocb->ki_pos;
}
}

// 惰性分配缓冲区。
if (!m->buf) {
m->buf = seq_buf_alloc(m->size = PAGE_SIZE);
if (!m->buf)
goto Enomem;
}
// 阶段1: 处理缓冲区中上次遗留的数据。
if (m->count) {
n = copy_to_iter(m->buf + m->from, m->count, iter);
m->count -= n;
m->from += n;
copied += n;
if (m->count) // 如果用户缓冲区满了,没能拷完,则直接返回。
goto Done;
}
// 阶段2: 填充缓冲区,至少获取一个非空记录。
m->from = 0;
p = m->op->start(m, &m->index); // 调用start回调
while (1) {
err = PTR_ERR(p);
if (!p || IS_ERR(p)) // 到达文件末尾或出错
break;
err = m->op->show(m, p); // 调用show生成内容
if (err < 0) // 硬错误
break;
if (unlikely(err)) // show返回SEQ_SKIP
m->count = 0;
if (unlikely(!m->count)) { // show生成了空记录
p = m->op->next(m, p, &m->index); // 继续下一条
continue;
}
if (!seq_has_overflowed(m)) // 成功获取记录且未溢出
goto Fill;
// 缓冲区溢出处理:释放旧buf,分配双倍大小的新buf,然后从头开始。
m->op->stop(m, p);
kvfree(m->buf);
m->count = 0;
m->buf = seq_buf_alloc(m->size <<= 1);
if (!m->buf)
goto Enomem;
p = m->op->start(m, &m->index); // 必须从头开始重放
}
// 到达文件末尾或出错,停止迭代。
m->op->stop(m, p);
m->count = 0;
goto Done;
Fill:
// 阶段3: 继续填充缓冲区,直到满足用户请求的大小或缓冲区满。
while (1) {
size_t offs = m->count;
loff_t pos = m->index;

p = m->op->next(m, p, &m->index); // 移动到下一记录
if (pos == m->index) { // 检查.next函数是否正确更新了index
pr_info_ratelimited("buggy .next function %ps did not update position index\n",
m->op->next);
m->index++;
}
if (!p || IS_ERR(p)) // 到达末尾或出错
break;
if (m->count >= iov_iter_count(iter)) // 如果已生成的数据足够用户本次读取
break;
err = m->op->show(m, p); // 继续生成
if (err > 0) { // show返回SEQ_SKIP
m->count = offs; // 恢复count,丢弃本次生成
} else if (err || seq_has_overflowed(m)) { // 出错或溢出
m->count = offs; // 恢复count,丢弃本次生成
break;
}
}
m->op->stop(m, p);
// 阶段4: 将最终缓冲区的内容拷贝到用户空间。
n = copy_to_iter(m->buf, m->count, iter);
copied += n;
m->count -= n; // 更新剩余字节数
m->from = n; // 更新下次拷贝的起始点
Done:
if (unlikely(!copied)) { // 如果一个字节都没拷出去
copied = m->count ? -EFAULT : err; // 要么是地址错误,要么是之前的错误码
} else {
iocb->ki_pos += copied; // 更新文件位置
m->read_pos += copied;
}
mutex_unlock(&m->lock); // 解锁
return copied;
Enomem:
err = -ENOMEM;
goto Done;
}
EXPORT_SYMBOL(seq_read_iter);

Seq_file Traversal and Buffer Management:实现可定位的虚拟文件

本代码片段是Linux seq_file框架的内部核心逻辑,特别是实现了lseek操作所需的traverse函数。其核心功能是:1) 高效地“快进”一个seq_file的迭代器到用户指定的字节偏移量;2) 智能地管理seq_file的内部缓冲区,包括按需分配和在内容超出缓冲区大小时自动进行扩容。这是seq_file能够支持文件定位(seeking)的关键所在,也是其能够健壮地处理任意大小输出内容的基础。

实现原理分析

traverse函数的实现原理是一种“模拟执行”或“重放”(replay)。由于seq_file的内容是动态生成的,无法直接跳转到某个字节偏移。因此,traverse必须从头开始,依次调用迭代器操作(.start, .next, .show),在内部生成(但大部分时间是丢弃)文件内容,并累加生成内容的字节数,直到找到包含目标偏移量的那个迭代点。

  1. 状态重置: traverse总是从一个干净的状态开始,它将迭代器索引(index)和缓冲区计数器(count)清零。
  2. 惰性缓冲区分配: 只有在真正需要(即offset > 0)时,它才会检查并分配第一个缓冲区(大小为PAGE_SIZE)。
  3. 迭代与累加: 函数进入一个循环,该循环:
    • 调用.start.next获取下一个数据项。
    • 调用.show将该数据项的文本表示“打印”到seq_file的内部缓冲区。.show函数会更新m->count来反映新生成的数据量。
    • 核心定位逻辑:
      a. 它检查pos + m->count(已生成的总字节数 + 当前项生成的字节数)是否已经超过了目标offset
      b. 如果是,说明目标位置就在当前缓冲区内。它计算出目标位置在当前缓冲区中的起始偏移量(m->from = offset - pos),并更新缓冲区中的有效数据量(m->count -= m->from),然后跳出循环。此时,seq_file的状态机就准备好了,下一次seq_read将从缓冲区的m->from位置开始返回数据。
      c. 如果未超过,它将当前项生成的字节数累加到pos,并重置m->count为0(因为我们不需要保留缓冲区中的数据,只需要它的长度),然后继续下一次迭代。
  4. 缓冲区溢出与自动扩容:
    • 在每次调用.show后,它都会检查seq_has_overflowed(m)。当一个数据项的.show输出超过了当前缓冲区的大小时,就会发生溢出。
    • 此时,函数会跳转到Eoverflow标签。在这里,它释放旧的、过小的缓冲区,然后将期望的缓冲区大小加倍m->size <<= 1),并尝试分配一个更大的新缓冲区。
    • 关键返回码: 它返回-EAGAIN。上层调用者(如seq_lseek)看到这个返回码后,必须重新调用traverse。这次调用将在一个更大的缓冲区上进行,最终很可能会成功。这个“重试”循环是seq_file能够处理任意大小内容的关键。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// seq_set_overflow: 标记seq_file的缓冲区已满。
static void seq_set_overflow(struct seq_file *m)
{
// 将计数器设置为缓冲区大小,这是一个明确的溢出信号。
m->count = m->size;
}

// seq_buf_alloc: 为seq_file的缓冲区分配内存。
static void *seq_buf_alloc(unsigned long size)
{
// 检查请求的大小是否超过了系统允许的单次I/O最大值。
if (unlikely(size > MAX_RW_COUNT))
return NULL;

// 使用kvmalloc分配内存,它会在有MMU时尝试vmalloc,无MMU时回退到kmalloc。
return kvmalloc(size, GFP_KERNEL_ACCOUNT);
}

// traverse: “快进”seq_file迭代器到指定的字节偏移量。
// 返回值: 0表示成功, 负的errno表示错误, 特别地-EAGAIN表示需要重试。
static int traverse(struct seq_file *m, loff_t offset)
{
loff_t pos = 0;
int error = 0;
void *p;

// 重置迭代器状态。
m->index = 0;
m->count = m->from = 0;
// 优化:如果目标是文件开头,直接返回。
if (!offset)
return 0;

// 惰性分配:如果缓冲区不存在,则分配一个初始大小(PAGE_SIZE)的缓冲区。
if (!m->buf) {
m->buf = seq_buf_alloc(m->size = PAGE_SIZE);
if (!m->buf)
return -ENOMEM;
}
// 调用start回调,开始遍历。
p = m->op->start(m, &m->index);
while (p) {
// 检查start/next的返回值是否为错误指针。
error = PTR_ERR(p);
if (IS_ERR(p))
break;
// 调用show回调,将当前项的内容生成到缓冲区。
error = m->op->show(m, p);
if (error < 0) // 如果show返回负数错误,则中断。
break;
if (unlikely(error)) { // 如果show返回SEQ_SKIP
error = 0;
m->count = 0; // 丢弃已生成的内容。
}
// 检查缓冲区是否溢出。
if (seq_has_overflowed(m))
goto Eoverflow;
// 调用next回调,移动到下一项。
p = m->op->next(m, p, &m->index);
// 检查目标偏移量是否落在当前生成的内容块中。
if (pos + m->count > offset) {
// 是,则计算在当前缓冲区中的起始偏移量(from)。
m->from = offset - pos;
// 并更新缓冲区中剩余的有效字节数。
m->count -= m->from;
break; // 找到了位置,退出循环。
}
// 否,则累加已处理的字节数。
pos += m->count;
m->count = 0; // 重置缓冲区计数器,丢弃内容。
if (pos == offset) // 精确到达目标偏移量。
break;
}
// 调用stop回调,进行清理。
m->op->stop(m, p);
return error;

Eoverflow: // 缓冲区溢出处理
m->op->stop(m, p);
kvfree(m->buf); // 释放过小的缓冲区。
m->count = 0;
// 将期望的缓冲区大小加倍,并尝试分配一个新缓冲区。
m->buf = seq_buf_alloc(m->size <<= 1);
// 如果分配失败,返回-ENOMEM;否则返回-EAGAIN,提示调用者重试。
return !m->buf ? -ENOMEM : -EAGAIN;
}

Seq_file Core Implementation:构建可迭代的虚拟文件

本代码片段是Linux内核seq_file框架的核心实现。其主要功能是提供了seq_openseq_readseq_lseek这三个基本函数,它们共同构成了一个可重用的、用于实现内核虚拟文件(如procfsdebugfs中的文件)的完整读操作和定位操作的后端。seq_file通过一种迭代器(iterator)模型,将底层内核数据结构(如链表)的遍历,与用户空间对文件的顺序读取操作优雅地解耦,实现了高效、健壮且内存友好的虚拟文件内容生成。

实现原理分析

该框架的实现原理是将一个文件的读操作分解为一系列小的、可恢复的步骤,由一个seq_file状态机来驱动。

  1. 初始化 (seq_open):

    • 这是创建一个seq_file实例的入口点。当一个虚拟文件的.open方法被调用时,它应该调用seq_open
    • 此函数从一个专用的slab缓存(seq_file_cache)中分配一个seq_file结构体。
    • 它将这个新分配的seq_file指针存储在file->private_data中,从而将文件对象与seq_file状态机实例关联起来。
    • 最关键的一步是保存调用者传入的seq_operationsop)指针。这个操作表定义了如何开始(.start)、推进(.next)、停止(.stop)遍历底层数据源,以及如何将一个数据项显示(.show)为文本。
  2. 读取 (seq_read):

    • 这是一个现成的、可以直接赋值给文件操作表(file_operations)的.read方法。
    • 它本身不包含复杂的逻辑,而是作为一个简单的包装器,将传统的read系统调用参数(用户缓冲区、大小、文件位置)转换成更现代的iov_iter(I/O向量迭代器)形式。
    • 它最终调用seq_read_iter(在此代码段中未显示),seq_read_iter是真正的状态机驱动函数。seq_read_iter会按需调用seq_operations中的start/next/show来填充seq_file的内部缓冲区,然后将缓冲区内容复制到用户空间,并更新所有状态(如文件位置、迭代器索引等)。
  3. 定位 (seq_lseek):

    • 这是一个现成的.llseek方法。它允许用户空间程序(如less或编辑器)在虚拟文件中前后移动。
    • 由于seq_file生成的内容是动态的,不能像普通文件那样简单地计算偏移量。lseek的实现必须“模拟”读取过程。
    • 它通过调用内部函数traverse(m, offset)来实现。traverse会从头开始(或者从一个已知的缓存位置开始)调用start/next,并计算每个元素生成的数据量,直到累计的数据量达到用户请求的offset。这是一个代价相对较高的操作,但它使得seq_file能够正确支持lseek
    • 整个操作由m->lock互斥锁保护,以防止与并发的read操作发生冲突。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// seq_open: 初始化一个sequential file。
// @file: 要初始化的文件对象。
// @op: 描述遍历序列的方法表。
int seq_open(struct file *file, const struct seq_operations *op)
{
struct seq_file *p;

// 警告:file->private_data应该为空,防止重复打开。
WARN_ON(file->private_data);

// 从专用的slab缓存中为seq_file实例分配内存。
p = kmem_cache_zalloc(seq_file_cache, GFP_KERNEL);
if (!p)
return -ENOMEM;

// 将seq_file实例与文件对象关联起来。
file->private_data = p;

mutex_init(&p->lock); // 初始化用于保护seq_file状态的互斥锁。
p->op = op; // 保存操作方法表。

// 无需增加引用计数:p的生命周期被约束在文件的生命周期之内。
p->file = file;

/*
* seq_file支持lseek()和pread()。它们完全不实现write(),
* 但我们为了历史原因在这里清除FMODE_PWRITE。
*/
file->f_mode &= ~FMODE_PWRITE;
return 0;
}
EXPORT_SYMBOL(seq_open);

// seq_read: sequential file的 ->read() 方法。
// @file: 要读取的文件。
// @buf: 用户空间缓冲区。
// @size: 最大读取字节数。
// @ppos: 文件中的当前位置。
ssize_t seq_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
struct iovec iov = { .iov_base = buf, .iov_len = size};
struct kiocb kiocb;
struct iov_iter iter;
ssize_t ret;

// 初始化一个同步的I/O控制块(kiocb)。
init_sync_kiocb(&kiocb, file);
// 初始化一个I/O向量迭代器,指向用户空间的目标缓冲区。
iov_iter_init(&iter, ITER_DEST, &iov, 1, size);

kiocb.ki_pos = *ppos;
// 调用真正的读迭代函数(seq_read_iter)来执行工作。
ret = seq_read_iter(&kiocb, &iter);
// 更新文件位置。
*ppos = kiocb.ki_pos;
return ret;
}
EXPORT_SYMBOL(seq_read);

// seq_lseek: sequential file的 ->llseek() 方法。
// @file: 文件对象。
// @offset: 新位置。
// @whence: SEEK_SET(绝对), SEEK_CUR(相对)。
loff_t seq_lseek(struct file *file, loff_t offset, int whence)
{
struct seq_file *m = file->private_data;
loff_t retval = -EINVAL;

mutex_lock(&m->lock); // 锁定seq_file状态机。
switch (whence) {
case SEEK_CUR:
offset += file->f_pos; // 计算绝对偏移。
fallthrough;
case SEEK_SET:
if (offset < 0)
break;
retval = offset;
// 如果请求的位置不是当前缓存的位置。
if (offset != m->read_pos) {
// 调用traverse来“快进”迭代器到指定偏移量。
// 如果traverse被中断,循环会继续。
while ((retval = traverse(m, offset)) == -EAGAIN)
;
if (retval) { // 如果traverse失败
// 强制重置所有状态,回到文件开头。
file->f_pos = 0;
m->read_pos = 0;
m->index = 0;
m->count = 0;
} else { // 如果traverse成功
m->read_pos = offset;
retval = file->f_pos = offset;
}
} else { // 如果请求的位置就是当前位置,直接更新f_pos。
file->f_pos = offset;
}
}
mutex_unlock(&m->lock);
return retval;
}
EXPORT_SYMBOL(seq_lseek);

Seq_file Helpers: 简化单视图虚拟文件的创建

本代码片段是Linux内核seq_file框架的一组高级辅助函数。其核心功能是极大地简化那些只需要显示一次性、非迭代内容(即“单一视图”)的虚拟文件的创建过程。对于许多procfsdebugfs中的文件,它们的内容不是一个需要遍历的列表,而是一块整体的信息。这组single_*系列函数通过提供一个预设的、非迭代的seq_operations模板,将创建一个虚拟文件的复杂性降低到只需要实现一个show函数。此外,它还提供了管理seq_file私有数据(private data)的便捷封装。

实现原理分析

该代码的实现原理是“封装”和“模板化”。它识别出seq_file使用中的一个常见模式——只调用一次show函数——并为此模式提供了专用的、简化的API。

  1. 非迭代模板 (single_start, single_next, single_stop):

    • 这三个函数构成了一个seq_operations的“非迭代”模板。
    • single_start: 只有在文件位置*pos为0时(即第一次读取),它才返回一个有效的令牌(SEQ_START_TOKEN)。对于任何后续的调用(*pos > 0),它都返回NULL,这会立即告知seq_file框架迭代已经结束。
    • single_next: 它直接返回NULL,并且递增文件位置。这确保了在show函数被调用一次之后,迭代立即终止。
    • single_stop: 这是一个空函数,因为非迭代模式下没有需要清理的状态。
  2. 动态操作封装 (single_open):

    • 这是该框架的核心。当一个文件系统(如procfs)需要打开一个“单一视图”文件时,它调用single_open
    • 此函数动态地分配一个新的seq_operations结构体。
    • 它将这个新结构体的.start, .next, .stop指针指向上面定义的非迭代模板。
    • 最关键的一步,它将调用者传入的、用于生成具体内容的show函数指针,赋值给新结构体的.show成员。
    • 最后,它调用seq_open,使用这个动态创建的操作结构来初始化seq_file。这意味着调用者无需再静态定义一个seq_operations结构体。
  3. 资源管理 (single_release, *_private函数):

    • single_release: 这是与single_open配对的释放函数。在文件关闭时,它会从seq_file实例中取回single_open动态分配的seq_operations结构体,并使用kfree将其释放,防止内存泄漏。
    • seq_open_private / seq_release_private: 这是一对非常方便的辅助函数,用于处理seq_file使用者需要自己私有状态数据的情况(在之前的input子系统代码中就用到了)。seq_open_privateseq_openkzalloc封装在一起,原子地完成了seq_file的初始化和私有数据区的分配。seq_release_private则在文件关闭时自动kfree这个私有数据区。

代码分析

“单一视图”模式的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// single_start: "单一视图"模式的start回调。
void *single_start(struct seq_file *p, loff_t *pos)
{
// 仅当文件位置为0(即第一次读取)时返回一个非NULL令牌,
// 否则返回NULL以结束迭代。
return *pos ? NULL : SEQ_START_TOKEN;
}

// single_next: "单一视图"模式的next回调。
static void *single_next(struct seq_file *p, void *v, loff_t *pos)
{
++*pos; // 增加文件位置。
return NULL; // 总是返回NULL,确保show函数只被调用一次。
}

// single_stop: "单一视图"模式的stop回调。
static void single_stop(struct seq_file *p, void *v)
{
// 无事可做。
}

// single_open: 打开一个"单一视图"seq_file的便捷函数。
// @file: 文件对象指针。
// @show: 调用者提供的、用于生成文件内容的show函数。
// @data: 传递给show函数的私有数据。
int single_open(struct file *file, int (*show)(struct seq_file *, void *),
void *data)
{
// 动态分配一个seq_operations结构体。
struct seq_operations *op = kmalloc(sizeof(*op), GFP_KERNEL_ACCOUNT);
int res = -ENOMEM;

if (op) {
// 使用"单一视图"模板填充start, next, stop回调。
op->start = single_start;
op->next = single_next;
op->stop = single_stop;
// 关键:将调用者提供的show函数关联起来。
op->show = show;
// 使用动态创建的操作结构打开seq_file。
res = seq_open(file, op);
if (!res)
// 如果成功,将私有数据存入seq_file实例中。
((struct seq_file *)file->private_data)->private = data;
else
kfree(op); // 如果失败,释放内存。
}
return res;
}
EXPORT_SYMBOL(single_open);

// single_release: 与single_open配对的释放函数。
int single_release(struct inode *inode, struct file *file)
{
// 从seq_file实例中获取动态分配的操作结构。
const struct seq_operations *op = ((struct seq_file *)file->private_data)->op;
int res = seq_release(inode, file);
kfree(op); // 释放它。
return res;
}
EXPORT_SYMBOL(single_release);

资源管理辅助函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// single_open_size: single_open的变体,允许预分配指定大小的缓冲区。
int single_open_size(struct file *file, int (*show)(struct seq_file *, void *),
void *data, size_t size)
{
char *buf = seq_buf_alloc(size); // 预分配缓冲区。
int ret;
if (!buf)
return -ENOMEM;
ret = single_open(file, show, data);
if (ret) {
kvfree(buf); // 如果single_open失败,释放缓冲区。
return ret;
}
// 将预分配的缓冲区关联到seq_file实例。
((struct seq_file *)file->private_data)->buf = buf;
((struct seq_file *)file->private_data)->size = size;
return 0;
}
EXPORT_SYMBOL(single_open_size);

// __seq_open_private: 分配私有数据并打开seq_file的内部函数。
void *__seq_open_private(struct file *f, const struct seq_operations *ops,
int psize)
{
int rc;
void *private;
struct seq_file *seq;

// 为调用者分配指定大小的私有数据区。
private = kzalloc(psize, GFP_KERNEL_ACCOUNT);
if (private == NULL)
goto out;

// 打开seq_file。
rc = seq_open(f, ops);
if (rc < 0)
goto out_free;

seq = f->private_data;
seq->private = private; // 将分配的私有数据关联到seq_file实例。
return private;

out_free:
kfree(private);
out:
return NULL;
}
EXPORT_SYMBOL(__seq_open_private);

// seq_open_private: __seq_open_private的包装器,返回0或错误码。
int seq_open_private(struct file *filp, const struct seq_operations *ops,
int psize)
{
return __seq_open_private(filp, ops, psize) ? 0 : -ENOMEM;
}
EXPORT_SYMBOL(seq_open_private);

// seq_release_private: 与seq_open_private配对的释放函数。
int seq_release_private(struct inode *inode, struct file *file)
{
struct seq_file *seq = file->private_data;

kfree(seq->private); // 释放私有数据区。
seq->private = NULL;
return seq_release(inode, file);
}
EXPORT_SYMBOL(seq_release_private);

seq_file_init - 初始化 seq_file 缓存

1
2
3
4
void __init seq_file_init(void)
{
seq_file_cache = KMEM_CACHE(seq_file, SLAB_ACCOUNT|SLAB_PANIC);
}

per_cpu 的哈希列表Seq_file迭代器:统一全局视图的分布式数据

本代码片段定义了两个关键的辅助函数,它们是 seq_file 框架与内核中一种常见高性能数据结构——per-CPU 哈希链表——之间的桥梁。其核心功能是让 seq_file 能够将一个分布在多个CPU核心上的、各自独立的哈希链表,表现为一个单一、连续、可迭代的线性列表。这使得像 /proc/locks 这样的接口可以在多核系统上高效且安全地展示一个全局的数据视图。

实现原理分析

为了在多核系统中减少锁竞争并提高性能,内核经常使用per-CPU数据。例如,file_lock_list 不是一个全局的大哈希表,而是每个CPU都有一个自己的小哈希表。当需要添加锁时,进程只需锁定当前CPU的哈希表,避免了全局争用。然而,当需要像 cat /proc/locks 那样查看所有锁时,就必须有一种方法来安全地遍历所有CPU上的所有哈希表。这两个函数就是为此而生。

  1. seq_hlist_start_percpu (定位起点):

    • 此函数实现了 seq_file 迭代器的 start 操作。seq_file 的工作方式是基于位置(pos)的。当用户读取文件时,start 函数必须找到 pos 所对应的那个节点。
    • 它的逻辑是:从CPU 0开始,逐个遍历所有可能的CPU核心
    • 在每个CPU上,它遍历该CPU私有的哈希链表。
    • 在遍历过程中,它不断递减 pos 计数器。当 pos 减到0时,就意味着找到了用户请求的起始节点,函数返回该节点。
    • 如果遍历完所有CPU上的所有节点 pos 仍未到0(即请求的位置超出了列表总长度),则返回NULL,表示迭代结束。
  2. seq_hlist_next_percpu (移动到下一个):

    • 此函数实现了 next 操作,即从当前节点 v 移动到下一个节点。
    • 快速路径: 首先,它检查当前节点 v其所在的CPU的哈希链表中是否有下一个节点(node->next)。如果有,直接返回它。这是最高效的情况,因为迭代在同一个CPU上继续。
    • 慢速路径 (跨CPU): 如果当前节点已经是其所在CPU链表的最后一个节点,函数就需要跳到下一个有数据的CPU。它使用 cpumask_next 从当前CPU号开始,查找下一个可能的CPU。
    • 一旦找到下一个CPU,它就返回那个CPU私有哈希链表的第一个节点
    • 如果扫描完所有剩余的CPU都没有找到任何节点,则返回NULL,表示整个(虚拟的)全局列表已遍历完毕。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/**
* seq_hlist_start_percpu - 开始一个per-CPU哈希链表数组的迭代
* @head: 指向per-CPU的hlist_head数组的指针
* @cpu: 指向CPU“游标”的指针
* @pos: 序列的起始位置
*
* 在 seq_file->op->start() 中调用。
*/
struct hlist_node *
seq_hlist_start_percpu(struct hlist_head __percpu *head, int *cpu, loff_t pos)
{
struct hlist_node *node;

// 从CPU 0开始,遍历所有可能存在的CPU。
// 在单核STM32H750上,这个循环只会执行一次(*cpu = 0)。
for_each_possible_cpu(*cpu) {
// 遍历当前CPU上的哈希链表。
hlist_for_each(node, per_cpu_ptr(head, *cpu)) {
// 这是一个倒计时器,直到找到pos指定的位置。
if (pos-- == 0)
return node; // 找到了起始节点,返回。
}
}
// 如果遍历完所有CPU的所有节点,pos仍然>0,说明请求的位置越界。
return NULL;
}
EXPORT_SYMBOL(seq_hlist_start_percpu);

/**
* seq_hlist_next_percpu - 移动到per-CPU哈希链表数组的下一个位置
* @v: 指向当前hlist_node的指针
* @head: 指向per-CPU的hlist_head数组的指针
* @cpu: 指向CPU“游标”的指针
* @pos: 序列的起始位置
*
* 在 seq_file->op->next() 中调用。
*/
struct hlist_node *
seq_hlist_next_percpu(void *v, struct hlist_head __percpu *head,
int *cpu, loff_t *pos)
{
struct hlist_node *node = v;

// 增加位置计数器。
++*pos;

// 快速路径:如果当前节点在它自己的链表中还有下一个节点,直接返回。
if (node->next)
return node->next;

// 慢速路径:当前CPU的链表已遍历完,需要寻找下一个有数据的CPU。
// 在单核STM32H750上,cpumask_next(*cpu,...) 将找不到下一个cpu,循环不会执行。
for (*cpu = cpumask_next(*cpu, cpu_possible_mask); *cpu < nr_cpu_ids;
*cpu = cpumask_next(*cpu, cpu_possible_mask)) {
// 获取下一个CPU的哈希链表头。
struct hlist_head *bucket = per_cpu_ptr(head, *cpu);

// 如果这个链表不为空,则返回它的第一个节点。
if (!hlist_empty(bucket))
return bucket->first;
}
// 遍历完所有剩余的CPU都没有找到节点,迭代结束。
return NULL;
}
EXPORT_SYMBOL(seq_hlist_next_percpu);