Linux设备驱动模型:总线、设备树、sysfs文件系统
目录
(3)struct subsys_private:每条总线维护的「私有管理容器」
想象一下:你给开发板写了一个点亮 LED 的驱动,烧进内核后,内核怎么知道 “这个驱动是给那块 LED 用的”?
在没有设备树、没有总线的年代,这些信息都要写死在代码里,换一块板子就要重写一遍驱动。后来 Linux 设计了一套更聪明的机制:用总线架构来统一管理所有硬件和驱动,让它们能自动 “找到彼此”。
今天我们就从这个最基础的问题开始,慢慢走进 Linux 设备模型的世界。
一、挂载根文件系统的本质
还记得我们裁剪编译内核时候的流程吗?配置编译选项--->编译--->挂载根文件系统--->拷贝Busybox命令行解释器到根文件系统中---->正常进入bash命令行,开始运行Linux。
但是你有没有想过,挂载根文件系统到底是在干什么?其实Linux内核的文件系统只是提供了一个入口,但里面是空的!好比墙上有个钉子,但是什么都没有挂。此时有一个集成的衣架,按照一定规律依次挂满了各种各样的衣服,然后最上方的衣架会被挂到这颗钉子上。于是Linux内核就可以从钉子作为入口,访问到衣架(根文件系统)上的所有衣服节点了。

这样设计的弊端:
虽然这样可以让Linux系统和文件系统最方便的协作,但是有一个缺点不可避免:内核永远只能从文件系统提供的入口/依次往下层访问,它本身无法知道你文件系统到底是啥样的。比如哪天文件系统出BUG了,某些目录、文件节点消失了,Linux也不知情。只有上层应用按照以前的路径访问时,才突然发现:诶,我那么大颗目录树呢?
二、sysfs虚拟文件系统
早期的Linux系统并没有意识到这样的问题,直接调用mknod接口在根文件系统下生成各种各样的设备文件。随着设备文件的发展,很多设备可能会偶尔掉线,热拔插等,即接触不良引起文件系统缺失。但设备文件仍然存在于/dev目录下(因为Linux没有在设备掉线时同步修改磁盘内容)即这个过程Linux内核从来都不知道,他早就把设备号等运行基础分配出去了,最后真正要用到该设备的时候才后知后觉发现了错误。
于是Linux为了让内核本身有感知文件系统的功能,不再只依赖磁盘的根文件系统进行目录管理了,而是在内核空间内生成一个虚拟文件系统,他和根文件系统高度类似,都是树形结构。但此时他只存在于软件数据结构层面。
不过sysfs虚拟文件系统是一个内核暴露给用户层的接口,就好比你浏览器上的UI界面。而他的底层实现依赖于总线+设备树文件。
其中总线说白了就是个数据结构体,起到管理设备、驱动结构体的作用。
三、设备树
设备树不是某个阶段的概念,而是全局通用的概念,只不过我们常常说的设备树是硬件描述文件这一种,但是后续如果别人说总线下也有设备树你也要能明白。
设备树是一种特定格式的硬件描述文件,它的核心使命是把 “板子上有什么硬件” 这件事,从代码里剥离出来,交给内核去自动识别。
在没有设备树的年代,硬件信息(比如 LED 接在哪个 GPIO 引脚、I2C 设备地址是多少)都硬编码在驱动代码里,换一块开发板就要重写一遍驱动,可移植性极差。设备树的出现彻底解决了这个问题:
- 它是硬件的 “说明书”:用文本形式清晰描述板载外设的类型、引脚、中断号、寄存器地址等信息。
- 内核启动时自动解析:Bootloader 会将编译后的设备树二进制(DTB)传给内核,内核遍历这份 “说明书”,把每一个外设节点转换成内核里的
platform_device对象 —— 相当于把纸面上的硬件,变成了内存里可管理的设备实体。- 它是驱动匹配的 “身份证”:每个设备树节点都有一个
compatible字符串(比如"my,led"),驱动程序通过匹配这个字符串,找到自己能控制的硬件,实现 “一套驱动适配多块开发板”。
/* I2C 控制器 */
i2c@12340000 {
compatible = "arm,versatile-i2c";
reg = <0x12340000 0x1000>;
#address-cells = <1>;
#size-cells = <0>;
status = "okay";
/* I2C 从设备:传感器 */
sensor@18 {
compatible = "ti,tmp102";
reg = <0x18>; /* I2C 地址 0x18 */
};
};

四、Platform 总线:设备与驱动的 “媒婆”
(1)总线是啥?有什么用?
设备树告诉内核 “有什么设备”,但内核还需要一个角色,帮这些设备找到能控制它们的驱动 —— 这就是Platform 总线,它是 Linux 为 “无专属总线设备”(比如直接连在 CPU 引脚上的 LED、按键、UART 等)设计的虚拟总线,本质是一套软件管理框架,而非物理线路。而其他有实体物理总线的设备(IIC、SPI等)Linux也会有一套独立的总线系统,但他们都继承自struct Bus。即无论什么类型的外设,Linux内核都是按照“设备-总线”的方式管理的。

