news 2026/10/21 5:48:32

Linux内核struct file深度解析:从VFS到驱动实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux内核struct file深度解析:从VFS到驱动实战

在Linux内核里,struct file可能是你写驱动、做文件系统或者排查内核问题时最常打交道的结构体之一。很多初学者把它和文件描述符(fd)混在一起,或者把它当作inode的另一种说法,结果看代码时常常一脸懵。这篇文章我想从实际使用角度出发,把struct file的定位、字段、生命周期以及典型坑位都梳理一遍,内容偏向内核模块开发和VFS相关调试,适合刚接触内核编程、或者写驱动时被file指针坑过的朋友。

我先说结论:struct file代表的是一个“已打开的文件实例”。一次open()调用,在内核里会创建这样一个对象,它记录你当前打开时的状态——读写位置、权限模式、函数操作表、私有数据等等。fd只是进程文件描述符表里的一个数字索引,它最终指向的是struct file。同一个进程可以有两个fd指向同一个struct file(比如dup),多个进程也可能共享同一个struct file(比如fork之后),所以它本质上和“文件在哪”(inode)是两码事,也和“路径怎么解析”(dentry)是两码事。

1. 先搞清楚 struct file 在 VFS 里的位置

1.1 从 open() 到 fd 的内核路径

用户程序执行int fd = open("/dev/xxx", O_RDWR)时,内核并不是直接返回一个数字,而是先经过系统调用层进入VFS。大致路径是:open()->do_sys_open()->do_filp_open()->path_openat()。这个过程中,内核会通过路径查找拿到对应的dentry和inode,然后调用alloc_file()或get_empty_filp()分配一个新的struct file对象,并且让这个对象和路径解析出来的dentry/inode建立关联。分配完成后,才会在进程的fdtable里找一个空闲的fd,把struct file指针放进fdtable中,最终把fd号返回给用户态。

这里有一个容易忽略的点:如果路径上某个组件不存在、或者权限检查失败,open会提前返回错误,此时struct file可能已经被分配但又被释放,也可能根本没走到分配这一步。所以你在file_operations的open回调里看到的file指针,已经是分配好并且和inode绑定完的对象。驱动代码里不要在open回调中做“判断inode类型”这种太晚的事情,比如想区分是主设备号还是子设备,更好的位置是在init阶段就明确绑定关系。

1.2 struct file、struct dentry、struct inode 的关系

把这个搞明白,VFS八成就算入门了。可以把进程想成餐厅顾客,fd是等餐时拿到的取餐牌,struct file是服务员手里的餐盘,dentry是菜单上的菜名条目,inode是后厨里那道菜的真实配料和做法。

取餐牌(fd)只是数字,一个顾客可以拿多个牌子,但可能对应同一个餐盘;餐盘(struct file)上面可能同时插着多个取餐牌,这就是dup/fork共享的场景;菜单条目(dentry)负责路径解析,同一个文件路径被打开很多次,dentry一般只有一个缓存对象;菜谱(inode)表示真实的磁盘索引节点,它不随open次数变化。

生命周期上,fd随进程描述表存在,进程死了fd就没了;struct file随引用计数存在,引用归零才释放;dentry和inode由VFS缓存管理,和是否打开没有直接关系,可能很久之后才被回收。所以,你在驱动里保存了一个file指针,绝对不等于文件“还在打开”,因为引用计数可能已经被清掉了,这就是很多驱动崩溃的源头。

2. 核心字段拆解:你真正需要关注的

2.1 f_mode、f_flags

struct file里有几个字段长得像但含义不同。以4.x内核为例,f_mode是fmode_t类型,记录这个文件实例支持的能力,比如FMODE_READ、FMODE_WRITE,这些位由VFS根据open的O_RDONLY/O_WRONLY/O_RDWR推断并设置。f_flags则是int类型,保存用户打开文件时传入的标志,比如O_CREAT、O_APPEND、O_NONBLOCK,也包括之后通过fcntl修改的状态标志。

为什么要分两个字段?因为能力位(f_mode)是内核用来判断“这个file对象允不允许读/写”的,属于内部安全判断;而f_flags反映用户期望的行为方式,比如O_APPEND决定每次写之前是否把位置挪到末尾。驱动里判断是否能读,应该检查f_mode & FMODE_READ,而不是看f_flags & O_RDONLY,因为O_RDONLY是0,直接按位判断会出错。这个坑我在review代码时见过不止一次。

2.2 f_pos 和 f_version

f_pos是当前文件的读写位置,loff_t类型,通常初始化为0。需要特别注意:多线程共享同一个struct file时,f_pos是共享的,所以两个线程同时read一个fd,读到的是连续的数据,而不是从同一个位置读到相同数据。这是很多并发程序读出乱序数据的根源。如果你想每次读写独立位置,应该用pread/pwrite,它们通过参数传入文件偏移,不依赖f_pos。

f_version这个字段看起来低调,却跟NFS这样的网络文件系统有很大关系。它用来标记file实例的“版本号”,比如一个文件被修改后,VFS中对应缓存可能需要失效。文件系统可以在每次状态变化时递增file_inode(file)->i_version,然后和file->f_version比较,判断自己的dentry缓存是否过期。对于普通驱动,f_version基本不用关心,但写文件系统时就会发现它是处理缓存一致性的重要字段。

2.3 f_op 与 private_data

f_op是指向struct file_operations的指针,这是struct file实现多态的关键。同一个inode可能对应不同的file_operations,比如字符设备inode打开时,VFS会根据设备号找到cdev,再把cdev->ops赋给file->f_op。之后所有read/write/ioctl调用都会通过file->f_op跳到对应函数。写驱动时,一般在模块初始化阶段注册file_operations,并在open回调中把设备相关的私有数据保存到file->private_data。

private_data是void*类型,最典型的用途是让驱动在一个open实例内保持状态。比如你的设备支持多路复用,open一次就代表一个新的连接,那可以在open时分配一个结构体保存接收缓冲区、标志位、等待队列,然后挂到file->private_data上。后续read/write直接从private_data里取出来用。这里有个硬性要求:类型一定要一致。我看到过有人在open里存struct a,在read里却强转成struct b,结果系统日志打一堆故障、模块直接段错误。建议定义统一的私有结构体,并且用container_of或直接类型转换时做一层防御检查,至少保证open之后才执行后续操作。

2.4 f_count 引用计数与 fd 的关系

f_count是atomic_long_t类型,记录这个struct file被引用的次数。每次通过fd访问时,内核的fget()会递增引用计数;每次调用fput()或fdput()会递减。当计数归零时,VFS会调用file->f_op->release()(也就是驱动里的release/close函数),然后释放这个file对象。

需要澄清一个常见误解:f_count不是“被多少个进程打开”,而是“当前持有该file指针的引用数量”。同一个进程里同一个fd可能被syscall内部多次引用,比如打开时先拿一次,进行I/O时再拿一次。驱动开发中不要自己直接修改f_count,因为里面的原子增量减量涉及锁和释放逻辑,手动改容易造成泄漏或UAF。如果需要长期保存file指针,应该用get_file(file)(本质是atomic_inc)和fput(file)配对使用。

2.5 其他字段:f_cred、f_owner、f_ra

f_cred保存打开文件时的进程凭据,包括uid、gid、安全上下文。VFS和LSM(安全模块)在做权限校验时经常使用它,而不是直接看当前进程的cred,因为文件打开后的安全策略应该基于打开时的身份。f_owner用于异步I/O通知,表示哪个进程、哪个进程组拥有这个文件,当设备产生事件时内核可以通过信号通知它。f_ra是file_ra_state结构,管理页面缓存预读的状态,普通块设备和文件系统会用到。在字符驱动里这些字段很少直接操作,但调试时看到它们别觉得奇怪。

3. 生命周期和引用计数管理

3.1 打开时如何分配和初始化

