驱动:私有数据为什么要在三个地方各挂一遍?

写字符设备驱动的人,大概率都困惑过这件事:

我就一个私有结构体 dev,为什么要反复存来存去?platform_set_drvdata 存一遍,cdev_add 嵌进去一遍,filp->private_data 再存一遍。这不重复吗?

答案一句话:内核是分层的,每一层只认识自己的结构体,不认识你的 privet_dev(私有数据结构体) 所以你得在每一层都留个 "挂钩",把你的数据挂回去。

这篇文章用一个写字楼的例子,把这件事彻底讲明白。


先抛出问题:三个场景,三个不同的入参

你的驱动夹在好几个内核子系统中间,每个子系统回调你时,手里只攥着它自己认识的结构体。

谁调你 递给你什么 你手上没有 怎么取回 dev
platform 总线 pdev inodefilp probe函数-> platform_get_drvdata(pdev)
VFS open inode + filp pdev open函数-> container_of(inode->i_cdev, ...)
VFS read/release filp pdevinode read函数-> filp->private_data

没法只靠一种方式取回 是有数据privet_dev。在 open 里根本没有内核传给probe函数的 pdev,调不了 platform_get_drvdata;在 read 里只有参数 filp(用户层打开的私有数据指针),连 open的inode参数 都没有,更没法 container_of(container_of 从设备实例cdev内反推出用户私有数据prive_dev )

每一层只把 "它认识的那块拼图" 给你,只能顺着那块拼图找回自己的私有结构体。这就是 "存来存去" 的真正原因 ------ 不是重复,是每层各挂各的钩。


写字楼类比:四层结构一目了然

把内核想象成一栋写字楼,你的驱动就是楼里的一家公司。楼里管事的分好几拨,每拨人只认自己的东西,所以你得在每层都留个联系方式。

第一层:用户程序 ------ 来办事的租客

用户写个 test.c,调用 open("/dev/字符设备文件节点"),就像一个租客走进写字楼大堂说:"我要找 【字符设备文件节点】 这家公司。"

他只认识 "文件路径" 这个概念,根本不知道你公司内部长啥样,也不关心。

第二层:VFS 文件层 ------ 前台物业

前台(VFS)是所有文件操作的统一接待处。它不认识你的公司,它只认 "文件" 和 "访客记录"。

当租客说要找 /dev/字符设备文件节点,前台做两件事:

  1. 查到这个文件的 inode(公司档案)
  2. 给租客造一张 filp(访客通行证)

然后前台把 inode + filp 递给你的 open 函数。注意:前台不给你 内核传给probe的 pdev(营业执照),因为它压根不知道 platform 总线 (挂载所有平台设备实例的顶层总线,pdev 为单个设备实例) 那套东西存在。

所以在 open 里得做两步:先用 (结构体容器查找工具) container_of(inode->i_cdev) 从门禁卡反推出公司地址,再顺手写在 filp->private_data 通行证背面。后面 read/release 来的人只有通行证,翻背面就能看到地址。

第三层:字符设备层 ------ 门禁系统

门禁系统管理整栋楼的所有房间。你在 probe 时做的

cdev_init(&dev->cdev, &ds18b20_fops);

dev->cdev.owner = THIS_MODULE;

cdev_add(&dev->cdev, dev_num, 1);将文件操作集+设备号加入内核,等于去门禁那里登记:

"我的 cdev 门牌卡嵌在公司结构体里面,房间号是 dev_num,以后有人查这个房间号就指给我。"

open 函数只会拿到 inode->i_cdev(就是注册进去的门禁卡地址),内核不知道外面还有一层 dev 结构体。 container_of 的作用: 用内部成员cdev的地址、结构体类型、成员名,算出外层 dev 结构体的起始内存地址

cdev 为什么要嵌在私有结构体里面 ?因为这样门禁扫到 cdev 时,能container_of 算出整个公司结构体的起始地址

第四层:Platform 总线 ------ 工商局物业管理处

platform 总线负责把设备和驱动撮合到一起。设备树里 compatible 匹配上了,就调你的 probe,给你一个 pdev(营业执照副本)。

你在 probe 里做 platform_set_drvdata(pdev, 私有数据结构体privet_dev),等于在工商局的档案上写:"这家公司的地址是 XXX"。以后 remove 要拆你的时候,工商局调你的 remove 函数,递给你同一个 pdev,你用 platform_get_drvdata 取回地址就能开始清理资源。


