4. 设备Class核心原理

在Linux设备模型中,Class(类)是一种从“功能视角”对设备进行归类的抽象机制。与Bus按物理连接方式划分设备、Device按具体硬件实例描述设备不同,Class回答的是“这个设备能提供什么功能、应该暴露给用户什么接口”。例如,所有网卡无论挂在PCI、USB还是虚拟总线上,都会归入net类;所有输入设备都会归入input类。正是Class机制,让用户空间能够以统一路径/sys/class/<class_name>/<device_name>访问设备,而不必关心底层总线拓扑。

4.1 class结构体:功能分类的载体

内核用struct class描述一个设备类,其核心字段如下:

struct class {
    const char      *name;              /* 类名,如 "net"、"input" */
    struct module   *owner;             /* 所属模块,用于引用计数 */
    const struct attribute_group **groups; /* 类级默认属性组 */
    const struct attribute_group **dev_groups; /* 类下所有设备共享的属性组 */
    struct kobject  *dev_kobj;          /* 指向 /sys/dev 下的 kobject */
    int (*dev_uevent)(const struct device *dev,
                      struct kobj_uevent_env *env); /* 设备uevent回调 */
    void (*class_release)(struct class *class);     /* 类释放回调 */
    void (*dev_release)(struct device *dev);        /* 设备释放回调 */
    int (*shutdown_pre)(struct device *dev);        /* 关机前回调 */
    const struct kobj_ns_type_operations *ns_type;
    const void *(*namespace)(const struct device *dev);
    ...
};

其中name决定/sys/class/下的目录名;dev_groups是Class最实用的字段之一,它让同一类下所有设备自动获得一组属性文件,无需每个驱动重复创建;dev_uevent则允许类在设备热插拔时向用户空间追加环境变量,是udev规则的重要输入来源。

4.2 class_register与class_unregister:注册与注销流程

Class的注册通过class_register()完成,注销通过class_unregister()完成。典型用法如下:

static struct class my_class = {
    .name = "mydev",
    .dev_groups = my_dev_groups,
};

static int __init my_init(void)
{
    int ret = class_register(&my_class);
    if (ret)
        return ret;
    return 0;
}

static void __exit my_exit(void)
{
    class_unregister(&my_class);
}

注册过程会在/sys/class/下创建以name命名的目录,并在/sys/class/的kobject层级中建立对应节点。注销时,内核会先移除该类下所有设备节点,再删除类目录,并等待引用计数归零。需要特别注意的是:class_unregister不会自动释放class结构体内存,若class是动态分配的,还需在class_release回调中释放。

class_register内部会调用class_create的等价逻辑,但class_create是宏封装,返回struct class *;而class_register接收调用者提供的结构体指针。二者不可混用释放方式。

4.3 class_attribute:类级属性的定义与导出

Class属性用于向用户空间暴露类级别的配置或状态。定义方式与设备属性类似,使用CLASS_ATTR_RW、CLASS_ATTR_RO等宏:

static ssize_t mode_show(struct class *cls,
                         struct class_attribute *attr, char *buf)
{
    return sprintf(buf, "%s\n", current_mode);
}

static ssize_t mode_store(struct class *cls,
                          struct class_attribute *attr,
                          const char *buf, size_t count)
{
    strncpy(current_mode, buf, sizeof(current_mode) - 1);
    return count;
}

static CLASS_ATTR_RW(mode);

static struct attribute *my_class_attrs[] = {
    &class_attr_mode.attr,
    NULL,
};
ATTRIBUTE_GROUPS(my_class_attrs);

将my_class_attrs赋给class.groups后,/sys/class/mydev/mode即可读写。类属性作用于整个类,而dev_groups作用于类下每个设备,二者层级不同,使用时需明确区分。

4.4 设备与Class的关联方式

设备与Class的关联通常由驱动在probe阶段完成,核心API是device_create()或device_register()配合device_add()。最常见的是:

struct device *dev;
dev = device_create(&my_class, parent, devt,
                    drvdata, "mydev%d", index);
if (IS_ERR(dev))
    return PTR_ERR(dev);

device_create会完成三件事:创建struct device、将其加入Class、在/sys/class/mydev/下生成以设备名命名的符号链接,指向/sys/devices/...中的真实设备节点。这样用户空间既可以通过Class路径访问,也可以通过设备树路径访问。

设备与Class关联及sysfs映射关系 驱动 probe device_create() struct class name = "mydev" struct device class = &my_class sysfs 视图 /sys/class/mydev/ mydev0 → 符号链接 /sys/devices/.../mydev0 真实设备节点 /sys/class/mydev/mode class_attribute 符号链接指向 用户空间 / udev 通过 /sys/class/mydev/ 统一访问 class_unregister() 移除类目录与设备节点

4.5 典型场景与误区复盘

以字符设备驱动为例,常见做法是:注册cdev后,用class_create创建类,再用device_create为每个次设备号创建设备节点。这样/dev/mydev0由udev根据/sys/class/mydev/mydev0的uevent自动生成,驱动无需手动调用mknod。

操作 API sysfs效果 常见误区
注册类 class_register() 创建 /sys/class/<name>/ 忘记检查返回值,导致后续device_create失败
创建设备 device_create() 创建符号链接与uevent dev_t冲突或parent为空导致路径异常
添加类属性 CLASS_ATTR_RW() 生成 /sys/class/<name>/xxx 与dev_groups混淆,属性挂错层级
注销类 class_unregister() 删除类目录 先注销类再销毁设备,导致空指针
注销顺序必须与注册顺序相反:先device_destroy()销毁类下所有设备,再class_unregister()注销类,最后释放动态分配的class内存。若顺序颠倒,内核会在移除类时遍历已失效的设备链表,引发oops。

理解Class的核心在于区分三个层次:class结构体是功能分类的元数据,class_register/unregister管理类的生命周期,class_attribute与dev_groups决定用户空间能看到什么。设备通过device_create挂入Class后,sysfs中便形成“类目录—符号链接—真实设备”的三段式视图,这正是Linux设备模型对用户空间保持统一接口的关键设计。