4. 设备驱动模型(LDM):kobject/ktype/kset、sysfs文件系统、设备与驱动绑定流程

Linux设备驱动模型(Linux Device Model, LDM)是内核管理设备、驱动、总线和系统资源的统一抽象框架。它通过kobject、ktype、kset等核心数据结构构建层次化对象树,并借助sysfs文件系统将内核对象暴露到用户空间。本章深入剖析LDM的基石组件,并详细拆解设备与驱动的绑定流程。

4.1 核心数据结构:kobject、ktype、kset

LDM的底层由三个相互关联的数据结构支撑,它们共同定义了内核对象的生命周期、属性接口和层次关系。

数据结构核心作用关键字段生命周期管理
kobject内核对象的基本单元,提供引用计数、名称和父节点name、refcount、parent、ktype、kset通过kobject_init()/kobject_add()/kobject_put()管理
ktype定义同一类kobject的公共操作(属性读写、release回调)sysfs_ops、release、default_attrs静态定义,由kobject引用
ksetkobject的集合容器,维护同类型对象的链表,并作为sysfs目录list、kobj(内嵌kobject)、uevent_ops通过kset_create_and_add()/kset_unregister()管理
关键理解: kobject是“节点”,ktype是“类行为”,kset是“集合目录”。三者配合实现设备树的层次化组织与统一操作接口。

4.1.1 kobject:内核对象的基本单元

kobject是LDM的最小构建块,它不直接描述设备,而是为更高级的对象(如device、driver、bus)提供公共基础设施。每个kobject包含:

  • 引用计数(refcount):通过kobject_get()/kobject_put()控制生命周期,当计数归零时自动调用ktype->release()释放内存。
  • 名称(name):在sysfs中对应的目录名或文件名。
  • 父子关系(parent):形成内核对象树,parent为NULL时挂载到顶级kset。
  • 关联的ktype和kset:决定对象的属性操作和所属集合。
// kobject核心结构(简化)
struct kobject {
    const char          *name;
    struct list_head    entry;          // 挂入kset的链表
    struct kobject      *parent;        // 父对象
    struct kset         *kset;          // 所属集合
    struct kobj_type    *ktype;         // 对象类型
    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;
};

4.1.2 ktype:定义对象的行为

ktype为同一类kobject提供统一的属性操作接口和析构函数。一个典型的ktype定义如下:

// 自定义ktype示例
static struct sysfs_ops my_sysfs_ops = {
    .show   = my_attr_show,
    .store  = my_attr_store,
};

static void my_release(struct kobject *kobj) {
    pr_info("Releasing kobject: %s\n", kobj->name);
    kfree(container_of(kobj, struct my_device, kobj));
}

static struct kobj_type my_ktype = {
    .sysfs_ops  = &my_sysfs_ops,
    .release    = my_release,
    .default_attrs = my_default_attrs,
};
开发提示: 每个kobject必须关联一个ktype,且ktype中的release()函数必须实现——这是内核安全释放对象的唯一途径。忘记实现release()会导致内核崩溃。

4.1.3 kset:对象的集合与目录

kset是kobject的容器,它本身也内嵌一个kobject,因此kset在sysfs中表现为一个目录,其包含的kobject作为子目录或文件出现。kset还负责处理uevent(用户空间事件),当设备添加/移除时通过kset->uevent_ops向用户空间发送热插拔消息。

// kset结构(简化)
struct kset {
    struct list_head    list;           // 挂载所有kobject的链表头
    spinlock_t          list_lock;
    struct kobject      kobj;           // 内嵌kobject,作为sysfs目录
    const struct kset_uevent_ops *uevent_ops;
};

4.2 sysfs文件系统:内核对象到用户空间的桥梁

sysfs是一个基于内存的虚拟文件系统,通常挂载在/sys目录。它将内核中的kobject层次结构以目录和文件的形式呈现给用户空间,使得用户可以通过读写文件来查看或修改内核对象的属性。

4.2.1 sysfs目录结构

典型的sysfs布局如下:

/sys/
├── block/          # 块设备
├── bus/            # 总线类型(pci、spi、i2c、platform等)
│   ├── pci/
│   ├── spi/
│   └── platform/
├── class/          # 设备类(net、input、tty等)
├── dev/            # 设备号映射
├── devices/        # 设备树(按硬件拓扑组织)
│   ├── platform/
│   └── pci0000:00/
├── firmware/       # 固件信息
├── fs/             # 文件系统信息
├── kernel/         # 内核参数
├── module/         # 内核模块
└── power/          # 电源管理

4.2.2 属性文件操作

每个kobject可以通过ktype->sysfs_ops定义属性文件的读写回调。用户空间读取/sys/.../attr_file时,内核调用show()函数;写入时调用store()函数。

// 属性文件读写示例
static ssize_t my_attr_show(struct kobject *kobj, struct attribute *attr, char *buf) {
    struct my_device *dev = container_of(kobj, struct my_device, kobj);
    return sprintf(buf, "%d\n", dev->my_value);
}

static ssize_t my_attr_store(struct kobject *kobj, struct attribute *attr,
                             const char *buf, size_t count) {
    struct my_device *dev = container_of(kobj, struct my_device, kobj);
    int ret = kstrtoint(buf, 10, &dev->my_value);
    return ret < 0 ? ret : count;
}
注意事项: sysfs属性文件的show()和store()函数运行在进程上下文,可以睡眠。但必须注意并发访问保护,通常使用互斥锁或原子操作。另外,属性文件名称长度限制为NAME_MAX(255字节)。

