找回密码
立即注册
搜索
发回帖 发新帖

6529

积分

0

好友

826

主题
发表于 昨天 16:30 | 查看: 16| 回复: 0

从这一节开始,我们进入设备模型这一章。

上一章我们学习了平台总线。SoC 内部集成了 I2C、SPI、USB、UART 等各类控制器,由于它们集成在芯片内部,不会主动向内核报告自己的存在。所以 Linux 设计了平台总线这条虚拟总线,先用设备树把这些控制器描述出来,再把每个控制器交给匹配的驱动。

不过控制器只是硬件的一部分,I2C、SPI、USB 这些控制器还会通过各自的信号线连接外部设备。比如 I2C 控制器通过 SCL 和 SDA 连接传感器和电源管理芯片,SPI 控制器通过时钟、数据和片选信号连接 Flash,USB 控制器下面则可以接入 Hub、键盘和 U 盘。

为了管理这些外部设备,Linux 又为它们分别建立了 I2C、SPI、USB 等总线,于是同一个系统里就同时存在着多条总线,而且这些总线之间还彼此相连。

以一个 I2C 温度传感器为例,它作为 I2C 设备挂在 I2C 总线上,而它所连接的 I2C 控制器本身又是平台总线上的一个设备。两者分属不同的总线,在硬件上却是一上一下的父子关系。这些设备分属的总线以及它们之间的连接关系,如下图所示:

设备分属的总线与跨总线的父子关系

那么内核是怎样把这些分属不同总线、又彼此相连的设备统一管理起来的呢?

这就要靠设备模型了,平台总线也好,I2C、SPI、USB 总线也好,都是建立在设备模型这套统一机制之上的。

1. 什么是设备模型

设备模型是 Linux 内核为系统中所有设备、驱动、总线和设备类别建立的一套统一的对象体系。

在这套体系里,每个设备、每个驱动、每条总线都对应内核中的一个对象,设备模型再通过这些对象之间的关系,把系统中的设备组织起来。这些关系主要有三种:

  • 拓扑关系: 每个设备都有自己的父设备。比如温度传感器挂在 I2C 控制器下面,I2C 控制器作为平台设备,默认又挂在一个名为 platform 的设备下面,整个系统的设备由此连成一棵树。
  • 绑定关系: 每个设备当前由哪个驱动管理。比如 I2C 控制器由控制器驱动管理,温度传感器由传感器驱动管理。
  • 分类关系: 每个设备对外提供什么功能。比如温度传感器属于硬件监控(hwmon)类,键盘属于输入(input)类。

在这些关系之上,设备模型还为所有设备提供了两项统一的服务:

  • 生命周期管理: 设备对象的注册、注销和释放遵循同一套规则,对象只有在没有任何使用者以后才会被真正释放。
  • 对用户态的呈现: 设备模型通过 sysfs 把这些对象和关系呈现为 /sys 下的目录,并在设备出现和消失时通过 uevent 通知用户态。

这样说可能还是有些抽象,我们可以把整个系统想象成一座城市,设备模型就相当于这座城市的户籍管理系统:

户籍管理 设备模型
每个居民都有一份统一格式的户口 每个设备都有一个统一的设备对象
户口上的住址按市、区、街道逐级排列 设备按父子关系组成一棵拓扑树
居民按工作单位登记,由单位安排岗位 设备按总线登记,由总线匹配驱动
居民按职业分类,不管在哪个单位工作 设备按功能分类,不管挂在哪条总线上
出生和迁入时要通知相关部门 设备出现时发出 uevent
户籍信息可以查询 设备信息可以通过 /sys 查看
销户以后,档案要等相关手续都办完才销毁 设备注销以后,对象要等所有使用者都释放才回收

户籍系统本身并不参与居民的具体工作,设备模型也一样,它不读写任何一个控制器的寄存器,只负责把所有设备登记清楚、把它们之间的关系组织好。

操作硬件仍然是各个驱动的工作,但有了这份统一的登记,匹配驱动、电源管理、用户态通知和对象释放这些公共事务,就可以由设备模型集中处理了。

2. 为什么需要设备模型

那么这些公共事务为什么一定要由设备模型集中处理,而不能交给各条总线自己完成呢?

