Linux内核分析之设备驱动-00

39.1 kobject、kset 与 ktype

kobject 是设备模型的"对象基类"——它不描述任何设备,只提供名字、树关系、引用计数、sysfs 投影与生命周期回调五件通用物。device/driver/bus 全部把它当第一个字段嵌入。本节拆解这个基类与它的两个伙伴。


39.1.1 kobject:五件通用物

// include/linux/kobject.h:64-88
struct kobject {
    const char      *name;      /* 对象名 (sysfs 目录名) */
    struct list_head    entry;      /* 挂 kset 的链 */
    struct kobject      *parent;    /* 父对象 (目录层级) */
    struct kset     *kset;      /* 所属集合 */
    const struct kobj_type  *ktype;     /* 类型: 属性+释放回调 */
    struct kernfs_node  *sd;        /* sysfs 目录项 (37.2 节 kernfs!) */
    struct kref     kref;       /* 引用计数 (17 章 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;
    ...
};

五件通用物与 sysfs 的对应:name→目录名、parent/kset→目录位置、kref→"目录存在多久"、ktype→目录里有哪些属性文件、sd→与 37.2.1 节 kernfs 节点的直连。kobject 永远动态分配、以 kref 管理——kobject_get/put 包装 kref;归零时经 ktype 的 release 回调释放(容器宏 container_of 反查宿主结构)。"put 之后不得再碰"的纪律由 release 回调统一执行,这是 C 语言设备驱动生命周期管理的全部答案。

39.1.2 ktype 与 kset

// include/linux/kobject.h:116-123
struct kobj_type {
    void (*release)(struct kobject *kobj);      /* 引用归零回调 */
    const struct sysfs_ops *sysfs_ops;      /* show/store 分发 */
    const struct attribute_group **default_groups;  /* 默认属性组 */
    const struct kobj_ns_type_operations *(*child_ns_type)(...);
    const struct ns_common *(*namespace)(...);
    void (*get_ownership)(...);
};
// :168
struct kset { ... /* 子 kobject 集合 + 自己也是 kobject + uevent 策略 */ };

ktype 与 kset 的分工常被混淆:ktype 回答"这类对象长什么样"(属性表+释放回调),kset 回答"这批对象住哪、怎么广播事件"(聚合目录 + kset_uevent_ops)。同一 kset 里的对象可有不同 ktype。kset 的 entry/parent 双向关系使"集合"既是一个目录又是一个链表——/sys/bus/pci/devices/ 就是典型 kset:枚举全部 PCI 设备目录。

39.1.3 uevent:向用户态广播

设备生灭的用户态通知 (lib/kobject_uevent.c):

 kobject_uevent(kobj, KOBJ_ADD/KOBJ_CHANGE/KOBJ_REMOVE...)
   ├─ 按 kset 的 uevent_ops 过滤/取子系统的名字
   ├─ 组装环境变量: ACTION= / DEVPATH= / SUBSYSTEM=
   │   + MODALIAS= (驱动自动加载的钥匙)
   ├─ 两条投递路径:
   │   netlink 广播 (48 章, udev 在线监听)
   │   + uevent 文件写入 (coldplug: udev 重启后
   │     重放 /sys 全部设备的 add 事件)
   └─ udev 收到: modprobe MODALIAS / 建设备节点 / 改权限

 32.3 节 signalfd 式的统一性: 用户态的一切
 设备管理 (命名/权限/模块加载) 都建立在
 这条"内核→netlink"的广播上

state_add_uevent_sent/remove_uevent_sent 位保证 add/remove 事件的配对性(不重复广播、remove 必达)——udev 数据库的一致性依赖此。


小结

kobject 以五件通用物(名字/树位置/引用/sysfs 投影/释放回调)充当设备模型基类,kref+release 回调构成 C 世界对象生命周期纪律,ktype 定形态、kset 定居所与广播策略,uevent 经 netlink+coldplug 双路径把设备生灭通知用户态。37.2 节 sysfs 的目录树在本节找到了内核本体。下一节看硬件拓扑如何经设备树变成这些对象。

39.2 设备树 (Device Tree) 与 platform_device

