ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux proc文件系统实现指南:从PDE到seq_file与write回调

Linux proc文件系统实现指南:从PDE到seq_file与write回调 简介操作系统实验报告围绕 Linux 0.11 上 proc 文件系统的实现展开适合正在学习操作系统课程、需要完成类似实验或理解虚拟文件系统原理的读者。报告覆盖实验目的、实验内容、详细步骤与核心代码重点说明 psinfo 结点的实现流程包括增加文件类型、修改 mknod、初始化函数、sys_read 分支以及 proc 处理函数并给出 get_psinfo、get_hdinfo、get_inodeinfo 等代码实现。同时针对“多次 read 之间进程状态变化”的思考题提出了基于文件指针与缓冲区的处理方案对理解文件系统与进程管理有直接帮助。资源为 1 个 docx 文档共 944KB内容紧凑可直接作为实验报告撰写参考或复习资料。已有 208 人学习下载适合需要借鉴完整实验思路与代码细节的同学。1. proc文件系统内核暴露给用户态的“内容签发窗口”ls /proc时看到的数字目录、meminfo、cpuinfo 这些文件在磁盘上占用是零。每次用 cat 读它们内核都会临时执行一段函数把结果拷给你。也就是说proc 里的“文件”其实是一组回调函数的外壳。计算机操作系统课程里实验8安排 proc 文件系统的实现目的就是让你自己注册一个节点让用户态读到你从内核里取到的数据。这个实验不是 Linux 独占BSD 也有类似机制但 Linux 的 /proc 最典型也是接触 VFS、内核模块、缓冲区管理三者交汇的最短路径。就算你是从零开始手搓操作系统的选手底层也绕不开“文件系统不存数据、只存函数指针”这套抽象。2. proc文件系统的内核台面PDE、inode与proc_ops回调组2.1 不落盘的inodeproc文件系统究竟“存”了什么一个 ext4 文件要能读必须有块号、有 i_size、有数据块映射。而 procfs 的 inode 在创建时就知道自己没有任何数据块。struct inode 仍然被分配但它的 i_op、i_fop 被设置成一组 procfs 专用的函数。真正持有文件元数据的是 struct proc_dir_entryPDE它挂在父目录的链表上记录 name、mode 和一组操作指针。用户open(/proc/myfile)走到 procfs 的 inode 查找时内核按名字找到 PDE再用它填充一个新 inode。因此“文件什么时候存在”取决于“PDE 是否在目录链表上”和磁盘没有任何关系。这一点是理解 proc 文件系统实现的关键创建文件就是往链表里添加一个 PDE删除文件就是从链表上摘掉它同时等所有打开它的 fd 关闭。2.2 proc_create 与 proc_ops接口的今与昔内核 3.10 之后推荐用 proc_create 创建单个文件节点。它的完整签名在不同内核版本上有差异2.6 时代是可能从《Linux 设备驱动》第三版上看到的 create_proc_entry后来并入 proc_create。到了 5.6第四个参数从 struct file_operations 换成了 struct proc_ops。两者成员名不同file_operationsproc_ops说明openproc_openopen 时执行常用于绑定 seq_filereadproc_read把数据拷贝到用户缓冲writeproc_write接收来自用户态的写入releaseproc_release关闭时释放 seq_file 资源llseekproc_lseek调整读位置通常委托 seq_lseek写实验模块前先确认当前内核版本5.6 以上用 proc_ops更早的用 file_operations 并把 .proc_* 换成 .owner、.read、.write 等命名。你可以直接在内核头文件里确认接口是否已经切换grep -E proc_create|proc_ops /lib/modules/$(uname -r)/build/include/linux/proc_fs.h这段命令看的是当前内核源码树里 proc_create 的原型和 struct proc_ops 是否存在。返回结果里如果有#define proc_create(name, mode, parent, ops)或者 struct proc_ops 的定义说明当前内核走的是新接口。Ubuntu 22.04 默认 5.15 内核属于 proc_ops 时代。2.3 从 open 到 read 的完整调用链当用户进程执行 open 时VFS 调用 proc_reg_open。它首先把 PDE 里保存的 proc_ops 取出来挂到 file-f_op 上这一步很重要procfs 正是靠这个替换让不同节点拥有各自回调而不是统一的通用读法。之后如果存在 proc_open 回调就转调进去。read 路径同理proc_reg_read 拿到 proc_ops-proc_read。如果你用 seq_fileproc_read 就是 seq_read它内部会调用你注册的 start/show/next/stop把多次 show 的结果拼起来一次性或分多次拷贝到用户缓冲区。整条链路的尽头就是“用户态在 read内核态在跑你的函数”这正是 proc 文件系统实现实验的核心体验。3. 实现proc文件系统第一个节点proc_mkdir与proc_create3.1 模块骨架与初始化实验最稳妥的开始是先创建目录再创建文件避免直接在 /proc 根目录铺开。先看最小可编译的模块#include linux/init.h #include linux/module.h #include linux/kernel.h #include linux/fs.h #include linux/proc_fs.h #include linux/uaccess.h #define EXP_PROC_DIR exp8 #define EXP_PROC_NODE version static struct proc_dir_entry *exp_parent; static ssize_t version_read(struct file *file, char __user *buf, size_t len, loff_t *off) { char version[] exp8 v0.1\n; return simple_read_from_buffer(buf, len, off, version, strlen(version)); } static const struct proc_ops exp_version_ops { .proc_read version_read, }; static int __init exp8_init(void) { exp_parent proc_mkdir(EXP_PROC_DIR, NULL); if (!exp_parent) return -ENOMEM; if (!proc_create(EXP_PROC_NODE, 0444, exp_parent, exp_version_ops)) { proc_remove(exp_parent); return -ENOMEM; } pr_info(exp8: /proc/exp8/version is ready\n); return 0; } static void __exit exp8_exit(void) { proc_remove(exp_parent); pr_info(exp8: proc node removed\n); } module_init(exp8_init); module_exit(exp8_exit); MODULE_LICENSE(GPL);这个模块的逻辑是proc_mkdir(exp8, NULL) 在 /proc 下建目录第二个参数传 NULL 表示父节点是 /procproc_create(version, 0444, exp_parent, exp_version_ops) 在这目录下创建文件。如果任何一步失败需要把已建好的节点全部清理掉避免留下半成品。version_read 里只用 simple_read_from_buffer。它接收 buf用户态目标、len用户缓冲区大小、off当前读写位置第四个参数是内核态数据源。它会从 off 位置开始拷贝自动更新 off返回值是实际拷贝字节数也就是 read 系统调用的返回值。第一次读返回字符串长度第二次读时 off 已经越过末尾返回 0cat 看到 EOF。3.2 Makefile与加载步骤编写 Makefileobj-m : exp8.o KDIR : /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) all: make -C $(KDIR) M$(PWD) modules clean: make -C $(KDIR) M$(PWD) clean编译后sudo insmod exp8.ko装载模块然后ls -l /proc/exp8/version应当能看到权限 0444 的文件cat /proc/exp8/version显示 exp8 v0.1。卸载用sudo rmmod exp8再查看 /proc/exp8 目录应已消失。insmod 和 rmmod 是一对如果 init 过程中返回非零insmod 会直接报错并把错误码打印出来。3.3 创建失败时的排查方向proc_create 返回 NULL 最常见的原因是名字冲突。如果你在另一个模块里已经建过同名 PDE这个调用会失败。排查方法先ls /proc/exp8看是否存在再用grep -r exp8 /proc看是不是其他模块占用或者换一个前缀不常见的目录名。dmesg 里模块 init 返回非零会打印init_module from exp8.ko failed结合 pr_info 能定位失败的是哪一步。4. 给proc文件系统加动态内容seq_file四回调4.1 为什么用 show 而不是 read从多行输出看 seq_file 的价值上一节的 version 节点只能输出静态字符串实验要求往往不止这样还要输出多个条目比如从内核数组或链表中取数据。如果继续用 read 回调就得自己维护 offset 偏移、处理用户缓冲区不够大时的分次返回、以及多次 read 之间的状态保持很容易写出 bug。seq_file 是内核给 procfs 开发者准备的通用序列化接口。它不是文件系统而是一个把“遍历一个数据结构”转换成“输出到用户缓冲区”的中间层。你只需实现四个回调start、next、stop、show。seq_read 负责在用户缓冲区和这些回调之间做分页、拷贝、推进。4.2 四个回调的握手协议回调何时进入返回值生命周期义务startseq_read 需要新一段数据时返回“当前位置的对象”指针返回 NULL 终止适合拿锁、取引用show拿到位置对象之后向 seq_file 写一行返回 0 继续非 0 停止只负责 seq_printf / seq_putsnextshow 成功结束之后返回下一个对象NULL 表示遍历结束推进 pos释放临时资源stop遍历结束或出错时void释放 start 里获取的锁 / 引用seq_read 会反复调用 start/show/next直到填满一个 page然后把这一页拷给用户。如果用户用 cat 读cat 会一直 read 到返回 0seq_read 在下一次 read 时用上次保存的 pos 续传直到 next 返回 NULL。所以“一次遍历”可能被拆进好几次 read但展示给调用者的语义是连续的文件流。4.3 完整实现从内核数组输出到 proc假设实验模块内部维护一张记录表#include linux/seq_file.h struct exp_item { int id; const char *name; long value; }; static struct exp_item exp_items[] { {1, cpu, 950}, {2, mem, 2048}, {3, io, 128}, }; #define EXP_ITEMS_NUM ARRAY_SIZE(exp_items) static void *exp_seq_start(struct seq_file *s, loff_t *pos) { if (*pos EXP_ITEMS_NUM) return NULL; return exp_items[*pos]; } static void *exp_seq_next(struct seq_file *s, void *v, loff_t *pos) { (*pos); if (*pos EXP_ITEMS_NUM) return NULL; return exp_items[*pos]; } static void exp_seq_stop(struct seq_file *s, void *v) { } static int exp_seq_show(struct seq_file *s, void *v) { struct exp_item *item v; seq_printf(s, %d %s %ld\n, item-id, item-name, item-value); return 0; } static const struct seq_operations exp_seq_ops { .start exp_seq_start, .next exp_seq_next, .stop exp_seq_stop, .show exp_seq_show, };start 的入参 pos 是“第几个条目”的指针把 pos 作为数组下标直接定位元素返回元素的地址。show 负责把这个地址当作 struct exp_item *用 seq_printf 写一行。next 把 pos 加一判断是否越界。由于数组是静态数据不需要在 stop 中做清理stop 留空即可。接下来把这四个回调接到 proc 节点上static int exp_seq_open(struct inode *inode, struct file *file) { return seq_open(file, exp_seq_ops); } static const struct proc_ops exp_seq_proc_ops { .proc_open exp_seq_open, .proc_read seq_read, .proc_lseek seq_lseek, .proc_release seq_release, };exp_seq_open 里的 seq_open 创建 struct seq_file 对象挂到 file-private_data。proc_ops 里必须同时给出 .proc_read seq_read 和 .proc_release seq_release。很多人写丢 release结果 open 时 seq_file 分配的内存在 close 时没有释放模块卸载就会泄漏甚至 oops。也正是因为 seq_file 需要 open 时初始化所以不能像第 3 章那样只给 .proc_read。把 exp_seq_proc_ops 交给 proc_create 并命名节点为 tablecat /proc/exp8/table就能看到三行数据。4.4 从数组到进程列表换掉 start/next 即可如果你想把实验升级成“输出当前全部进程”只需把 start/next 中的数组换成任务链表。数组版本 pos 是数组下标链表版本 pos 是遍历计数二者都遵循“pos 做游标”这一约定。以 for_each_process 为例static void *proc_seq_start(struct seq_file *s, loff_t *pos) { struct task_struct *p; loff_t n *pos; rcu_read_lock(); for_each_process(p) { if (n-- 0) return p; } return NULL; } static void *proc_seq_next(struct seq_file *s, void *v, loff_t *pos) { struct task_struct *p v; struct task_struct *next next_task(p); (*pos); return (next init_task) ? NULL : next; }start 里把 *pos 拷贝到局部变量 n用 n 做递减避免修改 *pos 导致 next 的游标错乱。next 里 *pos再判断 next_task 是否回到 init_task 锚点回到就返回 NULL 结束遍历。show 里输出 p-pid、p-comm、p-statestop 里别忘了 rcu_read_unlock。这个版本的完整代码留给你自己补齐它和数组版的差别只在数据获取层seq_file 框架不需要改。5. 让proc文件系统接受写入write回调解析用户态数据5.1 write回调的接口约束只读节点能看内核数据实验通常还要求能改。proc 文件系统实现 write 回调的套路和 read 对称proc_ops 里加一项 .proc_write。注意 write 函数收到的是用户态缓冲区和字节数不能直接访问 buf必须用 copy_from_user 把数据搬进内核空间。这里没有“校验和”等安全机制漏掉 copy_from_user 直接解引用 buf 会让内核访问任意用户地址产生 oops。write 返回值必须是“实际消费的字节数”。用户态 write 以这个返回值为准如果返回 0echo 会认为写入失败并报错如果返回负数系统调用返回该负数对应的错误码。最直接的理解echo 123 /proc/...实际上 write 了 4 个字节其中包含末尾换行正常实现应该返回 4。5.2 完整读写模块下面这个节点让用户态可以写入一个整数并在 read 时原样输出static int exp_value 100; static ssize_t exp_value_read(struct file *file, char __user *buf, size_t len, loff_t *off) { char kbuf[32]; int n snprintf(kbuf, sizeof(kbuf), %d\n, exp_value); return simple_read_from_buffer(buf, len, off, kbuf, n); } static ssize_t exp_value_write(struct file *file, const char __user *buf, size_t len, loff_t *off) { char kbuf[32]; long val; int ret; if (len 0 || len sizeof(kbuf)) return -EINVAL; if (copy_from_user(kbuf, buf, len)) return -EFAULT; kbuf[len] \0; if (kbuf[len - 1] \n) kbuf[len - 1] \0; ret kstrtoint(kbuf, 10, val); if (ret) return ret; exp_value val; *off len; pr_info(exp8: value set to %d\n, exp_value); return len; } static const struct proc_ops exp_value_ops { .proc_read exp_value_read, .proc_write exp_value_write, };exp_value_read 用 snprintf 把当前值格式化成字符串再交给 simple_read_from_buffer 搬运。exp_value_write 第一步做缓冲区边界检查len 必须大于 0 且小于等于 kbuf 大小减一因为后面要补 \0这一步防止栈上越界写。copy_from_user 返回值是“没能拷贝的字节数”为 0 才表示完全成功因此非零直接 return -EFAULT。kstrtoint 以十进制方式解析以 nul 结尾的字符串返回 0 表示成功。它不像 strtol 那么宽容输入 abc 会干净地返回 -EINVAL而不是静默解析出 0。如果用户端用 printf 写入带 \r\n 的字符串你还需要在去掉 \n 之后判断末尾是不是 \r一并置为 \0否则 kstrtoint 会拿到 123\r 而报错。最后 *off len 让文件偏移保持一致返回 len 告诉 VFS 整个缓冲区都被消费了。创建这个节点的代码和第 3 章一样把 proc_create 的 ops 换成 exp_value_opsmode 用 0644 或 0666 都可以0666 允许普通用户直接改。装好模块后验证写路径echo 250 /proc/exp8/value cat /proc/exp8/value看到输出 250 说明写入生效。echo 999999999999超出 int 范围时会返回write error: Invalid argument这是 kstrtoint 在起作用。5.3 同时提供 read 和 write 时 proc_ops 的边界实验里容易踩的坑是一个节点要读也要写却在 proc_ops 里漏了 .proc_read。此时 echo 能写成功但 cat 拿到的可能是错误号。另一个坑是 write 回调返回长度小于 len比如输入是 250\n 时只返回 3echo 会反复把剩余的 \n 再发送一次直到全部写完日志里可能出现两次 pr_info。规范做法就是“接受多少返回多少”。6. 验证proc文件系统实现的链路dmesg、strace与新内核接口核对6.1 用 strace 验证用户态行为加载模块后如果你想确认 cat 到底怎么触发回调可以在另一个终端跑sudo strace -f -e traceopenat,read,write,close cat /proc/exp8/value输出里能看到 cat 依次调用了 openat(AT_FDCWD, /proc/exp8/value, O_RDONLY)、read 到一批字节、关闭文件。如果 read 返回 0 之后还跟了一次 read说明读回调没有正确返回“应该返回的字节数”或者 off 处理有误。用 strace 对比自定义节点和 /proc/meminfo 的系统调用序列多数返回差异能暴露实现问题。6.2 用 dmesg 追踪回调调用次数在 read/write 实现里临时插入 pr_info然后echo test /proc/exp8/value dmesg | tail -20如果 pr_info 只出现一条且 write 返回无误说明 write 是原子完成的。如果插入到 show 里cat 之后会看到多次 show 日志这是 seq_read 按页填充所致不是 bug。模块卸载前再查一次 dmesg确保没有BUG: scheduling while atomic或其他告警。6.3 版本差异与本实验直接相关的 3 个检查点第一内核 5.6 以下用 file_operations 时注册结构体成员名是 .read/.write不是 .proc_read/.proc_write编译会直接报 unknown field。第二seq_file 方案必须有 .proc_open只读静态方案可以有也可以没有但多数教材使用 seq_file 时要配齐 seq_release。第三/proc 节点权限位 mode 不要加 S_IFREGproc_create 内部自己设置 inode 类型写 0444 或 0644 即可。如果参考书还是《Linux 内核设计与实现》对应的旧内核例子注意 create_proc_entry 已经彻底删掉了。把这些检查点写进实验报告同时把加载前后ls -l /proc/exp8的权限变化、strace 的 read 长度变化贴进去。procfs 里最典型的三个现象是“能 insmod 不能读”“能读不能写”“写完重启失效”前两个在 6.3 的检查点里都能定位第三个是常态procfs 节点每次开机都要由模块重新创建因此把创建函数放进模块 init 属于标准做法。本文还有配套的精品资源点击获取
返回列表