我们回到开头的温度传感器,再加上一个插在 USB 口上的 U 盘,内核围绕它们至少要处理下面几件事:

  • 层级关系: 温度传感器挂在哪个 I2C 控制器下面,U 盘又连在哪个 USB 控制器的哪个端口上。
  • 休眠顺序: 系统挂起时,子设备必须先于父设备停止工作,唤醒时 I2C 控制器要先恢复,传感器才能继续通信。
  • 用户态通知: U 盘插入以后,/dev 下要出现对应的设备节点,需要的驱动模块也要被加载。
  • 释放时机: U 盘拔出时可能还有程序在读它,内核要等到什么时候才能释放这个设备占用的内存。

这几件事都跨越了总线的边界,只靠某一条总线自己的代码是处理不好的。

2.1 各总线独立管理设备的局限

在设备模型出现之前,早期的 Linux 中每种总线都有自己的一套设备管理方式,当时的驱动模型会为自己管理的设备维护一棵树,有的甚至只是一张链表,不同总线之间并没有统一的做法。

每条总线只管自己下面的设备,在总线内部这样做没有问题。可是设备之间的上下级关系经常跨越总线,每条总线只记下了自己的那一段,整个内核里没有一棵完整的设备树。

温度传感器和 I2C 控制器就是这样一对设备。传感器归 I2C 这一侧管理,控制器归另一套代码管理,"传感器挂在这个控制器下面"这层关系落在两者的交界处,哪一侧都没有完整的记录,而休眠顺序恰恰要依赖这层关系。

统一的设备模型正是为了解决这个问题而出现的,它随 2.6 内核进入稳定版本,设计目标就是即插即用、电源管理和热插拔,正好对应了上面这几类跨总线的需求。

2.2 电源管理与设备拓扑

电源管理最能体现这种统一拓扑的必要性。系统挂起时,子设备必须先于父设备停止工作,否则父设备一旦关闭,子设备就失去了完成挂起操作的通路。比如 I2C 控制器先停了下来,传感器驱动就再也无法通过 I2C 总线把传感器设置成低功耗状态。

设备模型用一张全局链表解决了这个问题。

每个设备注册时,device_pm_add() 都会把它追加到全局链表 dpm_list 的末尾:

/* drivers/base/power/main.c */
void device_pm_add(struct device *dev)
{
    ...
    list_add_tail(&dev->power.entry, &dpm_list);
    ...
}

子设备注册时,它的父设备通常已经注册完成,所以这张链表天然就是按照父设备在前、子设备在后的顺序排列的。

到了系统挂起的时候,设备会先按原有顺序进入 dpm_prepared_list,dpm_suspend() 再从这张链表的末尾开始逐个处理,恢复时 dpm_resume() 则从链表的开头开始:

/* drivers/base/power/main.c */
int dpm_suspend(pm_message_t state)
{
    ...
    while (!list_empty(&dpm_prepared_list)) {
        struct device *dev = to_device(dpm_prepared_list.prev);
        ...
    }
    ...
}

void dpm_resume(pm_message_t state)
{
    ...
    while (!list_empty(&dpm_suspended_list)) {
        dev = to_device(dpm_suspended_list.next);
        ...
    }
    ...
}

dpm_suspend() 每次取的是 prev,也就是最后注册的设备,dpm_resume() 每次取的是 next,也就是最先注册的设备。以温度传感器所在的这条链为例,挂起和恢复的顺序如下图所示:

dpm_list 决定的挂起与恢复顺序

所以系统挂起时子设备先停,唤醒时父设备先恢复,这个顺序由设备模型维护的全局链表决定,与设备挂在哪条总线上无关。

2.3 热插拔通知与释放时机

U 盘这样可以随时插拔的设备,又带来了另外两个问题。

第一个问题是通知用户态。U 盘插入后,内核需要告诉用户态出现了一个新设备,再由 udev 或 mdev 这样的程序创建 /dev 下的节点、加载需要的驱动模块。设备模型为所有总线提供了同一种通知方式,设备注册时由内核发出 uevent 事件,广播给用户态。 uevent 通过 netlink(内核与用户态之间的一种套接字通信机制)发送,用户态程序不必关心设备来自 USB 还是 I2C,只要按统一的格式处理这些事件就可以了。

