资讯详情

资讯详情

Linux内核proc文件系统实践:从proc_ops到seq_file实现动态节点

简介一份操作系统实验8的完整报告围绕0.11平台上proc文件系统的实现展开适合正在学习虚拟文件系统与进程管理的大学生或自学者参考。报告详细说明psinfo结点的设计步骤包括新增文件类型、修改mknod和初始化函数、在sys_read中增补处理分支并给出获取进程、磁盘及索引节点信息的关键代码能够帮助读者理解如何让psinfo结点输出进程的PID、PPID、状态、优先级、TTY和时间等字段。除核心实现外还讨论了扩展结点和多次读取间数据一致性两个思考题从文件指针与缓冲区管理角度解释避免数据混乱的方法并给出显示CPU、内存、硬盘利用率的节点构想。整份资料为单个Word文档大小约944KB内容包含实验目的、步骤、代码与测试结果结构清晰适合直接对照实践或作为实验报告范文。目前已有208人学习下载尤其适合需要完整实验思路与可运行代码的操作系统课程设计者。1. 实验8 不是把 /proc 重写一遍而是实现你自己的内核节点很多人听到“proc文件系统的实现”第一反应是去把内核 fs/proc 目录下的代码读一遍然后试图复刻一个 /proc。真正做过 linux 内核模块开发的人都知道实验报告里的“实现”落到可验收的标准是让系统里出现一个由你注册的 /proc 节点比如/proc/nova/status并在 cat 它的时候输出你自定义的内容。这个节点不占磁盘空间不经过块设备层打开、读取、写入都走 VFS 分发到你的回调函数。这个过程把 linux操作系统的文件抽象、设备模型和进程管理串在一条链上适合用来做实验8的交付物。这篇就按“最小节点 → 动态内容 → 进程目录 → 验证排错 → 触发式控制节点”的顺序把 proc 文件系统从接口到落地讲清楚。2. proc 文件系统是怎么把自己挂进 VFS 的proc_ops 与最小节点2.1 先理解 procfs 在内核里的定位procfs 是注册在 VFS 之下的一种伪文件系统。普通文件系统把 inode 映射到磁盘块procfs 则把 inode 映射到内核对象或回调函数。你在用户态执行open(/proc/xxx, O_RDONLY)内核 VFS 层根据路径找到对应目录项后不会去读磁盘而是调用该文件系统提供的 inode 操作和文件操作。Linux 5.6 起proc 文件系统不再直接使用struct file_operations而是引入了一层struct proc_ops。原因是 proc 节点的回调在语义上和普通文件有差别独立出 proc 专用接口后可以减少运行时分支也让模块作者明确知道自己写的是 proc 文件而不是磁盘文件。老内核里用file_operations写的 proc 模块在新内核上会编不过需要把.read、.open这类字段名换成.proc_read、.proc_open。2.2 最小实现注册一个只读节点先写一个最小模块注册/proc/demo_status内容是一段静态字符串。这个例子能跑通就说明 procfs 的注册、回调和卸载整条链路已经通了。#include linux/module.h #include linux/proc_fs.h #include linux/uaccess.h #include linux/fs.h #define PROC_ENTRY demo_status static char *demo_msg proc demo alive\n; static ssize_t demo_read(struct file *fp, char __user *buf, size_t len, loff_t *off) { return simple_read_from_buffer(buf, len, off, demo_msg, strlen(demo_msg)); } static const struct proc_ops demo_proc_ops { .proc_read demo_read, }; static int __init demo_init(void) { struct proc_dir_entry *entry; entry proc_create(PROC_ENTRY, 0, NULL, demo_proc_ops); if (!entry) { pr_err(proc_create failed\n); return -ENOMEM; } return 0; } static void __exit demo_exit(void) { proc_remove(pde_get(demo_proc_ops ! NULL ? /* 简化 */ NULL : NULL)); }上面的退出函数写得潦草正常写法是在demo_init里保存entry指针卸载时直接proc_remove(entry)。proc_create的第一个参数是文件名第二个是权限位传 0 表示使用系统默认的 0444第三个参数为NULL时挂在/proc顶层最后一个参数是回调集合。simple_read_from_buffer是内核提供的辅助函数它内部会处理*off偏移和用户态地址校验。很多初学写法是自己实现copy_to_user结果忘记判断*off导致 cat 连续读到同样内容终端刷屏停不下来。能不用手写偏移逻辑就尽量不用。2.3 读接口的三条规则proc 文件读取至少有三条规则要守住。第一read回调返回 0 表示 EOFcat 读到 0 才停止如果你忽略*off每次调用都返回同样长度读进程会一直循环。第二用户态指针buf不能直接解引用必须经过copy_to_user或simple_read_from_buffer否则会触发内核页错误严重时直接 panic。第三len是用户态本次 read 请求的长度通常不保证能装下全部输出像/proc/cpuinfo那样超长内容要么等下次 read 继续读要么用 seq_file 分片输出。实验里最常见的验收行为是cat /proc/demo_status它实际发生两次 read第一次拿到数据第二次拿到 0你的回调必须让第二次返回 0cat 才会正常退出。记住procfs 的 read 不是“一次性给全量”而是要遵守常规文件 read 的偏移语义。3. 用 seq_file 生成动态 proc 文件系统内容3.1 为什么单靠 read 回调不够静态字符串用simple_read_from_buffer可以应付但 proc 节点真正的用途是动态输出。比如要列出当前系统所有进程的 pid 和名字数量不定一次 read 可能只申请了 4KB 缓冲区如果硬拼一个超大 buffer还要反复处理偏移代码很快被 offset 逻辑淹没。内核为此提供了 seq_file 机制专门给 procfs 这类“顺序输出”的文件用。seq_file 的思路是把输出拆成多个“记录”。内核维护一个内部缓冲区show回调向缓冲区里追加一段内容缓冲区满了就返回给用户态下次 read 继续执行下一个记录。你不需要关心用户态 buffer 到底多大也不需要维护自己的偏移seq_file 的迭代器会替你管理位置。3.2 先写出最简单的 single_open 版本如果只是要输出一次快照用single_open最省事。它在打开文件时执行一次生成函数把全部内容放进一个 seq_file 缓冲区之后 read 从这个缓冲区复制数据。#include linux/seq_file.h #include linux/proc_fs.h static int demo_show(struct seq_file *m, void *v) { seq_printf(m, pid 1 - %s\n, systemd); seq_printf(m, time - %lld\n, ktime_get_real_seconds()); return 0; } static int demo_open(struct inode *inode, struct file *file) { return single_open(file, demo_show, NULL); } static const struct proc_ops demo_proc_ops { .proc_open demo_open, .proc_read seq_read, .proc_lseek seq_lseek, .proc_release single_release, };注意proc_ops里的.proc_open是打开节点时执行.proc_read必须设成seq_read.proc_release设成single_release。这套组合把“open 时生成、read 时分批给用户态”的流程接上了。demo_show里可以使用seq_printf、seq_puts、seq_write输出目标不是用户态 buffer而是 seq_file 的内部缓冲区所以非常安全。single_open适合多少条输出的场景我的经验是几百条以内可以接受因为它是在 open 时一次性执行完生成函数如果里面遍历了大量数据open 会卡住而且即使没人 read只要打开了文件生成工作也已经发生。对 proc 节点来说open 应该是轻量操作。3.3 用 seq_operations 控制逐条输出更贴近真实 /proc 的做法是自定义seq_operations让每一条记录在 read 的循环里逐个产出。下面实现一个/proc/nova/tasks节点每次 show 输出一个进程的 pid 和名字配合 stop 结束遍历。static void *tasks_start(struct seq_file *m, loff_t *pos) { if (*pos 1) return NULL; return SEQ_START_TOKEN; } static void *tasks_next(struct seq_file *m, void *v, loff_t *pos) { (*pos); return NULL; } static void tasks_stop(struct seq_file *m, void *v) { } static int tasks_show(struct seq_file *m, void *v) { struct task_struct *p; rcu_read_lock(); for_each_process(p) { seq_printf(m, %d %s\n, p-pid, p-comm); } rcu_read_unlock(); return 0; } static const struct seq_operations tasks_seq_ops { .start tasks_start, .next tasks_next, .stop tasks_stop, .show tasks_show, };上面这个写法其实是“一条记录内输出全部内容”并没有真正用上迭代器分片。更标准的做法是让start定位到第一个 tasknext移动到下一个 taskshow只输出当前 task但在实验代码里把遍历整体放进show更直观副作用是输出会被整体塞进一次 seq 缓冲。两种方式都能过验收区别在于内存占用和分片粒度。标准做法需要把pos映射到 task 链表的位置配合for_each_process的迭代状态稍微复杂一些适合后续再优化。seq_file 给开发者的真正好处是自动管理了缓冲区和偏移你只要保证show里每次输出的是完整一行就不会出现“读到半行”的情况。这正是实验8里动态 proc 内容最应该展示的设计点。4. 把 proc 文件系统做成进程信息目录遍历 task_struct 与命名空间4.1 目录树与权限位的设计real /proc 不是一个扁平目录而是按 pid 为目录、内部再挂 status、stat、cmdline 等文件。做实验时不需要复制全部结构但至少要展示“一个 proc 子系统目录”的概念并在其中提供多个节点。常见做法是注册一个/proc/nova目录里面放version、uptime、tasks三个文件。目录用proc_mkdir文件用proc_create卸载时一个proc_remove就能递归清理。static struct proc_dir_entry *nova_dir; static int __init nova_init(void) { nova_dir proc_mkdir(nova, NULL); if (!nova_dir) return -ENOMEM; proc_create(version, 0444, nova_dir, version_ops); proc_create(uptime, 0444, nova_dir, uptime_ops); proc_create(tasks, 0444, nova_dir, tasks_ops); return 0; } static void __exit nova_exit(void) { proc_remove(nova_dir); }权限位0444表示所有用户只读0644表示 root 可写、其他用户可读。proc 节点毕竟是内核接口写操作意味着让用户态直接触发内核逻辑权限给大了容易出安全问题。实验里读节点给0444足够写节点给0200或0644时要确认校验逻辑完善。4.2 遍历 task_struct 的锁与命名空间问题在tasks_show里使用for_each_process遍历内核进程链表要注意几点。首先必须用rcu_read_lock保护遍历否则进程退出时链表节点可能被释放产生 use-after-free。其次p-comm是进程名长度固定为TASK_COMM_LEN读取不需要额外加锁但如果你要读p-mm、p-fs这类引用计数对象必须拿对应的锁或get_task_struct增加引用。task_struct里的pid字段在较新内核中与命名空间相关。默认 init 命名空间里p-pid和用户看到的一致如果你在容器里加载模块会发现遍历到的 pid 范围不同。这是 proc 文件系统在容器场景下的经典问题一个进程在不同 pid namespace 里看到的 pid 不同而内核遍历返回的是全局 pid必须通过pid_nr_ns做转换。实验不涉及容器的话可以先不处理但要在代码注释里写明这个边界。和“计算机操作系统”课程里的进程状态结合看/proc 其实就是把task_struct的字段“投影”成文件系统视图的过程。R/S/D/Z这些状态枚举原本只存在于内核源码里proc 让它们在用户态以字符串出现。做实验时顺便把task-__state、exit_state映射成可读字符也是很有价值的练习但不建议直接引用内核未导出的task_state_to_char函数因为它在很多内核配置里没有导出符号。4.3 用快照策略避免读一半进程消失直接遍历链表输出线程安全但输出不是一致快照两次 read 之间可能有很多进程创建和退出用户态看到的数据是“一段时间内的混搭”。如果实验要求展示“读取时刻的进程列表”可以改成在 open 时分配一个数组把 pid 和 comm 拷贝进去之后 read 只输出数组内容。这就是 procfs 里常见的“快照 vs 实时”取舍。struct task_snapshot { int pid; char comm[TASK_COMM_LEN]; }; static struct task_snapshot *snap; static int snap_count; static int snap_open(struct inode *inode, struct file *file) { struct task_struct *p; int n 0; kfree(snap); snap NULL; snap_count 0; rcu_read_lock(); for_each_process(p) n; rcu_read_unlock(); snap kzalloc(n * sizeof(*snap), GFP_KERNEL); if (!snap) return -ENOMEM; rcu_read_lock(); for_each_process(p) { snap[snap_count].pid p-pid; strscpy(snap[snap_count].comm, p-comm, TASK_COMM_LEN); snap_count; } rcu_read_unlock(); return single_open(file, snap_show, NULL); }这种写法把数据采集和格式化输出分离open 时采样一次show 时只做纯格式化读到的内容一致性更好。代价是 open 成本变高而且长期打开的 fd 不会感知新进程。真实 /proc 采取的是按需读取和动态遍历实验里你能把“为什么选快照”讲清楚就已经比单纯贴遍历代码深入一层。5. proc 文件系统实验的编译、验证与三个必查点5.1 最小 Makefile 与内核头文件内核模块不能像普通用户态程序一样直接 gcc 编译必须借助当前内核的 build 目录。下面这个 Makefile 是通用模板obj-m nova_proc.o nova_proc-objs : main.o tasks.o KERNELDIR ? /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) all: $(MAKE) -C $(KERNELDIR) M$(PWD) modules clean: $(MAKE) -C $(KERNELDIR) M$(PWD) clean把main.c和tasks.c放在同目录执行make生成nova_proc.ko。不同发行版装内核开发包的命令不一样Ubuntu 20.04.6 LTS 这类 Debian 系用apt install linux-headers-$(uname -r)Red Hat 系、openEuler 以及麒麟、UOS 等国产发行版一般用dnf install kernel-devel kernel-headers部分银河麒麟版本还需要保证内核版本与 headers 完全对应连uname -r末尾的构建号都不能差。加载和卸载命令就三条sudo insmod nova_proc.ko ls /proc/nova cat /proc/nova/tasks sudo rmmod nova_proc如果 cat 能列出进程列表说明 procfs 的注册、VFS 回调和 seq_file 输出链路全部打通。rmmod不报 busy说明没有进程持有节点文件描述符proc_remove正常执行。5.2 失败时先检查这三处编译过不去先看是不是把struct proc_ops写成了struct file_operations或者把.proc_read写成了.read。这两个错误在 Linux 5.6 以上的内核编译时直接报错报错信息会指到结构体定义附近但不会告诉你“应该用 proc_ops”容易绕弯路。插入模块报Invalid module format绝大多数是内核 headers 版本和运行内核不一致。用modinfo nova_proc.ko查看vermagic再对比uname -r两者必须完全一样如果启用了安全启动还会遇到签名校验失败需要在固件里关闭 Secure Boot 或给模块签名。加载成功但 cat 报Permission denied看目录和文件的 mode。proc 节点注册时传 0 或 0444对非 root 用户是只读的如果proc_create时把 mode 写成了0200任何普通用户读都会失败连 root 之外的所有编辑器打开都会报权限错误。另外/proc/nova目录由proc_mkdir默认创建其权限通常是 0555文件权限由文件自己控制不要混为一谈。5.3 用 strace 确认读路径走的是 proc 回调验证 proc 文件系统是否真的按预期工作可以用 strace 观察系统调用序列。拿cat /proc/nova/version为例strace -e openat,read,close cat /proc/nova/version输出里会看到openat(AT_FDCWD, /proc/nova/version, O_RDONLY) 3随后几次read(3, ...)最后一次 read 返回 0。这段输出证明用户态确实把路径交给了 VFSVFS 通过 proc 文件系统定位到了节点。如果这里 open 返回ENOENT说明注册没有生效或目录层级不对如果 read 返回EINVAL多半是 seq_file 回调里起始位置或迭代器返回了非法值。实际排错还可以配合 dmesg 看pr_err输出。一个实用的技巧是在每个回调入口加一行pr_info记录 open、read、release 的调用顺序加载后立刻cat再dmesg | tail就能看到 proc 文件系统在用户态动作背后的回调时序。这个方法对理解 file 接口生命周期很有帮助。6. 触发式 proc 节点把读文件变成一次内核控制命令proc 的另一个常见设计用途是“控制面”不是读数据而是写命令。比如一个模块维护内部计数器用户态写reset清空写dump触发日志输出。实现时在proc_ops里增加.proc_write回调里用copy_from_user接收数据然后执行对应动作。static ssize_t cmd_write(struct file *fp, const char __user *buf, size_t len, loff_t *off) { char cmd[16]; if (len sizeof(cmd)) return -EINVAL; if (copy_from_user(cmd, buf, len)) return -EFAULT; cmd[len] \0; if (strncmp(cmd, reset, 5) 0) counter 0; else if (strncmp(cmd, dump, 4) 0) pr_info(counter%lu\n, counter); else return -EINVAL; return len; }写回调执行在内核进程上下文可以做加锁、遍历、触发延迟工作但不要在cmd_write里做长时间睡眠或大块内存分配。注意len可能包含换行符比较前最好用strim去掉首尾空白。*off是否需要更新取决于设计如果每次 write 都当成一条独立命令就不递增如果模拟文件追加语义就按常规文件处理。很多偷偷摸摸的代码把控制逻辑写在读回调里导致 cat 一个只读节点也会改变内核状态这种副作用对调用方完全不可预期实验里应该避免。触发式节点的价值在于把 proc 文件系统从“数据出口”变成“接口面”。你可以把一张参数表映射为/proc/nova/param写一行配置读回当前配置也可以让节点只对特定用户组开放写权限。做到这一步proc 文件系统的实现就不再是打印几条字符串而是具备了一个内核和用户态之间双向交互的最小闭环。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →