Linux 内核设备模型与驱动核心深度实战:从 kobject 到设备树的完整链路透视
Linux 内核设备模型(Device Model)是驱动开发的骨架,统一描述了总线、设备、驱动、类之间的关系。本文从 kobject 底层数据结构出发,完整剖析 bus_type/device/driver 三大核心、sysfs 文件系统映射、设备树(Device Tree)解析、电源管理框架,并结合实际驱动代码展示一个完整的 platform driver 开发流程。掌握设备模型,才能真正理解 Linux 驱动是如何被发现、匹配、绑定和管理的。
一、设备模型的设计哲学与核心动机
在 2.5 内核之前,Linux 缺乏统一的设备管理框架。驱动程序自行维护设备列表,即插即用、电源管理、设备拓扑等功能难以协调引入。设备模型的引入解决了四个核心问题:
- 统一抽象:以 bus/device/driver 三元组描述所有外设关系,无论物理总线还是虚拟总线
- 生命周期管理:通过引用计数自动释放不再使用的设备和驱动对象
- 用户空间可见:通过 sysfs 将内核设备拓扑完整暴露,为 udev/mdev 提供信息源
- 电源管理:统一管理休眠/唤醒时设备的挂起/恢复顺序,避免电源泄漏
设备模型的核心数据结构是一个层次化树状结构:根是 devices_subsys 和 bus,向下展开各种总线类型(pci_bus_type, platform_bus_type 等),每个总线挂载设备,设备绑定驱动,驱动提供操作函数集。
二、kobject:一切皆对象的基石
kobject 是设备模型的最底层基体,提供引用计数、sysfs 表示、父子关系管理三大功能。几乎所有设备模型对象(bus_type, device, driver_private)都内嵌 kobject。
// include/linux/kobject.h
struct kobject {
const char *name;
struct list_head entry;
struct kobject *parent; // 在 sysfs 中的父目录
struct kset *kset; // 所属对象集合
struct kobj_type *ktype; // 操作函数集与释放函数
struct kernfs_node *sd; // sysfs 目录项
struct kref kref; // 引用计数
unsigned int state_initialized:1;
unsigned int state_in_sysfs:1;
unsigned int uevent_suppress:1;
};
struct kobj_type {
void (*release)(struct kobject *kobj);
const struct sysfs_ops *sysfs_ops;
struct attribute **default_attrs;
const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *);
const void *(*namespace)(struct kobject *);
};
kobject 通过维护 kref(引用计数)来自动管理生命周期:当计数归零时,调用 ktype->release() 释放对象。parent 指针在 sysfs 中建立目录层级,kset 则将同类型对象归为一组,便于批量操作(如统一发送 uevent)。
2.1 kset:对象集合与热插拔事件
kset 是一组同类 kobject 的集合,同时负责过滤和转发热插拔事件(uevent)。kset 本身也是 kobject,因此可以嵌套,形成 sysfs 目录树。
struct kset {
struct list_head list; // 所有属于此 kset 的 kobject
spinlock_t list_lock;
struct kobj kobj; // 内嵌 kobject
const struct kset_uevent_ops *uevent_ops;
};
uevent_ops 定义了 kset 如何处理热插拔事件过滤(filter)和环境变量添加(uevent)。drivers 通常无需直接操作 kset,通过 device_add() 和 driver_register() 等高层 API 间接触发。
2.2 引用计数与自动释放
kobject_get() 和 kobject_put() 分别增加和减少引用计数。这一机制确保驱动在持有设备指针时设备不会被意外释放:
struct kobject *kobject_get(struct kobject *kobj) {
if (kobj) {
if (!kobj->state_initialized)
WARN(1, KERN_WARNING "kobject: '%s' (%p): is not "
"initialized, yet kobject_get() is being "
"called.\n", kobj->name, kobj);
kref_get(&kobj->kref);
}
return kobj;
}
void kobject_put(struct kobject *kobj) {
if (kobj) {
if (kobj->state_initialized)
kref_put(&kobj->kref, kobject_release); // release()
}
}
设备模型中典型的引用链是:device 被 bus 引用、被 driver_private 引用、被 open 的文件描述符引用、被 sysfs inode 引用。任意路径释放时,引用计数自动递减,归零时调用 device->release()。
三、bus_type:总线类型的核心抽象
bus_type 描述一种总线类型(如 PCI、USB、Platform),定义了该总线的设备与驱动的匹配方法、电源管理回调、以及总线级别的私有字段。内核通过 bus_register() 注册总线,驱动程序不需要自己分配 bus_type。
struct bus_type {
const char *name;
const char *dev_name;
struct device *dev_root;
const struct attribute_group **bus_groups;
const struct attribute_group **dev_groups;
const struct attribute_group **drv_groups;
int (*match)(struct device *dev, struct device_driver *drv); // 匹配函数
int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
int (*probe)(struct device *dev); // 探测函数(可选)
void (*remove)(struct device *dev);
void (*shutdown)(struct device *dev);
int (*suspend)(struct device *dev, pm_message_t state);
int (*resume)(struct device *dev);
const struct dev_pm_ops *pm;
const struct iommu_ops *iommu_ops;
struct subsys_private *p; // 私有数据
struct lock_class_key lock_key;
};
3.1 match 函数:设备驱动的配对核心
match() 决定了何时将某个设备与某个驱动绑定。不同总线的 match 策略不同:
- platform_bus:对比 compatible 字符串(来自设备树或 ACPI)与 driver.of_match_table
- pci_bus:对比 PCI vendor/device ID、subsystem ID、class 等
- usb_bus:匹配 USB 设备描述符中的 vendor/product/interface
- i2c_bus:匹配 I2C 地址表与 driver.id_table
当 match 返回 1 时,总线调用 probe() 完成绑定。这种机制即 Linux 驱动模型中广泛使用的 "probe/remove" 回调模式。
3.2 总线的 sysfs 节点布局
每个总线在 /sys/bus/<name>/ 目录下拥有四个标准目录:
/sys/bus/platform/
├── devices/ # 该总线上所有设备(符号链接到 /sys/devices/...)
├── drivers/ # 该总线上所有已注册的驱动
├── drivers_autoprobe # 控制是否自动 probe
└── uevent # 写入 "add" 触发 uevent
/sys/devices/ 目录维护了硬件拓扑的物理视图:
/sys/devices/
├── platform/ # platform 总线设备
│ ├── serial8250 # 串口控制器
│ ├── gpio-leds # LED 设备
│ └── mydevice.0 # 自定义设备
├── system/ # CPU、时钟等非总线设备
├── virtual/ # 虚拟设备(如 loopback)
└── ...
四、device 结构体:设备实例的完整描述
struct device 是设备模型中最直观的实体,代表一个物理或逻辑设备实例,内嵌 kobject 用于生命周期管理。
struct device {
struct device *parent; // 父设备(用于设备树设备)
struct kobject kobj;
struct bus_type *bus; // 所属总线类型
struct device_driver *driver; // 绑定的驱动
void *platform_data;
void *driver_data; // 驱动私有数据(由 driver 设置)
struct device_node *of_node; // 设备树节点
struct fwnode_handle *fwnode; // 固件节点(ACPI/DT 抽象)
struct class *class; // 所属 class
const struct attribute_group **groups;
void (*release)(struct device *dev);
struct iommu_group *iommu_group;
struct iommu_device *iommu;
bool offline_disabled : 1;
bool offline : 1;
};
4.1 device_register() 的调用流程
int device_register(struct device *dev) {
device_initialize(dev);
return device_add(dev);
}
int device_add(struct device *dev) {
// 1. 验证父设备与总线设置
// 2. 设置 kobj.kset = devices_subsys.kset(加入全局 kset)
// 3. 在 sysfs 中创建目录 /sys/devices/.../
// 4. 创建 dev 属性文件(用于 mknod)
// 5. 将设备加入 bus->klist_devices
// 6. 调用 bus_probe_device(dev) → 尝试为该设备匹配驱动
// 7. 添加 class 关联
// 8. 如果有父设备,链接到父的 children 链表
}
这个流程的第六步至关重要:device_add() 会自动尝试为该设备匹配驱动。这是 Linux 即插即用的核心——新设备注册时自动 probe,无需手动干预。
4.2 device_create():快速创建设备节点
驱动开发者常用 device_create() 在 /sys/class/<class> 下快速创建设备节点,同时生成 /dev/<name>:
struct device *device_create(struct class *cls, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...);
// 典型调用:
struct class *my_class = class_create(THIS_MODULE, "my_device");
struct device *dev = device_create(my_class, NULL, dev_num, NULL, "mydevice.%d", 0);
五、device_driver 与驱动注册
device_driver 描述一个驱动程序实例。驱动可以独立于设备存在(如模块插入时),可以在设备之后注册。
struct device_driver {
const char *name;
struct bus_type *bus; // 所属总线
struct module *owner;
const char *mod_name;
int (*probe)(struct device *dev);
void (*remove)(struct device *dev);
void (*shutdown)(struct device *dev);
int (*suspend)(struct device *dev, pm_message_t state);
int (*resume)(struct device *dev);
const struct of_device_id *of_match_table; // 设备树匹配表
const struct acpi_device_id *acpi_match_table; // ACPI 匹配表
const struct pci_device_id *id_table; // PCI ID 表
const struct attribute_group **groups;
struct driver_private *p;
};
5.1 驱动注册的内在逻辑
驱动通过 driver_register() 注册,核心流程:
- 强制 driver->bus->match 存在,用于后续配对
- 调用 bus_add_driver(drv) 将驱动加入 bus->klist_drivers 链表
- 遍历 bus->klist_devices,对每个未绑定驱动的设备调用 driver_attach()
- driver_attach() 对每个设备调用 bus->match(dev, drv),若命中则调用 probe()
- 在 sysfs 中创建 /sys/bus/pci/drivers/<drvname>/ 目录
这意味着:设备先注册会自动匹配驱动;驱动先注册也会自动遍历设备。这是支持热插拔的关键。
5.2 probe/remove 生命周期函数
probe 函数通常在以下上下文中调用:
- 调用上下文:进程上下文,可持有 mutex 和 semaphore
- 调用时可睡眠(用于设备初始化等待)
- 返回 0 表示成功,负数表示失败(-EBUSY 等)
- 失败时驱动不会绑定到设备,device->driver 保持 NULL
典型 probe 流程:
static int my_driver_probe(struct platform_device *pdev) {
struct my_dev *dev;
struct resource *res;
int irq, ret;
// 1. 分配设备私有数据结构
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
// 2. 获取 I/O 资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
dev->base = devm_ioremap_resource(&pdev->dev, res);
// 3. 获取中断
irq = platform_get_irq(pdev, 0);
ret = devm_request_irq(&pdev->dev, irq, my_handler, 0, "mydev", dev);
// 4. 初始化 clk、gpio、regulator
dev->clk = devm_clk_get(&pdev->dev, NULL);
// 5. 注册到子系统(如 cdev, misc, input, hwmon 等)
ret = misc_register(&dev->miscdev);
// 6. 保存私有数据
platform_set_drvdata(pdev, dev);
return 0;
}
六、Platform 总线:SOC 设备的虚拟家园
Platform 总线是最简单也是哑(虚拟)的总线,用于片上系统设备(如 UART、I2C 控制器、GPIO 等)。这些设备没有物理总线用于自动枚举,而是通过硬编码注册或设备树描述。
6.1 platform_device 注册方式
传统方式(板级文件):直接在板初始化代码中注册 platform_device,指定资源数组。现代方式:通过设备树描述,由 OF 代码自动展开为 platform_device。
// 方式一:传统板级文件注册
static struct resource uart0_resources[] = {
[0] = {
.start = 0x10000000,
.end = 0x100000FF,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = IRQ_UART0,
.end = IRQ_UART0,
.flags = IORESOURCE_IRQ,
},
};
static struct platform_device uart0_device = {
.name = "ns16550",
.id = 0,
.num_resources = ARRAY_SIZE(uart0_resources),
.resource = uart0_resources,
.dev.platform_data = NULL,
};
// 在 board_init() 中:
platform_device_register(&uart0_device);
现代设备树方式(推荐):在 DTS 中描述设备,内核启动时解析并自动生成 platform_device。
/* dts 文件 */
uart0: serial@10000000 {
compatible = "ns16550a";
reg = <0x10000000 0x100>;
interrupts = <GIC_SPI 32 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clk_uart>;
clock-names = "uartclk";
status = "okay";
};
6.2 Platform 的匹配过程
platform_match() 是 platform 总线的核心匹配函数,依次尝试:
- OF(Open Firmware)匹配:对比 device_node->compatible 与 driver.of_match_table
- ACPI ID 匹配:对比 ACPI ID 表
- ID Table 匹配:platform_driver.id_table->name 与 platform_device->name
- 直接名字匹配:driver->name 与 platform_device->name
OF 匹配是最常见的方式。例如一个 DTS 节点 compatible = "vendor,mydev" 会被匹配到声明`of_device_id`表为 `{ .compatible = "vendor,mydev" }`的驱动。
七、设备树(Device Tree):硬件的源代码化
设备树(Device Tree)是一种描述硬件拓扑的数据结构文件(DTS),由内核解析生成内部树(device_node 链)。它替代了传统架构(ARM 等)在板级文件硬编码硬件信息的方式,使得同一内核 image 可以驱动不同板卡。
7.1 设备树核心概念与语法
DTS 由节点(node)、属性(property)、包含关系组成:
/* 根节点 */
/ {
compatible = "myvendor,myboard";
model = "My Board V1.0";
#address-cells = <1>; // 子节点的 reg 地址字段数
#size-cells = <1>; // 子节点的 reg 大小字段数
/* 总线节点 */
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges; // 地址空间映射(子空间 → 父空间)
/* 设备节点 */
mydevice@20000000 {
compatible = "myvendor,mydevice";
reg = <0x20000000 0x1000>;<#address #size>
interrupts = <GIC_SPI 48 IRQ_TYPE_LEVEL_HIGH>;
interrupt-parent = <&gic>;
clocks = <&clk_bus>;
clock-names = "apb_pclk";
status = "okay"; // "okay" 表示启用, "disabled" 表示禁用
};
};
};
关键属性说明:
- compatible:驱动匹配的关键字符串,格式 "vendor,model"。可以有多个,内核按顺序尝试匹配
- reg:设备寄存器的物理地址范围,#address-cells 和 #size-cells 定义字段数
- interrupts:中断说明符,内容取决于中断控制器(如 GIC 的 SPI/PPI 类型)
- status:设备状态。"okay" 或缺失表示启用,"disabled" 表示节点存在但不可用
- ranges:地址翻译表,将子总线的地址映射到父总线地址空间
7.2 设备树到内核数据结构的展开
内核启动时,of_core 子系统执行以下步骤:
- DTB 扫描:bootloader 加载 DTB(dtb 格式)物理地址,内核 __dtb_start/__dtb_end 获取边界
- unflatten:early_init_dt_scan() 解析平铺 DTB 为内存中的设备树(device_node 链表树)
- of_platform_populate():遍历 device_node 树,为每个状态为 enabled 的节点创建 platform_device
- 与驱动匹配:platform_device 注册时调用 of_match_bus() 将设备树 compatible 与驱动 of_match_table 匹配
这样,硬件信息在 DTS 中描述一次,内核自动展开设备并匹配驱动,无需板级代码手动注册。
7.3 驱动获取设备树数据的 API
驱动在 probe 中通过 of_* 系列 API 读取设备树属性:
static int my_probe(struct platform_device *pdev) {
struct device_node *np = pdev->dev.of_node;
struct resource res;
u32 val;
const char *str;
const __be32 *prop;
// 1. 读取 u32 属性
of_property_read_u32(np, "vendor,clock-freq", &val);
// 2. 读取字符串
of_property_read_string(np, "label", &str);
// 3. 获取 reg 资源
of_address_to_resource(np, 0, &res);
// 4. 获取中断
int irq = of_irq_get(np, 0);
// 5. 获取 clocks
struct clk *clk = of_clk_get(np, 0);
// 6. 获取 gpio
struct gpio_desc *reset_gpio = gpiod_get_optional(&pdev->dev, "reset", GPIOD_OUT_LOW);
// 7. 获取自定义 data
platform_get_drvdata(pdev); // 获取 probe 中设置的数据
dev_get_drvdata(&pdev->dev);
return 0;
}
7.4 设备树绑定(Binding):硬件描述到驱动的可执行协议
设备树绑定描述了给定硬件节点的格式规范——哪些属性是必须的、可选的、可选值等。绑定文档位于内核源码 Documentation/devicetree/bindings/ 下,描述格式日益转为 YAML 模式(由 dt-schema 规范验证)。
一个典型的绑定片段:
# Documentation/devicetree/bindings/serial/myvendor-serial.yaml
compatible: "myvendor,myuart"
properties:
compatible:
items:
- enum:
- myvendor,myuart
reg:
minItems: 1
maxItems: 2
interrupts:
maxItems: 1
clocks:
minItems: 1
clock-names:
items:
- const: apb_pclk
required:
- compatible
- reg
- interrupts
- clocks
八、sysfs 与用户空间交互
sysfs 将内核对象映射为 /sys 下的目录和属性文件,是驱动暴露配置接口的标准方式。每个 kobject 对应 /sys 目录,每个 attribute 对应一个文件。
8.1 属性定义与读写
// 定义属性
static ssize_t my_show(struct device *dev, struct device_attribute *attr, char *buf) {
return sprintf(buf, "%d\n", my_dev->value);
}
static ssize_t my_store(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count) {
int val;
sscanf(buf, "%d", &val);
my_dev->value = val;
return count;
}
static DEVICE_ATTR(value, 0644, my_show, my_store);
// 注册属性文件 /sys/devices/.../mydevice/value
device_create_file(&pdev->dev, &dev_attr_value);
// 移除
device_remove_file(&pdev->dev, &dev_attr_value);
8.2 Attribute Groups 与批量注册
当属性较多时使用 attribute_group 批量注册和移除:
static struct attribute *my_attrs[] = {
&dev_attr_value.attr,
&dev_attr_mode.attr,
NULL,
};
static const struct attribute_group my_group = {
.attrs = my_attrs,
.name = "my_group", // 可选,创建子目录
};
// 注册
sysfs_create_group(&pdev->dev.kobj, &my_group);
// 移除
sysfs_remove_group(&pdev->dev.kobj, &my_group);
8.3 通过 sysfs 触发操作:ioctl 的轻量替代
相比 ioctl 的复杂性和非标准化,sysfs 属性文件提供了类 shell 的简洁接口:
// 用户空间
echo 42 > /sys/devices/mydevice/value
cat /sys/devices/mydevice/value
// 如果驱动需要锁保护,使用 mutex_lock() 包裹 store/show
九、驱动与设备的高级绑定技术
9.1 Class:按功能分类设备
struct class 是设备模型中的“分类”概念——不关心总线,只关心功能。例如 /sys/class/net/ 包含所有网卡,无论它们是 PCI、USB 还是 platform 设备。
struct class {
const char *name;
struct module *owner;
const struct attribute_group **class_groups;
const struct attribute_group **dev_groups;
struct kobj dev_kobj;
int (*dev_uevent)(struct device *dev, struct kobj_uevent_env *env);
char *(*devnode)(struct device *dev, umode_t *mode);
void (*class_release)(struct class *class);
void (*dev_release)(struct device *dev);
};
常见类的实例:
- class/misc:杂项设备,主设备号 10
- class/net:网络设备(net_device 自动注册此类)
- class/input:输入设备(鼠标、键盘、触摸屏等)
- class/hwmon:硬件监控设备(温度传感器、风扇等)
9.2 Device Links:跨总线的设备依赖管理
Device Links(Linux 4.10+)允许在来自不同总线的设备之间建立明确的依赖关系。当 GPU、IOMMU、PCI 等设备之间存在功能性依赖时,device link 保证运行时电源管理(runtime PM)时的引用计数一致,确保低功耗时各设备按正确顺序休眠和唤醒。
// 创建 device link
struct device_link *link = device_link_addconsumer, supplier,
DL_FLAG_STATELESS |
DL_FLAG_PM_RUNTIME |
DL_FLAG_RPM_ACTIVE);
// 当 consumer 初始化时执行 runtime_get(supplier)
// 当 consumer 断电时执行 runtime_put(supplier)
9.3 deferred_probe:延迟初始化直到依赖设备注册
当设备树中的依赖关系(如 clocks, resets, gpios, power-domains)尚未准备时,驱动应返回 -EPROBE_DEFER,内核稍后自动重试 probe。这避免了驱动注册时的强制顺序。
static int my_probe(struct platform_device *pdev) {
struct clk *clk = devm_clk_get(&pdev->dev, NULL);
if (IS_ERR(clk)) {
if (PTR_ERR(clk) == -EPROBE_DEFER)
return -EPROBE_DEFER; // 时钟提供者尚未注册,稍后重试
return PTR_ERR(clk);
}
// 其他初始化...
}
十、电源管理(PM):从系统级到设备级
Linux 设备模型提供分层电源管理框架,覆盖系统级休眠(STR/S3、STD/S4、关机)和运行时电源管理(Runtime PM)。
10.1 dev_pm_ops:设备电源操作
struct dev_pm_ops {
int (*prepare)(struct device *dev);
void (*complete)(struct device *dev);
int (*suspend)(struct device *dev);
int (*resume)(struct device *dev);
int (*freeze)(struct device *dev);
int (*thaw)(struct device *dev);
int (*poweroff)(struct device *dev);
int (*restore)(struct device *dev);
int (*suspend_late)(struct device *dev);
int (*resume_early)(struct device *dev);
int (*freeze_late)(struct device *dev);
int (*thaw_early)(struct device *dev);
int (*poweroff_late)(struct device *dev);
int (*restore_early)(struct device *dev);
int (*suspend_noirq)(struct device *dev);
int (*resume_noirq)(struct device *dev);
int (*freeze_noirq)(struct device *dev);
int (*thaw_noirq)(struct device *dev);
int (*poweroff_noirq)(struct device *dev);
int (*restore_noirq)(struct device *dev);
/**
* 运行时管理(非系统级):
* runtime_suspend/runtime_resume/runtime_idle
* 由 runtime PM 子系统调用
*/
};
10.2 设备休眠顺序:拓扑排序保证正确性
内核根据设备在设备树中的父子顺序,将设备组织为休眠链表。子设备先于父设备休眠(suspend),父设备先于子设备恢复(resume):
// 内核休眠代码片段:按拓扑相反顺序排列设备
// 子设备先休眠,父设备后休眠
// 反之亦然
list_for_each_entry_reverse(dev, &dpm_list, power.entry) {
// dev 可能是子设备
ret = device_suspend(dev);
}
这种顺序避免了子设备尝试访问父设备寄存器时父设备可能已断电的情况。
10.3 Runtime PM:无负载时自动断电
Runtime PM 允许单个设备在无负载时进入低功耗,而不影响系统全局状态:
// 驱动在 probe 中初始化 Runtime PM
pm_runtime_enable(&pdev->dev);
pm_runtime_set_autosuspend_delay(&pdev->dev, 1000); // 1 秒后自动休眠
// 使用设备时:
pm_runtime_get_sync(&pdev->dev); // 唤醒设备,refcount+1
// ... 使用设备 ...
pm_runtime_put_sync(&pdev->dev); // refcount-1,如果归零则可能休眠
// 在操作中返回 device 是否运行时挂起:
if (pm_runtime_suspended(dev)) {
// 错误路径——可能设备已休眠,需先唤醒
}
十一、实战:从零编写一个 Platform 字符设备驱动
将前述知识点整合为一个完整驱动示例——基于设备树描述的 platform 字符设备驱动。
// my_char_driver.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/of.h>
#include <linux/of_device.h>
#define MYDEV_NAME "mychardev"
struct my_dev {
struct cdev cdev;
void __iomem *regs;
struct device *dev;
};
static int my_open(struct inode *inode, struct file *filp) {
struct my_dev *dev = container_of(inode->i_cdev, struct my_dev, cdev);
filp->private_data = dev;
return 0;
}
static int my_release(struct inode *inode, struct file *filp) {
return 0;
}
static ssize_t my_read(struct file *filp, char __user *buf,
size_t count, loff_t *pos) {
struct my_dev *dev = filp->private_data;
u32 val = readl(dev->regs + 0x00);
char kbuf[32];
int n = snprintf(kbuf, sizeof(kbuf), "reg0=0x%08x\n", val);
return simple_read_from_buffer(buf, count, pos, kbuf, n);
}
static ssize_t my_write(struct file *filp, const char __user *buf,
size_t count, loff_t *pos) {
struct my_dev *dev = filp->private_data;
char kbuf[32];
int val;
if (copy_from_user(kbuf, buf, min(count, sizeof(kbuf) - 1)))
return -EFAULT;
kbuf[min(count, sizeof(kbuf) - 1)] = 0;
if (kstrtoint(kbuf, 16, &val))
return -EINVAL;
writel(val, dev->regs + 0x00);
return count;
}
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
.llseek = noop_llseek,
};
#define MYDEV_MAX_INSTANCES 4
static const struct of_device_id my_of_match[] = {
{ .compatible = "myvendor,mychardev" },
{ }
};
MODULE_DEVICE_TABLE(of, my_of_match);
static int my_probe(struct platform_device *pdev) {
struct my_dev *mydev;
struct resource *res;
int ret, id = pdev->id;
mydev = devm_kzalloc(&pdev->dev, sizeof(*mydev), GFP_KERNEL);
if (!mydev)
return -ENOMEM;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
mydev->regs = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(mydev->regs))
return PTR_ERR(mydev->regs);
cdev_init(&mydev->cdev, &my_fops);
mydev->cdev.owner = THIS_MODULE;
ret = cdev_add(&mydev->cdev, MKDEV(240, id), 1);
if (ret)
return ret;
mydev->dev = device_create(my_class, &pdev->dev,
MKDEV(240, id), NULL,
MYDEV_NAME ".%d", id);
if (IS_ERR(mydev->dev)) {
cdev_del(&mydev->cdev);
return PTR_ERR(mydev->dev);
}
platform_set_drvdata(pdev, mydev);
dev_info(&pdev->dev, "probed, regs=%p\n", mydev->regs);
return 0;
}
static void my_remove(struct platform_device *pdev) {
struct my_dev *mydev = platform_get_drvdata(pdev);
device_destroy(my_class, MKDEV(240, pdev->id));
cdev_del(&mydev->cdev);
}
static struct platform_driver my_driver = {
.driver = {
.name = "mychardev",
.of_match_table = my_of_match,
.owner = THIS_MODULE,
},
.probe = my_probe,
.remove = my_remove,
};
static struct class *my_class;
static dev_t mydev_base;
static int __init my_init(void) {
int ret;
my_class = class_create(THIS_MODULE, "mychardev");
if (IS_ERR(my_class))
return PTR_ERR(my_class);
ret = alloc_chrdev_region(&mydev_base, 0, MYDEV_MAX_INSTANCES, MYDEV_NAME);
if (ret)
goto err_class;
ret = platform_driver_register(&my_driver);
if (ret)
goto err_chrdev;
return 0;
err_chrdev:
unregister_chrdev_region(mydev_base, MYDEV_MAX_INSTANCES);
err_class:
class_destroy(my_class);
return ret;
}
static void __exit my_exit(void) {
platform_driver_unregister(&my_driver);
unregister_chrdev_region(mydev_base, MYDEV_MAX_INSTANCES);
class_destroy(my_class);
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("My Character Platform Driver");
设备树节点示例:
mydevice@20000000 {
compatible = "myvendor,mychardev";
reg = <0x20000000 0x1000>;
status = "okay";
};
十二、驱动开发中的模式与最佳实践
12.1 devm_* 系列函数:自动资源管理
为避免 remove 中逐一释放资源,内核提供了 devm_* 系列函数。资源在设备销毁时自动释放,减少泄漏风险:
// 传统方式:需要手动每个释放err_iounmap:
iounmap(base);
err_clk_put:
clk_put(clk);
err_free_dev:
kfree(dev);
// devm_ 方式:自动释放
base = devm_ioremap_resource(&pdev->dev, res);
clk = devm_clk_get(&pdev->dev, NULL);
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
irq = platform_get_irq(pdev, 0);
ret = devm_request_irq(&pdev->dev, irq, handler, 0, "mydev", dev);
// remove 中无需任何释放代码!
devm_* 在内核驱动中应当是首选的资源管理方式。
12.2 driver_data 的使用
platform_set_drvdata() 和 platform_get_drvdata() 用于在设备实例上保存驱动私有数据。建议在 probe 开头保存,在 probe 和 remove 中使用:
// 函数签名简洁——所有上下文都存在 drvdata 中
static int my_probe(struct platform_device *pdev) {
struct my_dev *dev = platform_get_drvdata(pdev); // 从 drvdata 取回
// ... 使用 dev->regs, dev->irq 等 ...
return 0;
}
12.3 驱动加载顺序与 late_initcall
内核驱动默认在 module_init 阶段注册。Framework 驱动(如 clock framework、regulator framework、pin control)通常较早初始化,晚加载驱动可使用 late_initcall()。正确做法是依赖 -EPROBE_DEFER 机制,让内核自动重试。
12.4 中断与 DMA 处理设备模型关系
- 中断:platform_get_irq(pdev, 0) 从设备树获取中断号。中断与设备树绑定关系由设备节点 interrupts 属性驱动
- DMA:设备树通过 dmas 属性指定 DMA 通道。dma_request_chan() 从设备树自动获取通道。设备模型自动管理 DMA 状态
- DMA 一致性:dma_alloc_coherent() 返回的内存一致性由设备 coherent_dma_mask 控制。设备可通过设备树 dma-coherent 属性标记
十三、核心数据结构关系全景图
Linux 设备模型中的核心关系如下:
┌─────────────────────────────────────────────────────────────┐
│ devices_subsys (kset) │
│ │ │
│ ┌──────────────────┼──────────────────┐ │
│ ▼ ▼ ▼ │
│ platform_bus_type pci_bus_type usb_bus_type │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │platform │ │ pci_dev │ │ usb_dev │ │
│ │ device │ │ │ │ │ │
│ │ (of_node) │ │ (of_node) │ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ ▼ │ │ ▼ │ │ │ │
│ │device_ │ │pci_ │ │usb_ │ │
│ │ driver │ │ driver │ │ driver │ │
│ │ │ │ │ │ │ │ │ │
│ │ ▼ │ │ ▼ │ │ │ │
│ │module │ │module │ │module │ │
│ └───────────┘ └───────────┘ └───────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ class (按功能分类: class/net, class/input, class/misc) │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ sysfs: /sys/devices/ + /sys/bus/ + /sys/class/ │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Device Tree → platform_device → 自动匹配 platform_driver│ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
十四、高频问题与调试技巧
14.1 驱动 probe 没有被调用
常见原因排查:
- compatible 不匹配:DTS 中 compatible 字符串必须与驱动 of_match_table 完全一致(包括 vendor 前缀)
- status = "disabled":设备树节点被禁用,OF 核心不会展开为 platform_device
- 依赖资源缺失:时钟/复位/gpio 提供者尚未注册,驱动返回 -EPROBE_DEFER 但被忽略
- of_match_table 未声明:驱动缺少 .of_match_table 指针或表项
调试方法:
// 检查设备是否被展开为 platform_device
ls /sys/devices/platform/ | grep mydevice
// 检查驱动是否注册
ls /sys/bus/platform/drivers/ | grep mydriver
// 检查设备绑定状态
cat /sys/devices/platform/mydevice.0/driver/module/name
// 内核日志中搜索 probe 失败原因
dmesg | grep -E "mydevice|mydriver"
14.2 reg 属性解析错误
设备树节点的 #address-cells 和 #size-cells 被父节点定义。如果在子节点中错误地使用错误的字段数,of_address_to_resource() 会失败。
14.3 sysfs 属性文件权限
DEVICE_ATTR 定义的权限决定了用户空间是否可读写。若 store 函数中使用了 mutex_lock(),则在 sysfs 中不能使用 0222(只写),因为 sysfs 不持有调用者的 mutex。
14.4 模块卸载后设备仍在使用
驱动中持有 device 或 kobject 引用时,模块引用计数不会归零,rmmod 会失败。devm_* 函数自动管理,不会泄漏。手动 kobject_get() 必须对应 kobject_put()。
十五、生产环境最佳实践十条
- 优先使用设备树(DTS):新平台坚持用设备树描述硬件,弃用板级文件的 platform_device 硬编码注册
- devm_* 优先:probe 中所有资源分配使用 devm_* 系列函数,避免手动释放逻辑
- 正确处理 -EPROBE_DEFER:在依赖资源不可用时返回 -EPROBE_DEFER,让内核自动重试
- of_match_table 始终声明:platform_driver 必须声明 .of_match_table 和 MODULE_DEVICE_TABLE
- parent 指针必须正确:platform_device->dev.parent 设为正确的父设备,确保 sysfs 拓扑和电源管理顺序正确
- 避免全局变量:驱动私有数据保存在 driver_data 中,支持多实例
- sysfs 属性格式统一:每个属性只输出一个值,单位明确;遵循 Documentation/ABI 中的已有约定
- 中断请求使用 devm_request_irq:避免 remove 中忘记释放中断
- DMA 在 probe 中设置 mask:dma_set_mask_and_coherent() 应在 probe 开头调用,失败说明平台不支持,不应继续初始化
- device link 管理依赖:跨设备总线的设备依赖关系使用 device_link_add() 而非直接引用计数管理
十六、总结
Linux 内核设备模型是一套层次化、引用计数驱动的对象管理系统。从 kobject 的引用计数和 sysfs 映射,到 bus_type 的设备驱动配对机制,再到设备树对硬件拓扑的统一描述——这些组件共同构成了 Linux 驱动开发的骨架。掌握设备模型不仅意味着能正确编写驱动,更意味着理解设备如何被发现、绑定、初始化和电源管理。每一个字符设备驱动、网络设备驱动、块设备驱动,都是这套模型的实例化。
设备模型的设计哲学是"一切皆层次":在 sysfs 中表现为目录树,在设备树中表现为节点父子关系,在电源管理中表现为拓扑排序下的挂起/恢复链。理解这种层次性,是成为内核驱动开发者的关键一步。
技术深度决定系统稳定性的上限。设备模型看似平凡,实则是 Linux 内核20年积累的工程智慧的浓缩。

发表评论 取消回复