内核分配struct file时,函数路径大概是alloc_empty_file()->__alloc_file()。它会从filp_cachep内存池分配一片对象,执行简单的初始化,比如设置f_count为1、f_mode/f_flags、把f_op初始化为&def_file_ops。之后路径解析完成、打开具体设备或文件时,VFS再用真实的文件操作表替换f_op。文件系统或驱动的open方法正是在这个阶段被调用的。

在驱动的open回调中,你拿到的struct file已经具备基本合法性,但是private_data是空的,你需要自己决定要不要设置。很多新手在open里只保存到全局变量,而不用private_data,这在单设备场景能跑,但一旦同一个驱动管理多个实例(多个设备节点或者多个打开连接),全局变量就会互相覆盖。正确做法是每次open时分配独立的状态块,挂到file->private_data。对应的,在release回调中释放这个状态块,并把private_data置空,防止悬垂指针。

3.2 close/dup 时的引用计数变化

当进程执行close(fd),内核先通过fd号拿到struct file指针,然后从fdtable中清除这条记录,最后调用fput()递减引用计数。如果此刻计数仍然大于0,说明还有别的fd或别的机制持有它,那么文件不会真正关闭,release()回调也不会执行,只会把那些内部持有的状态更新一下。

dup的情形很典型:复制fd时,内核调用get_file()把新的fd指向同一个struct file,同时递增f_count。这样两个fd共享同一个file对象、共享同一个f_pos。关闭其中一个,只减一次引用,file还继续存在,直到所有fd都close干净,release才会执行。理解这一点对处理“last close”逻辑很重要,比如串口设备中最后一次关闭时要关闭底层硬件、释放DMA缓冲区,你需要在release回调里检查是否真的是最后释放,而不是每关一个fd就做一次破坏性操作。

3.3 严防UAF:引用与锁的配合

驱动开发中最经典的UAF场景是这样的:你在一个read调用里把file指针保存到了全局链表,想着后续某个工作队列要唤醒它;结果用户立刻close了fd,引用计数归零,struct file被释放;后面工作队列访问这个全局链表里的指针,一用就炸。解决办法是在保存指针时使用get_file()拿一个额外的引用,用完了再fput()释放,保证在持有期间对象不被销毁。

另外,遍历进程打开文件也是高危操作。如果直接拿task->files->fdt->fd[i]访问,不想办法处理并发的close,就可能拿到一个已经释放的file指针。内核源码里通常用rcu_read_lock()和fcheck()来获取fd对应的file指针,因为fdtable保护的是RCU语义。不建议驱动模块自己遍历fdtable,能用get_files_struct、fcheck_task等现成API就别自己造轮子,锁和生命周期问题真的很容易翻车。

4. 实战:自己动手观察和操作 struct file

4.1 在驱动里正确使用 file->private_data

写一个典型字符驱动时,file_operations通常长这样:

static int my_open(struct inode *inode, struct file *file) { struct my_dev *dev = container_of(inode->i_cdev, struct my_dev, cdev); struct my_open_state *st; st = kzalloc(sizeof(*st), GFP_KERNEL); if (!st) return -ENOMEM; st->dev = dev; init_waitqueue_head(&st->read_wait); mutex_init(&st->lock); file->private_data = st; return 0; } static ssize_t my_read(struct file *file, char __user *buf, size_t count, loff_t *pos) { struct my_open_state *st = file->private_data; // 使用 st->dev 和 st->lock 等字段 ... }

这里最关键的是open里保存了private_data,后续所有回调都从file指针取。这样做的好处是:如果同一个设备节点被打开两次,两个open实例拥有各自独立的缓冲区,互不干扰。release里也要对应地kfree,别泄漏。

有些驱动把设备对象本身直接放到private_data,比如file->private_data = dev,也不是不行,但缺点是多实例打开时共享同一份数据,加锁会很频繁。更稳妥的做法是每个open实例建立一层“会话状态”,里面再指向设备对象本身。

4.2 用 bpftrace 观察 open 时的 file 分配

想要快速理解struct file的创建和释放过程,不需要写内核模块,我习惯用bpftrace挂几个kprobe。比如在alloc_empty_file函数入口打印调用栈,或者在__fput看释放路径。

bpftrace -e 'kprobe:alloc_empty_file { printf("alloc file pid=%d name=%s\n", pid, comm); stack(); }'

这种方式很适合排查谁在频繁打开文件导致file对象泄漏。如果看到很多alloc但对应的__fput没有发生,那基本可以断定有引用没释放。当然,生产环境能不能用bpftrace取决于权限和内核配置,至少开发机上很好用。

4.3 用 crash 命令查看当前对象引用

在内核崩溃转储或者渗透式调试时,crash工具非常有效。假设你找到某个struct file的地址,可以打印它的字段:

struct file 0xffff888123450000

输出中会看到f_count的值。如果它是一个很大的数字,说明引用泄漏,可能来自某个内核模块持有引用没有释放。再配合search命令寻找所有task结构里fdtable指向该地址的fd,可以判断有哪些进程还在使用。手动排查把f_count和“fd数量”对不上时,值得怀疑的方向包括:有内核线程持有引用、有socket关联、有epoll或者io_uring借用的引用。

5. 常见问题与排查技巧实录

5.1 引用泄漏导致的 file 无法释放

一个典型的故障表现是:系统运行几天后报“too many open files”,但用户态进程的fd数并没有超标。检查/proc/slabinfo里的filp项,会发现active对象特别多。原因往往是某个内核模块在持有struct file时只保存了指针,没有用get_file()增加引用,或者用了get_file()但没有在释放路径上调用fput()。

排查技巧:先看filp缓存计数是否持续增长;再用bpftrace统计__fput和fget调用次数;最后在可疑模块代码里审计“保存file指针”的位置,确保每一处获取引用都有配对释放。记住一个原则:谁把引用计数加一,谁负责减一。如果模块里需要跨线程传递file指针,一定要显式get_file(),别指望持有者后来帮你补。

5.2 共享 f_pos 引发的数据错乱

用户程序多线程同时读写同一个fd,结果发现数据重复或顺序错乱,这通常是共享f_pos的问题。由于所有fd可能指向同一个struct file,f_pos就是公共的,两个线程同时read,A读了100字节,B接着读到的是从100字节开始的数据,双方都无法预期。解决办法是使用pread/pwrite,它们不走f_pos,而是直接用参数偏移。驱动侧看到loff_t *pos其实传的是file->f_pos或者临时偏移变量,不要假设调用方一定改动了它。还有一种典型场景:用户用了dup复制fd,期望两边独立位置,实际上共享同一位置,这也是struct file对象共享造成的,注意别把fd复制当成文件复制。

5.3 private_data 类型错乱导致的崩溃

一个真实案例:某驱动在open里存了一个struct input_state,在ioctl里却直接强转成struct device_state,结果访问偏移字段时访问到错误内存,系统直接报Oops。排查时先看崩溃地址和访问对象是否在kmalloc分配的范围内,再看类型是否匹配。我的经验是给每个私有状态结构体增加一个魔力数字(magic number),比如初始化时写上STATE_MAGIC,在每次访问前检查magic,如果不匹配就打印日志,能快速定位mis-typed。

5.4 持续关注 f_op 的变化

有些虚拟设备文件会在open时动态替换file->f_op,比如根据打开方式选择不同的实现。这种写法虽然合法,但容易导致后续其他模块误判。比如你原本期望的file->f_op指向驱动A,后来被替换成驱动B,那么之前缓存的某个操作索引可能无效。调试时打印file->f_op的值,能帮助你确认调用路径是否如预期。我建议尽量少在open之后修改f_op,除非你有很强的理由,并且要做好同步。

5.5 struct file 常用字段速查表

字段类型典型用途注意事项
f_modefmode_t文件能力标志,判断可读可写用FMODE_READ/FMODE_WRITE,别和O_RDONLY混
f_flagsint用户打开标志/状态标志O_APPEND、O_NONBLOCK等,fcntl也会修改
f_posloff_t当前文件读写位置共享前要lock,多线程注意乱序
f_opstruct file_operations*文件操作表可能在open后被替换,调试时留意
private_datavoid*驱动/文件系统私有数据open里设置,release里释放,注意类型
f_countatomic_long_t引用计数不要手动赋值,用get_file/fput
f_credconst struct cred*打开时凭据用于安全校验,勿随意使用
f_ownerstruct fown_struct异步信号属主用于SIGIO等异步通知
f_rastruct file_ra_state预读状态块设备/文件系统关注

这张表可以作为你调试时的快速参考,实际内核版本不同字段顺序可能变化,但语义基本稳定。

回头说个人体会。我一开始看struct file,总觉得它和fd是一对一的关系,直到遇到dup和fork共享位置的问题才彻底理解“file是打开实例”这个含义。现在排查文件相关bug时,我习惯先问自己三个问题:这个file是从哪里来的引用?它的生命周期谁在管理?还有没有可能被别的机制共享?把这三个问题想清楚,大部分和struct file相关的坑都能避开。最后分享一个小技巧:调试时给get_file和fput挂上kprobe,打印file地址和调用栈,比单纯看日志能更快定位引用泄漏。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 5:48:21

Ubuntu 18.04自建本地镜像源:apt-mirror内网部署全流程实战

我们搞运维的,最烦的就是给一堆机器装环境的时候走公网,要么慢得要死,要么干脆断网没法装。Ubuntu 18.04系统到现在还有不少老项目在用,如果你手头刚好有几十台机器要按同样的软件包列表部署,与其一台台去apt update蹭…

作者头像 李华
网站建设 2026/10/21 5:47:57

浙江省新苗计划申请书模板:创新立项申请书写作指南与避坑技巧

简介:这份资源是浙江省大学生科技创新活动计划(新苗人才计划)创新立项申请书的Word模板,面向准备申报该计划的高校本科生与指导教师,帮助解决申报书格式不规范、栏目填写无参考、项目论证思路不清等问题。压缩包内共1个…

作者头像 李华
网站建设 2026/10/21 5:47:11

S7-1500杀菌机项目:一级二级代码步骤引擎设计思路与实现

我从头到尾用S7-1500做杀菌机项目,其实是去年的事情。当时甲方给的一句话很直白:后续换产品、调工艺,操作工在触摸屏上改代码就行,程序只能动一次。这就逼着我把整套杀菌控制逻辑拆成一张“代码表”,再用一级代码和二级…

作者头像 李华
网站建设 2026/10/21 5:47:10

从零搭建eBPF内核调试开发环境:内核检查、工具链到verifier排错

如果你最近在调研eBPF,大概率会在第一步就卡住:教程说跑一个 minmal 示例,你复制粘贴、敲下 make,结果是一整屏报错。BPF 这套东西的 API 本身并不复杂,复杂的是它脚下那套环境——内核版本、BTF、编译链、挂载点、权限…

作者头像 李华
网站建设 2026/10/21 5:47:00

机器人导航地图模块设计:从栅格到落盘的全流程实践

做机器人导航的朋友应该都有过这种经历:底盘能跑、雷达能转,但真到了建图那一步,各种妖魔鬼怪就全冒出来了。我手头这套代号里带个 lingbot 的导航工程,最初地图代码是直接写进主程序的,建图、存图、定位用图全缠在一起…

作者头像 李华
网站建设 2026/10/21 5:46:59

Tomcat 7.0.108 Windows部署指南:JDK6/7兼容的稳定Web容器

简介:本资源是专为64位Windows系统提供的Apache Tomcat 7.0.108官方二进制发行版,面向Java Web初学者、高校教学实践者及轻量级项目部署开发者,解决Servlet/JSP应用本地运行、调试与教学演示环境搭建问题。压缩包共640个文件,含22…

作者头像 李华