它的核心工作只有三步:
- 收设备:收集设备树解析生成的所有
platform_device,挂到自己的设备链表上。- 收驱动:当驱动模块(
.ko文件)加载时,驱动会将platform_driver注册到 Platform 总线。- 做匹配:遍历设备和驱动,对比
compatible字符串 —— 一旦匹配成功,就调用驱动的probe函数,让驱动正式接管硬件,完成初始化(比如申请 GPIO、注册中断、创建设备节点)。
如果说设备树是 “硬件花名册”,Platform 总线就是 “婚姻介绍所”,负责把硬件和驱动精准配对。
(2)总线在源码中的结构体:行为结构体

我上图框出的几个大家了解一下,其余的暂时不需要学习:
1.
const char *name—— 总线名字
- 作用:标识这条总线的名称(如
"platform"、"i2c"、"spi"),会在/sys/bus/目录下生成对应文件夹。- 意义:是内核和用户态识别总线的唯一标识,比如
ls /sys/bus就能看到所有注册的总线。
2.
int (*match)(struct device *dev, struct device_driver *drv)—— 匹配函数
- 作用:内核用来判断「某个设备
dev是否能被某个驱动drv处理」的核心逻辑。- 典型场景:
- Platform 总线:通过设备树
compatible字符串和驱动of_device_id表匹配。- I2C 总线:通过设备地址和驱动支持的设备列表匹配。
- 意义:设备驱动模型的 “媒人”,决定了设备和驱动能否绑定。
3.
int (*probe)(struct device *dev)—— 探测函数
- 作用:当
match函数返回成功后,总线会调用此函数,完成设备的初始化工作。- 典型工作:
- 映射寄存器地址、申请中断、初始化硬件。
- 注册字符设备、输入设备等上层接口。
- 意义:设备真正 “活起来” 的入口,你写驱动时的
probe函数最终会被这里触发。
4.
void (*remove)(struct device *dev)—— probe 的对立面
- 作用:设备卸载时调用,负责清理
probe中申请的所有资源。- 典型工作:
- 释放中断、注销设备接口。
- 取消寄存器映射、关闭硬件时钟。
- 意义:和
probe成对出现,保证资源不泄漏,是驱动稳定性的关键。
而platform总线作为各种SOC外设的虚拟总线,他肯定也是继承于Struct Bus的,他的定义如下:

这其中我们看到了许多.name .match的用法,他其实C99 标准的「指定初始化器」语法,专门用于给结构体成员赋值,也是 Linux 内核最推荐的写法。我们之前在学C语言的时候,是首先声明一个结构体,但是无法直接在结构体声明的时候赋默认值的,而是要等到创建的时候再写,这样就比较麻烦,尤其是在Linux内核中一个结构体可能有大量的成员,所以采取了这种写法,更加方便、且不容易因为维护调换了成员位置而出错。
那么platform总线其实没有对struct Bus进行扩展,而是单纯的给其赋初值,就成为了截然不同的类型。这也是C语言版继承+多态的常见做法。
(3)struct subsys_private:每条总线维护的「私有管理容器」
它是 Linux 驱动核心(driver core)为每条总线维护的「私有管理容器」,专门用来管理单条总线的所有设备、驱动和内部状态,而不是把所有总线都放一起。

简单说:
- 每注册一条总线(比如
platform_bus_type、pci_bus_type),内核都会单独创建一个subsys_private实例。- 这个实例只服务于这一条总线,是总线背后的「大管家」,负责:
- 管理挂在这条总线下的所有设备(
klist_devices)- 管理这条总线上的所有驱动(
klist_drivers)- 维护总线的锁、通知链、sysfs 目录结构等内部状态
(4)总线框架

五、从设备树到sysfs系统的全流程
上面的讲解足以帮助大家宏观理解Linux驱动开发中的3层架构:设备树、总线、sysfs文件系统,但是总的流程是什么样子呢,可以看看下面的图。

下面是详细的解释:
(1)内核解析设备树,创建struct device结构体
当内核读取设备树的信息后,会把他们解析成struct device结构体(这一步只是把文本信息翻译成结构体,本质仍然是树)。不过这个struct device只是父类,由于总线架构中有多种总线类型:IIC、SPI、Platform等,所以真正生成的结构体黑丝是struct device的子类,而把struct device作为公共部分及后续链表的节点标记。
这里注意一点:
struct device管的是硬件拓扑结构,包含设备在哪,设备怎么连接。而cdev则管用户态该怎么访问这个设备,包含设备号、file_operations等。

在这个过程中,有一个成员叫做kobject,他是未来给sysfs文件系统映射用的,有个了解即可。
(2)总线架构的2个链表进行匹配绑定
我们说一个总线会有两条链表:设备链表(device链表)和驱动链表(driver链表)。调用bus_type中的回调函数match进行探测匹配,讲device链表和driver链表的.compatible字段一一比对,如果是一样的则匹配成功。进一步调用probe函数,它将两个链表节点的指针互相绑定后,创建cdev等上层用于能看得到的接口。

当你绑定完成后,用户态就可以通过cdev提供的file_operations找到该设备文件的驱动函数了,从而完成整个驱动的编写流程。
而目前我们只看了设备结构体的创建过程,实际上还差驱动结构体,他也是需要你通过特定接口先注册到Linux内核中的,这个我们在写代码的时候再详细说明,你只需要记住:每次驱动链表、设备链表任意一个发生变化的时候,总线都会自动调用match函数进行遍历匹配。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)