当前位置: 首页 > java >正文

深入理解 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,它把 openwriterelease 等操作函数“挂”到了设备上。以后应用层调用相应的系统调用时,内核就会根据这个表找到驱动里的对应函数。

四、和 C++ 虚函数表的类比

如果你熟悉 C++,会发现这和“虚函数表”的机制非常类似。

  • 在 C++ 里,一个含有虚函数的类,编译器会为它生成一张“虚函数表”(vtable),里面存放虚函数的地址。通过基类指针调用虚函数时,实际上就是查这张表,然后跳转到对应的实现。

  • 在 Linux 驱动里,file_operations 就是一张“函数指针表”。当用户空间调用系统调用时,内核就会查这张表,然后调用驱动里对应的函数。

两者的共同点是:

  • 都是通过函数指针实现“动态绑定”。
  • 都允许外部调用在运行时决定真正执行哪个函数。

区别在于:

  • C++ 的虚函数表是编译器自动生成的。
  • Linux 驱动的 file_operations 完全由程序员手动填写。

可以说,Linux 驱动就是用最原始的 C 语言机制,手工实现了类似 C++ 多态的功能

五、总结

  1. C 语言里可以用函数指针保存函数地址,并通过指针调用函数。
  2. 函数指针可以放在结构体中,形成一套“操作方法表”。
  3. Linux 内核提供的 struct file_operations 就是这样一张“方法表”,它把应用层的系统调用映射到驱动里的具体实现函数。
  4. file_operations 和 C++ 的虚函数表非常类似,本质上都是通过函数指针来实现运行时的动态调用。

(完)

http://www.xdnf.cn/news/19294.html

相关文章:

  • 学习Python中Selenium模块的基本用法(11:弹窗处理)
  • Day18_【机器学习—交叉验证与网格搜索】
  • 【ROS2】ROS2 基础学习教程 、movelt学习
  • PostgreSQL 数据库灾备要点与举例说明**
  • Spring Data Redis 的使用方法
  • 电子战:多功能雷达工作模式识别
  • [光学原理与应用-339]:ZEMAX - Spot Diagram(点列图)是评估光学系统成像质量的核心工具,它通过几何光线追迹直观展示像差对成像的影响。
  • 模拟实现STL中的list容器
  • 行内元素块元素
  • Coze源码分析-API授权-添加新令牌-后端源码
  • mysql权限user表赋权操作修改
  • 【大语言模型 30】指令微调数据工程:高质量数据集构建
  • 计算机算术7-浮点基础知识
  • 面试tips--MyBatis--<where> where 1=1 的区别
  • Burgan Bank Türkiye 如何借助 Elastic 改造可观测性和安全性
  • 【LeetCode 热题 100】62. 不同路径——(解法四)组合数学
  • Scikit-learn Python机器学习 - Scikit-learn加载数据集
  • 49.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--扩展功能--集成网关--Refit跨服务调用
  • Photoshop - Ps Camera Raw 滤镜
  • 爱普生L3255打印机故障记录
  • 算法(②排序算法)
  • 在word以及latex中引用zotero中的参考文献
  • JVM架构图是怎样的?
  • Python - 机器学习:从 “教电脑认东西” 到 “让机器自己学规律”
  • 第7.5节:awk语言 switch 语句
  • Kubernetes 部署与发布完全指南:从 Pod 到高级发布策略
  • Ruoyi-vue-plus-5.x第一篇Sa-Token权限认证体系深度解析:1.3 权限控制与注解使用
  • Python爬虫实战:构建Widgets 小组件数据采集和分析系统
  • c++--线程休眠/sleep
  • springboot提前注册bean