跳转到内容
新建笔记

Sysfs:对象、属性与资源生命周期

sysfs 将内核对象及其属性呈现为用户空间可读写的目录和文件。它适合少量、明确、长期稳定的设备配置与状态;大量数据流更适合字符设备等接口。Linux 6.8 的 sysfs 建立在 kernfs 上,不能简单解释成“把硬件放进一个 ramfs”。

1. 目录、对象类型与属性

跳转到“1. 目录、对象类型与属性”
概念作用
kobject对象的名字、父子层级、引用计数等基础设施;加入 sysfs 后通常对应目录
kobj_type一类对象的释放方式、sysfs 操作和默认属性组;每个 kobject 关联一个类型
attribute文件名和权限等元数据,通常对应一个属性文件
kobj_attribute在 attribute 基础上提供以 kobject 为上下文的 show/store 回调
device_attribute在 struct device 上定义属性,常用 DEVICE_ATTR_RO/RW 等宏
attribute_group组织一组属性,可整体添加和撤销;不是另一个对象的引用计数
kset将一组 kobject 组织在同一集合中,自身也含有一个 kobject

原笔记的目录、对象与类型对应示意

图中把每个文件分别连到一个 kobj_type,容易误导:一个 kobject 只有一个 ktype,它的多个文件由多个 attribute 描述,不是每个文件另配一种对象类型。

设备通常已经属于 struct device,应优先把属性附着到该设备,或使用已有子系统。LED 优先使用 LED class / gpio-leds;不能为了“有一个 sysfs 文件”就另建一个与设备生命周期无关的根级 kobject。

接口正确的使用含义
kobject_init、kobject_add初始化已分配对象,再加入层级;初始化的对象最后需要 kobject_put
kobject_init_and_add合并上述两步;失败后仍要 kobject_put,不能直接丢弃对象
kobject_create_and_add分配普通动态 kobject 并加入层级;适合不需要自定义外层对象的简单场景
kobject_get / kobject_put增减引用;最后一个引用释放时才执行类型的 release
kobject_del从层级和 sysfs 撤销对象;不等于释放所有引用或立即 kfree
kobject_set_name旧式命名辅助接口;常规创建可直接向 kobject_add 传格式化名称,不能用它随意改已发布对象的名字
kobject_rename / kobject_move涉及已发布名称或父目录变更;用户 ABI 可能依赖路径,应谨慎设计

kobject 通常嵌入驱动自己的结构体,release 用 container_of 找回外层对象并释放。不能在仍有引用时提前释放外层内存,也不能认为 kobject_put 每次都等于“立即删除”。将自定义 kobject 放在临时栈变量里同样不能满足引用寿命。kobject 官方说明

原笔记单独列出的 kobject_create() 在 Linux 6.8 是内部静态函数;模块使用公开的 kobject_create_and_add 或显式初始化接口。实现自定义 release 还要保证该回调执行时其代码仍然存在:例如 CONFIG_DEBUG_KOBJECT_RELEASE 可故意延迟最终清理,不能假定一次 kobject_put 返回就一定执行完模块中的释放函数。

3. kset 的集合与目录并非同一关系

跳转到“3. kset 的集合与目录并非同一关系”

kset 的成员链表、锁和内嵌 kobject 原图

kset 通过成员链表组织对象,并可提供 uevent 相关操作。设置 kobj->kset 时,若创建时没有明确父对象,kset 的内嵌 kobject 可成为默认父对象;显式指定父对象时,集合归属与目录父子关系可以不同。

对驱动公开的常用接口包括 kset_init/register/unregister、kset_create_and_add、kset_get/put 和 kset_find_obj。kset_find_obj 找到的对象带引用,使用后需要 kobject_put。原笔记列出的内部 kset_create、kobj_kset_join/leave 不能当作任意模块都可调用的公开 API。

对象的 kobj_type 中,Linux 6.8 使用 default_groups 表达默认属性组;旧示例的 default_attrs 字段不能直接复制到该版本。sysfs_ops 负责属性操作的转发,kobj_sysfs_ops 可用于 kobj_attribute。Linux 6.8 的 child_ns_type 回调说明子对象使用哪种命名空间操作,namespace 返回当前对象的命名空间标签;这些供相应子系统管理 sysfs 的命名空间隔离,普通软件属性示例无需自行填入。

