深入理解 Linux 驱动中的 file_operations:从 C 语言函数指针到类比 C++ 虚函数表
深入理解 Linux 驱动中的 file_operations:从 C 语言函数指针到类比 C++ 虚函数表
在 Linux 字符设备驱动开发中,struct file_operations
是最核心的结构之一。它是用户空间和内核驱动之间的桥梁。很多初学者第一次看到下面这样的代码时,往往会一头雾水:
static const struct file_operations fops = {.owner = THIS_MODULE,.open = led_open,.write = led_write,.release = led_close,
};
为什么要定义这样一个结构体?为什么里面全是函数指针?这些函数什么时候会被调用?这一切其实和 C 语言的函数指针机制密切相关,甚至可以和 C++ 的虚函数表对应起来。本文就来一步步讲清楚。
一、C 语言函数指针的基础
在 C 语言里,函数本质上也是一段内存中的代码,因此函数也有自己的地址。函数指针就是用来保存函数地址的变量。
例如,我们先定义一个普通函数:
#include <stdio.h>// 定义一个普通函数,接受两个整数,返回它们的和
int add(int a, int b) {return a + b;
}
然后定义一个函数指针,并指向这个函数:
int main() {// 定义一个函数指针,能指向 “接受两个 int 参数并返回 int”的函数int (*func_ptr)(int, int);// 让函数指针指向 add 函数func_ptr = add;// 通过函数指针调用 add,相当于执行 add(2, 3)int result = func_ptr(2, 3);printf("result = %d\n", result); // 输出 result = 5return 0;
}
可以看到,函数指针就是把函数名的“地址”存到一个变量里,然后通过这个变量间接调用函数。
二、结构体中的函数指针
函数指针不仅可以单独使用,还可以放在结构体中。这样就能把一组操作方法“打包”在一个结构体里。
例如,我们模拟一个“操作”结构体:
#include <stdio.h>// 定义一个结构体,里面放两个函数指针
struct operations {void (*say_hello)(void); // 指向一个打印 hello 的函数void (*say_bye)(void); // 指向一个打印 bye 的函数
};// 定义两个具体的函数
void hello_func(void) {printf("Hello!\n");
}void bye_func(void) {printf("Bye!\n");
}int main() {// 定义一个结构体变量 ops,并填充函数指针struct operations ops = {.say_hello = hello_func,.say_bye = bye_func,};// 调用函数指针,就像虚函数一样实现了“动态调用”ops.say_hello(); // 输出 Hello!ops.say_bye(); // 输出 Bye!return 0;
}
这样,一个结构体就能代表“一套操作方法”。这和 Linux 驱动里的 struct file_operations
是完全一样的思路。
三、Linux 驱动中的 file_operations
Linux 内核中,struct file_operations
就是专门用来描述一个设备文件能做什么操作的结构体。它里面放了很多函数指针,例如:
.open
:当用户调用open("/dev/xxx")
时会执行的函数.read
:当用户调用read(fd, buf, size)
时会执行的函数.write
:当用户调用write(fd, buf, size)
时会执行的函数.release
:当用户调用close(fd)
时会执行的函数
我们来看一个实际的驱动代码片段(以 LED 灯驱动为例),并加上详细注释:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>#define DEV_NAME "led_drv"// 1. open 函数,当应用层 open("/dev/led_drv") 时调用
static int led_open(struct inode *inode, struct file *file) {printk("led_open called\n");return 0;
}// 2. write 函数,当应用层 write(fd, buf, size) 时调用
static ssize_t led_write(struct file *file, const char __user *buf,size_t count, loff_t *ppos) {printk("led_write called, user wants to send %zu bytes\n", count);return count; // 假装写成功
}// 3. release 函数,当应用层 close(fd) 时调用
static int led_close(struct inode *inode, struct file *file) {printk("led_close called\n");return 0;
}// 定义 file_operations 结构体,并把函数指针填进去
static const struct file_operations fops = {.owner = THIS_MODULE, // 一般写 THIS_MODULE,用来防止模块被卸载.open = led_open, // open 对应 led_open.write = led_write, // write 对应 led_write.release = led_close, // close 对应 led_close
};static int major; // 保存主设备号// 模块加载时执行
static int __init led_init(void) {major = register_chrdev(0, DEV_NAME, &fops); printk("led driver loaded, major = %d\n", major);return 0;
}// 模块卸载时执行
static void __exit led_exit(void) {unregister_chrdev(major, DEV_NAME);printk("led driver unloaded\n");
}module_init(led_init);
module_exit(led_exit);MODULE_LICENSE("GPL");
这段代码的核心就在于 fops
,它把 open
、write
、release
等操作函数“挂”到了设备上。以后应用层调用相应的系统调用时,内核就会根据这个表找到驱动里的对应函数。
四、和 C++ 虚函数表的类比
如果你熟悉 C++,会发现这和“虚函数表”的机制非常类似。
-
在 C++ 里,一个含有虚函数的类,编译器会为它生成一张“虚函数表”(vtable),里面存放虚函数的地址。通过基类指针调用虚函数时,实际上就是查这张表,然后跳转到对应的实现。
-
在 Linux 驱动里,
file_operations
就是一张“函数指针表”。当用户空间调用系统调用时,内核就会查这张表,然后调用驱动里对应的函数。
两者的共同点是:
- 都是通过函数指针实现“动态绑定”。
- 都允许外部调用在运行时决定真正执行哪个函数。
区别在于:
- C++ 的虚函数表是编译器自动生成的。
- Linux 驱动的
file_operations
完全由程序员手动填写。
可以说,Linux 驱动就是用最原始的 C 语言机制,手工实现了类似 C++ 多态的功能。
五、总结
- C 语言里可以用函数指针保存函数地址,并通过指针调用函数。
- 函数指针可以放在结构体中,形成一套“操作方法表”。
- Linux 内核提供的
struct file_operations
就是这样一张“方法表”,它把应用层的系统调用映射到驱动里的具体实现函数。 file_operations
和 C++ 的虚函数表非常类似,本质上都是通过函数指针来实现运行时的动态调用。
(完)