大家好,我是王鸽,这篇文章主要是介绍回调函数在驱动的实际中使用,具体有哪些驱动使用过着,另外说的是任何技术都是为了产品服务,不要为了学习技术而学习。一、Linux 驱动中回调函数的核心应用场景
回调函数在驱动中最核心的价值是:将 “通用框架逻辑” 和 “具体硬件逻辑” 解耦(内核框架定义回调接口,驱动开发者实现具体回调函数)。常见场景包括:
- 设备模型回调(probe/remove、suspend/resume);
- 总线驱动回调(I2C/SPI 设备匹配、通信回调);
二、典型场景 1:中断处理中的回调(最常用)
硬件中断发生时,内核中断框架会调用驱动注册的中断处理回调函数,这是驱动中最典型的回调场景。
代码示例:GPIO 中断回调
#include<linux/module.h>#include<linux/platform_device.h>#include<linux/interrupt.h>#include<linux/gpio.h>// 定义设备私有数据(存储中断号、GPIO号等)struct gpio_dev_data { int gpio_num; // GPIO 编号 int irq_num; // 中断号 // 自定义回调:上层逻辑可注册该回调处理中断事件 void (*irq_callback)(int gpio, void *data); void *callback_data; // 回调函数的私有数据};// 内核中断处理函数(框架回调驱动的核心函数)staticirqreturn_tgpio_irq_handler(int irq, void *dev_id){ struct gpio_dev_data *dev_data = (struct gpio_dev_data *)dev_id; // 检查自定义回调是否注册,若注册则调用(驱动内的二级回调) if (dev_data->irq_callback) { dev_data->irq_callback(dev_data->gpio_num, dev_data->callback_data); } // 打印中断信息(调试用) dev_info(&platform_device->dev, "GPIO %d 中断触发\n", dev_data->gpio_num); return IRQ_HANDLED;}// 驱动提供的“注册中断回调”接口(给上层/其他模块调用)voidgpio_register_irq_callback(struct gpio_dev_data *dev_data, void (*cb)(int, void *), void *data) { dev_data->irq_callback = cb; dev_data->callback_data = data;}EXPORT_SYMBOL_GPL(gpio_register_irq_callback);// 设备 probe 函数(驱动初始化)staticintgpio_dev_probe(struct platform_device *pdev){ struct gpio_dev_data *dev_data; int ret; // 1. 分配私有数据 dev_data = devm_kzalloc(&pdev->dev, sizeof(*dev_data), GFP_KERNEL); dev_data->gpio_num = 18; // 示例:GPIO18 // 2. 请求 GPIO 并配置为中断模式 ret = gpio_request(dev_data->gpio_num, "irq-gpio"); if (ret < 0) { dev_err(&pdev->dev, "GPIO 请求失败\n"); return ret; } // 3. 将 GPIO 映射为中断号 dev_data->irq_num = gpio_to_irq(dev_data->gpio_num); // 4. 注册中断回调(核心:告诉内核中断发生时调用 gpio_irq_handler) ret = request_irq(dev_data->irq_num, // 中断号 gpio_irq_handler, // 中断回调函数(核心) IRQF_TRIGGER_RISING, // 上升沿触发 "gpio-irq-driver", // 中断名称 dev_data); // 传递给回调的私有数据 if (ret < 0) { dev_err(&pdev->dev, "注册中断失败\n"); goto err_gpio; } platform_set_drvdata(pdev, dev_data); return 0;err_gpio: gpio_free(dev_data->gpio_num); return ret;}// 设备 remove 函数staticintgpio_dev_remove(struct platform_device *pdev){ struct gpio_dev_data *dev_data = platform_get_drvdata(pdev); free_irq(dev_data->irq_num, dev_data); // 释放中断 gpio_free(dev_data->gpio_num); // 释放 GPIO return 0;}// 平台驱动结构体(内核框架回调 probe/remove)static struct platform_driver gpio_dev_driver = { .probe = gpio_dev_probe, // 设备匹配时回调 .remove = gpio_dev_remove, // 设备卸载时回调 .driver = { .name = "gpio-irq-driver", .owner = THIS_MODULE, },};module_platform_driver(gpio_dev_driver);MODULE_LICENSE("GPL");MODULE_DESCRIPTION("Linux 驱动中断回调示例");
驱动加载→probe 初始化(申请 GPIO / 注册中断)→中断触发(执行内核回调 + 自定义回调)→驱动卸载→remove 释放资源;用gpio_dev_data封装私有数据(GPIO 号、中断号、回调函数、回调私有数据),通过回调解耦 “中断底层处理” 和 “上层业务逻辑”;关键说明
- 核心回调关系
request_irqgpio_irq_handlergpio_register_irq_callback开放给上层的回调注册接口,提升驱动复用性,允许上层 / 其他模块注册自定义回调,中断发生时会自动调用这个自定义回调;
- 驱动内部又定义了
irq_callback 自定义回调 —— 这是驱动回调上层逻辑的函数(解耦中断处理和业务逻辑)。
- 私有数据传递
request_irq 的最后一个参数 dev_data 会被传递到中断回调函数中,用于在回调中访问设备私有数据(驱动开发的通用技巧)。 - 回调函数约束中断回调函数(
irq_handler_t 类型)必须符合固定原型:
typedef irqreturn_t (*irq_handler_t)(int irq, void *dev_id);- 返回值必须是
IRQ_HANDLED(处理成功)或 IRQ_NONE(未处理)。
gpio_irq_handler:是内核调用的“一级回调”,必须遵循irqreturn_t(*)(int,void*)格式;dev_id:就是request_irq时传入的dev_data,用来在中断处理函数中获取 GPIO 号、回调函数等信息;自定义回调irq_callback:驱动提供的 “二级回调”,允许上层模块(比如应用层 / 其他驱动)注册自己的逻辑,无需修改中断处理核心代码,体现了回调的灵活性。看到EXPORT_SYMBOL_GPL(gpio_register_irq_callback);说明其他模块可通过gpio_register_irq_callback接口注册自定义回调,流程如下:voidmy_custom_callback(int gpio, void *data) { printk(KERN_INFO "自定义回调:GPIO%d触发中断,私有数据:%s\n", gpio, (char*)data);}// 注册gpio_register_irq_callback(dev_data, my_custom_callback, "my_data");
那么中断触发时,会先执行my_custom_callback,再打印驱动的日志。
| | | |
|---|
| | gpio_irq_handler/probe/remove | |
| | | |
资源释放顺序:先释放中断(依赖 GPIO),再释放 GPIO,符合内核资源管理的 “申请逆序释放” 原则。三、典型场景 2:总线驱动中的回调(I2C/SPI 为例)
Linux 总线驱动框架(I2C/SPI)大量使用回调函数,框架定义 probe/remove 等接口,驱动开发者实现具体逻辑。
代码示例:I2C 设备驱动回调
#include<linux/module.h>#include<linux/i2c.h>// I2C 设备读寄存器回调(驱动内部通用逻辑)staticinti2c_dev_read_reg(struct i2c_client *client, u8 reg, u8 *val){ struct i2c_msg msgs[2] = { { .addr = client->addr, .flags = 0, .len = 1, .buf = ® }, { .addr = client->addr, .flags = I2C_M_RD, .len = 1, .buf = val }, }; int ret = i2c_transfer(client->adapter, msgs, 2); return ret == 2 ? 0 : -EIO;}// 1. probe 回调:设备树/ID 匹配时内核调用staticinti2c_dev_probe(struct i2c_client *client, conststruct i2c_device_id *id){ u8 val; dev_info(&client->dev, "I2C 设备匹配成功,地址:0x%02x\n", client->addr); // 调用内部回调读取寄存器 if (i2c_dev_read_reg(client, 0x00, &val) == 0) { dev_info(&client->dev, "读取寄存器 0x00 = 0x%02x\n", val); } return 0;}// 2. remove 回调:设备卸载时内核调用staticinti2c_dev_remove(struct i2c_client *client){ dev_info(&client->dev, "I2C 设备卸载\n"); return 0;}// 3. suspend/resume 回调:休眠/唤醒时调用(可选)staticinti2c_dev_suspend(struct device *dev){ dev_info(dev, "I2C 设备休眠\n"); return 0;}staticinti2c_dev_resume(struct device *dev){ dev_info(dev, "I2C 设备唤醒\n"); return 0;}// 定义休眠唤醒回调结构体static const struct dev_pm_ops i2c_dev_pm_ops = { .suspend = i2c_dev_suspend, .resume = i2c_dev_resume,};// I2C 设备 ID 表static const struct i2c_device_id i2c_dev_id[] = { { "my-i2c-dev", 0 }, { }};MODULE_DEVICE_TABLE(i2c, i2c_dev_id);// I2C 驱动结构体(注册各类回调)static struct i2c_driver i2c_dev_driver = { .driver = { .name = "my-i2c-dev", .owner = THIS_MODULE, .pm = &i2c_dev_pm_ops, // 休眠唤醒回调 }, .probe = i2c_dev_probe, // 匹配回调 .remove = i2c_dev_remove, // 卸载回调 .id_table = i2c_dev_id, // ID 匹配表};// 注册 I2C 驱动(内核会回调 probe)module_i2c_driver(i2c_dev_driver);MODULE_LICENSE("GPL");
关键说明
- 框架级回调
i2c_driver 中的 probe/remove 是内核 I2C 子系统定义的回调接口,当总线上匹配到设备时,内核自动调用 probe;卸载设备时调用 remove。 - 回调函数原型约束I2C probe 回调必须符合
i2c_probe_func_t 类型:
typedef int (*i2c_probe_func_t)(struct i2c_client *client, const struct i2c_device_id *id);
- 驱动开发者只需实现逻辑,无需关心 “何时被调用”(由内核框架管理)。
四、典型场景 3:异步处理回调(工作队列)
驱动中禁止在中断上下文执行耗时操作,通常将耗时逻辑封装为回调函数,交给工作队列异步执行。
代码示例:工作队列回调
#include<linux/module.h>#include<linux/workqueue.h>#include<linux/interrupt.h>// 定义工作队列和工作项static struct workqueue_struct *my_wq;static struct work_struct my_work;// 私有数据struct work_data { int count; char msg[32]; struct work_struct my_work; // 必须包含work_struct成员 };static struct work_data wd = { .count = 0, .msg = "工作队列回调", .my_work = WORK_STRUCT_INITIALIZER(work_handler) // 可选,也可以用INIT_WORK};// 工作队列回调函数(耗时操作放在这里)staticvoidwork_handler(struct work_struct *work){ struct work_data *data = container_of(work, struct work_data, my_work); data->count++; printk(KERN_INFO "工作队列回调执行:count = %d, msg = %s\n", data->count, data->msg); // 模拟耗时操作(如硬件配置、数据处理) msleep(100);}// 中断顶半部(快速处理,触发工作队列)staticirqreturn_tirq_handler(int irq, void *dev_id){ // 将工作项提交到工作队列(异步执行 work_handler 回调) queue_work(my_wq, &wd.my_work); return IRQ_HANDLED;}staticint __init my_driver_init(void){ // 1. 创建工作队列 my_wq = create_singlethread_workqueue("my-wq"); if (!my_wq) return -ENOMEM; // 2. 初始化工作项(绑定回调函数)(绑定wd的my_work成员) INIT_WORK(&wd.my_work, work_handler); // 3. 注册中断(省略:中断触发后调用 irq_handler,进而触发工作队列回调) return 0;}staticvoid __exit my_driver_exit(void){ // 刷新工作队列(等待回调执行完成) flush_workqueue(my_wq); // 销毁工作队列 destroy_workqueue(my_wq);}module_init(my_driver_init);module_exit(my_driver_exit);MODULE_LICENSE("GPL");
中断顶半部 + 工作队列底半部的异步处理模式(把耗时操作从中断上下文移到进程上下文)中断触发时,顶半部(irq_handler) 快速响应(只做最精简的操作:提交工作项),立即返回,不阻塞中断;工作队列(my_wq) 异步执行底半部(work_handler),处理耗时操作(比如 msleep 模拟的硬件配置 / 数据处理);用container_of(从工作项指针反向推导包含它的私有数据结构体指针)关联工作项和私有数据,通过回调实现 “中断触发→异步执行耗时逻辑” 的解耦。驱动初始化(创建队列 + 初始化工作项)→ 中断触发(顶半部提交工作项)→ 工作队列异步执行底半部(处理耗时逻辑)→ 驱动卸载(刷新 + 销毁队列);其中create_singlethread_workqueue("my-wq")创建名为my-wq的单线程工作队列,内核会为这个队列创建一个内核线程,专门处理提交的工作项;INIT_WORK(&my_work, work_handler)核心!把my_work工作项和work_handler回调函数绑定,后续queue_work提交这个工作项时,就会执行work_handler;代码中 “注册中断” 部分省略,但逻辑上是告诉内核:某个中断触发时调用irq_handler。关键说明
- 回调解耦中断顶半部(
irq_handler)只做快速处理,耗时逻辑交给工作队列回调(work_handler),避免阻塞中断。 - 中断上下文(顶半部)有严格限制:不能睡眠(比如
msleep)、不能执行耗时操作、不能调用可能阻塞的函数。工作队列把耗时操作移到 “进程上下文”(内核线程),避开这些限制。 - 容器宏使用
container_of 是驱动中获取回调私有数据的核心宏,通过工作项指针反向获取包含它的结构体指针。 - 代码中
container_of(work, struct work_data, my_work)看起来绕,实际是: - 已知
work是struct work_data里my_work成员的指针; - 通过这个指针反向算出整个
work_data结构体(wd)的指针,从而能访问count和msg; - 这是 Linux 内核中 “从成员找结构体” 的标准写法,目的是关联工作项和私有数据。
- 异步执行的本质
queue_work只是 “提交任务”,不会等待work_handler执行完成,顶半部和底半部是两个独立的执行上下文(中断上下文 vs 进程上下文)。 - 关键易错点
container_of的第三个参数必须是结构体中work_struct成员的名字;卸载驱动前必须flush_workqueue,避免残留工作项;中断顶半部绝对不能执行耗时 / 睡眠操作,所有慢逻辑都放工作队列。
五、Linux 驱动回调函数的核心注意事项
回调函数原型必须严格匹配内核框架定义的回调(如中断处理、probe)有固定原型,返回值 / 参数不匹配会导致编译错误或内核崩溃。例如:
- 中断回调返回值必须是
irqreturn_t,不能是 int; - I2C probe 回调第一个参数必须是
struct i2c_client *。
上下文约束(核心!)
| | | |
|---|
| | 休眠(msleep)、申请内存(GFP_KERNEL)、耗时操作 | |
| | | |
| | | |
驱动自定义的回调(如示例中的 irq_callback),调用前必须检查是否为 NULL,否则会触发内核 OOPS:if (dev_data->irq_callback) { dev_data->irq_callback(...); // 安全调用}
若回调函数被多线程 / 中断调用,需加锁保护共享数据:spinlock_t irq_lock; // 自旋锁(中断上下文用)// 回调中加锁spin_lock(&irq_lock);dev_data->count++; // 共享数据操作spin_unlock(&irq_lock);
EXPORT_SYMBOL 控制回调可见性若回调函数需要被其他模块调用,需用 EXPORT_SYMBOL_GPL 导出;仅内部使用则无需导出,避免符号污染。
总结
- 核心价值Linux 驱动中回调函数是 “框架与驱动、驱动与上层” 解耦的核心,内核框架定义回调接口,驱动实现具体逻辑;
- 典型场景中断处理、总线设备 probe/remove、工作队列异步处理是最常用的回调场景;
- 关键约束回调原型必须匹配、严格遵守执行上下文规则、做好空指针检查和锁保护;
- 核心技巧通过
dev_id/container_of 传递私有数据,是驱动回调中获取上下文的通用方法。
回调函数是 Linux 驱动模块化设计的灵魂,掌握不同场景下的回调用法,能大幅提升驱动的可维护性和兼容性。
谢谢点赞阅读文章!