第二个问题是释放时机。U 盘被拔出时,可能还有程序打开着它,内核中也可能还有其他代码持有指向这个设备对象的指针,如果此时直接释放内存,后面再访问这个对象就会出错。设备模型为每个设备对象维护了引用计数,只有当最后一个使用者放弃引用以后,对象才会被真正释放。

这些需求都要求由设备模型集中处理拓扑、电源管理、通知和释放,各条总线只负责自己特有的部分。

3. 平台总线背后的设备模型

前面我们看到,设备模型能够统一处理层级关系、休眠顺序、用户态通知和释放时机,前提是每个设备无论挂在哪条总线上,在内核里都由同一种通用对象来表示。各条总线的驱动框架都建立在这个前提之上,我们上一章学习的平台总线也不例外。

上一章我们用平台总线写控制器驱动时,控制器由 platform_device 描述,驱动由 platform_driver 实现,二者匹配成功后,内核调用驱动的 probe() 完成初始化。平台总线的底层原理就是设备模型。

一般的教程都会先讲设备模型,上一章之所以先讲平台总线,是因为它离大家更近,也更容易理解。到了这个阶段,我们已经有了足够的知识储备,就可以开始深入学习设备模型了。设备模型是 BSP 工程师真正的必修课,也是面试中经常会问到的内容,用来考察你是否掌握了驱动框架在内核中的底层实现。

下面我们从 platform 的两个核心对象入手,把平台驱动中的对象定义、注册和匹配逐一对应到设备模型上。

3.1 platform 对象内嵌的通用对象

platform_device 定义在 include/linux/platform_device.h 中:

struct platform_device {
    const char      *name;
    int             id;
    bool            id_auto;
    struct device   dev;
    u64             platform_dma_mask;
    struct device_dma_parameters dma_parms;
    u32             num_resources;
    struct resource *resource;
    /* 省略其他字段 */
};

其中的 dev 是 struct device 类型,它是设备模型中表示"一个设备"的通用对象。 platform_device 在它外面补充了名字、编号以及寄存器和中断等资源,这些是平台设备特有的信息。

驱动这一侧也是同样的结构,struct platform_driver 中内嵌了一个 struct device_driver driver,它是设备模型中表示"一个驱动"的通用对象。

设备模型只认识内嵌的 struct device 和 struct device_driver,platform 层在需要时再把它们还原成外层对象。 而还原依靠的就是 container_of() 宏:

#define to_platform_device(x) container_of((x), struct platform_device, dev)

container_of() 根据成员 dev 在结构体中的偏移,从 struct device 的地址倒推出外层 platform_device 的地址。所以 platform 层从设备模型那里拿到 struct device 以后,就是用这个宏找回 platform_device 再交给驱动的。

不只是平台总线,其他总线的设备对象采用的也是这种内嵌方式:

总线 设备对象 内嵌的通用对象
platform struct platform_device struct device dev
I2C struct i2c_client struct device dev
USB struct usb_device struct device dev
PCI struct pci_dev struct device dev

也就是说,无论设备来自哪条总线,在设备模型看来都是一个 struct device,因此层级关系、电源管理和生命周期都可以围绕它统一处理。拿户籍管理来类比,struct device 就是每个设备都必须有的那份户口,各总线的设备对象则在户口之外补充了自己需要的档案。

这里还有一个容易被忽略的细节,在 struct pci_dev 中,dev 被放在了结构体中间靠后的位置。内核文档 driver-model/overview.rst 专门解释过,这样安排是内核有意为之,目的是让开发者在总线层和通用层之间切换时想清楚自己操作的是哪个对象,避免把指针直接强制转换成另一种类型。所以在内核中,从 struct device 回到外层对象,一律使用 container_of() 这类宏,而不依赖成员在结构体中的位置。

3.2 平台总线的注册接口

对象定义好以后,接下来就要把驱动和设备注册进内核。我们注册平台驱动时调用的 platform_driver_register() 其实是一个宏,它最终调用的是 __platform_driver_register():

/* drivers/base/platform.c */
int __platform_driver_register(struct platform_driver *drv,
                               struct module *owner)
{
    drv->driver.owner = owner;
    drv->driver.bus = &platform_bus_type;