三拨人各管各的,互不通气

回到写字楼类比,就明白为什么不能只用一个了:

  • 前台(VFS) 没有权限看工商局档案,它只管发通行证
  • 工商局(platform 总线) 不管访客进出,它只在开业 / 关店注销时出现
  • 门禁系统(cdev) 只认门牌卡,不认识营业执照也不认识访客记录

三拨人各管各的、信息不互通。作为公司老板,唯一的办法就是在每个部门专属存档位置,都留存一份指向同一套私有数据的地址。

这不是重复存储数据本体,只是多处存放同一个结构体指针,是各子系统仅识别自身结构体、只提供专属私有插槽的分层设计规则,必须按对应规范挂载。


那为什么不用全局变量 g_dev 一个搞定?

g_dev 确实能 "一个搞定"------ 任何函数直接读就行。但它有两个硬伤:

第一,多设备就崩。 板子上接两个 DS18B20(不同 GPIO)?两个设备实例,但全局变量只能存一个地址,第二个覆盖第一个,全乱。

第二,驱动规范里全局变量是反面教材。 正经写法都是 "每设备一套、随设备走",每个实例各存各的,互不干扰。

所以那些 "挂在结构体里" 的机制,本质优势就是支持多设备实例。


一张表总结:四层挂钩机制

函数 / 机制 所属分层 作用 生命周期
platform_set/get_drvdata platform 总线层 pdev->driver_data 存 / 取指针 随设备存在
cdev_add + container_of 字符设备层 把 cdev 注册进内核表,open 时靠它反推 dev 随设备存在
filp->private_data VFS 文件层 文件层的 "驱动专用抽屉" 随单次 open 存在,close 销毁
g_dev 全局变量 自定义全局 简易兜底,不支持多设备 随模块存在

1、随设备存在(platform /cdev)

生命周期绑定单个硬件设备实例 场景:板子挂载 2 路 DS18B20(两路独立设备)

  1. 第一路设备树匹配 → probe 运行,生成 dev1、pdev1、cdev1
  2. 第二路设备树匹配 → probe 运行,生成 dev2、pdev2、cdev2
  3. 操作 1:卸载其中一路设备(解绑设备树)
    • 这一路的 dev、pdev、cdev 立刻销毁
    • 另一路设备、驱动模块依然完好运行
  4. 操作 2:rmmod 卸载驱动模块
    • 所有设备的 dev、pdev、cdev 全部销毁

关键点:模块还在,单个设备可以单独销毁

2、随模块存在(全局 g_dev)

生命周期绑定整个驱动 ko 模块

  1. insmod 加载模块,g_dev 立刻创建,全局唯一一份
  2. 哪怕板子上两路 DS18B20,g_dev 只能存其中一个地址,第二个直接覆盖
  3. 就算解绑其中一路硬件设备,g_dev 不会消失,内存一直占用
  4. 只有执行 rmmod 卸载整个驱动模块,g_dev 才会被释放

关键点:只要模块没卸载,全局变量永远存在,不受硬件设备插拔 / 解绑影响

核心区分一句话

  • 随设备:硬件设备消失,数据销毁;模块还在,其他设备不受影响。
  • 随模块:只要驱动模块没卸载,全局变量一直存在,跟硬件设备是否在线无关。

极简对比例子

模块 = 一栋写字楼 设备 = 写字楼里一间店铺

  1. 随设备:店铺倒闭关门(remove),店铺内全部资料销毁,写字楼(模块)照常营业、其他店铺正常。
  2. 随模块:写字楼不拆(不 rmmod),大堂贴的全局传单(g_dev)永远挂在那,不管里面店铺开不开。

补充一点:

container_of 那条路跟前两条还不一样。

前两条指 platform_set/get_drvdata、filp->private_data,这两种是你显式写代码手动存入指针;cdev 嵌入这条路不靠手动存指针,靠内存布局反推 ------ 你把 cdev 嵌进结构体、注册进内核,内核在 open 时自动把 cdev 地址填进 inode->i_cdev。

拆分对应

  1. 第一条:platform_set_drvdata (pdev, dev) → 手动把 dev 指针存入 pdev 的 driver_data
  2. 第二条:filp->private_data = dev → open 里手动把 dev 指针存入 filp 私有字段
  3. 第三条:cdev 内嵌 + container_of → 无手动存指针操作,依靠结构体内存偏移反向计算 dev 地址

