简单来说,底层的核心逻辑是基于C语言的“多态”实现,通过函数指针表 (Function Pointer Table) 进行的“间接调用”。
内核本身(VFS层)并不“认识” 你的 my_led_write 函数。它只知道一个通用的“合同”或“接口”(即 struct file_operations),你的驱动在注册时,把自己的函数地址“登记”在了这份合同上。
当用户调用 write() 时,内核会通过一系列的查找,最终找到你登记的那个函数地址,然后调用它。
🏛️ 底层逻辑的三大组件
内核的这种设计,依赖于三个关键的结构体,它们共同构成了这个调用框架:
1. struct file_operations (fops)
这是**“合同” (Interface)**。
-
它是什么? 这是一个包含大量函数指针的结构体(如
.open,.read,.write,.ioctl等)。 -
它的作用? 它定义了一个字符设备可以被如何操作的所有标准方法。内核只关心这个“合同”,不关心是谁(哪个驱动)来实现它。
-
你的角色: 在你的 LED 驱动中,你定义了一个
led_fops变量,并填充了你实现的函数(如.write = my_led_write)。
2. struct cdev (字符设备)
这是**“登记处” (Registry)**。
-
它是什么? 代表一个“字符设备”的内核抽象。
-
它的作用? 它的核心作用是将一个设备号 (dev_t) 与一份**“合同” (
file_operations)** 绑定在一起。 -
你的角色: 你在
init函数中调用cdev_init(&led_cdev, &led_fops),这一步就是**“绑定”;然后调用cdev_add(),这一步就是“向内核注册”**,告诉内核:“嘿,设备号 240:0 的操作方法,请参考led_fops!”
3. struct file 和 struct inode
这是**“上下文” (Context)**。
-
inode(索引节点): 代表一个文件系统中的文件(如/dev/my_led)。它在内核中是唯一的。它包含了这个文件的元数据,最重要的是,它包含了设备号 (dev_t)。 -
file(打开的文件): 代表一个进程打开的文件实例。当用户open()一个文件时,内核会创建一个struct file。- 最关键的一点: 在
open的过程中,内核会根据inode里的设备号,通过cdev登记处,找到你驱动的file_operations(即led_fops),然后将这个led_fops的地址存储到新创建的struct file对象的f_op字段中。
- 最关键的一点: 在
⛓️ 完整调用链 (以 write() 为例)
现在,我们把一个用户空间的 write() 调用串联起来,看看内核是如何一步步找到你的 my_led_write 函数的。
前提: 你的驱动已加载 (my_led_init 已执行)。alloc_chrdev_region 分配了设备号 (假设 240:0),cdev_add 已将 led_cdev (绑定了 led_fops) 注册到内核。
阶段一:用户空间 open() (准备工作)
-
用户空间: 应用程序调用
fd = open("/dev/my_led", O_RDWR); -
系统调用: 触发
sys_open()系统调用,进入内核空间。 -
VFS (内核): 内核的 VFS (虚拟文件系统) 层开始工作。
-
它沿着路径名找到
/dev/my_led对应的inode。 -
VFS 发现这个
inode是一个字符设备文件 (S_IFCHR)。 -
VFS 从
inode中读取设备号 (dev_t),即 240:0。
-
-
cdev查找: VFS 使用这个设备号 240:0,去cdev登记处(一个哈希表)查找。 -
找到驱动: VFS 找到了你注册的
led_cdev! -
创建
file对象: 内核创建一个新的struct file对象来代表这个打开的实例。 -
核心步骤 (绑定): 内核从
led_cdev中取出file_operations指针 (即led_fops),并将其赋值给file->f_op。
// VFS 内部的伪代码
struct file *new_file = create_file_struct();
new_file->f_op = led_cdev->ops; // led_cdev->ops 就是你的 led_fops
-
调用驱动的
open: VFS 检查file->f_op->open是否存在。如果存在(即你的led_fops.open),就调用它。 -
返回
fd:open完成,sys_open返回文件描述符fd给用户空间。这个fd在进程中唯一标识了第 6 步创建的struct file对象。
阶段二:用户空间 write() (执行调用)
-
用户空间: 应用程序调用
write(fd, "1", 1); -
系统调用: 触发
sys_write()系统调用,进入内核空间。 -
VFS (内核):
-
VFS 使用
fd找到对应的struct file对象 (就是阶段一第 6 步创建的那个)。 -
VFS 检查这个
struct file对象,确保它可写。
-
-
核心分发 (Dispatch): VFS 执行类似如下的逻辑(这是关键!):
// VFS 内部的伪代码 (sys_write -> vfs_write)
struct file *filp = find_file_by_fd(fd);
// 检查 "合同" 上有没有 write 方法
if (filp->f_op && filp->f_op->write) {
// 如果有,就调用它!
ret = filp->f_op->write(filp, user_buf, count, &filp->f_pos);
}
-
执行驱动代码:
-
因为
filp->f_op指向的就是你的led_fops。 -
所以
filp->f_op->write指向的就是你的my_led_write函数。 -
内核间接调用了你的
my_led_write函数,CPU 控制权转移到你的驱动代码。
-
-
驱动工作: 你的
my_led_write函数被执行。它使用copy_from_user拷贝数据,然后(虚拟地)点亮 LED。 -
返回: 你的
my_led_write返回1(写入的字节数)。 -
VFS (内核): VFS 拿到返回值,
sys_write随之返回。 -
用户空间: 应用程序的
write()调用返回1。
📈 调用链图示
这个流程可以简化为:
User Space -> System Call -> VFS (Generic Layer) -> Function Pointer Call -> Device Driver (Specific Code)
[用户空间]
write(fd, ...);
|
v
[内核空间 - 系统调用]
sys_write(fd, ...);
|
v
[内核空间 - VFS]
vfs_write(...) {
// 1. 根据 fd 找到 struct file *filp
struct file *filp = fget(fd);
// 2. 找到 filp->f_op (这个在 open 时已被设为 led_fops)
// 3. 执行函数指针
filp->f_op->write(filp, ...); <---- 核心跳转!
} |
|
+--------------------------------+
|
v
[内核空间 - 你的驱动]
my_led_write(struct file *filp, ...) {
// ...
// copy_from_user(...)
// 控制硬件
// ...
return count;
}
核心总结
内核调用驱动的逻辑,就是**“注册与回调”**:
-
注册 (Registration): 你的驱动
init时,通过cdev_add把一个包含函数指针的 “合同” (file_operations) 注册到内核的cdev系统中。 -
回调 (Callback): 当 VFS 需要对你的设备进行操作时,它不关心你的驱动叫什么,它只通过
file对象找到那份 “合同”,然后调用 “合同” 上约定的函数指针。