3. ktype 与属性:如何为内核对象定义操作接口

好,咱们接着聊。上一章我们把 kobject 和 kset 的基本概念捋了一遍,知道了它们怎么在 sysfs 里搭起一个「对象树」。但有个关键问题:光有对象还不够,你得能操作它。比如一个 LED 设备,用户空间想读它的亮度值,或者想设置闪烁频率——这些操作接口从哪来?

答案就在 ktypeattribute 身上。说白了,ktype 定义了「同一类对象共享的行为」,而 attribute 定义了「每个对象暴露给用户空间的属性文件」。我当年第一次看这部分代码时,觉得这俩东西绕来绕去的,后来踩了几个坑才真正明白它们的妙处。

3.1 ktype:一类对象的「行为模板」

ktype 的全称是 kobject type,它不是一个对象实例的类型,而是一组操作函数的集合。你可以把它理解成 C++ 里的虚函数表——同一类 kobject 共享同一个 ktype。

来看它的定义(内核 6.x 版本):

struct kobj_type {
    void (*release)(struct kobject *kobj);
    const struct sysfs_ops *sysfs_ops;
    const struct attribute_group **default_groups;
    const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *kobj);
    const void *(*namespace)(struct kobject *kobj);
    void (*get_ownership)(struct kobject *kobj, kuid_t *uid, kgid_t *gid);
};

这里面最重要的三个字段:

  • release:释放函数。当 kobject 引用计数归零时调用,负责回收内存。这个必须实现,否则会内存泄漏。
  • sysfs_ops:sysfs 操作函数集。定义了如何读写属性文件。
  • default_groups:默认属性组。kobject 创建时自动在 sysfs 下生成这些属性文件。

核心思想:ktype 把「同类对象的共性操作」提取出来,避免每个 kobject 都重复实现一遍。你想想看,如果系统里有 100 个 LED 设备,每个都要写一遍 release 函数,那得多痛苦?

3.2 attribute:暴露给用户空间的「旋钮」

attribute 就是 sysfs 里的一个文件。用户空间读它,就是读内核数据;写它,就是写内核数据。定义非常简单:

struct attribute {
    const char      *name;
    umode_t         mode;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
    bool            ignore_lockdep:1;
    struct lock_class_key   *key;
    struct lock_class_key   skey;
#endif
};

实际开发中,我们很少直接用 struct attribute,而是用它的包装宏 __ATTRDEVICE_ATTR。举个例子:

static struct attribute my_attrs[] = {
    &dev_attr_brightness.attr,
    &dev_attr_frequency.attr,
    NULL,  // 必须以 NULL 结尾
};

这里 dev_attr_brightness 是用 DEVICE_ATTR 宏生成的。宏展开后,它会定义一个 struct device_attribute 结构体,里面包含了属性名、权限位、以及 show/store 函数指针。

个人习惯:我一般用 DEVICE_ATTR_RWDEVICE_ATTR_RO 这类带权限后缀的宏,代码更清晰。比如 DEVICE_ATTR_RO(brightness) 自动生成只读属性,省得自己写 mode 位。

3.3 sysfs_ops:连接属性和实际操作的桥梁

光有属性文件还不够,你得告诉内核:当用户读/写这个文件时,到底执行什么代码?这就是 sysfs_ops 的职责。

struct sysfs_ops {
    ssize_t (*show)(struct kobject *kobj, struct attribute *attr, char *buf);
    ssize_t (*store)(struct kobject *kobj, struct attribute *attr, const char *buf, size_t count);
};

show 函数负责把内核数据格式化成字符串,返回给用户空间。store 函数负责把用户空间传来的字符串解析成内核数据。我见过不少新手在这两个函数里犯错误,最常见的就是缓冲区溢出

我曾经接手过一个项目,前任工程师在 store 函数里直接用 sprintf 往固定大小的缓冲区写数据,结果用户传了个超长字符串,直接导致内核崩溃。记住:buf 的大小是 PAGE_SIZE(通常 4096 字节),但你不能假设用户输入一定小于这个值。一定要做长度检查!

一个典型的 show/store 实现长这样:

static ssize_t brightness_show(struct device *dev,
                               struct device_attribute *attr, char *buf)
{
    struct my_led *led = dev_get_drvdata(dev);
    return sysfs_emit(buf, "%d\n", led->brightness);
}

static ssize_t brightness_store(struct device *dev,
                                struct device_attribute *attr,
                                const char *buf, size_t count)
{
    struct my_led *led = dev_get_drvdata(dev);
    unsigned long val;
    int ret;

    ret = kstrtoul(buf, 10, &val);
    if (ret)
        return ret;

    if (val > 255)
        return -EINVAL;

    led->brightness = val;
    // 实际硬件操作...
    return count;
}

注意这里用了 sysfs_emit 而不是 sprintfsysfs_emit 是内核提供的安全版本,会自动检查缓冲区边界。嗯,这个细节很重要。

3.4 attribute_group:批量管理属性文件

当你的设备有十几个属性时,一个个注册就太麻烦了。attribute_group 就是用来批量管理的:

struct attribute_group {
    const char      *name;
    umode_t         (*is_visible)(struct kobject *, struct attribute *, int);
    struct attribute    **attrs;
    struct bin_attribute    **bin_attrs;
};

如果 name 为 NULL,属性文件直接挂在 kobject 目录下。如果 name 不为 NULL,比如 name = "config",那么所有属性会放在 /sys/.../config/ 子目录下。这个特性在组织复杂设备时特别有用。

我做过一个多通道数据采集卡,每个通道有 20 多个属性。如果全堆在根目录下,用户根本找不到。后来我用 attribute_group 按通道分组,每个通道一个子目录,用户一看就明白。

3.5 完整示例:一个虚拟温度传感器

光说不练假把式。咱们写一个完整的例子,把上面所有知识点串起来。假设我们要实现一个虚拟温度传感器,它有一个只读的 temp 属性和一个可写的 offset 属性。

// 定义属性
static struct device_attribute dev_attr_temp = 
    __ATTR(temp, 0444, temp_show, NULL);
static struct device_attribute dev_attr_offset = 
    __ATTR(offset, 0644, offset_show, offset_store);

// 属性列表
static struct attribute *sensor_attrs[] = {
    &dev_attr_temp.attr,
    &dev_attr_offset.attr,
    NULL,
};

// 属性组
static const struct attribute_group sensor_group = {
    .attrs = sensor_attrs,
};

// 在 probe 函数中注册
static int sensor_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    int ret;

    // ... 初始化硬件 ...

    ret = sysfs_create_group(&dev->kobj, &sensor_group);
    if (ret) {
        dev_err(dev, "failed to create sysfs group\n");
        return ret;
    }

    return 0;
}

// 在 remove 函数中注销
static int sensor_remove(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    sysfs_remove_group(&dev->kobj, &sensor_group);
    return 0;
}

避坑指南sysfs_create_groupsysfs_remove_group 必须成对出现。我曾经在热拔插设备中忘记调用 remove,结果设备拔出后,sysfs 里还挂着僵尸文件,用户一读就 oops。嗯,血的教训。

3.6 本章知识体系图

下面这张图展示了 ktype、attribute、sysfs_ops 之间的关系:

ktype 与属性核心架构 用户空间 (User Space) sysfs 文件系统 attribute (属性文件) name + mode + show/store 函数指针 sysfs_ops (操作桥梁) show() → 内核数据 → 字符串 store() → 字符串 → 内核数据 kobj_type (行为模板) release + sysfs_ops + default_groups 数据流向 用户读: ↓ 用户写: ↑ 内核数据 在 kobject 私有结构中

3.7 小结

咱们这一章把 ktype 和 attribute 的核心机制讲透了。总结几个要点:

  • ktype 是模板,同一类 kobject 共享 release 函数和 sysfs_ops。
  • attribute 是文件,每个文件对应一个可读/可写的内核数据项。
  • sysfs_ops 是翻译官,负责在字符串和内核数据结构之间转换。
  • attribute_group 是打包工具,批量管理属性文件,还能按子目录组织。

下一章咱们会深入 devicedevice_driver 的绑定机制,看看内核是怎么把驱动和硬件设备匹配起来的。嗯,那部分更有意思。