Linux内核学习24--驱动通信(TODO)

发布时间:2026/8/7 9:08:57
Linux内核学习24--驱动通信(TODO) 1 内核驱动间通信的5种方式根据不同的业务场景内核驱动通信通常采用以下几种极简模式1. 导出符号 函数回调最直接、最常用原理驱动 A 使用EXPORT_SYMBOL(my_func)将函数暴露给全局驱动 B 声明后直接调用。场景就像前面分析的 Pinctrl/SPI 驱动通信、或者网卡驱动调用物理层PHY驱动接口。2. 内核事件通知链Notifier Chains原理发布-订阅模式Observer Pattern。驱动 A 注册一个观察者驱动 B 发生某个事件如电源状态改变、网络链路断开时广播通知内核会自动按顺序调用注册的回调函数。场景系统电源管理休眠通知、CPU 调频事件广播。3. 内核异步事件网关内核异步通知 / Workqueue原理驱动 A 向驱动 B 传递一个数据结构如struct work_struct然后唤醒驱动 B 的工作队列或 Tasklet 进行处理。场景中断下半部Bottom Half数据处理、异步 DMA 完成通知。4. 共享内存指针直接数据交换原理驱动 A 分配一块内存 Buffer把指针甚至包含读写锁直接传给驱动 B。场景音视频 Buffer 传递如 V4L2 框架中的 dma-buf 共享。5. 内核总线/子系统中介如 Pinctrl, I2C, SPI 框架原理驱动 A 和 B 都不直接认识对方双方都通过内核 Subsystem Core 的标准接口交互也就是我们前面代码示例实现的my_core.ko模式。2 导出符号函数回调代码provider.c#include linux/module.h #include linux/kernel.h int my_add(int a, int b) { return a b; } /* * 导出给其他ko使用 */ EXPORT_SYMBOL(my_add); static int __init provider_init(void) { printk(provider loaded\n); return 0; } static void __exit provider_exit(void) { printk(provider unloaded\n); } module_init(provider_init); module_exit(provider_exit); MODULE_LICENSE(GPL);编译生成provider.ko。consumer.c#include linux/module.h #include linux/kernel.h /* * 声明外部函数 */ extern int my_add(int a, int b); static int __init consumer_init(void) { int result; result my_add(10,20); printk(result%d\n, result); return 0; } static void __exit consumer_exit(void) { printk(consumer unloaded\n); } module_init(consumer_init); module_exit(consumer_exit); MODULE_LICENSE(GPL);编译生成consumer.koinsmod provider.ko查看cat /proc/kallsyms | grep my_addj说明这个符号已经进入kernel symbol table。再insmod consumer.ko输出provider loaded result304. 如果先加载consumer例如insmod consumer.ko会失败类似Unknown symbol my_add原因内核找不到my_add5. 那modprobe为什么可以自动加载依赖因为modprobe consumer会查看modules.dep例如consumer.ko: provider.ko于是自动load provider | | load consumer6. 对应到pinctrl例子其实Linux里面就是类似TLMM kopinctrl-qcom.ko EXPORT_SYMBOL(...)或者更常见注册pinctrl_register()提供pinctrl_opsSPI kospi-qcom.ko调用pinctrl_select_state()但是注意它调用的是pinctrl core不是pinctrl-qcom.ko真实关系spi-qcom.ko | | v pinctrl core (内核公共代码) | | v pinctrl-qcom.ko类似consumer.ko | | v exported API | | provider.ko但是Linux框架通常再加一层抽象。7. 还有一种更常见注册回调很多Linux驱动不是直接EXPORT_SYMBOL而是Astruct xxx_ops ops { .set xxx_set, }; register_xxx(ops);Bops-set();比如GPIOpinctrlregulatorclkreset controller大量使用这种方式。原因比直接函数调用更灵活。所以总结简单ko互调A.ko func() EXPORT_SYMBOL(func) ↓ B.ko extern func() 调用Linux驱动框架consumer driver | | framework API | | provider driver | | hardware你前面理解的SPI → pinctrl → TLMM其实就是后面这种更高级、更解耦的版本。你现在已经把Linux驱动模型的核心串起来了。下面用一组最精简、标准的 Linux 内核 C 语言代码模拟这种“提供者Provider- 核心框架Core- 消费者Consumer”的三个.ko模块交互机制。你可以把它们看作是TLMM 驱动、内核 Pinctrl 核心框架与SPI 驱动的极简缩影。1. 核心框架模块my_core.ko(充当内核 Pinctrl 框架)它定义统一的标准接口维护函数指针并导出 API 符号EXPORT_SYMBOL供其他两个.ko调用。my_core.h(头文件)C#ifndef _MY_CORE_H_ #define _MY_CORE_H_ /* 定义 Provider 需要实现的操作回调函数结构体 */ struct my_provider_ops { int (*set_pin_mode)(int pin, int mode); }; /* 暴露给 Provider 的注册 API */ int register_my_provider(struct my_provider_ops *ops); /* 暴露给 Consumer 的服务 API */ int my_core_set_pin(int pin, int mode); #endifmy_core.cC#include linux/module.h #include linux/init.h #include my_core.h MODULE_LICENSE(GPL); /* 全局变量保存 Provider 注册上来的函数指针 */ static struct my_provider_ops *g_ops NULL; /* 1. 供 Provider (.ko) 调用的注册 API */ int register_my_provider(struct my_provider_ops *ops) { g_ops ops; pr_info([Core] Provider 成功注册函数指针!\n); return 0; } EXPORT_SYMBOL(register_my_provider); /* 2. 供 Consumer (.ko) 调用的服务 API */ int my_core_set_pin(int pin, int mode) { if (!g_ops || !g_ops-set_pin_mode) { pr_err([Core] 错误: 没有 Provider 注册!\n); return -ENODEV; } pr_info([Core] 收到 Consumer 请求准备转发给 Provider...\n); /* 核心关键通过函数指针间接调用 Provider 的代码 */ return g_ops-set_pin_mode(pin, mode); } EXPORT_SYMBOL(my_core_set_pin); static int __init my_core_init(void) { pr_info([Core] my_core 框架加载成功\n); return 0; } static void __exit my_core_exit(void) { pr_info([Core] my_core 框架卸载\n); } module_init(my_core_init); module_exit(my_core_exit);2. 硬件提供者模块provider_tlmm.ko(模拟 TLMM 驱动)它负责直接操作“硬件”加载时向my_core登记自己的回调函数。provider_tlmm.cC#include linux/module.h #include linux/init.h #include my_core.h MODULE_LICENSE(GPL); /* 真实的硬件操作函数写入寄存器 */ static int real_hardware_set_pin(int pin, int mode) { pr_info([TLMM Provider] 真实写底层硬件寄存器: 管脚 GPIO%d 配置为模式 %d\n, pin, mode); return 0; } /* 填充操作回调 */ static struct my_provider_ops tlmm_ops { .set_pin_mode real_hardware_set_pin, }; static int __init provider_init(void) { pr_info([TLMM Provider] 驱动加载中...\n); /* 向 Core 注册自己 */ register_my_provider(tlmm_ops); return 0; } static void __exit provider_exit(void) { pr_info([TLMM Provider] 驱动卸载\n); } module_init(provider_init); module_exit(provider_exit);3. 硬件消费者模块consumer_spi.ko(模拟 SPI 驱动)它完全不知道provider_tlmm.ko的存在只需要调用my_core.h提供的标准函数。consumer_spi.cC#include linux/module.h #include linux/init.h #include my_core.h MODULE_LICENSE(GPL); static int __init consumer_init(void) { pr_info([SPI Consumer] 驱动 Probe 初始化...\n); /* Consumer 完全不认识 TLMM只调用 Core 暴露的统一 API */ pr_info([SPI Consumer] 请求将 GPIO19 设置为 CS0 (模式 3)\n); my_core_set_pin(19, 3); return 0; } static void __exit consumer_exit(void) { pr_info([SPI Consumer] 驱动卸载\n); } module_init(consumer_init); module_exit(consumer_exit);4. 统一编译的MakefileMakefileobj-m my_core.o obj-m provider_tlmm.o obj-m consumer_spi.o KDIR ? /lib/modules/$(shell uname -r)/build all: make -C $(KDIR) M$(PWD) modules clean: make -C $(KDIR) M$(PWD) clean5. 运行效果与日志验证按照依赖顺序依次加载这三个.ko模块Bash# 1. 先加载核心框架 sudo insmod my_core.ko # 2. 再加载 Provider (TLMM) sudo insmod provider_tlmm.ko # 3. 最后加载 Consumer (SPI) sudo insmod consumer_spi.ko通过dmesg查看内核打印日志输出如下Plaintext[Core] my_core 框架加载成功 [TLMM Provider] 驱动加载中... [Core] Provider 成功注册函数指针! [SPI Consumer] 驱动 Probe 初始化... [SPI Consumer] 请求将 GPIO19 设置为 CS0 (模式 3) [Core] 收到 Consumer 请求准备转发给 Provider... [TLMM Provider] 真实写底层硬件寄存器: 管脚 GPIO19 配置为模式 3总结代码逻辑consumer_spi.ko和provider_tlmm.ko没有头文件依赖consumer_spi.c内部没有引用任何 TLMM 的头文件只包含了my_core.h。函数指针作为桥梁my_core.ko内部保存了g_ops函数指针consumer_spi.ko调用 API 时my_core.ko通过指针跳转到provider_tlmm.ko的代码中执行。完美解耦如果以后换了一个平台用provider_mtk.ko替换掉provider_tlmm.koconsumer_spi.ko的代码和编译过程一行都不用改

相关新闻