2. Linux内核文件操作结构
在 Linux 内核中,一切皆文件。字符设备、块设备、网络套接字乃至普通文件,都通过统一的文件操作接口与用户空间交互。理解 struct file、struct inode 和 file_operations 这三个核心结构体,是编写设备驱动的基础。它们分别代表了打开的文件实例、文件的元数据以及操作函数表。
- struct file:每个 open() 系统调用创建一个实例,保存当前读写位置、标志位、私有数据等。
- struct inode:每个文件(设备节点)只有一个,保存权限、大小、设备号等静态信息。
- file_operations:函数指针集合,驱动通过它向 VFS 注册 read、write、poll、ioctl 等回调。
2.1 struct file:打开的文件实例
struct file 是内核中表示一个已打开文件描述符的核心结构。每当用户进程调用 open() 时,内核就会分配一个新的 struct file 实例。该实例的生命周期从 open() 开始,到 close() 结束。多个进程可以同时打开同一个设备文件,每个进程拥有独立的 struct file 实例。
struct file {
struct path f_path; // 文件路径信息
struct inode *f_inode; // 指向关联的 inode
const struct file_operations *f_op; // 文件操作函数表(驱动核心)
unsigned int f_flags; // 文件打开标志(O_RDONLY, O_NONBLOCK 等)
loff_t f_pos; // 当前读写偏移量(对设备驱动通常无意义)
void *private_data; // 私有数据指针(驱动最常用字段)
struct address_space *f_mapping; // 地址映射(用于页缓存)
atomic_long_t f_count; // 引用计数
// ... 其他字段
};
在非阻塞 I/O 驱动开发中,f_flags 和 private_data 是最常使用的两个字段:
- f_flags:驱动通过检查
filp->f_flags & O_NONBLOCK来判断用户空间是否以非阻塞模式打开设备。这是实现非阻塞 read/write 的关键。 - private_data:驱动通常在
open()方法中将自定义的设备结构体指针赋值给filp->private_data,后续的 read、write、poll 等操作通过该指针访问设备状态。
2.2 struct inode:文件的元数据
struct inode 是 VFS 中代表文件本身的数据结构。与 struct file 不同,inode 在文件系统中是唯一的——同一个设备节点无论被打开多少次,都只对应一个 inode 实例。它存储了文件的静态属性,如权限、大小、设备号、操作函数表等。
struct inode {
umode_t i_mode; // 文件类型与权限(S_IFCHR | 0666)
kdev_t i_rdev; // 设备号(主设备号 + 次设备号)
loff_t i_size; // 文件大小(设备文件通常为 0)
const struct file_operations *i_fop; // 默认文件操作(open 时复制到 file->f_op)
void *i_private; // 私有数据指针(驱动常用)
unsigned long i_ino; // inode 编号
atomic_t i_count; // 引用计数
struct list_head i_devices; // 设备链表
// ... 其他字段
};
在字符设备驱动中,inode 最重要的作用是获取设备号和关联私有数据:
- i_rdev:通过
imajor(inode)和iminor(inode)宏提取主次设备号,用于区分同一驱动下的不同设备实例。 - i_private:驱动可以在
open()中通过container_of或直接赋值,将自定义设备结构体挂载到 inode 上,实现设备实例的查找。
| 对比维度 | struct file | struct inode |
|---|---|---|
| 生命周期 | open() 创建,close() 销毁 | 文件创建时生成,删除时销毁 |
| 实例数量 | 每个打开操作一个实例 | 每个文件只有一个实例 |
| 存储内容 | 动态状态(位置、标志、私有数据) | 静态元数据(设备号、权限、大小) |
| 驱动关注点 | f_flags(非阻塞标志)、private_data | i_rdev(设备号)、i_private |
| 引用方式 | 通过 fd 查找 | 通过 dentry 或 file->f_inode 访问 |
2.3 file_operations:驱动行为函数表
file_operations 是驱动与 VFS 之间的契约。它是一个包含大量函数指针的结构体,驱动通过填充这些指针来注册自己的 read、write、poll、open、release 等回调函数。当用户空间调用系统调用时,VFS 会通过 file->f_op 找到对应的驱动函数并执行。
struct file_operations {
struct module *owner; // 通常设为 THIS_MODULE
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
__poll_t (*poll) (struct file *, struct poll_table_struct *);
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
// ... 其他可选函数指针
};
对于非阻塞 I/O 和 poll 机制,以下三个函数指针最为关键:
- open:初始化设备,分配资源,设置
filp->private_data。 - read / write:实现数据读写,必须检查
filp->f_flags & O_NONBLOCK来决定是否阻塞。 - poll:实现 poll/select/epoll 机制的核心,返回设备当前可读/可写状态,并将等待队列加入 poll_table。
2.4 三者协作:从 open 到 poll 的完整链路
理解三个结构体如何协作,是掌握驱动开发的关键。以下是一个典型的非阻塞 I/O 场景中,从用户空间调用 open() 到 poll() 的完整链路:
- 用户调用 open("/dev/mydev", O_RDWR | O_NONBLOCK):VFS 根据路径找到 dentry 和 inode。
- VFS 分配 struct file 实例:将
f_flags设置为O_RDWR | O_NONBLOCK,f_inode指向设备 inode。 - VFS 调用 inode->i_fop->open(inode, filp):驱动在
my_open()中初始化设备,并将设备结构体指针存入filp->private_data。 - 用户调用 poll(fd, &fds, timeout):VFS 通过
filp->f_op->poll(filp, poll_table)调用驱动的my_poll()。 - 驱动在 my_poll() 中:检查设备状态(如缓冲区是否有数据),调用
poll_wait()将等待队列加入 poll_table,返回POLLIN | POLLRDNORM等掩码。
- 混淆 file 与 inode:不要在
inode中存储每个打开实例的动态状态(如当前读写位置),应该使用file->private_data。 - 忽略 f_flags 检查:在
read()和write()中必须检查filp->f_flags & O_NONBLOCK,否则非阻塞模式会退化为阻塞行为。 - f_op 赋值时机:字符设备驱动通常在
cdev_init()时设置fops,但也可以在open()中动态替换filp->f_op(如实现设备节点差异化)。
2.5 典型驱动模板:open 中的初始化模式
以下是一个标准的字符设备驱动 open() 实现,展示了 inode 和 file 的典型用法:
// 自定义设备结构体
struct my_device {
struct cdev cdev;
spinlock_t lock;
wait_queue_head_t read_wait;
char buffer[1024];
int data_ready;
};
// open 回调
static int my_open(struct inode *inode, struct file *filp)
{
// 从 inode 中获取设备结构体(通过 container_of 或 cdev 宏)
struct my_device *dev = container_of(inode->i_cdev,
struct my_device, cdev);
// 将设备结构体指针存入 file 的私有数据区
filp->private_data = dev;
// 检查非阻塞标志(可选,用于记录日志)
if (filp->f_flags & O_NONBLOCK)
pr_info("Device opened in non-blocking mode\n");
return 0;
}
// file_operations 定义
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
.write = my_write,
.poll = my_poll,
.release = my_release,
};
通过这种模式,驱动在 open() 中建立了 inode → 设备结构体 → file 私有数据 的完整链路,后续所有操作都可以通过 filp-private_data 快速访问设备状态。