核心总结

不是内核 "故意存来存去" 折腾人,而是内核分层设计 + 每层只递给你它自己的结构体 + 全局变量不支持多设备 ,逼得你在每一层用那一层规定的 "挂钩" 把同一个 dev 挂回去。

这么多函数每个都属于不同子系统的标准接口,是内核保持 "子系统通用、驱动自己搭桥" 的代价 ------ 也是正确写法。

理解了这一点,再看驱动代码里那些 set_drvdataprivate_datacontainer_of,就不会觉得是冗余代码了,它们各司其职,缺一不可。

内核拆分多套独立子系统的核心底层原因
1. 单一通用结构体无法适配所有硬件场景

板子硬件五花八门:I2C、SPI、Platform、PCI、字符设备、块设备...... 如果内核只用一套统一结构体承载所有硬件,结构体必须塞满所有硬件的字段,绝大多数硬件只会用到其中 1% 字段,极度臃肿浪费内存,无法通用。 所以内核按硬件通信方式、文件访问逻辑拆分成独立子系统:Platform 总线层、字符设备层、VFS 文件层,每套子系统只维护自身最小、精简的结构体,只处理自己领域的逻辑。

2. 分层解耦:各子系统互不依赖、可单独迭代
  • Platform 总线只管硬件设备匹配、设备树解析,完全不用关心用户读写文件;
  • VFS 文件层只管 open/read/close 文件操作,完全不用关心底层硬件是 I2C 还是 GPIO;
  • 字符设备层只管设备号、cdev、文件操作集,不绑定任何总线硬件。

任何一层代码修改、升级,不会影响另外两层,内核迭代、维护成本大幅降低。 如果所有逻辑揉在一套接口里,改一处功能会导致全内核驱动大面积兼容崩溃。

3. 回调函数入参固定,每层只能传递自己的结构体

内核调用驱动回调函数时,只会传递当前子系统专属结构体,不会跨层传递别的层对象:

  • 总线回调 probe:只给 pdev,没有 filp/inode;
  • 文件回调 read:只给 filp,没有 pdev/inode;
  • open 回调:只给 inode+filp,没有 pdev。

各层无法互通对方结构体,只能各自提供专属挂钩(driver_data /private_data/cdev 内嵌),让驱动自己搭桥关联私有 dev。

4. 一套通用接口,兼容千万款不同驱动

内核子系统是标准化通用框架,不绑定任何一款硬件驱动。 内核本身完全不认识你自定义的 struct ds18b20_dev,它只认识 platform_device / struct file / struct cdev 这些公共通用结构体。 所以每个子系统预留专属私有数据插槽,让任意驱动都能挂载自己的私有数据,实现 "内核框架通用,驱动业务自定义"。

一句话总结核心原因

为了分层解耦、精简内存、独立迭代、接口标准化,内核拆分出独立子系统;分层带来的代价就是每层只能传递自身结构体,驱动必须用各层专属 API 搭桥获取私有数据。

相关推荐
2601_956319881 小时前
最新量化软件怎么选,先按能力短板匹配工具类型
人工智能·python
Tbisnic1 小时前
26.AI大模型:RNN、LSTM、GRU
人工智能·python·rnn·gru·大模型·llm·lstm
ClouGence2 小时前
跨云、跨地域数据同步,这样更省心
数据库·sql·saas
七夜zippoe2 小时前
OpenClaw 实战:智能文档助手——从问答到生成的完整方案
人工智能·python·机器学习·openclaw·智能文档
TDengine (老段)2 小时前
TDengine 函数完整参考 — 聚合、时序、字符串、时间、数学
大数据·数据库·物联网·时序数据库·iot·tdengine·涛思数据
卡提西亚2 小时前
leetcode-179. 最大数
python·算法
浩瀚地学2 小时前
【面试算法笔记】0302-哈希表-哈希表实现
java·经验分享·笔记·算法·面试
ECT-OS-JiuHuaShan2 小时前
严格证明:还原论是ASCII,整体论是UTF-8的历史意义和价值
开发语言·人工智能·算法·量子计算
ShallWeL2 小时前
【机器学习】(19)—— 数据特性与标签
人工智能·算法·机器学习