3. ktype 与属性:如何为内核对象定义操作接口
好,咱们接着聊。上一章我们把 kobject 和 kset 的基本概念捋了一遍,知道了它们怎么在 sysfs 里搭起一个「对象树」。但有个关键问题:光有对象还不够,你得能操作它。比如一个 LED 设备,用户空间想读它的亮度值,或者想设置闪烁频率——这些操作接口从哪来?
答案就在 ktype 和 attribute 身上。说白了,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,而是用它的包装宏 __ATTR 或 DEVICE_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_RW、DEVICE_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 而不是 sprintf。sysfs_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_group 和 sysfs_remove_group 必须成对出现。我曾经在热拔插设备中忘记调用 remove,结果设备拔出后,sysfs 里还挂着僵尸文件,用户一读就 oops。嗯,血的教训。
3.6 本章知识体系图
下面这张图展示了 ktype、attribute、sysfs_ops 之间的关系:
3.7 小结
咱们这一章把 ktype 和 attribute 的核心机制讲透了。总结几个要点:
- ktype 是模板,同一类 kobject 共享 release 函数和 sysfs_ops。
- attribute 是文件,每个文件对应一个可读/可写的内核数据项。
- sysfs_ops 是翻译官,负责在字符串和内核数据结构之间转换。
- attribute_group 是打包工具,批量管理属性文件,还能按子目录组织。
下一章咱们会深入 device 和 device_driver 的绑定机制,看看内核是怎么把驱动和硬件设备匹配起来的。嗯,那部分更有意思。