1. 为什么Linux内核的"设备模型"值得单独写一篇
先说个亲身经历。我刚入行做嵌入式驱动开发那会儿,最崩溃的不是看不懂字符设备驱动怎么写,而是每次要理解一段代码,都会碰到一堆绕不开的名词:kobject、kset、ktype、bus、class、uevent……每个概念单独搜都能看到不少文章,但串在一起就完全懵了——它们到底谁管谁?和我的驱动又有什么关系?
直到后来硬着头皮啃了好几遍《Linux设备驱动程序》和内核源码,又实际踩过几次因为没搞懂设备模型而半夜上线、第二天被业务方找上门的坑,才真正意识到:设备模型是整个Linux内核里极少见的、几乎撑起所有"硬件"相关代码的公共骨架。从你插上一个USB鼠标,到手机里的传感器上报数据,到服务器上热插拔一块NVMe盘,背后全是这套模型在工作。
这篇东西,是想给那些已经有了Unix/Linux基础、却一直对"内核设备模型"觉得隔着一层纸的朋友看的。我会把设备和驱动的关系、kobject和sysfs那套抽象体系、平台设备和设备树的衔接逻辑,从"它为什么要这样设计"讲起,再落到"我怎么用它写代码、怎么用它排查问题"。
写内核文章最怕的就是"字都认识,连起来不知道在说什么"。所以我尽量用真问题、真代码、真场景来讲,不堆术语。
2. 内核目录下万物皆对象:kobject、kset与ktype的第一性认识
2.1 kobject是什么,为什么"万物皆对象"在这里成立
Linux内核用C语言写的,但设备模型这部分代码,处处透着"面向对象"的味道。struct kobject就是那个"根类"——它相当于一个顶层基类,被嵌入到几乎所有的设备模型相关结构体里。
看一眼定义(不同内核版本略有差异,但核心字段稳定):
struct kobject { const char *name; // 对象名字,sysfs里的目录名 struct list_head entry; // 挂在kset中的链表节点 struct kobject *parent; // 对象在层次结构中的父节点 struct kset *kset; // 对象所属的集合 struct kobj_type *ktype; // 对象的行为"类":属性、release等 struct kernfs_node *sd; // sysfs目录项,表示在/sys里的存在感 struct kref kref; // 引用计数,内核态对象的生死关键 unsigned int state_initialized:1; unsigned int state_in_sysfs:1; unsigned int state_add_uevent_sent:1; unsigned int state_remove_uevent_sent:1; unsigned int uevent_suppress:1; };说实话,第一次看到这个结构体时我也震惊:一个对象竟然靠"嵌入"而不是"继承"来实现复用。C语言没有继承,所以内核的做法是在你自定义的大结构体里,把struct kobject作为第一个或某个成员嵌进去,比如后面要去解析的struct device和struct device_driver,都是这么干的。
面向对象的好处在这里非常明显:内核里所有设备模型相关的对象都具有统一的形态和统一的共性操作——它们都能被放到sysfs里显示、都能有属性文件、都能被引用计数管理、都可以被"释放"操作回收。你就把kobject想象成所有"内核通知栏公告牌"上的一个钩子,什么对象要上墙公告,就先得装一个它。
2.2 引用计数kref:对象生命周期管理的核心
struct kref是一个极其精巧的小结构,本质就一个原子引用计数:
struct kref { atomic_t refcount; };而围绕它有一堆操作:kref_init()把计数置1,kref_get()增加引用,kref_put()减少引用并在计数归零时调用release函数。为什么要这么讲究?因为内核里设备、驱动、总线这些对象都有多个不同路径的持有者:驱动核心持有它、sysfs的目录项持有它、某个正在进行的IO操作也可能持有它。你没法确定"最后一个使用者"是谁,于是只好用计数的方式管理。
举个例子:一个USB设备插入之后,struct usb_device的kobject会被sysfs、usb核心、可能还在阻塞状态等待其上报的某个URB回调共同引用。如果代码里提前释放了device对象,但URB回调还没跑完,一访问就是野指针,直接内核崩溃。相反,如果不释放,设备拔了内存还在,就是内核对象泄漏。所有内核开发者都该养成条件反射:get必须配put,init必须配release的实现。
从实践角度看,引用计数引出的一个经典坑是:在release回调里不能依赖任何同样受引用计数保护的其他对象,因为你的对象被释放时,其他对象可能早就没了。这个我后文讲probe失败时会再次提到。
2.3 kset:对象的容器,也是"热插拔事件"的广播员
struct kset可以简单理解为"一组kobject的集合"。它在设备模型里有两个作用:一是把每个kobject用链表串起来,方便内核遍历相同类型的对象;二是提供了一组操作接口(uevent_ops),用于在对象添加或移除时向用户空间发送热插拔事件。
struct kset { struct list_head list; // 该集合下的所有kobject链表 struct kobject kobj; // kset本身也是一个kobject,所以可以嵌套 const struct kset_uevent_ops *uevent_ops; };注意一个细节:kset自己内部包含一个kobject,这意味着集合本身也能挂到另一个集合下面,形成层次。而不是把"容器"和"被容器"分成两种完全不相干的东西,这种设计在映射到sysfs时特别自然——一个目录既可以是它父目录里的"条目",也是它自己子目录的"根"。
那ktype呢?它定义了某个kobject的"行为":默认的attribute集合、show/store操作、以及最关键的release回调。同一个kset里通常共享同一个ktype,但并不是强制的。用画图类比的话:
kobject是一个"树上的节点"kset是一根"树枝",把同类节点串在一起ktype是"树种的基因",决定节点怎么展示、怎么被移除
这三个概念是后面所有设备模型话题的基石。你只要理解"设备也是一个挂在sysfs里的kobject,它属于某个kset(如devices),并且通过ktype定义了它的属性展示方式",后续就好办多了。
3. sysfs:让枯燥的内核对象从幕后走到台前
3.1 sysfs是什么,它是怎么把kobject"投影"出来的
说设备模型必然绕不开sysfs。Linux在2.6内核引入了sysfs,挂载在/sys目录,把内核里设备模型的层次关系、属性信息原原本本地暴露给用户态。这样做的最大价值,是把一个平时只在内核内存里运转的"设备关系网",变成了一件你可以用ls、cat直接观察的实体。
在sysfs中,一个kobject就是一个目录,一个attribute就是一个文件。文件读取走show()回调,文件写入走store()回调。所以实际上一行cat /sys/class/net/eth0/address,内核里跑的正是网卡驱动的某个函数,把MAC地址填到你看到的输出缓冲区里。
拿我调试过的一个RTC驱动举例:设备树里挂了一个外部RTC芯片,驱动加载后,/sys/class/rtc/rtc0下面会出现date、time、wakealarm等属性文件。用户在应用层cat /sys/class/rtc/rtc0/date,走的链路在sysfs层面就是:
VFS layer -> kernfs (sysfs文件系统) -> attribute .show回调 -> 驱动自定义读函数这也是为什么说"一切皆文件"在Linux里不是一句口号——内核对象都能通过文件系统的形式暴露给用户空间。
3.2 一个驱动属性从定义到上墙的完整过程
写一个简单的属性,核心结构不用多,两个回调足矣:
static ssize_t my_attr_show(struct device *dev, struct device_attribute *attr, char *buf) { return sysfs_emit(buf, "%d\n", some_device_state); } static ssize_t my_attr_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { int val; if (sscanf(buf, "%d", &val) != 1) return -EINVAL; some_device_state = val; return count; } static DEVICE_ATTR_RO(my_attr); // 只读属性 static DEVICE_ATTR_RW(my_attr); // 如果要可读可写,就换这个这里DEVICE_ATTR_RO宏展开后本质上构造了一个struct device_attribute,并且自动把回调绑定到我们写的my_attr_show上。之后在设备的probe函数里调用:
device_create_file(dev, &dev_attr_my_attr);或者用sysfs_create_group()批量创建。当设备注册时,/sys/devices/platform/.../my_attr这个文件就出现了。
我见过不少初学者会把show回调写成sprintf(buf, ...)—— 这在旧内核没问题,但新内核里推荐用sysfs_emit(),因为它能保证不再发生缓冲区越界,而且自动处理页边界情况。这属于那种不踩一次就记不住的细节,我早期在新内核上调试时,曾经因为用sprintf把一个超过PAGE_SIZE的内容写进属性,结果读出来的数据被截断,看起来玄学,实际就是缓冲区上限。
3.3 sysfs的目录层次设计:从devices、driver到class
走进任何一台Linux机器,/sys底下最关键的几个目录是:
/sys/devices:所有真实设备的全局层次,按物理拓扑排布(如PCI总线域/设备/功能号);/sys/bus:按总线类型组织设备与驱动,比如pci、usb、platform;/sys/class:按功能分类的设备视图,比如网络接口、块设备、RTC等;/sys/block、/sys/firmware、/sys/module等按具体子系统划分的内容。
同一个设备对象,会在多个目录下同时出现。这就是设备模型里一个很重要的设计思想:同一个对象可以从不同视角被观察。物理视角(devices)、拓扑视角(bus)、功能视角(class)各有各的用处——应用层看功能,内核管理看物理,驱动绑定时看总线。
比如你的显卡是PCI设备,它在/sys/devices/pci0000:00/0000:00:02.0下存在,也在/sys/bus/pci/devices/0000:00:02.0下存在(这两个路径通常是指向同一对象的符号链接),同时如果它绑定了某个drm驱动,还会在/sys/class/drm/card0暴露功能入口。理解了这个多视图设计,以后排查"为什么这个设备怎么没有出现在某个目录"的问题就简单了。
4. device、driver与bus的三角关系,以及probe是怎么发生的
4.1 三个核心结构体的定位与真实语义
Linux设备模型中,真正的主角不是kobject,而是围绕它构建的三个大结构:struct device、struct device_driver、struct bus_type。
struct device:描述一个物理设备或者逻辑设备。它保存了设备名称、父设备、总线、电源管理状态、资源、设备树节点等大量信息;struct device_driver:描述一段能驱动某类设备的代码。它提供probe、remove、shutdown、suspend/resume等回调接口,还知道自己适用的设备ID表;struct bus_type:描述一种通信总线。它定义了匹配规则、u事件处理、电源管理、DMA、热插拔等机制。
内核里可以说"任何孤立的设备都没有意义",一个设备必须知道自己挂在哪条总线上,一个驱动也必须声明自己适配哪条总线。这条总线的存在,把设备和驱动从"各自独立"的状态变成了"有可能绑定"的状态。
4.2 设备驱动匹配的三种主要机制
总线的存在最终是为了配对。我以最典型的platform总线、i2c总线、pci总线来举例说明匹配机制,三种不同思路:
第一种:id_table 硬匹配
驱动声明一个ID表,列出它支持的所有设备名/ID。设备侧也有自己的名字或ID。当总线进行match时,查表对比,命中即匹配。i2c设备最典型:
static const struct i2c_device_id xxx_id[] = { { "bme280", 0 }, { "bmp280", 0 }, { } }; MODULE_DEVICE_TABLE(i2c, xxx_id);第二种:device tree 兼容性匹配
设备树节点里写compatible = "bosch,bme280";,驱动里写of_device_id的.compatible字段。总线在match时优先查设备树匹配:
static const struct of_device_id xxx_of_match[] = { { .compatible = "bosch,bme280", }, { } }; MODULE_DEVICE_TABLE(of, xxx_of_match);第三种:设备主动创建设备名匹配
有些总线没有严格ID表,比如platform总线有一条兜底逻辑:如果设备名(device.name)和驱动名(driver.name)完全一致,也能匹配上。这就是为什么很多平台驱动里platform_driver.name = "xxx_platform"会恰好对应设备树节点中被解析出来的xxx_platform设备名。
4.3 probe流程是"人手交接"的过程
当匹配成功后,总线会调用驱动里的probe(),把struct device *dev传进去。驱动在这个函数里完成的事情,相当于"锅炉工接到锅炉房钥匙后的第一步巡检":
- 从设备节点/资源表中获取IO地址、中断号、时钟频率等资源;
- 申请IO内存、注册中断、初始化硬件;
- 初始化各种子结构(如输入设备、网络设备、字符设备框架);
- 创建一个或多个
struct device的"从设备",进一步暴露功能; - 如果失败,返回错误码,驱动核心会把匹配关系回滚,设备状态变成 "unbound"。
每个总线的match和probe逻辑略有区别,比如PCI总线的match会看vendor/device ID,USB会看interface class,但整体哲学是统一的:总线和驱动核心只负责撮合,具体硬件怎么初始化完全交给驱动的probe。
这里有个值得记住的调试要点:设备匹配失败时,/sys/bus/xxx/devices/下设备还在,但在si_driver这个符号链接(或驱动目录)里没有对应驱动。而驱动加载失败时,/sys/bus/xxx/drivers/下的驱动没有绑定任何设备。用ls -l一看便知,比在内核日志里翻天覆地找错误要快得多。
4.4 一张流程图式的对应关系(非图表,用列表形式列清楚)
为了把上面读起来比较"散"的关系压实,我直接列一份最常见的配对状态清单:
- 设备存在,驱动已加载,匹配成功:
/sys/bus/platform/devices/xxx/driver指向驱动的符号链接; - 设备存在,驱动已加载,匹配失败:
/sys/bus/platform/devices/xxx/driver不存在,但driver_override属性可以强行指定驱动; - 设备存在,驱动未加载:驱动目录里没有对应条目,
lsmod也看不到模块; - 设备未创建,驱动却已加载:说明设备树或ACPI没有声明该设备,或设备枚举尚未完成。
在上面基础上,再配合dmesg | grep xxx、/proc/device-tree和udevadm info,大多数绑定问题都能在分钟级定位。
5. 设备模型的"入口":从注册一个platform_driver开始
5.1 为什么平台设备是嵌入式/非PCI设备的事实标准
现代Linux内核里,你写的大部分字符设备驱动、传感器驱动、GPIO驱动,其实都是挂在platform总线上的。因为PCI、USB这些总线都有硬件探测机制(枚举是硬件帮你做的),而SoC内部的许多设备(UART、I2C控制器等)没有类似标准发现协议,只能靠编译时固件描述或设备树描述。platform总线就是对这些"无硬件枚举能力"设备的一种抽象,本质上它是个软件总线,设备列表由系统初始化时注册的设备树节点或者板级初始化代码生成。
举个例子,在设备树里写:
mydevice: mydevice@1c00000 { compatible = "myvendor,mydevice"; reg = <0x01c00000 0x1000>; interrupts = <0 45 4>; };内核启动时,of_platform_default_populate_init()会遍历设备树,把这个节点变成一个platform_device,注册到内核总线里。随后platform_bus_type.match会和你的驱动去对表,匹配成功就probe。
5.2 手写一个最简platform驱动骨架
为了让上面的机制不提虚的,给你一个最简驱动骨架,这个骨架我自己每次新板子起笔都是拿它改的:
#include <linux/module.h> #include <linux/platform_device.h> #include <linux/of.h> #include <linux/of_device.h> static int my_probe(struct platform_device *pdev) { struct resource *res; void __iomem *base; int ret; res = platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -ENXIO; base = devm_ioremap_resource(&pdev->dev, res); if (IS_ERR(base)) return PTR_ERR(base); devm_platform_ioremap_resource(pdev); /* 等价做法,返回映射后的虚拟地址 */ ret = devm_request_irq(&pdev->dev, platform_get_irq(pdev, 0), my_isr, 0, "mydevice", my_data); if (ret < 0) return ret; dev_info(&pdev->dev, "probe ok, base=%px\n", base); return 0; } static void my_remove(struct platform_device *pdev) { dev_info(&pdev->dev, "remove\n"); } static const struct of_device_id my_of_match[] = { { .compatible = "myvendor,mydevice", }, { } }; MODULE_DEVICE_TABLE(of, my_of_match); static struct platform_driver my_driver = { .probe = my_probe, .remove = my_remove, .driver = { .name = "mydevice", .of_match_table = my_of_match, }, }; module_platform_driver(my_driver); MODULE_LICENSE("GPL");有几个点我特别说一下。
devm_前缀的函数(device managed)是设备模型送给驱动开发者最大的礼物。用了它,你就不用在remove或错误分支里手工释放资源——内核会在设备unbind时自动帮你释放。这个设计大幅降低了资源泄漏概率。真实项目里我几乎不再手动request_region+ioremap+release_region那套老组合,全都交给devm_系列。
platform_get_resource(pdev, IORESOURCE_MEM, 0)是从哪里来的资源?它的来源是设备树里的reg属性。而platform_get_irq(pdev, 0)对应的是设备树的interrupts属性。也就是说,资源和中断不是驱动自己编的,而是由设备节点提供,驱动为消费者。
5.3 注册宏module_platform_driver到底做了什么
这个宏展开后做了三件事:
- 定义一个
platform_driver实例(即你填写的那个结构体); - 定义
module_init调用的注册函数:platform_driver_register(&你的驱动结构体); - 定义
module_exit调用的注销函数:platform_driver_unregister(&你的驱动结构体);
手动拆开会发现,它跟你写的:
static int __init xxx_init(void) { ... } static void __exit xxx_exit(void) { ... } module_init(xxx_init); module_exit(xxx_exit);是完全一致的。用宏可以减少模板代码,也让驱动挂载/卸载逻辑一目了然。
6. class、devtmpfs和uevent:设备模型与用户空间的"最后一公里"
6.1 从内核设备到/dev/xxx:设备节点是怎么来的
设备模型不只是内核里的抽象,它必须能支撑用户空间的日常使用。/dev/下面那些节点,就是通过设备模型 +devtmpfs+udev三者协作完成的。
内核设备在注册时,如果所属的子系统使用了device_create()之类的接口,会自动在 devtmpfs 里创建设备节点。class的核心作用就是把"一类具有相同功能的设备"组织起来,并分配设备号。例如:
static struct class *my_class; static int __init my_init(void) { my_class = class_create("myclass"); device_create(my_class, dev, MKDEV(major, minor), NULL, "mydev%d", minor); }这样用户空间立刻能看到/dev/mydev0,不需要额外脚本。在新内核里class_create的接口有变化,它简化成传入owner参数即可:
my_class = class_create("myclass");6.2 uevent与热插拔:内核怎么通知用户空间的
另一条线是uevent。当一个设备从内核里加入或者移除时,设备模型会生成一条环境事件,包含ACTION=add或ACTION=remove、DEVPATH、SUBSYSTEM等键值对。用户空间的udevd(systemd-udevd)收到之后,按照/usr/lib/udev/rules.d和/etc/udev/rules.d里的规则执行任务。
这就解释了为什么你插上一个U盘,/dev/sda会自动出现,为什么拔掉之后它又能自动消失。实际上这块逻辑是:USB子系统创建usb_device-> 驱动核心检测到新增 -> 生成uevent -> udev处理 -> 根据规则创建设备节点或者启动挂载。
调试这类问题时,udevadm monitor是你的得力工具,它能实时打印内核发出的uevent。
6.3 设备模型在电源管理里的隐蔽角色
设备模型的另一个重要作用时刻你可能没留意过——系统挂起/唤醒(suspend/resume)。内核在休眠时并不是随便谁想睡就睡,而是按照设备模型里的拓扑次序逐级处理:先挂起子设备,再挂起父设备,唤醒时顺序相反。
这个行为是通过struct dev_pm_ops挂到总线和驱动上的。你在driver结构体里填的.pm = &my_pm_ops,会在设备模型历遍时被调用。很多嵌入式项目里出现的"睡眠后醒不来""某个设备唤醒时没恢复寄存器",最后查下来都是设备树里电源域层级和驱动pm回调不太对。
这也是设备模型抽象的一个隐藏价值:它让所有设备在电源状态迁移时都遵循统一框架,驱动开发者只需要在框架里填写自己的回调。
7. 实战排错:设备模型问题排查的三板斧
7.1 第一板斧:查ls /sys与符号链接
设备模型调试时,我最先做的永远是看三个地方:
/sys/bus/<bus>/devices/下面有没有设备?/sys/bus/<bus>/drivers/下面有没有驱动?- 设备的
driver符号链接是否指向对应驱动?
如果设备目录存在但driver指向不存在,说明match失败,接下来要审视设备树 compatible 是否和驱动的of_match_table完全一致——注意,一旦有大小写差异、多余空格、换行,都匹配不上。
7.2 第二板斧:在驱动加载路径上埋trace
设备模型框架本身不太需要调试,但驱动绑定过程需要。我在probe前后加上打印:
dev_info(&pdev->dev, "myprobe enter\n"); ... dev_info(&pdev->dev, "myprobe leave success\n");再配dynamic_debug或直接看dmesg,就能知道是入口都没进去(多半是match没成功),还是进去后某个资源获取失败(多半是设备树资源不对)。
如果还不够,用ftrace或者tracefs里的events/device/dev_add等tracepoint,能拿到更底层的设备模型事件流,比如设备在哪一步创建、哪个驱动绑定被拒绝。
7.3 第三板斧:/proc/device-tree与实际的硬件地址比对
设备树与设备模型衔接出问题时,我喜欢直接看运行时的设备树子目录:
ls /proc/device-tree/ocp/mydevice/ cat /proc/device-tree/ocp/mydevice/compatible cat /proc/device-tree/ocp/mydevice/reg如果这些内容和你写进驱动of_match_table里的不一样,优先修正设备树而不是改驱动。改驱动去适配一个错误描述硬件的设备树,属于错误的源头还没解决就先去捂住出水口,后续隐患很大。
8. 从设备模型看Linux内核设计的几层深意
8.1 为什么要有这么复杂的抽象层,直接操作硬件不好吗
这个问题我早年问过自己。后来写了不少驱动,维护过几套板级SDK之后想明白了:如果每个驱动都直接操作寄存器、直接管理自己的生命周期、直接分配设备号,那么内核里的每个驱动就是一座孤岛。系统集成的时候,孤儿设备没人管电源、热插拔没法通知用户态、多个驱动抢同样设备号、设备节点混乱——这些问题会爆炸性地增加。
设备模型的存在,是为了把"硬件驱动代码"和"公用的系统管理能力"解耦。前者是产品相关、五花八门,后者是通用能力、人人需要。可以说设备模型就是这些通用能力的制度化基础设施。
8.2 引用计数、sysfs、uevent三者的共同哲学
把设备模型的几个要素放在一起看,能看到一个统一的哲学:内核里发生的每一件事,都要以可观察、可追溯、可管理的方式呈现出来。
- 引用计数保证"可安全释放";
- sysfs保证"可查看、可配置";
- uevent保证"可感知状态的变更";
- 总线加驱动架构保证"可按统一规则绑定"。
这对我们写业务代码也有借鉴意义——模块与模块之间,永远要有一层清晰的公共协议层,而不是让每个业务方直接互相调用。Linux内核用了几十年时间证明,这种"底盘式"的抽象设计是应对复杂度最有用的武器。
8.3 掌握设备模型的价值:从被框架限制到驾驭框架
说句实在话,如果你只是"能用框架写一个hello驱动",设备模型对你来说可能只是仪式感。但一旦你要做以下任何一件事——多个时钟域设备的电源管理、背光调节与设备树绑定的资源协调、子系统之间异步热插拔的竞态规避、在sysfs上给用户态开调试口——你就离不开设备模型的理解。
我见过不少工程师背熟了platform_driver模板,却不知道它的probe为什么被调、失败后为什么devm资源还能被释放、设备节点怎么从 sysfs 里消失。结果遇到真正难一点的问题就无从下手。把设备模型这层关系想明白了,你就是从"模板使用者"变成了"框架理解者"。
9. 一些从实践中来、书本上少写的话
最后说点写代码之外的题外话。
设备模型设计得很优雅,但它是几十年来无数硬件、无数驱动磨合出来的结果。你读它的时候,不要指望每个 API 的引入都像教科书那样富有浪漫色彩。很多函数是新版本为了修某个bug而加的,很多字段是为了兼容某个特定厂商的硬件而留的。所以看源码时务必带上git blame或者版本历史,你会有一种打通任督二脉的感觉。
我自己在调试一个触摸屏驱动的经历里,最有价值的一课是:设备树里的 "status = disabled" 会把节点彻底藏起来,但 sysfs 里仍然会看到总线目录下有残留设备名,只是没有在/sys/devices/platform/里展开成真实设备。这种"既有又没有"的状态,如果不理解设备模型的“设备纳入、驱动绑定”两阶段,会绕很久。
再分享一个小习惯:我每次接手一个新板子,会先做一次系统性的设备模型盘点,把所有在跑的设备在 /sys 下的路径和绑定的驱动都记录下来,作为板级调试的基线。后面一旦出现某个外设不工作,只需对比基线,就能快速锁定是设备树没声明、驱动没绑定、还是硬件电源没就绪。
设备模型不是内核里最容易懂的部分,但绝对是最值得花时间理解的部分之一。它像一张组织结构图,把硬件、驱动、用户空间、电源管理全捏合在一起。把这张图刻进脑子里,以后遇到 Linux 里任何"为何我的设备没反应"的问题,你前进的方向都会清晰很多。