    return driver_register(&drv->driver);
}

这个函数先把内嵌 device_driver 的 bus 指向 platform_bus_type,标明这个驱动属于平台总线,再把内嵌对象交给 driver_register()。而 driver_register() 正是设备模型提供的通用接口,I2C、SPI 和 USB 驱动注册时,最终调用的也都是它。

设备一侧的做法也一样,platform_device_add() 中与设备模型相关的是下面几行:

/* drivers/base/platform.c */
int platform_device_add(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    ...
    if (!dev->parent)
        dev->parent = &platform_bus;

    dev->bus = &platform_bus_type;
    ...
    ret = device_add(dev);
    ...
}

它先为设备指定父设备和所属总线,然后把内嵌的 struct device 交给设备模型的 device_add()。我们用设备树描述的控制器节点,在启动时会沿 OF 的路径转换成 platform_device,最终由 of_device_add() 调用的同样是 device_add()。

无论驱动还是设备,platform 层只负责标明它们属于平台总线,真正的注册都交给了设备模型的通用接口。

3.3 platform_bus_type 与 driver core 的分工

可以看到,两侧注册时都把 bus 指向了 platform_bus_type,那么这个对象里到底放了什么呢?

/* drivers/base/platform.c */
const struct bus_type platform_bus_type = {
    .name          = "platform",
    .dev_groups    = platform_dev_groups,
    .match         = platform_match,
    .uevent        = platform_uevent,
    .probe         = platform_probe,
    .remove        = platform_remove,
    .shutdown      = platform_shutdown,
    .dma_configure = platform_dma_configure,
    .dma_cleanup   = platform_dma_cleanup,
    .pm            = &platform_dev_pm_ops,
};

从定义可以看出,platform_bus_type 只是设备模型中 struct bus_type 的一个实例,除了名字和默认属性,它填写的全是回调函数,用来告诉设备模型在几个关键节点上怎样处理平台设备,其中最重要的是这几个:

  • match: 指向 platform_match(),判断设备和驱动是否匹配,依次尝试 driver_override、设备树 compatible、ACPI、id_table 和名字。
  • probe: 指向 platform_probe(),把通用对象还原成 platform_device 和 platform_driver,再调用驱动自己的 probe()。
  • uevent: 指向 platform_uevent(),在设备事件中补充平台设备的 MODALIAS 等信息。
  • pm: 指向平台设备的电源管理操作。

其中 platform_probe() 的核心就是两次还原和一次调用:

/* drivers/base/platform.c */
static int platform_probe(struct device *_dev)
{
    struct platform_driver *drv = to_platform_driver(_dev->driver);
    struct platform_device *dev = to_platform_device(_dev);
    ...
    if (drv->probe) {
        ret = drv->probe(dev);
        ...
    }
    ...
}

我们在驱动 probe() 中拿到的 pdev 正是在这里由 to_platform_device() 从通用的 struct device 还原出来的。

那么这些回调是由谁来调用的呢?

调用它们的是设备模型的核心部分,也就是 drivers/base/ 下的 driver core。驱动或设备注册以后,driver core 会把它们登记到总线上并遍历另一方,在需要判断匹配时调用 bus->match,匹配成功后再调用 bus->probe,最后记录下绑定关系。sysfs 目录的创建和 uevent 的发送同样由 driver core 在注册过程中完成。

把一次平台驱动注册按调用层次展开,可以看到两部分的分工:

平台层与 driver core 的分工

平台总线只是设备模型上的一层薄壳,它告诉设备模型怎样判断匹配、怎样调用驱动,登记、遍历、绑定、sysfs 和 uevent 都由设备模型完成。

放到户籍管理的类比里,平台总线就像一家单位的人事部门,它制定招聘标准、办理入职手续,而户口登记、档案管理和迁入迁出的通知,都由统一的户籍系统负责。整章开篇把设备模型放到了平台总线之后,就是希望大家先把驱动写出来,再回头看这一层。现在再回头看就会发现,platform_driver_register() 到 probe() 之间那段看不见的过程,几乎全部发生在设备模型里。

我认为只会填写 platform_driver 的成员还谈不上理解平台驱动。 理解了设备模型,我们才知道内核在这些回调前后做了什么,驱动出问题时也才知道该去哪里找原因。