x86 的设备靠 PCI/ACPI 枚举,嵌入式 ARM64/RISC-V 的 SoC 外设没有可枚举总线——设备树(Device Tree,DT)以"数据描述硬件"取代"代码描述硬件":硬件布局写成 .dts 编译成 .dtb 传给内核,内核解析后生成 platform_device。本节拆解 DT 的结构与 platform 机制。


39.2.1 设备树的节点与绑定

// include/linux/of.h:48(内核侧节点, 节选要点)
struct device_node {
    const char *name;
    phandle phandle;
    const char *full_name;
    ...
    struct property *properties;    /* 属性链 */
    ...
    struct device_node *parent;
    struct device_node *child;
    struct device_node *sibling;
    ...
};
DT 源码片段 (SoC 的 UART 节点):

 soc {
     compatible = "acme,soc";
     #address-cells = <1>;
     #size-cells = <1>;
     ranges;
     uart0: serial@10010000 {
         compatible = "acme,uart", "generic-8250";
         reg = <0x10010000 0x1000>;   /* 寄存器基址+长度 */
         interrupts = <GIC_SPI 5 IRQ_TYPE_LEVEL_HIGH>;
         clocks = <&clk 3>;
         status = "okay";
     };
 };

 解析结果: device_node 树 = DT 文本的结构化镜像
 "compatible" 是驱动的钥匙: 字符串列表,
   从特异到通用 ("acme,uart" → "generic-8250"),
   驱动的 of_match_table 逐串比对

compatible 匹配的内核入口 of_device_is_compatible()(drivers/of/base.c:379)——of_match_table 的匹配规则与 39.4 节 driver 匹配函数对齐。属性即 ABI:reg/interrupts/clocks 的编码规则由"DT binding 文档"约定,设备树与驱动跨版本兼容全靠它——这是内核少有的数据驱动硬件设计(ACPI 是 x86 的对应物,Windows 固件共用)。

39.2.2 platform_device:不可枚举总线的设备

platform 总线的角色:

 SoC 外设 (UART/SPI 控制器/GPIO...) 无 PCI/USB
 这样的可枚举协议 — 内核虚构 platform 总线:
   设备: platform_device (来自 DT 解析 / ACPI / 旧式
         board 文件硬编码)
   驱动: platform_driver (probe 挂到匹配的设备)
 匹配优先级: of_match_table > id_table > 名字

 DT → 设备的批量生成:
   of_platform_populate(root, matches, lookup, parent)
   (drivers/of/platform.c:443)
     深度遍历 device_node 树, 对有 compatible 的节点
     建 platform_device 并注册 → 进 39.4 节的
     绑定流水线
// include/linux/platform_device.h:234(驱动模板, 节选)
struct platform_driver {
    int (*probe)(struct platform_device *);
    void (*remove)(struct platform_device *);
    ...
    struct device_driver driver;    /* 基类: 39.1.1 的 kobject 链 */
    const struct platform_device_id *id_table;
    bool prevent_deferred_probe;
};
// drivers/base/platform.c:901
int __platform_driver_register(struct platform_driver *drv, struct module *owner)

驱动的标准模板就此定型(全书 40-43 章的驱动示例皆此形):

static const struct of_device_id my_of_match[] = {
    { .compatible = "acme,uart" },
    { }
};
MODULE_DEVICE_TABLE(of, my_of_match);

static struct platform_driver my_driver = {
    .probe  = my_probe,
    .remove = my_remove,
    .driver = {
        .name = "acme-uart",
        .of_match_table = my_of_match,
        .pm = &my_pm_ops,   /* 电源管理钩子 */
    },
};
module_platform_driver(my_driver);  /* 注册+注销宏 */

设备树叠加(device tree overlay)与 status = "okay"/"disabled" 使同一内核映像适配同代不同板卡——设备树因此是嵌入式 Linux 的"硬件 ABI",5.4 节 ARM64 引导的 dtb 传递正是它的入口。


小结

设备树以"数据描述硬件"取代枚举协议:compatible 字符串是设备与驱动的钥匙、reg/interrupts/clocks 属性携带资源、of_platform_populate 把节点树批量变成 platform_device;platform 总线为不可枚举外设提供 device/driver 三角的标准舞台,module_platform_driver 模板定型全部本书后续驱动示例。下一节看这些对象如何以属性文件的形式暴露给用户。

