[TOC]
fs/proc 进程和系统信息伪文件系统
历史与背景
这项技术是为了解决什么特定问题而诞生的?
proc 文件系统(procfs)的诞生是为了提供一个标准的、统一的、基于文件的接口,来取代早期Unix系统中那些不安全、不可移植的进程和内核信息获取方法。在proc出现之前,像ps这样的工具需要通过直接读取内核内存(例如,通过特殊的设备文件/dev/kmem)来获取进程信息。这种方法存在几个严重的问题:
- 安全风险:允许用户空间程序任意读取内核内存是一个巨大的安全漏洞。
- 稳定性差:这种方法严重依赖于特定内核版本的数据结构布局。内核的任何微小改动都可能导致用户空间工具失效甚至使系统崩溃。
- 不可移植:每个硬件架构和操作系统变体都有不同的内存布局,使得编写可移植的监控工具几乎不可能。
procfs通过创建一个**伪文件系统(pseudo-filesystem)**解决了这些问题。它将内核内部的动态数据和状态信息,以普通文件的形式呈现给用户空间,使得任何程序都可以使用标准的open(), read(), write()系统调用来安全、稳定地访问这些信息。
它的发展经历了哪些重要的里程碑或版本迭代?
- 起源与采纳:
procfs的概念并非Linux首创,其思想源于早期Unix系统(如System V Release 4)和Plan 9。Linux在早期就采纳并极大地扩展了这一概念。 - 从进程到系统(The “Kitchen Sink” Era):最初,
procfs主要用于暴露进程信息(因此得名proc)。但它的便利性使其迅速成为暴露各种内核信息的“万能接口”。内核开发者开始将网络统计(/proc/net)、内存信息(/proc/meminfo)、设备列表等所有东西都加入到proc中。 /proc/sys的引入:一个重要的里程碑是引入了/proc/sys目录,它与sysctl系统调用相对应,提供了一个动态读写内核可调参数的接口。这使得系统管理员可以在不重启系统的情况下实时调整内核行为。- 混乱与反思 (The Rise of sysfs):
proc的无限扩张导致其结构变得混乱,缺乏统一的规范。不同的文件格式各异,使得自动化解析变得困难。为了解决这个问题,内核在2.6版本引入了一个新的、结构更清晰的伪文件系统——sysfs。sysfs的设计目标是严格地将内核中的设备模型(kobjects)以“一个文件一个值”的原则暴露出来。 - 职责划分:随着
sysfs的成熟,新的开发原则被确立:sysfs用于表示设备树和设备属性。procfs回归其本源,主要用于报告进程信息,并保留那些已经成为事实标准(de facto standard)的系统信息接口。
目前该技术的社区活跃度和主流应用情况如何?
procfs是任何现代Linux系统中一个不可或缺的核心组件。它非常稳定和成熟,并且被几乎所有的系统监控和管理工具所依赖:
- 核心工具:
ps,top,htop,free,lsof,netstat,vmstat等基础命令都严重依赖procfs来获取数据。 - 系统配置:通过写入
/proc/sys下的文件仍然是调整内核参数最直接、最常用的方法。 - 事实上的API:尽管存在性能和格式上的一些问题,
procfs提供的接口已经成为一个事实上的标准API,被无数的脚本和应用程序所使用。
核心原理与设计
它的核心工作原理是什么?
procfs是一个内存中的、动态生成的伪文件系统。它并不存储在任何物理硬盘上。
- VFS集成:
procfs作为一个文件系统类型注册到内核的虚拟文件系统(VFS)层。当用户挂载proc时,VFS会调用procfs的初始化函数。 - 动态节点创建:
procfs中的目录和文件不是预先存在的。当用户空间进程尝试访问(如ls或open)proc中的一个路径时,VFS会将请求传递给procfs的实现。procfs会根据路径动态地查找或创建对应的目录项(dentry)和inode。例如,当访问/proc/123时,procfs会检查是否存在PID为123的进程,如果存在,就动态创建一个代表该目录的inode。 - 读写处理函数:
procfs中每个文件的inode都关联了一组特定的处理函数(handler functions),而不是指向磁盘上的数据块。- 读操作 (
read): 当用户读取一个proc文件(如cat /proc/meminfo)时,对应的读处理函数被调用。该函数会直接访问内核的内部数据结构(如全局的内存统计变量),将这些二进制数据动态格式化为人类可读的文本字符串,然后返回给用户空间。 - 写操作 (
write): 当用户写入一个proc文件(如echo 1 > /proc/sys/net/ipv4/ip_forward)时,对应的写处理函数被调用。该函数会解析用户传入的字符串,并调用相应的内核函数来修改内核内部的变量。
- 读操作 (
它的主要优势体现在哪些方面?
- 统一和标准的接口:使用所有人都熟悉的文件I/O API,无需特殊的库或系统调用。
- 实时性:提供的信息是内核状态的实时快照,总能反映系统的最新情况。
- 人类可读性:大部分文件是文本格式,便于系统管理员直接查看和调试。
- 易于脚本化:可以非常方便地被
shell,awk,grep等标准Unix工具处理。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 性能开销:内核将二进制数据格式化为文本,用户空间再解析文本,这个过程相比直接的二进制接口(如
netlink或sysctl系统调用)有显著的性能开销。 - 格式不一致:由于其历史发展,
procfs中不同文件的输出格式缺乏统一标准,使得编写健壮的解析器变得困难。 - 非原子性:读取一个包含多行信息的大文件(如
/proc/stat)不是一个原子操作。在读取过程中,内核的状态可能已经发生变化,导致读取到的不同行可能来自不同时间点的快照。 - 结构混乱:如前所述,它混合了进程信息、硬件信息、网络统计和配置等多种不同类型的数据,逻辑结构不如
sysfs清晰。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?
procfs是获取进程和核心系统状态信息的唯一标准接口。
- 进程监控与管理:
ps -ef通过遍历/proc/[PID]目录并读取/proc/[PID]/stat和/proc/[PID]/cmdline等文件来实现。lsof(List Open Files) 通过扫描所有/proc/[PID]/fd/目录来找出进程打开的文件。
- 系统资源监控:
free命令的数据直接来自/proc/meminfo。uptime的数据来自/proc/uptime和/proc/loadavg。vmstat和iostat的核心数据来自/proc/stat和/proc/diskstats。
- 内核参数运行时调整:
sysctl net.ipv4.ip_forward=1命令实际上就是向/proc/sys/net/ipv4/ip_forward文件写入1。
是否有不推荐使用该技术的场景?为什么?
- 获取设备模型信息:当需要了解系统中的设备拓扑结构、设备与驱动的绑定关系、或设备的特定属性(如电源状态)时,应该使用
sysfs(/sys)。sysfs提供了与内核设备模型严格对应的、结构化的视图。 - 高性能网络监控:对于需要高频率、低延迟地获取网络统计数据或通知的应用,使用
netlink套接字是比轮询/proc/net/dev等文件更高效的方案。 - 内核调试:对于内核开发者进行深度调试,
debugfs(/sys/kernel/debug) 提供了更灵活、更底层的接口,但其API不保证稳定性。
对比分析
请将其 与 其他相似技术 进行详细对比。
| 特性 | procfs (/proc) |
sysfs (/sys) |
debugfs (/sys/kernel/debug) |
tmpfs |
|---|---|---|---|---|
| 核心用途 | 进程和系统状态报告,以及内核参数调整。 | 表示内核的设备模型,管理设备和驱动。 | 内核调试,暴露内部状态给内核开发者。 | 内存中的文件系统,用于通用目的的临时文件存储。 |
| 数据来源 | 内核动态生成的文本数据。 | 内核的kobject数据结构,严格遵循“一文件一值”。 | 内核开发者任意导出的调试信息。 | 用户写入的数据。 |
| API稳定性 | 大部分是稳定的(事实标准),但不做绝对保证。 | 有严格的API稳定性保证,用户空间可以依赖它。 | 完全不保证稳定,随时可能更改。禁止在生产应用中依赖它。 | 作为文件系统,其API是稳定的。 |
| 结构 | 历史形成,逻辑结构有时不清晰。 | 高度结构化,与设备树紧密对应。 | 完全无结构,由开发者自行组织。 | 由用户自行组织。 |
| 典型场景 | ps, top, free, sysctl |
udev, systemd-hwdb, 电源管理工具。 |
调试内存泄漏,跟踪锁竞争等。 | /dev/shm, /run |
include/uapi/linux/stat.h
文件类型和文件权限标志
1 |
|
include/linux/proc_fs.h
proc_create_seq 创建一个 proc 文件系统的序列文件
1 |
fs/proc/inode.c
proc_init_kmemcache 初始化 proc 文件系统的内核缓存
1 | void __init proc_init_kmemcache(void) |
fs/proc/base.c
pid_entry 结构体定义
DIR表示目录REG表示常规文件ONE表示单个文件NOD表示节点文件LINK表示符号链接
1 | struct pid_entry { |
tid_base_stuff 线程级文件项
- tid_base_stuff 则用来描述与单个线程(task/thread id)相关的 proc 文件条目,通常这些条目位于 /proc/
/ 下。这个数组中定义的条目往往和线程特定的信息有关,比如与线程的文件描述符、调度信息或统计数据等。
1 | static const struct pid_entry tid_base_stuff[] = { |
tgid_base_stuff 线程组级文件项
- tgid_base_stuff 用于描述与线程组(即整个进程)相关的 proc 文件条目,也就是说,这个数组定义了当你访问 /proc/
(通常 与进程号相同)时所显示的各种文件。例如,像 maps、cmdline、status 等条目的创建和对应的操作会在这里注册。
1 | static const struct pid_entry tgid_base_stuff[] = { |
pid_entry_nlink 计算 PID 相关的链接计数
1 | /* |
set_proc_pid_nlink 设置进程 PID 相关的链接计数
1 | /* 注意: |
fs/proc/generic.c
proc_alloc_inum 分配一个 inode 编号
1 | static DEFINE_IDA(proc_inum_ida); |
proc_match 用于比较 proc 目录条目的名称
1 | static int proc_match(const char *name, struct proc_dir_entry *de, unsigned int len) |
pde_subdir_find 查找 proc 子目录
1 | static struct proc_dir_entry *pde_subdir_find(struct proc_dir_entry *dir, |
xlate_proc_name 翻译 proc 名称
1 | /* |
__proc_create 创建一个新的 proc 目录条目
1 | static struct proc_dir_entry *__proc_create(struct proc_dir_entry **parent, |
pde_subdir_insert 将一个新的 proc 目录项插入到父目录的子目录红黑树中
1 | static bool pde_subdir_insert(struct proc_dir_entry *dir, |
proc_register 将一个新的目录项 dp 注册到 proc 文件系统的父目录 dir 中
1 | /* 返回注册的条目,或在失败时释放 dp 并返回 NULL */ |
proc_mkdir 创建一个新的 proc 目录
1 | /* |
proc_create_mount_point 创建一个新的 proc 挂载点
- 挂载点
- 定义:挂载点是一个目录,用于将某个文件系统或设备挂载到该目录上。挂载点本身并不存储数据,而是作为访问挂载的文件系统或设备的入口。
- 功能:挂载点的主要作用是将外部文件系统(如硬盘分区、网络文件系统、虚拟文件系统等)与系统的目录结构连接起来。
- 动态性:挂载点的内容取决于挂载的文件系统或设备。例如,挂载 nfsd 后,该目录的内容由 NFS 服务动态生成。
- 访问:挂载点的内容通常由挂载的文件系统提供,用户通过挂载点访问该文件系统的数据。
1 | struct proc_dir_entry *proc_create_mount_point(const char *name) |
proc_create_reg 创建一个 proc 文件系统的常规文件
1 | struct proc_dir_entry *proc_create_reg(const char *name, umode_t mode, |
pde_set_flags 设置 proc 目录项的标志
1 | static void pde_set_flags(struct proc_dir_entry *pde) |
proc_create_seq_private 创建一个 proc 文件系统的序列文件
- 动态生成内容:序列文件的内容不是静态存储的,而是通过内核中的回调函数动态生成。适合展示需要实时计算或动态更新的数据。
- 分块输出:序列文件支持分块输出,避免一次性生成大量数据导致内存占用过高。用户读取文件时,内核会逐块生成数据并返回。
1 | struct proc_dir_entry *proc_create_seq_private(const char *name, umode_t mode, |
fs/proc/self.c
- 在 Linux 中,/proc/self 是一个 动态符号链接,始终指向 当前进程的 /proc/PID 目录
proc_self_init 为 /proc/self 文件系统条目分配唯一的 inode 编号
1 | static unsigned self_inum __ro_after_init; |
fs/proc/thread_self.c
- /proc/thread-self 是 Linux 内核中 proc 文件系统的一个“魔法”符号链接(magic symlink),它类似于 /proc/self,不过专门指向当前线程的 proc 目录,而 /proc/self 指向当前进程的 proc 目录。
- /proc/self:当进程访问这个符号链接时,它解析为当前进程的目录(通常是 /proc/
)。 - /proc/thread-self:当线程访问这个链接时,它解析为对应于当前线程的目录,即实际路径类似于 /proc/
/task/ 。这种设计允许在多线程进程中区分并访问属于每个线程的独立信息。
使用场景 在多线程环境下,有时需要访问每个线程特定的信息,例如调度、资源统计、文件描述符等。使用 /proc/thread-self 可以确保线程所查看或操作的信息仅限于该线程本身,而不会出现混淆(而 /proc/self 总是指向整个进程)
proc_thread_self_init 为 thread-self 文件系统条目分配唯一的 inode 编号
1 | static unsigned thread_self_inum __ro_after_init; |
fs/proc/proc_tty.c
proc_tty_init 初始化 /proc/tty 目录
1 | /* |
fs/proc/root.c
proc_init_fs_context
1 | static const struct fs_context_operations proc_fs_context_ops = { |
proc_root_init 定义和初始化 Linux 内核中的 proc 文件系统
1 | void __init proc_root_init(void) |
fs/proc/nommu.c (无MMU版本):展示内核空间内存区域布局
本代码片段实现了在没有内存管理单元(MMU)的Linux系统(通常称为uClinux)上,/proc/maps 这个虚拟文件的后端逻辑。在标准的、有MMU的Linux系统上,/proc/[pid]/maps 用于显示一个进程的虚拟内存地址空间布局。然而,在无MMU系统中,没有进程独立的虚拟地址空间,所有代码和数据都运行在一个共享的、平坦的物理地址空间中。因此,这个文件被重新定义,用于展示内核自身所知道的所有**物理内存区域(vm_region)**的布局。它是一个关键的调试工具,用于理解无MMU系统上的内存是如何被内核划分和使用的。
实现原理分析
该文件的实现与我们之前分析的 /proc/locks 非常相似,同样是基于 seq_file 的迭代器模型。
核心数据结构 (
nommu_region_tree):- 内核在无MMU模式下,会维护一个全局的红黑树(red-black tree)
nommu_region_tree。 - 这个树中的每一个节点都是一个
vm_region结构体,它描述了一段连续的物理内存区域,包括其起始地址(vm_start)、结束地址(vm_end)、访问权限(vm_flags)以及是否与某个文件关联(vm_file)。 - 红黑树被用来高效地存储和查找这些内存区域,保证它们按地址有序且不重叠。
- 内核在无MMU模式下,会维护一个全局的红黑树(red-black tree)
Seq_file 迭代器实现:
nommu_region_list_start: 当用户开始读取/proc/maps时调用。
a.down_read(&nommu_region_sem): 获取保护nommu_region_tree的读写信号量的共享读锁。这确保了在遍历期间,树的结构不会被其他任务修改。
b.for (p = rb_first(...); ...): 它从红黑树的第一个节点(地址最低的区域)开始遍历,通过不断递减pos来定位到用户请求的起始位置。nommu_region_list_next: 当需要下一个条目时调用。它简单地调用rb_next(v)来获取红黑树中的下一个节点,逻辑非常简单。nommu_region_list_show: 这是核心的显示函数。对于迭代器返回的每一个红黑树节点_p,它首先通过rb_entry宏获取其宿主结构体vm_region的指针,然后调用nommu_region_show来格式化输出。nommu_region_list_stop: 当读取结束时调用,它只做一件事:up_read(&nommu_region_sem),释放读锁。
信息格式化 (
nommu_region_show):- 这个函数负责将一个
vm_region结构体中的信息,转换成与标准/proc/maps格式类似的一行文本。 - 它会输出:
- 地址范围:
vm_start-vm_end。在无MMU系统上,这些是物理地址。 - 权限:
rwxp或rwxs等。r=读,w=写,x=执行。第四位表示共享属性:p=私有,s=共享(可写),S=共享(不可写)。 - 文件偏移: 如果区域与文件映射相关,显示其在文件中的偏移量。
- 设备与Inode: 如果与文件相关,显示文件所在设备的主/次设备号和inode号。
- 文件路径: 如果与文件相关,显示文件的路径。
- 地址范围:
- 这个函数负责将一个
初始化 (
proc_nommu_init):- 在文件系统初始化阶段,通过
proc_create_seq在/proc的根目录下创建一个名为maps的文件(注意,它不在[pid]子目录下),并将其与proc_nommu_region_list_seqop操作集关联起来。
- 在文件系统初始化阶段,通过
代码分析
1 | // ... (头文件包含) ... |
fs/proc/cmdline.c 文件实现:向用户空间展示内核启动参数
本代码片段实现了Linux内核中一个基础但非常重要的信息接口:/proc/cmdline 虚拟文件。其核心功能极其简单直接:当用户空间程序读取这个文件时,它会原封不动地返回内核在启动时接收到的命令行参数。这些参数通常由引导加载程序(bootloader,如U-Boot, GRUB等)传递给内核,用于控制内核的各种行为,例如指定根文件系统的位置、设置内核调试选项、配置硬件参数等。
实现原理分析
该文件的实现是 /proc 文件系统中最简单直接的一种,它使用了 proc_create_single 这个便捷的辅助函数,专门用于创建那些内容固定不变、只需一个show函数就能处理的 /proc 文件。
内核启动参数的存储:
- 在内核启动的极早期,引导加载程序传递的命令行字符串会被解析并存储在一个全局的、静态的字符数组
saved_command_line中。同时,其长度被保存在saved_command_line_len中。 - 这部分存储逻辑发生在内核自举(bootstrapping)代码中,早于
procfs的初始化。
- 在内核启动的极早期,引导加载程序传递的命令行字符串会被解析并存储在一个全局的、静态的字符数组
show函数的实现 (cmdline_proc_show):- 这是一个典型的
seq_fileshow函数,但由于内容极其简单,它不需要任何复杂的迭代逻辑。 seq_puts(m, saved_command_line): 将全局变量saved_command_line的全部内容一次性地写入seq_file的缓冲区。seq_putc(m, '\n'): 在末尾添加一个换行符,以符合UNIX文本文件的惯例。- 函数总是返回0,表示显示成功。
- 这是一个典型的
文件创建与初始化 (
proc_cmdline_init):- 时机:
fs_initcall确保此函数在内核文件系统子系统初始化期间被调用。 proc_create_single: 这是一个高级的procfs辅助函数,它极大地简化了创建简单文件的过程。"cmdline": 在/proc根目录下创建的文件名。0: 文件权限。这里设置为0,但procfs最终会应用默认权限,通常是0444(所有用户只读)。NULL: 父目录,NULL表示在/proc根目录下创建。cmdline_proc_show: 将我们之前定义的show函数与这个文件关联起来。
pde_make_permanent(pde): 这是一个重要的调用。它将这个proc条目标记为“永久的”。这意味着即使创建它的模块(在本例中是内建内核)被卸载(虽然内建内核不会),这个/proc条目也不会被移除。这保证了像/proc/cmdline这样基础的接口在系统的整个生命周期内都是可用的。pde->size = ...: 手动设置proc条目的大小。这主要是一个给ls -l等命令看的“提示”,让它能显示一个正确的、非零的文件大小。
- 时机:
代码分析
1 | // ... (头文件包含) ... |
/proc/consoles 文件实现:展示已注册的内核控制台
本代码片段实现了 /proc/consoles 这个虚拟文件。其核心功能是遍历内核中所有已注册的**控制台(console)**驱动,并将每个控制台的详细信息格式化成人类可读的文本,展示给用户空间。这个文件对于系统管理员和嵌入式开发者来说,是检查和确认哪些设备(如UART、虚拟终端、netconsole等)正在作为内核消息输出目的地的关键工具。
实现原理分析
与我们之前分析过的许多 /proc 文件一样,/proc/consoles 的实现也严格遵循了 seq_file 的迭代器模型。
核心数据结构 (
console_list):- 内核维护一个全局的哈希链表
console_list,其中包含了所有通过register_console()注册的struct console实例。 - 这个链表由一个专门的互斥锁
console_mutex(通过console_list_lock/unlock宏访问)保护,以防止在遍历或修改时发生竞态条件。
- 内核维护一个全局的哈希链表
Seq_file 迭代器实现:
c_start: 当用户开始读取文件时调用。
a.console_list_lock(): 获取互斥锁,以安全地遍历console_list。
b.for_each_console(con) ...: 这是一个宏,用于遍历console_list。它通过不断递减pos来定位到用户请求的起始控制台实例。c_next: 当需要下一个条目时调用。它使用hlist_entry_safe来安全地获取链表中的下一个console实例。_safe变体可以在遍历时安全地处理节点的移除。show_console_dev: 核心的显示函数。对于迭代器返回的每一个console对象,它都被调用来格式化输出。c_stop: 当读取结束时调用,它只做一件事:console_list_unlock(),释放互斥锁。
信息格式化 (
show_console_dev):- 这个函数负责将一个
struct console结构体中的信息,转换成一行详细的文本。 - 设备号获取:
con->device(con, &index): 它会调用控制台驱动提供的device回调函数。这个回调函数返回与该控制台关联的TTY驱动(tty_driver)以及它的索引号(index)。console_lock(): 在调用device回调前后,它会获取并释放console_lock。这是为了与切换虚拟终端(VT)等操作同步,因为这些操作可能会动态地改变哪个TTY是“前景控制台”(fg_console)。MKDEV(...): 根据获取到的主设备号(major)和次设备号(minor_start + index),计算出该控制台对应的设备号(dev_t)。
- 标志位解析: 它遍历一个预定义的
con_flags数组,将con->flags中的二进制标志位(如CON_ENABLED,CON_CONSDEV)转换成易于理解的单字符标志(如E,C)。 - 格式化输出: 最后,它使用
seq_printf将所有信息格式化输出,包括:- 名称和索引: 如
ttyS0。 - 读/写/清屏能力:
R,W,U标志。 - 标志字符串: 如
(ECB a)。 - 设备号: 如
4:64。
- 名称和索引: 如
- 这个函数负责将一个
初始化 (
proc_consoles_init):- 在文件系统初始化阶段,通过
proc_create_seq在/proc根目录下创建名为consoles的文件,并将其与consoles_op操作集关联。
- 在文件系统初始化阶段,通过
代码分析
1 | // ... (头文件包含) ... |
/proc/cpuinfo 文件创建:为CPU信息展示提供VFS接口
本代码片段负责创建 /proc/cpuinfo 这个虚拟文件,并将其与一组文件操作(proc_ops)关联起来。它本身并不实现具体的CPU信息展示逻辑,而是充当一个桥梁,将 /proc/cpuinfo 这个文件系统节点链接到真正负责生成内容的、由体系结构特定代码(例如 arch/arm/kernel/setup.c)实现的 seq_file 操作集 cpuinfo_op。
实现原理分析
这个文件的实现展示了 procfs 中一种更通用的文件创建方式,它直接使用 proc_ops 结构,而不是像之前看到的 proc_create_single 或 proc_create_seq 那样的高度封装的辅助函数。
分离的
seq_operations:extern const struct seq_operations cpuinfo_op;- 这一行是理解本文件功能的关键。它声明了一个外部的
seq_operations结构体cpuinfo_op。这意味着start,next,show,stop这些真正负责遍历CPU、格式化CPU信息的函数,是在别的文件中定义的。 - 这种设计是高度模块化的体现。
/proc/cpuinfo的内容是强体系结构相关的(ARM, x86, MIPS等CPU的特性完全不同),因此,将其内容生成的逻辑放在各自的体系结构代码 (arch/...) 中是唯一合理的设计。而文件创建的逻辑是通用的,所以放在fs/proc目录下。
open方法的实现 (cpuinfo_open):- 当一个用户进程打开
/proc/cpuinfo文件时,VFS会调用cpuinfo_proc_ops中指定的.proc_open函数,即cpuinfo_open。 return seq_open(file, &cpuinfo_op);cpuinfo_open的工作非常简单:它调用seq_open。seq_open是seq_file框架的核心函数,它会:
a. 分配一个seq_file实例。
b. 将这个实例与外部的cpuinfo_op操作集关联起来。
c. 将这个seq_file实例存储在struct file的private_data字段中。- 从这一刻起,对这个已打开文件的所有后续操作(如
read)都将由seq_file框架接管。
- 当一个用户进程打开
proc 文件操作 (
cpuinfo_proc_ops):- 这是一个
proc_ops结构体,它定义了/proc/cpuinfo的VFS接口。 .proc_flags = PROC_ENTRY_PERMANENT;: 将此文件标记为永久的。.proc_open: 指向我们上面分析的cpuinfo_open。.proc_read_iter,.proc_lseek,.proc_release: 它们分别指向seq_file框架提供的通用读、定位和释放函数(seq_read_iter,seq_lseek,seq_release)。这些通用函数会从file->private_data中取出seq_file实例,并调用其内部关联的cpuinfo_op中的start/next/show/stop来完成工作。
- 这是一个
初始化 (
proc_cpuinfo_init):- 使用
proc_create函数,在/proc根目录下创建一个名为cpuinfo的文件,并将其所有文件操作指向我们定义的cpuinfo_proc_ops。
- 使用
代码分析
1 | // ... (头文件包含) ... |
/proc/devices 文件实现:展示已注册的设备驱动
本代码片段实现了 /proc/devices 这个虚拟文件。其核心功能是向用户空间提供一个当前内核中所有已注册的字符设备和块设备驱动程序的列表。输出内容分为两部分,分别列出字符设备和块设备,并显示每个驱动程序所注册的**主设备号(Major Number)**以及其名称。这个文件是Linux系统中一个基础的、用于查看和管理设备驱动的接口。
实现原理分析
该文件的实现同样基于 seq_file 的迭代器模型,但其迭代方式非常独特:它不是遍历一个链表,而是遍历一个数字范围,即从0到最大设备主设备号。
数字迭代器 (Numeric Iterator):
devinfo_start: 当开始读取时,它检查请求的位置*pos是否在有效的主设备号范围内 (< BLKDEV_MAJOR_MAX + CHRDEV_MAJOR_MAX)。如果有效,它直接返回pos指针本身作为迭代器v。这意味着迭代器v在这个实现中,就是指向当前要处理的主设备号的指针。devinfo_next: 当需要下一个条目时,它简单地将*pos加一,然后检查是否越界。如果未越界,返回新的pos指针。devinfo_stop: 无事可做,因为没有需要清理的资源。- 这种设计巧妙地将一次文件读取过程转换成了一个对所有可能的主设备号的遍历。
分发与显示 (
devinfo_show):- 这是核心的显示函数。它接收迭代器
v(即pos指针),并解引用得到当前要处理的主设备号i。 - 分发逻辑:
a.if (i < CHRDEV_MAJOR_MAX): 函数首先检查i是否在字符设备的主设备号范围内。如果是,它就调用字符设备子系统提供的chrdev_show(f, i)函数,让该子系统去查找并显示注册在主设备号i上的驱动信息。
b.#ifdef CONFIG_BLOCK ...: 如果内核配置了块设备支持,它会继续处理块设备的主设备号。i -= CHRDEV_MAJOR_MAX将索引重新映射到块设备的主设备号范围(从0开始),然后调用块设备子系统提供的blkdev_show(f, i)。 - 打印表头: 当
i为0时(在各自的范围内),函数会打印出 “Character devices:” 或 “Block devices:” 的表头,以组织输出格式。
- 这是核心的显示函数。它接收迭代器
初始化 (
proc_devices_init):- 通过
proc_create_seq在/proc根目录下创建名为devices的文件,并将其与devinfo_ops操作集关联起来。 pde_make_permanent(pde)确保该文件在系统生命周期内始终存在。
- 通过
代码分析
1 | // ... (头文件包含) ... |