4. 设备模型的组成

平台总线用到的 struct device、struct device_driver 和 struct bus_type,只是设备模型的一部分。要看清设备模型的全貌,还需要把它放到整个系统的层次中来看。

4.1 设备模型的分层架构

下图从上到下画出了设备模型在整个系统中的位置:

设备模型的分层架构

这几层各自承担的工作如下:

  • 用户空间: udev、mdev 根据 uevent 创建设备节点、加载驱动模块,应用程序则通过 /sys 查看和配置设备。
  • 具体总线: platform、I2C、SPI、USB、PCI 等总线在通用对象之外补充各自的资源和匹配规则,前面分析的平台总线就属于这一层。
  • 核心对象: device、device_driver、bus_type 和 class 分别描述设备、驱动、总线和功能类别,driver core 围绕它们完成登记、匹配、绑定和电源管理。
  • 底座: kobject、kset 和 ktype 为所有对象提供名字、层级和引用计数,/sys 下的目录和发给用户态的 uevent 都产生在这一层。

我们写驱动时直接打交道的是最上面的具体总线,真正把所有设备组织到一起的,是下面的核心对象和底座。

4.2 四类核心对象与 bus、class 的分工

四类核心对象中,struct device、struct device_driver 和 struct bus_type 我们在平台总线中已经见过,它们分别表示一个设备、一个驱动和一条总线。第四类 struct class 表示一类功能相同的设备,比如输入设备、RTC、网络设备和硬件监控设备。

bus 和 class 都在给设备分组,只是分组的角度不同。

比如一个 USB 键盘和一个 I2C 触摸屏,它们挂在两条完全不同的总线上,键盘驱动 usbkbd.c 注册的是 usb_driver,触摸屏驱动 goodix.c 注册的是 i2c_driver,但两者在初始化时都会调用 input_register_device(),把自己注册成一个输入设备:

/* drivers/hid/usbhid/usbkbd.c */
error = input_register_device(kbd->dev);
...
module_usb_driver(usb_kbd_driver);

/* drivers/input/touchscreen/goodix.c */
error = input_register_device(ts->input_dev);
...
module_i2c_driver(goodix_ts_driver);

于是它们在 input 类中成了同一类设备,用户态通过 /dev/input/eventX 以完全相同的方式读取按键和触摸事件,不必关心底层用的是 USB 还是 I2C。

这里还有一个细节值得注意,注册进 input 类的并不是键盘或触摸屏在总线上的那个设备对象。input 子系统会另外创建一个属于 input 类的 struct device,并把总线上的设备设为它的父设备,比如 usbkbd.c 中就有一句 input_dev->dev.parent = &iface->dev。键盘和触摸屏在设备模型中的位置如下图所示:

bus 与 class 两个维度

所以一个硬件在设备模型中往往对应一条父子链,总线上的设备负责与驱动绑定,class 中的设备负责向用户态提供功能。 两者的区别可以归纳成下表:

bus class
分组依据 设备通过哪种总线连接 设备对外提供什么功能
主要职责 匹配设备与驱动,调用 probe 为同类设备提供统一的用户态接口
match、probe 回调 有 没有
典型例子 platform、I2C、USB input、rtc、hwmon

bus 回答设备怎样连接、由哪个驱动管理,class 回答设备提供什么功能、用户怎样访问,两者是组织设备的两个正交维度。 放到户籍的类比里,bus 相当于工作单位,class 相当于职业。同一家医院里既有医生也有会计,而所有医院的医生又可以归到同一个职业类别里。

4.3 /sys 中的三种组织方式

设备之间的父子关系,加上 bus 和 class 这两种分组,最终都体现在 /sys 的目录结构中。以 LM75 这类常见的 I2C 温度传感器为例,假设它挂在 0 号 I2C 控制器上,地址为 0x48,它在 /sys 中会出现在下面几个位置:

/sys 中的设备本体与索引

图中左侧是这个传感器在拓扑树中的完整位置,对应的路径是 /sys/devices/platform/<I2C 控制器>/i2c-0/0-0048/hwmon/hwmon0。其中 <I2C 控制器> 是平台设备,i2c-0 是控制器驱动注册的 I2C 适配器,0-0048 是按"总线号-地址"命名的传感器,hwmon0 则是硬件监控子系统为它创建的 class 设备。