39.3 sysfs 设备属性导出

37.2 节从 sysfs 视角看过属性机制;本节回到驱动开发视角——设备模型如何把"一个 kobject + 属性组"变成 /sys/devices/... 下的目录,以及设备级属性(uevent、power、driver 链接)的来源。


39.3.1 device 的 sysfs 投影

一个 PCI 设备在 sysfs 的完整投影:

 /sys/devices/pci0000:00/0000:00:1f.3/
   ├── uevent            ← 39.1.3 节的冷plug 出口
   ├── driver -> ../../../bus/pci/drivers/hda_intel   (符号链接)
   ├── subsystem -> ../../../bus/pci
   ├── vendor / device / class / irq / resource...
   │      ← bus 专属属性 (pci_bus_type 的 dev_groups)
   ├── power/            ← 电源管理属性 (runtime_status/wakeup)
   ├── iommu/            ← 43 章 IOMMU 组
   └── ...私有属性组

 层级即拓扑: 父目录 = 物理父设备 (PCI 根桥→设备)
 — kobject.parent 的链就是 39.1.1 节 device 树

三层属性来源汇成一个目录:bus 层(bus_type.dev_groups,类型公共属性)、class 层(class.dev_groups,功能属性如 net 设备的 statistics)、driver/device 层(驱动自定义 attribute_group)。device_register()(drivers/base/core.c)装配时依次 sysfs_create_groups。

39.3.2 驱动自定义属性

// 设备属性的驱动模板 (device.h 的宏族):

 static ssize_t fw_version_show(struct device *dev,
            struct device_attribute *attr, char *buf)
 {
    struct my_dev *m = dev_get_drvdata(dev);
    return sysfs_emit(buf, "%u.%u\n", m->fw_major, m->fw_minor);
 }
 static DEVICE_ATTR_RO(fw_version); /* 只读属性 */

 static const struct attribute_group my_group = {
    .attrs = (struct attribute *[]) {
        &dev_attr_fw_version.attr,
        NULL,
    },
 };
 // probe 里: device_add_group(&pdev->dev, &my_group)

show/store 的缓冲纪律:show 只能写一页且必须用 sysfs_emit(37.2.2 节格式化安全);store 收到的是以 NUL 结尾的整页副本——解析必须容忍尾随换行。dev_get_drvdata/dev_set_drvdata 是 probe 与属性回调间传递私有数据的标准通道(probe 里 platform_set_drvdata(pdev, priv))。

链接文件的三个方向都是设备模型自动维护的:driver -> 指向绑定驱动、bus 的 devices ->/drivers -> 互指、supplier:/consumer: 链接表达资源依赖(39.4 节 device_links 的 sysfs 投影)。属性写即配置的边界:sysfs 只放"一人一值",多字段/二进制一律走 ioctl/netlink——37.2.3 节分工纪律在设备层的执行。


小结

设备级 sysfs 目录由三层属性源装配(bus 公共/class 功能/driver 私有),拓扑即 kobject 父链,driver/subsystem/supplier 链接自动维护;驱动以 DEVICE_ATTR_* 宏 + attribute_group 声明属性,dev_drvdata 传递私有态,sysfs_emit 守住格式化安全。属性导出是设备模型"状态可见"的一半;下一节回到"对象如何被绑定"——注册与 probe。

39.4 设备注册与自动探测 (probe)

设备与驱动的配对是设备模型的心脏:任一方注册都可能触发绑定,绑定流程(really_probe)按序执行依赖检查、资源配置、驱动 probe;资源未就绪则以 EPROBE_DEFER 延迟重试。本节解剖这条流水线。


39.4.1 bus_type:匹配的裁判

// include/linux/device/bus.h:83(节选要点)
struct bus_type {
    const char *name;
    const struct attribute_group **bus_groups;
    const struct device_type *dev_groups;
    int (*match)(struct device *dev, struct device_driver *drv);
    int (*uevent)(...);
    int (*probe)(struct device *dev);
    void (*remove)(struct device *dev);
    ...
    int (*dma_configure)(struct device *dev);   /* 43 章接口 */
};
// include/linux/device/driver.h:98
struct device_driver {
    const char *name;
    const struct bus_type *bus;
    struct module *owner;
    int (*probe)(...);
    ...
    const struct of_match_table *of_match_table;    /* 39.2 节钥匙 */
    ...
    bool suppress_bind_attrs;
};

