Linux 内核三大核心结构体详解(驱动开发视角)

file 结构体、file_operations 结构体、inode 结构体是 Linux 内核中文件 / 设备管理的核心基石,三者分工明确、紧密协作,共同完成 "用户态访问内核态文件 / 设备" 的全流程。对于驱动开发而言,理解这三个结构体的含义、关联及用法,是实现驱动功能(如 read/write/mmap)的关键。

一、inode 结构体:文件 / 设备的 "底层元数据身份证"

1. 核心定位

inode(index node,索引节点)是内核用来描述文件 / 设备底层属性的静态元数据集合------ 相当于文件 / 设备的 "身份证",存储的是 "文件 / 设备是什么、属于谁、对应什么资源" 等固定信息,与 "文件是否被打开" 无关(一个文件 / 设备只有一个 inode,不管被打开多少次)。

对于驱动开发,设备文件(/dev 下的节点)的 inode 是关联 "设备" 与 "驱动" 的关键纽带

  1. 核心成员(驱动开发重点关注)

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 访问。

  1. 核心作用

存储静态元数据:记录文件 / 设备的固定属性(类型、权限、设备号、所有者等),这些属性不会因文件被打开 / 关闭而变化。

关联设备与驱动:设备文件的 inode 通过 i_rdev(设备号)或 i_cdev(字符设备),让内核找到对应的驱动程序(比如字符设备通过 i_cdev 直接关联 file_operations)。

标识唯一文件 / 设备:每个文件 / 设备在文件系统中对应唯一的 inode(通过 inode 号区分),即使文件名被修改,inode 号和元数据也不变。

  1. 驱动场景应用示例

字符设备驱动中,注册 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 结构体指针作为参数。

  1. 核心成员(驱动开发重点关注)

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 结构体。

  1. 核心作用

记录动态访问状态:存储当前打开实例的读写偏移量、访问模式、打开标志等,确保每次访问的上下文独立。

关联驱动操作接口:通过 f_op 指向驱动的 file_operations,让内核知道如何调用驱动的具体实现。

传递驱动私有数据:通过 private_data 让驱动的不同回调函数(open/read/write)共享设备信息,是驱动模块化设计的关键。

  1. 驱动场景应用示例

驱动中通过 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 指向的驱动函数。

  1. 核心成员(驱动开发重点关注)

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 设置串口波特率)。

  1. 核心作用

定义驱动的操作能力:明确用户态可以对设备执行哪些操作(如是否支持 mmap、是否支持异步通知)。

映射系统调用到驱动实现:将用户态的系统调用(如 read、ioctl)与驱动的具体函数绑定,是用户态与驱动交互的 "桥梁"。

保证驱动的模块化:驱动通过实现 file_operations 结构体,将操作接口统一暴露给内核,无需关心内核如何调用,只需专注于底层硬件交互。

  1. 驱动场景应用示例

驱动中初始化 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 等功能。后续编写更复杂的驱动时,核心还是围绕这三个结构体的关联与协作,只是需要根据硬件特性扩展回调函数的实现。

相关推荐
怪我冷i4 小时前
wsl Ubuntu切换中科大源
linux·windows·ubuntu·ai编程·ai写作
FenceRain4 小时前
Linux 使用脚本删除文件
linux
QT 小鲜肉4 小时前
【Linux命令大全】001.文件管理之chgrp命令(实操篇)
android·linux·运维·笔记
qq13267029404 小时前
grafana 未授权访问漏洞设置iptables指定IP访问,拒绝其他所有IP
linux·服务器·网络·iptables·防火墙策略
春日见4 小时前
ubuntu以前可以联网,突然无法上网了
linux·服务器·ubuntu·debug
鸠摩智首席音效师4 小时前
如何在 CentOS 上设置 Apache Worker MPM ?
linux·centos·apache
Neolnfra4 小时前
SMB、FTP、MySQL... 配置不当,即是漏洞
linux·数据库·mysql·安全·网络安全·系统安全·安全架构
一直都在5724 小时前
数据结构入门:哈希表和树结构
数据结构·算法·散列表
一点晖光4 小时前
centos安装ffmpeg环境
linux·ffmpeg·centos