图中右侧的三项都是符号链接,它们分别从平台总线、I2C 总线和 hwmon 类的角度指向同一批设备的本体:

  • /sys/devices: 按父子关系组织的设备树,每个设备在这里有且只有一个目录,是设备的本体。
  • /sys/bus: 按总线分组,每条总线的 devices 目录下是指向本体的链接,drivers 目录下是注册到这条总线上的驱动。
  • /sys/class: 按功能分组,其中的设备同样是指向本体的链接。

/sys/devices 按硬件层级保存每个设备的本体,/sys/bus 和 /sys/class 则按总线和功能为同一批设备建立索引。

路径开头的 /sys/devices/platform 也值得留意。它对应一个名为 platform 的设备,platform_bus_init() 在注册平台总线之前,先注册了这个设备:

/* drivers/base/platform.c */
int __init platform_bus_init(void)
{
    ...
    error = device_register(&platform_bus);
    ...
    error = bus_register(&platform_bus_type);
    ...
}

没有指定父设备的平台设备都会挂在它下面,前面 platform_device_add() 中的 dev->parent = &platform_bus 说的就是它,而 /sys/bus/platform 对应的才是平台总线本身。同一个名字,一个是拓扑树中的节点,一个是总线,正好对应了设备模型组织设备的两个不同维度。放到户籍的类比里,这就像户籍系统中只有一份按住址编排的户籍底册,按单位和按职业编排的名册都只是指向这份底册的索引。

4.4 kobject、ktype 与 kset

那么 /sys 下的这些目录又是怎样产生的呢?这要从 struct device 的第一个成员说起:

/* include/linux/device.h */
struct device {
    struct kobject kobj;
    struct device  *parent;
    ...
};

kobj 是一个 struct kobject,它是设备模型最底层的对象。每个 kobject 在 /sys 下对应一个目录,kobject 之间的父子关系决定了目录的嵌套关系。 /sys/devices 下的那棵设备树,正是由各个 struct device 中内嵌的 kobject 组成的。

和 kobject 配合工作的还有 ktype 和 kset,三者的分工如下:

  • kobject: 记录对象的名字、父对象和引用计数,在 /sys 中对应一个目录。
  • ktype: 描述同一类 kobject 的共同行为,最重要的是引用计数归零时怎样释放对象,以及 /sys 中的属性文件怎样读写。
  • kset: 把一组 kobject 组织成集合,它本身也是一个 kobject。/sys/devices、/sys/bus、/sys/class 这几个顶层目录都由 kset 构成,kobject 发送 uevent 时也要经由所属的 kset 上报。

device、driver、bus、class 这四类核心对象,都是建立在 kobject、ktype 和 kset 这层底座之上的。 放到户籍的类比里,kobject 是每个人的那张户口登记表,ktype 规定了同一类户口怎样注销、怎样查询,kset 则是把同一辖区的户口装订在一起的户籍簿。

5. 总结

这一节我们从整体上认识了 Linux 设备模型。它是内核为所有设备、驱动、总线和设备类别建立的统一对象体系,记录设备之间的拓扑、绑定和分类关系,并统一处理电源管理、用户态通知和对象释放这些跨总线的公共事务。

平台总线正是建立在这套体系之上的一条具体总线,它只为设备模型提供匹配、probe 等回调,注册、遍历、绑定、sysfs 和 uevent 都由 driver core 完成。所以我们在 /sys 中看到的目录结构、驱动 probe() 前后发生的事情以及设备休眠唤醒的先后顺序,都可以回到设备模型中找到原因。

下一节我们从 struct device 的第一个成员 kobject 开始,看看它记录了哪些信息、怎样被内嵌到其他对象中使用,以及 /sys 下的目录是怎样由它创建出来的。如果你对内核设备管理、驱动框架的实现细节有兴趣,也欢迎在云栈社区和更多 BSP 开发者交流讨论。




上一篇:React 列表分页怎么选:翻页、加载更多还是无限滚动?
下一篇:2024年iF设计奖家居产品盘点:智能恒温器、除湿机与空调设计亮点
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-20 05:59 , Processed in 0.082332 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表