属性结构体连接 show 和 store 回调的原始示意

原图将单个属性传给 sysfs_create_files,接口不匹配:单属性用 sysfs_create_file(kobj, &attr.attr);复数接口需要以 NULL 结束的属性指针数组。变量名也应统一为实际定义的对象,不能混写 led_attr_k、led_attr 或不存在的 .kattr。

show 通常输出一个标量和换行,用 sysfs_emit 控制长度;不要每次输出一段不稳定的说明文字。sysfs 会把结果缓存在一次读取对应的缓冲区,支持用户短读与 seek;这不要求驱动自己对每个用户字节重复采样硬件。

store 收到的是内核缓冲区,sysfs 在数据末尾附加 NUL;不需要再 copy_from_user。成功返回消费的字节数,非法输入或硬件处理失败返回负错误码。若接口要求完整命令,应按完整 token 验证:原来的 memcmp(buf, "on", 2) 会把 on-anything 当作成功,也没有先确认短输入长度。

本例用 sysfs_streq 接受恰好的 0/1/on/off 及一个结尾换行,并另外拒绝 count 范围内的 NUL。kstrtobool 适合其文档约定的布尔输入,但 Linux 6.8 实现按开头字符判断,不能用它证明尾部垃圾已被严格拒绝。数值接口可使用 kstrtoint/kstrtouint,仍需定义取值范围及所需的完整输入检查。sysfs 规则;布尔解析实现

文件权限属于 ABI 的一部分。0644 表示所有者可读写、其他用户只读;不要把硬件控制属性随意设为所有人可写。并发访问还需锁保护,sysfs 不会自动把不同文件或不同调用者的操作串行化。

保存为 vlog_sysfs.c。示例只暴露一个软件对象,在 /sys/kernel/vlog_demo/enabled 保存布尔值;kobject 由公开帮助函数分配,最终释放函数属于内核,示例不再实现一个需要跨越模块卸载的自定义释放回调。