总线是匹配算法的载体:pci_bus_type.match 比对设备 ID 表、platform_bus_type.match 走 compatible 字符串、i2c/spi/scsi 各有变体。probe 的调用序是"先 bus->probe(总线钩子,如 PCI 的资源使能)再 driver->probe(驱动本体)"。

39.4.2 really_probe:绑定主流程

// drivers/base/dd.c:667-760(节选)
static int really_probe(struct device *dev, const struct device_driver *drv)
{
    ...
    if (defer_all_probes) {
        /* ...device_block_probing() ... wait_for_device_probe()... */
        return -EPROBE_DEFER;
    }

    link_ret = device_links_check_suppliers(dev);   /* :681 依赖就绪检查 */
    if (link_ret == -EPROBE_DEFER)
        return link_ret;
    ...
re_probe:
    device_set_driver(dev, drv);
    ret = pinctrl_bind_pins(dev);           /* 引脚配置 */
    if (ret)
        goto pinctrl_bind_failed;

    if (dev->bus->dma_configure) {          /* DMA 域配置 */
        ret = dev->bus->dma_configure(dev); /* :709 43 章入口 */
        if (ret)
            goto pinctrl_bind_failed;
    }
    ...
    ret = dev->bus->probe(dev);     /* 总线钩子 */
    if (ret)
        goto probe_failed;

    if (drv->probe) {
        ret = drv->probe(dev);      /* 驱动 probe */
        ...
    }
    ...
    driver_bound(dev);          /* :459 绑定完成: 链接/电源 */
    ...
}
probe 的检查清单 (代码序 = 执行序):

 [1] 全局探测封锁? → defer (device_block_probing 场景)
 [2] device_links_check_suppliers: supplier 驱动都 bound?
     (时钟/GPIO/regulator 的"消费者→提供者"图,
      phandle 引用经 DT 建立)
 [3] pinctrl 引脚、[4] dma_configure (43.2 节)
 [5] bus->probe → drv->probe
 [6] driver_bound (:459): 标 bound、consumer 依赖
     释放 (等待本驱动的下游设备获准重试)

 任何一步失败: probe_failed 路径清理
 (remove 已 probe 的部分、release 资源)

39.4.3 EPROBE_DEFER:延迟协议

 deferred probing (内核最优雅的"稍后再试"):

 驱动 probe 里资源未就绪 (时钟驱动还没注册):
   return -EPROBE_DEFER;
 → 设备放回 deferred 链, 不算失败
 触发源: 任何驱动成功 bound / 模块加载 / 手动
   /sys/.../drivers_probe 写入 → 重扫 deferred 链
 副作用控制: prevent_deferred_probe (platform.h:239
   区字段) 标"此驱动不许延迟"(串口控制台类)

 device_links 是它的声明式升级:
   DT/固件声明依赖 → 内核自动建 supplier-consumer
   链 → [2]/[6] 两端自动管理 — 驱动不再手写
   "时钟没好吗? 那我 -EPROBE_DEFER"

双向注册的对称性:device_register() 先于驱动 → 尝试匹配全部已注册驱动,失败进 unbound 队列等待;driver_register() 后于设备 → 沿总线枚举全部未绑定设备逐个 __driver_probe_device(dd.c:801/883 两处 really_probe 调用点即"设备驱动"与"驱动扫描设备"两条入口)。bind/unbind 的 sysfs 钩子(drivers/.../bind 文件写入)提供人工强制配对——调试驱动匹配的利器。


小结

绑定流水线由 bus_type 裁决匹配(PCI 比 ID 表、platform 比 compatible),really_probe 按序执行"全局封锁→supplier 依赖→pinctrl→dma_configure→bus probe→driver probe→driver_bound";EPROBE_DEFER 把"资源未就绪"变成可重试的软状态,device_links 以声明式依赖图取代手写延迟。设备与驱动双向注册、sysfs bind/unbind 人工兜底——设备被发现、配对、可见的闭环完成。设备模型四章至此齐备;下一章进入第一个驱动类型:字符设备。