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

📅 发布时间:2026/7/18 3:53:58
驱动:私有数据为什么要在三个地方各挂一遍? 写字符设备驱动的人大概率都困惑过这件事我就一个私有结构体dev为什么要反复存来存去platform_set_drvdata存一遍cdev_add嵌进去一遍filp-private_data再存一遍。这不重复吗答案一句话内核是分层的每一层只认识自己的结构体不认识你的 privet_dev(私有数据结构体)。所以你得在每一层都留个 挂钩把你的数据挂回去。这篇文章用一个写字楼的例子把这件事彻底讲明白。先抛出问题三个场景三个不同的入参你的驱动夹在好几个内核子系统中间每个子系统回调你时手里只攥着它自己认识的结构体。谁调你递给你什么你手上没有怎么取回 devplatform 总线pdevinode、filpprobe函数- platform_get_drvdata(pdev)VFS openinode filppdevopen函数- container_of(inode-i_cdev, ...)VFS read/release仅filppdev、inoderead函数- 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/字符设备文件节点前台做两件事查到这个文件的inode公司档案给租客造一张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_drvdataplatform 总线层在pdev-driver_data存 / 取指针随设备存在cdev_add container_of字符设备层把 cdev 注册进内核表open 时靠它反推 dev随设备存在filp-private_dataVFS 文件层文件层的 驱动专用抽屉随单次 open 存在close 销毁g_dev全局变量自定义全局简易兜底不支持多设备随模块存在1、随设备存在platform /cdev生命周期绑定单个硬件设备实例场景板子挂载 2 路 DS18B20两路独立设备第一路设备树匹配 → probe 运行生成 dev1、pdev1、cdev1第二路设备树匹配 → probe 运行生成 dev2、pdev2、cdev2操作 1卸载其中一路设备解绑设备树这一路的 dev、pdev、cdev 立刻销毁另一路设备、驱动模块依然完好运行操作 2rmmod 卸载驱动模块所有设备的 dev、pdev、cdev 全部销毁关键点模块还在单个设备可以单独销毁。2、随模块存在全局 g_dev生命周期绑定整个驱动 ko 模块insmod 加载模块g_dev 立刻创建全局唯一一份哪怕板子上两路 DS18B20g_dev 只能存其中一个地址第二个直接覆盖就算解绑其中一路硬件设备g_dev 不会消失内存一直占用只有执行 rmmod 卸载整个驱动模块g_dev 才会被释放关键点只要模块没卸载全局变量永远存在不受硬件设备插拔 / 解绑影响。核心区分一句话随设备硬件设备消失数据销毁模块还在其他设备不受影响。随模块只要驱动模块没卸载全局变量一直存在跟硬件设备是否在线无关。极简对比例子模块 一栋写字楼 设备 写字楼里一间店铺随设备店铺倒闭关门remove店铺内全部资料销毁写字楼模块照常营业、其他店铺正常。随模块写字楼不拆不 rmmod大堂贴的全局传单g_dev永远挂在那不管里面店铺开不开。补充一点container_of 那条路跟前两条还不一样。前两条指platform_set/get_drvdata、filp-private_data这两种是你显式写代码手动存入指针cdev 嵌入这条路不靠手动存指针靠内存布局反推 —— 你把 cdev 嵌进结构体、注册进内核内核在 open 时自动把 cdev 地址填进 inode-i_cdev。拆分对应第一条platform_set_drvdata (pdev, dev) → 手动把 dev 指针存入 pdev 的 driver_data第二条filp-private_data dev → open 里手动把 dev 指针存入 filp 私有字段第三条cdev 内嵌 container_of → 无手动存指针操作依靠结构体内存偏移反向计算 dev 地址核心总结不是内核 故意存来存去 折腾人而是内核分层设计 每层只递给你它自己的结构体 全局变量不支持多设备逼得你在每一层用那一层规定的 挂钩 把同一个dev挂回去。这么多函数每个都属于不同子系统的标准接口是内核保持 子系统通用、驱动自己搭桥 的代价 —— 也是正确写法。理解了这一点再看驱动代码里那些set_drvdata、private_data、container_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/inodeopen 回调只给 inodefilp没有 pdev。各层无法互通对方结构体只能各自提供专属挂钩driver_data /private_data/cdev 内嵌让驱动自己搭桥关联私有 dev。4. 一套通用接口兼容千万款不同驱动内核子系统是标准化通用框架不绑定任何一款硬件驱动。 内核本身完全不认识你自定义的struct ds18b20_dev它只认识platform_device/struct file/struct cdev这些公共通用结构体。 所以每个子系统预留专属私有数据插槽让任意驱动都能挂载自己的私有数据实现 “内核框架通用驱动业务自定义”。一句话总结核心原因为了分层解耦、精简内存、独立迭代、接口标准化内核拆分出独立子系统分层带来的代价就是每层只能传递自身结构体驱动必须用各层专属 API 搭桥获取私有数据。