Linux 设备模型学习笔记(1)

Linux 设备模型学习笔记(1)

前言

最近在学习 Linux 驱动开发,从简单的字符设备再到平台总线,深刻体会到了 Linux 内核的博大精深,期间阅读了不少前辈们写的文章,资料等等,再结合我自己的一些思考和理解,整理成这篇笔记。

1. 为什么需要设备模型?

在 Linux 发展的早期,驱动开发可以算得上是非常简单粗暴,写驱动就是申请设备号,操作硬件等等。Linux 是支持所有不同功能的硬件设备的,这是它的优点,但这也导致 Linux 内核中有相当大一部分代码都是设备驱动相关的,并且随着后来嵌入式系统的爆发(特别是ARM SoC),这种情况愈发严重。早期那种简单粗暴的驱动开发方式已经不再适用了,因为它会带来下面几个问题:

第一,代码的低复用导致的代码规模爆发式增长 。在早期的那种驱动开发方式中,硬件信息与驱动代码是强耦合的,也就是我们常说的硬编码,这种方式的特点是驱动代码里面不仅包含了控制硬件的方法 ,还包含了硬件设备的详细信息 。比如,我们需要控制板子上的一个 LED ,在这个板子上 LED 在 GPIO 3 上面,而另一块板子上 LED 在 GPIO 4 上面,那么相同逻辑的 led 驱动代码由于硬件信息的不同,就要写两遍。硬件信息的硬编码导致驱动代码无法跨板子复用,这就是代码规模爆发式增长的原因,且其中大部分代码都是重复的。

第二,电源管理的问题 。当系统要休眠时,Linux 就需要按顺序关闭设备。关闭设备的原则是先关闭子设备,在关闭父设备 。那我们常见的鼠标来举例,系统必须先关闭挂载在 USB 控制器上的鼠标,然后才能断开 USB 控制器的电源。但是系统里面有那么多设备,如果没有一个树状结构来记录设备的父子关系,内核根本就不知道关闭电源的正确顺序,容易导致系统崩溃。

第三,用户空间无法知道设备的具体信息。如果用户想知道系统里面究竟挂了几个鼠标,一个 I2C 总线上究竟连接了几个芯片。没有统一的模型,内核就无法向用户空间展示完整的设备拓扑图。

这三个问题,是早期的驱动开发方式带来的问题,也正是设备模型出现后解决的问题。

Linux 设备模型的出现,就是为了将硬件的描述硬件的驱动 分离,并通过总线来管理他们,最终形成一棵有序的

2. Linux 设备模型的基本概念

2.1 总线 Bus

在物理概念中,总线是连接 CPU 和外设的导线,比如USB,I2C,SPI等。但在 Linux 软件模型中,Bus是一种管理机制

我们来看看内核源码中对于总线的解释,相关内容可以在下面目录中找到:

bash 复制代码
include/linux/device.h

这段注释是内核源码中对于总线的描述,大致意思如下:总线是处理器与一个或多个设备之间的通信通道 。在设备模型的框架下,所有设备都通过总线连接,即使这是一个内部的、虚拟的"平台"总线 。总线之间可以互相连接,例如,一个 USB 控制器通常就是一个 PCI 设备。设备模型反映了总线与其所控制设备之间的实际连接关系。总线由 bus_type 结构体表示。该结构体包含了总线的名称、默认属性、总线方法、电源管理操作以及驱动程序核心的私有数据。

对这段注释进行一个总结,其实可以分为两点:

第一,在 Linux 中,总线是 CPU 和一个或多个设备直接交流的通道。

第二,为了将设备模型抽象出来,所有的设备都要连接到总线上,这个总线可以是 CPU 内部总线,也可以是虚拟的平台总线。

在 CPU 内部的众多总线中,每个总线上都维护着两个链表,分别是 Devices 链表Drivers 链表 。Devices 链表中存放着所有挂在这条总线上的设备 。Drivers 链表中存放着所有这条总线上的驱动

而总线最重要的功能就是将设备和驱动进行匹配。每当有一个新设备插入或者有一个新驱动被加载,总线就会开始执行匹配程序。

下面是内核源码中bus_type结构体的部分内容:

里面成员很多,我们挑几个重要的看看。

第 123 行是总线名称,比如 "Platform" , "I2C" 等等。

第 130 行是匹配函数,当有新设备或新驱动加入总线时,内核就是通过自动调用这个函数,来判断两者是否配对的。

第 132 行是初始化函数 ,匹配成功后,内核会调用这个函数,进而调用驱动程序里probe初始化函数。

2.2 设备 Device