vlog_sysfs.c
// SPDX-License-Identifier: GPL-2.0-only
#include <linux/kobject.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/string.h>
#include <linux/sysfs.h>
struct vlog_object {
struct kobject *kobj;
struct mutex lock;
bool enabled;
};
static struct vlog_object state;
static ssize_t enabled_show(struct kobject *kobj,
struct kobj_attribute *attr, char *buf)
{
struct vlog_object *s = &state; /* This module exposes one software object. */
bool value;
mutex_lock(&s->lock);
value = s->enabled;
mutex_unlock(&s->lock);
return sysfs_emit(buf, "%u\n", value ? 1U : 0U);
}
static ssize_t enabled_store(struct kobject *kobj,
struct kobj_attribute *attr,
const char *buf, size_t count)
{
struct vlog_object *s = &state;
bool value;
/* sysfs appends NUL; reject embedded NUL and compare whole tokens. */
if (memchr(buf, '\0', count))
return -EINVAL;
if (sysfs_streq(buf, "1") || sysfs_streq(buf, "on"))
value = true;
else if (sysfs_streq(buf, "0") || sysfs_streq(buf, "off"))
value = false;
else
return -EINVAL;
mutex_lock(&s->lock);
s->enabled = value;
mutex_unlock(&s->lock);
return count;
}
static struct kobj_attribute enabled_attr =
__ATTR(enabled, 0644, enabled_show, enabled_store);
static struct attribute *vlog_attrs[] = { &enabled_attr.attr, NULL };
ATTRIBUTE_GROUPS(vlog);
static int __init vlog_init(void)
{
int ret;
mutex_init(&state.lock);
state.kobj = kobject_create_and_add("vlog_demo", kernel_kobj);
if (!state.kobj)
return -ENOMEM;
ret = sysfs_create_groups(state.kobj, vlog_groups);
if (ret) {
kobject_put(state.kobj);
state.kobj = NULL;
return ret;
}
return 0;
}
static void __exit vlog_exit(void)
{
/* Drain attribute callbacks while this module's state/code still exist. */
sysfs_remove_groups(state.kobj, vlog_groups);
kobject_put(state.kobj); /* Its release function belongs to the kernel. */
}
module_init(vlog_init);
module_exit(vlog_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Software-only kobject and sysfs attribute example");

同目录 Makefile 填写 obj-m += vlog_sysfs.o,使用 目标内核的构建方法。在已经加载它的实验系统中,cat /sys/kernel/vlog_demo/enabled 读取状态,printf 'on\n' > /sys/kernel/vlog_demo/enabled 写入;无效 token 会失败并保持旧值。

本例只拥有一个软件状态,因此回调使用同一个静态状态结构。退出时先撤销属性组,使活动属性访问结束,再放掉 kobject 引用;即使内核调试选项延迟它的最终清理,也不会再调用已经卸载的模块释放函数。真实多设备实例应使用设备模型提供的上下文,不能把这个单对象示例的静态状态复制成多实例驱动。

6. 真实设备属性与撤销顺序

跳转到“6. 真实设备属性与撤销顺序”

device_create_file(dev, &dev_attr_name) 是设备属性的封装,底层仍使用 sysfs。它把文件放到该设备对象的实际目录,不是保证所有文件都直接出现在 /sys/devices 根下,也不因“封装得更高”就自动解决未初始化指针和生命周期错误。多属性通常使用属性组和相应的 group API。

platform 驱动可以先分配并初始化每实例状态,再用 devm_device_add_groups 发布属性。它注册得晚,解绑时就先撤销属性,之后才能释放较早申请的 GPIO 或内存。完整 GPIO 示例见 设备树平台驱动,其中逻辑极性由 GPIO 描述符解释,可睡眠的 GPIO 操作用 gpiod_set_value_cansleep。

属性存在只说明内核对象有接口,不保证外设仍处于正常供电或可访问状态。可拔除、运行时电源管理、工作队列及硬件错误都需要驱动自己的处理策略。若一个写操作必须同时更新多个值,还要设计事务边界,不能期待用户依次写两个文件就天然原子化。

属性还可以属于已注册的总线或驱动,而不是某一个设备实例。总线级属性适合总线整体的配置或统计,驱动级属性适合驱动整体的状态;某块设备自己的开关仍应挂在该设备上。

Linux 6.8 对象当版声明宏与生成的属性对象创建和移除
struct bus_typeBUS_ATTR_RO/RW/WO(name) 生成 bus_attr_namebus_create_file(bus, &bus_attr_name) / bus_remove_file(bus, &bus_attr_name)
struct device_driverDRIVER_ATTR_RO/RW/WO(name) 生成 driver_attr_namedriver_create_file(driver, &driver_attr_name) / driver_remove_file(driver, &driver_attr_name)

宏依据名称绑定 name_show / name_store,通常在文件作用域加 static;只读或只写宏只需要对应方向的回调。Linux 6.8 的总线回调首参是 const struct bus_type *,驱动回调首参是 struct device_driver *,不能直接复用设备或 kobject 回调签名。原稿的无后缀 BUS_ATTR(...)、DRIVER_ATTR(...) 属于旧接口写法,复制前必须核对目标版本。总线属性头文件;驱动属性头文件

创建前应确保总线/驱动对象已注册并且回调所用状态已初始化,检查创建返回值;清理时先移除属性,再注销其所属对象和释放状态。这些文件通常分别出现在 /sys/bus/<bus>/ 与 /sys/bus/<bus>/drivers/<driver>/,不应把驱动整体状态伪装成某个设备实例的状态。

sysfs 属性会成为用户程序依赖的 ABI。正式驱动应说明文件位置、单位、范围、读写格式、错误与状态语义,并按内核要求记录 ABI;大量日志、任意二进制协议和不稳定的调试内容不适合随意塞进永久属性。

完整模块在 Ubuntu 6.8.0-146-generic 的 x86-64 构建资料下通过编译与 MODPOST;正文属性回调做了主机模型测试,覆盖合法命令、尾部垃圾、嵌入 NUL、非法输入不改状态和状态读取。没有在当前主机加载模块,验证不包含真实 sysfs 并发解绑或硬件访问。