file 结构体、file_operations 结构体、inode 结构体是 Linux 内核中文件 / 设备管理的核心基石,三者分工明确、紧密协作,共同完成 "用户态访问内核态文件 / 设备" 的全流程。对于驱动开发而言,理解这三个结构体的含义、关联及用法,是实现驱动功能(如 read/write/mmap)的关键。
一、inode 结构体:文件 / 设备的 "底层元数据身份证"
1. 核心定位
inode(index node,索引节点)是内核用来描述文件 / 设备底层属性的静态元数据集合------ 相当于文件 / 设备的 "身份证",存储的是 "文件 / 设备是什么、属于谁、对应什么资源" 等固定信息,与 "文件是否被打开" 无关(一个文件 / 设备只有一个 inode,不管被打开多少次)。
对于驱动开发,设备文件(/dev 下的节点)的 inode 是关联 "设备" 与 "驱动" 的关键纽带。
- 核心成员(驱动开发重点关注)
inode 结构体定义在 <linux/fs.h> 中,成员众多,驱动开发中最核心的是以下几个:
struct inode {
umode_t i_mode; // 文件/设备类型 + 权限(核心!)
dev_t i_rdev; // 设备号(主设备号+次设备号,仅设备文件有效)
struct cdev *i_cdev; // 指向字符设备驱动的cdev结构体(仅字符设备有效)
struct super_block *i_sb; // 指向所属文件系统的超级块(如ext4、sysfs)
uid_t i_uid; // 所有者ID
gid_t i_gid; // 所属组ID
loff_t i_size; // 文件/设备内存大小(如驱动缓冲区大小)
struct timespec64 i_atime; // 最后访问时间
struct timespec64 i_mtime; // 最后修改时间
void *i_private; // 私有数据指针(驱动可自定义存储信息)
};
关键成员解读(驱动视角):
i_mode:最核心的成员之一,用掩码标识「文件 / 设备类型」和「访问权限」:
设备类型掩码:S_IFCHR(020000,字符设备,对应 /dev 下节点的第一个字符 c)、S_IFBLK(060000,块设备,对应节点的 b);
权限掩码:S_IRUSR(读权限)、S_IWUSR(写权限)等(和 chmod 命令的权限对应)。
驱动中可通过 S_ISCHR(inode->i_mode) 判断是否为字符设备,S_ISBLK() 判断是否为块设备。
i_rdev:仅设备文件(字符 / 块设备)有效,存储的是该设备的「主设备号 + 次设备号」(dev_t 类型,和之前讲的设备号完全对应)。内核通过 MAJOR(inode->i_rdev) 和 MINOR(inode->i_rdev) 提取设备号,找到对应的驱动。
i_cdev:仅字符设备有效,指向字符设备驱动的 struct cdev 结构体(cdev 是字符设备驱动的核心管理结构体,包含了驱动的 file_operations)。内核通过 inode->i_cdev 直接关联到字符设备驱动的操作接口。
i_private:驱动自定义的私有数据指针,可在驱动初始化时存储设备相关的结构体(如设备的硬件配置、缓冲区地址等),后续在 file 结构体的回调中通过 inode->i_private 访问。
- 核心作用
存储静态元数据:记录文件 / 设备的固定属性(类型、权限、设备号、所有者等),这些属性不会因文件被打开 / 关闭而变化。
关联设备与驱动:设备文件的 inode 通过 i_rdev(设备号)或 i_cdev(字符设备),让内核找到对应的驱动程序(比如字符设备通过 i_cdev 直接关联 file_operations)。
标识唯一文件 / 设备:每个文件 / 设备在文件系统中对应唯一的 inode(通过 inode 号区分),即使文件名被修改,inode 号和元数据也不变。
- 驱动场景应用示例
字符设备驱动中,注册 cdev 时,会将 cdev 结构体与 inode 关联:
struct cdev my_cdev;
cdev_init(&my_cdev, &my_fops); // 绑定file_operations
cdev_add(&my_cdev, dev, 1); // dev是设备号,将cdev与inode的i_cdev关联
当用户 open("/dev/mychr") 时,内核通过设备节点找到对应的 inode,再通过 inode->i_cdev 找到驱动的 file_operations。
二、file 结构体:文件 / 设备的 "动态访问上下文"
1. 核心定位
file 结构体是内核用来描述 **"被打开后的文件 / 设备实例"** 的动态数据集合 ------ 相当于 "当前访问的通行证",存储的是 "当前如何访问、访问到哪里" 等动态状态,每次调用 open() 函数打开文件 / 设备,内核都会创建一个新的 file 结构体(即使是同一个文件 / 设备,多次 open 会生成多个 file)。
对于驱动开发,file 结构体是「用户态与驱动交互的核心上下文载体」,驱动的所有操作回调(read/write/mmap)都会接收 file 结构体指针作为参数。
- 核心成员(驱动开发重点关注)
file 结构体同样定义在 <linux/fs.h> 中,核心成员如下:
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path; // 指向文件的路径(包含inode指针)
const struct file_operations *f_op; // 指向驱动的file_operations结构体(核心!)
loff_t f_pos; // 文件读写偏移量(动态变化)
unsigned int f_flags; // 打开文件时的标志(如O_RDWR、O_NONBLOCK)
unsigned int f_mode; // 访问模式(如FMODE_READ、FMODE_WRITE)
void *private_data; // 驱动私有数据指针(驱动开发最常用!)
struct file_ra_state f_ra; // 预读状态(优化IO性能)
struct address_space *f_mapping; // 指向文件的地址空间(关联inode)
};
关键成员解读(驱动视角):
f_op:核心中的核心!指向该文件 / 设备对应的 file_operations 结构体(驱动实现的操作接口集合)。内核通过 file->f_op->read 找到驱动的 read 回调函数,这是用户态系统调用映射到驱动实现的关键。
f_pos:文件读写偏移量(单位:字节),动态变化:
比如用户调用 read(fd, buf, 100) 后,f_pos 会自动增加 100;
驱动可通过修改 file->f_pos 控制读写位置(如 lseek 系统调用的实现);
每个 file 结构体的 f_pos 独立(多次 open 的不同进程,读写偏移量互不影响)。
f_flags:打开文件时的标志,由 open() 函数的参数指定,驱动需根据标志处理逻辑:
常用标志:O_RDWR(可读可写)、O_RDONLY(只读)、O_WRONLY(只写)、O_NONBLOCK(非阻塞模式)、O_APPEND(追加模式)。
示例:驱动的 read 回调中,若 file->f_flags & O_NONBLOCK,则需返回非阻塞状态(无数据时返回 -EAGAIN)。
private_data:驱动开发中最常用的成员!用于存储驱动的自定义设备结构体(如包含设备缓冲区、硬件寄存器地址、锁等信息的结构体),相当于 "驱动的全局变量容器":
在 open 回调中,将设备结构体指针赋值给 file->private_data;
在后续的 read/write/mmap 回调中,通过 file->private_data 快速获取设备信息,无需全局变量。
f_path:包含 struct dentry(目录项)和 struct inode *inode,通过 file->f_path.dentry->d_inode 可获取对应的 inode 结构体。
- 核心作用
记录动态访问状态:存储当前打开实例的读写偏移量、访问模式、打开标志等,确保每次访问的上下文独立。
关联驱动操作接口:通过 f_op 指向驱动的 file_operations,让内核知道如何调用驱动的具体实现。
传递驱动私有数据:通过 private_data 让驱动的不同回调函数(open/read/write)共享设备信息,是驱动模块化设计的关键。
- 驱动场景应用示例
驱动中通过 private_data 传递设备信息(最典型用法):
// 自定义设备结构体
struct my_dev {
char *buf; // 设备缓冲区
struct mutex lock; // 互斥锁(解决并发访问)
dev_t dev_num; // 设备号
};
struct my_dev my_device; // 实例化设备结构体
// open回调函数:给private_data赋值
static int mydev_open(struct inode *inode, struct file *filp) {
// 将设备结构体指针存入file的private_data
filp->private_data = &my_device;
mutex_lock(&my_device.lock); // 加锁保护
return 0;
}
// read回调函数:从private_data获取设备信息
static ssize_t mydev_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) {
struct my_dev *dev = filp->private_data; // 取出设备结构体
int ret;
// 从设备缓冲区拷贝数据到用户空间(简化)
ret = copy_to_user(buf, dev->buf + *f_pos, count);
if (ret == 0) {
*f_pos += count; // 更新读写偏移量
return count;
}
return -EFAULT;
}
三、file_operations 结构体:驱动的 "操作接口清单"
1. 核心定位
file_operations 结构体是驱动提供给内核的 "操作函数指针集合"------ 相当于驱动的 "操作说明书",定义了用户态可以对文件 / 设备执行的所有操作(如读、写、映射、关闭),以及这些操作对应的驱动实现函数。
它是「用户态系统调用」与「驱动底层实现」之间的直接桥梁:用户调用 read() 系统调用,内核最终会执行 file_operations->read 指向的驱动函数。
- 核心成员(驱动开发重点关注)
file_operations 结构体定义在 <linux/fs.h> 中,成员是一系列函数指针,驱动可根据需求实现或置为 NULL(未实现的操作会返回 -ENOTTY 错误),核心成员如下:
struct file_operations {
struct module *owner; // 驱动模块所有者(必须设为THIS_MODULE)
int (*open) (struct inode *, struct file *); // 打开文件/设备(第一个被调用)
int (*release) (struct inode *, struct file *); // 关闭文件/设备(最后一个被调用)
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); // 读数据(用户态→内核态)
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); // 写数据(内核态→用户态)
loff_t (*llseek) (struct file *, loff_t, int); // 修改读写偏移量(lseek系统调用)
int (*mmap) (struct file *, struct vm_area_struct *); // 内存映射(之前学的mmap)
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); // 设备控制(ioctl系统调用)
unsigned int (*poll) (struct file *, struct poll_table_struct *); // IO多路复用(poll/select)
int (*fasync) (int, struct file *, int); // 异步通知(信号驱动IO)
// 其他次要成员...
};
关键成员解读(驱动视角):
owner:必须设置为 THIS_MODULE(宏定义,指向当前驱动模块),作用是告诉内核 "该操作接口属于哪个模块",确保模块被使用时不会被卸载(避免驱动被卸载后,内核还调用其函数导致崩溃)。
open:用户调用 open() 时触发,是驱动中第一个被执行的回调函数。核心用途:
初始化设备硬件(如配置寄存器);
给 file->private_data 赋值;
加锁保护设备,避免并发访问冲突;
检查设备状态(如是否已被占用)。
release:用户调用 close() 时触发,是最后一个被执行的回调函数。核心用途:
释放硬件资源(如关闭寄存器);
解锁设备;
清理 private_data 中的临时数据。
read/write:用户调用 read()/write() 时触发,是数据传输的核心接口:
read:从驱动(内核态)向用户态拷贝数据,需用 copy_to_user() 函数(安全拷贝,避免用户态非法地址);
write:从用户态向驱动(内核态)拷贝数据,需用 copy_from_user() 函数;
参数中的 loff_t *f_pos 是读写偏移量,驱动需根据它确定数据位置,并更新偏移量。
mmap:用户调用 mmap() 时触发,实现内核 / 设备内存到用户虚拟地址的映射(之前详细学过,驱动需通过 remap_pfn_range() 建立映射)。
unlocked_ioctl:用户调用 ioctl() 时触发,用于设备控制(如设置设备参数、获取设备状态),支持自定义命令(如 CMD_SET_BAUD 设置串口波特率)。
- 核心作用
定义驱动的操作能力:明确用户态可以对设备执行哪些操作(如是否支持 mmap、是否支持异步通知)。
映射系统调用到驱动实现:将用户态的系统调用(如 read、ioctl)与驱动的具体函数绑定,是用户态与驱动交互的 "桥梁"。
保证驱动的模块化:驱动通过实现 file_operations 结构体,将操作接口统一暴露给内核,无需关心内核如何调用,只需专注于底层硬件交互。
- 驱动场景应用示例
驱动中初始化 file_operations 并注册:
// 实现各个回调函数(open/read/write/release/mmap)
static int mydev_open(struct inode *inode, struct file *filp) { /* 实现逻辑 */ }
static ssize_t mydev_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { /* 实现逻辑 */ }
static ssize_t mydev_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) { /* 实现逻辑 */ }
static int mydev_mmap(struct file *filp, struct vm_area_struct *vma) { /* 实现逻辑 */ }
static int mydev_release(struct inode *inode, struct file *filp) { /* 实现逻辑 */ }
// 初始化file_operations结构体
static const struct file_operations mydev_fops = {
.owner = THIS_MODULE, // 必须设置为当前模块
.open = mydev_open, // 绑定open回调
.read = mydev_read, // 绑定read回调
.write = mydev_write, // 绑定write回调
.mmap = mydev_mmap, // 绑定mmap回调
.release = mydev_release, // 绑定release回调
.llseek = no_llseek, // 不支持lseek,用内核提供的默认实现
};
// 字符设备驱动注册时,将fops与cdev关联
cdev_init(&my_cdev, &mydev_fops); // 初始化cdev,绑定fops
cdev_add(&my_cdev, dev_num, 1); // 注册cdev到内核
四、三大结构体的核心关联(驱动访问全流程)
理解三者的关联,才能真正掌握驱动与内核的交互逻辑。下面结合「用户访问字符设备」的完整流程,拆解三者的协作:
流程步骤(以 open("/dev/mychr") → read() → close() 为例):
用户执行****open("/dev/mychr", O_RDWR):
内核通过 /dev/mychr 设备节点,找到对应的 inode 结构体(inode 存储设备号和 cdev 指针);
内核创建一个新的 file 结构体,将 file->f_op 指向 inode 关联的 file_operations(即驱动的 mydev_fops);
内核调用 file->f_op->open(inode, file)(驱动的 mydev_open 回调),驱动在 open 中初始化设备并给 file->private_data 赋值。
用户执行****read(fd, buf, 100):
内核通过文件描述符 fd,找到对应的 file 结构体;
内核调用 file->f_op->read(file, buf, 100, &file->f_pos)(驱动的 mydev_read 回调);
驱动通过 file->private_data 获取设备结构体,从设备缓冲区读取数据,通过 copy_to_user() 拷贝到用户态 buf;
驱动更新 file->f_pos(读写偏移量),返回读取的字节数。
用户执行****close(fd):
内核通过 fd 找到 file 结构体;
内核调用 file->f_op->release(inode, file)(驱动的 mydev_release 回调),驱动释放资源、解锁;
内核销毁该 file 结构体(inode 结构体仍存在,直到文件被删除)。
关联关系总结:
- inode:是 "设备的身份证",告诉内核 "这是哪个设备、对应哪个驱动";
- file:是 "当前访问的通行证",告诉内核 "当前怎么访问、访问到哪里";
- file_operations:是 "设备的操作说明书",告诉内核 "该怎么操作这个设备";
- 协作逻辑:用户拿着通行证(file),通过身份证(inode)找到设备,按照说明书(file_operations)操作设备。
五、驱动开发中的关键注意事项
inode 的 i_cdev 与 i_rdev:
字符设备驱动必须初始化 i_cdev 并关联到 file_operations;
块设备驱动不使用 i_cdev,而是通过 i_rdev 的设备号关联驱动。
file 的 private_data 使用:
必须在 open 回调中赋值,后续回调中读取,避免使用全局变量(支持多设备实例);
若涉及并发访问,需在 open 中加锁,release 中解锁(如 mutex 锁)。
file_operations 的 owner 设置:
必须设为 THIS_MODULE,否则模块可能被误卸载,导致系统崩溃;
未实现的回调函数可置为 NULL,或使用内核提供的默认实现(如 no_llseek)。
数据拷贝安全:
read/write 回调中,必须使用 copy_to_user()/copy_from_user() 拷贝数据(内核态不能直接访问用户态地址,避免非法地址导致崩溃);
拷贝前需检查用户态地址的合法性(可通过 access_ok() 函数)。
总结
|-----------------|---------------|-----------|----------------|
| 结构体 | 核心定位 | 核心关键字 | 驱动开发核心用途 |
| inode | 文件 / 设备的静态元数据 | 身份证、设备号 | 关联设备与驱动,存储固定属性 |
| file | 打开后的动态访问上下文 | 通行证、偏移量 | 记录访问状态,传递私有数据 |
| file_operations | 驱动的操作接口清单 | 说明书、回调函数 | 映射系统调用到驱动实现 |
这三个结构体是 Linux 驱动开发的 "基石",无论字符设备、块设备还是杂项设备驱动,都离不开它们的协作。编写字符设备驱动 demo 时,就是通过初始化 file_operations、关联 inode 与 cdev、利用 file 的 private_data 传递设备信息,实现 read/write/mmap 等功能。后续编写更复杂的驱动时,核心还是围绕这三个结构体的关联与协作,只是需要根据硬件特性扩展回调函数的实现。