Device 结构体描述的是硬件设备信息,在现代 ARM Linux 中,这些信息主要来源于设备树

设备树里面包含的信息有下面几种:

第一种是资源信息,比如寄存器地址,中断号,DMA 通道等等。

第二种是拓扑信息,描述该设备在设备树中的位置,比如它是谁的父节点,又是谁的子节点。

第三种是状态信息,描述设备现在处于开启状态还是关闭状态。

struct device 就像一个基类,实际开发中,我们通常使用它的派生类,如 struct platform_device

核心思想是将硬件变动隔离起来,就是将所有变动的硬件信息封装在 Device 里面,通过 DTS 传给内核,从而实现硬件信息和驱动程序的解耦。

2.3 驱动 Driver

Driver 结构体描述的是对应的硬件应该怎么使用。里面的信息主要是包含操作逻辑,比如怎么初始化,怎么读写,怎么处理中断等等。驱动代码里面不应该出现具体的物理地址,地址应该从传入的 Device 指针里面取出来。

在代码层面,驱动由 struct device_driver 表示,对于使用设备树的 ARM Linux,我们需要特别关注 of_match_table 成员:

这里同样只截取了一部分内容,我们看看最重要的几个成员。

第 299 行是驱动名称。

第 300 行指明该驱动属于哪条总线。

第 308 行,是驱动用来与设备匹配的关键成员。

2.4 类 Class

到这里,我们大概已经知道 Bus 是按照物理连接对设备进行分类,比如一个设备到底是属于 USB 设备还是 I2C 设备。

而类 Class 是按照功能属性对设备进行分类的。

举个例子:

从总线角度来看:一个 USB 鼠标属于 USB 总线,蓝牙鼠标属于蓝牙总线。

而从类的角度来看:他们二者都是属于 input 类,也就是输入类的。

从这个例子中不难看出,Class 向用户空间提供了一个标准的接口,不管底层的物理总线是什么,只要属于同一个 Class ,应用程序就可以用相同的方式来操作设备,屏蔽了底层的差异。

我们可以在 /sys/class/目录下看到所有的类:

2.5 它们是怎么协同工作的

在理解了上面的概念之后,我们来梳理一下内核管理硬件的整个流程:

上面我们提到过,Bus 维护着两个链表。一个是 klist_devices 设备链表,里面是等待配对的硬件设备。另一个是 klist_drivers驱动链表,里面是等待干活的驱动。

当内核解析设备树时,会将节点转化为对应的 Device 结构体并注册到总线上,这时 Devices 链表中的设备加一。

当用户加载内核模块(使用insmod命令)时,Driver 结构体被注册到总线上,这时 Drivers 链表中的驱动加一。

每当 Devices 链表或者 Drivers 链表中增加新成员,总线都会调用.match函数,拿着新来的成员去和对方链表的每一个成员进行比对。常用的平台总线中通常是通过比对compatible属性字符串来决定配对是否成功。

一旦匹配成功,总线就会调用.probe函数进行初始化,这个总线的.probe函数最终会调用驱动的.probe函数,把具体的 Device 结构体传给 Driver。注意:总线先调用自己的 probe , 进而调用驱动提供的 probe

.probe 函数中,驱动通常会调用子系统的接口,将自己注册到对应的 Class 中,从而在 /dev/sys 下生成文件,供用户层操作。

至此,驱动程序正式拿到了硬件的控制权,然后开始干活。

相关推荐
南囝coding3 小时前
CSS终于能做瀑布流了!三行代码搞定,告别JavaScript布局
前端·后端·面试
踏浪无痕3 小时前
Go 的协程是线程吗?别被"轻量级线程"骗了
后端·面试·go
一枝小雨3 小时前
【OTA专题】18 OTA性能优化:优化bootloader存储空间与固件完整性校验(CRC)
stm32·单片机·性能优化·嵌入式·freertos·ota·bootloader
一只叫煤球的猫4 小时前
为什么Java里面,Service 层不直接返回 Result 对象?
java·spring boot·面试
求梦8205 小时前
字节前端面试复盘
面试·职场和发展
C雨后彩虹5 小时前
书籍叠放问题
java·数据结构·算法·华为·面试
我是海飞5 小时前
杰理 AC792N WebSocket 客户端例程使用测试教程
c语言·python·单片机·websocket·网络协议·嵌入式·杰理
不脱发的程序猿6 小时前
CAN总线如何区分和识别帧类型
单片机·嵌入式硬件·嵌入式·can
码农水水6 小时前
中国电网Java面试被问:流批一体架构的实现和状态管理
java·c语言·开发语言·面试·职场和发展·架构·kafka