4.3 设备与驱动绑定流程

设备与驱动的绑定是LDM的核心机制,它决定了哪个驱动来管理哪个设备。绑定流程涉及总线、设备、驱动三方的协作,整个过程由内核自动完成,也可以通过sysfs手动触发。

4.3.1 绑定流程总览

下图展示了设备与驱动绑定的完整流程:

设备与驱动绑定流程 设备注册 device_register() 驱动注册 driver_register() 总线匹配 bus->match() 匹配成功? 绑定失败 驱动探测 driver->probe() probe成功? 绑定失败 绑定完成 说明: 设备注册或驱动注册时都会触发总线匹配。 匹配成功则调用probe(),失败则等待后续注册。

4.3.2 绑定流程详细步骤

  1. 设备注册:调用device_register()将设备添加到对应总线的设备链表,并触发总线匹配。
  2. 驱动注册:调用driver_register()将驱动添加到对应总线的驱动链表,并遍历总线上的所有设备进行匹配。
  3. 总线匹配:调用总线定义的bus->match()函数,比较设备和驱动的ID表(如of_match_table、id_table)。
  4. 驱动探测:匹配成功后,调用driver->probe()函数,驱动在此函数中初始化设备、分配资源、注册中断等。
  5. 绑定完成:probe()返回0表示成功,设备与驱动建立关联;返回负值表示失败,绑定回滚。

4.3.3 总线匹配函数示例

以platform总线为例,其match函数实现如下:

// platform总线匹配函数(简化)
static int platform_match(struct device *dev, struct device_driver *drv) {
    struct platform_device *pdev = to_platform_device(dev);
    struct platform_driver *pdrv = to_platform_driver(drv);

    /* 1. 尝试OF(设备树)匹配 */
    if (of_driver_match_device(dev, drv))
        return 1;

    /* 2. 尝试ACPI匹配 */
    if (acpi_driver_match_device(dev, drv))
        return 1;

    /* 3. 尝试ID表匹配 */
    if (pdrv->id_table)
        return platform_match_id(pdrv->id_table, pdev) != NULL;

    /* 4. 尝试名称匹配(驱动名与设备名相同) */
    return strcmp(pdev->name, drv->name) == 0;
}
匹配优先级: 设备树匹配 > ACPI匹配 > ID表匹配 > 名称匹配。驱动开发者应优先使用设备树或ID表进行精确匹配,避免依赖名称匹配的模糊性。

4.3.4 probe()函数典型实现

驱动probe()函数是设备初始化的入口,典型实现包含以下步骤:

static int my_driver_probe(struct platform_device *pdev) {
    struct my_device *priv;
    struct resource *res;
    int ret;

    /* 1. 分配私有数据结构 */
    priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    /* 2. 获取硬件资源(IO内存、中断等) */
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    priv->base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(priv->base))
        return PTR_ERR(priv->base);

    /* 3. 获取中断号并注册中断处理 */
    priv->irq = platform_get_irq(pdev, 0);
    if (priv->irq < 0)
        return priv->irq;

    ret = devm_request_irq(&pdev->dev, priv->irq, my_isr, 0,
                           pdev->name, priv);
    if (ret)
        return ret;

    /* 4. 初始化硬件(复位、配置寄存器等) */
    my_hw_init(priv);

    /* 5. 创建sysfs属性文件(可选) */
    // device_create_file(&pdev->dev, &dev_attr_my_value);

    /* 6. 保存私有数据到设备 */
    platform_set_drvdata(pdev, priv);

    dev_info(&pdev->dev, "device initialized successfully\n");
    return 0;
}

4.4 手动绑定与解绑

除了自动绑定,内核还提供了sysfs接口允许用户手动控制绑定关系:

sysfs路径操作说明
/sys/bus/<bus>/drivers/<driver>/bind写入设备名称手动将设备绑定到该驱动
/sys/bus/<bus>/drivers/<driver>/unbind写入设备名称手动解绑设备与驱动
/sys/bus/<bus>/devices/<device>/driver_override写入驱动名称强制指定设备使用的驱动
# 手动绑定示例:将spi0.0设备绑定到spidev驱动
echo "spi0.0" > /sys/bus/spi/drivers/spidev/bind

# 手动解绑
echo "spi0.0" > /sys/bus/spi/drivers/spidev/unbind

# 强制指定驱动
echo "my_driver" > /sys/bus/platform/devices/my_device/driver_override
echo "my_device" > /sys/bus/platform/drivers/my_driver/bind
调试警告: 手动解绑正在使用的设备可能导致系统不稳定或数据丢失。在解绑前应确保设备不再被任何应用程序或内核子系统使用。生产环境中应避免手动操作绑定关系。

4.5 本章小结

Linux设备驱动模型通过kobject/ktype/kset构建了统一的内核对象管理框架,sysfs文件系统将内核对象层次透明地暴露给用户空间,而设备与驱动的绑定流程则实现了硬件设备与软件驱动的自动匹配与初始化。理解LDM是编写高质量设备驱动的基石,也是深入理解内核设备管理机制的关键。