Skip to content

LV110-属性文件示例

一、属性文件创建 demo

1. demo 源码

我们实现这样一个功能,自定义一个 kobject:

c
typedef struct __SKOBJ_{
    char skobj_name[32];
    char name_attr[32];
    int data;
    struct kobject kobj;
}skobj_t;

创建两个属性文件,对应 name_attr 和 data,可以通过 cat 命令和 echo 命令修改和显示这两个的值。demo 可以看这里:05_device_model/06_attr_file_demo1

c
#include <linux/init.h> /* module_init module_exit */
#include <linux/kernel.h>
#include <linux/module.h> /* MODULE_LICENSE */

#include <linux/kobject.h>
#include <linux/slab.h>

#include "./timestamp_autogenerated.h"
#include "./version_autogenerated.h"
#include "./sdrv_common.h"

// #undef PRT
// #undef PRTE
#ifndef PRT
#define PRT printk
#endif
#ifndef PRTE
#define PRTE printk
#endif

typedef struct __SKOBJ_{
    char skobj_name[32];
    char name_attr[32];
    int data;
    struct kobject kobj;
}skobj_t;

skobj_t *skobject1 = NULL; // 定义 kobject 指针变量:skobject1

/**
 * @brief  dynamic_kobj_release()
 * @note   定义 kobject 的释放函数
 * @param [in]
 * @param [out]
 * @retval 
 */
static void dynamic_kobj_release(struct kobject *kobj)
{
    skobj_t *pskobj = container_of(kobj, skobj_t, kobj);
    PRT("kobject: (%p) \n", kobj);
    kfree(pskobj);
}

/**
 * @brief  skobj_attr_show()
 * @note   读属性操作函数, cat 属性文件时调用这个函数,会把对应的值显示到终端
 * @param [in]
 * @param [out]
 * @retval 
 */
ssize_t skobj_attr_show(struct kobject *kobj, struct attribute *attr, char *buf)
{
    ssize_t count = 0;
    skobj_t *pskobj = container_of(kobj, skobj_t, kobj);

    if(strcmp(attr->name, "skobj_attr_name") == 0) // skobj_attr_name 属性对应 pskobj-> name 成员
    {
        count = sprintf(buf, "%s\n", pskobj->name_attr);
    }
    else if(strcmp(attr->name, "skobj_attr_data") == 0) // skobj_attr_data 属性对应 pskobj-> data 成员
    {
        count = sprintf(buf, "%d\n", pskobj->data);
    }
    PRT("attr->name=%s count=%d\n", attr->name, count);
    return count;
}

/**
 * @brief  skobj_attr_store()
 * @note   写属性操作文件, echo 属性文件时调用这个函数,用于 echo x 中的 x 写入到属性对应的成员变量中
 * @param [in]
 * @param [out]
 * @retval 
 */
ssize_t skobj_attr_store(struct kobject *kobj, struct attribute *attr, const char *buf, size_t count)
{
    skobj_t *pskobj = container_of(kobj, skobj_t, kobj);

    PRT("attr->name=%s count=%d\n", attr->name, count);
    if(strcmp(attr->name, "skobj_attr_name") == 0)      // skobj_attr_name 属性对应 pskobj-> name 成员
    {
        sscanf(buf, "%s\n", pskobj->name_attr);
    }
    else if(strcmp(attr->name, "skobj_attr_data") == 0) // skobj_attr_data 属性对应 pskobj-> data 成员
    {
        sscanf(buf, "%d\n", &pskobj->data);
    }

    return count;
}

// 自定义的 sysfs_ops 结构体,包含 show 和 store 函数指
struct sysfs_ops attr_sysfs_ops = {
    .show  = skobj_attr_show,
    .store = skobj_attr_store,
};

// 自定义的 kobj_type 结构体,包含释放函数、默认属性和 sysfs_ops
struct kobj_type stype = {
    .release = dynamic_kobj_release,
    .sysfs_ops = &attr_sysfs_ops,
};

// 每一个注册的 attribute 结构都是一个属性文件,这里创建两个属性文件
struct attribute s_attr_name = {
    .name = "skobj_attr_name",
    .mode = S_IRWXUGO,
};

struct attribute s_attr_data = {
    .name = "skobj_attr_data",
    .mode = S_IRWXUGO,
};

/**
 * @brief  sdriver_demo_init()
 * @note   
 * @param [in]
 * @param [out]
 * @retval 
 */
static __init int sdriver_demo_init(void)
{
    int ret = 0;
	printk("*** [%s:%d]Build Time: %s %s, git version:%s ***\n", __FUNCTION__,
           __LINE__, KERNEL_KO_DATE, KERNEL_KO_TIME, KERNEL_KO_VERSION);
    PRT("sdriver_demo module init!\n");
    

    // 创建 kobject 的第二种方法
    // 1.使用 kzalloc 函数分配了一个 kobject 对象的内存
    skobject1 = kzalloc(sizeof(skobj_t), GFP_KERNEL);
    if(!skobject1)
    {
        ret = -1;
        goto err_kzalloc;
    }
    snprintf(skobject1->skobj_name, sizeof(skobject1->skobj_name), "skobject1");

    // 2.初始化并添加到内核中,名为 "skobject1"。
    ret = kobject_init_and_add(&skobject1->kobj, &stype, NULL, "%s", skobject1->skobj_name);
    if(ret < 0)
    {
        PRTE("kobject_init_and_add fail!ret=%d\n", ret);
        goto err_kobject_init_and_add;
    }
    //创建 s_attr_a 属性文件
    ret = sysfs_create_file(&skobject1->kobj, &s_attr_name);
    if(ret < 0)
    {
        PRTE("sysfs_create_file s_attr_name fail!ret=%d\n", ret);
        goto err_sysfs_create_file_1;
    }
    //创建 s_attr_b 属性文件
    ret = sysfs_create_file(&skobject1->kobj, &s_attr_data);
    if(ret < 0)
    {
        PRTE("sysfs_create_file s_attr_data fail!ret=%d\n", ret);
        goto err_sysfs_create_file_2;
    }
    return 0;

err_sysfs_create_file_2:
    sysfs_remove_file(&skobject1->kobj, &s_attr_name);
err_sysfs_create_file_1:
    kobject_put(&skobject1->kobj);
err_kobject_init_and_add:
    kfree(skobject1);
err_kzalloc:
	return ret;
}

/**
 * @brief  sdriver_demo_exit()
 * @note   
 * @param [in]
 * @param [out]
 * @retval 
 */
static __exit void sdriver_demo_exit(void)
{
	// 删除创建的属性文件,好像不删除的话,在释放 kobj 的时候会自动删除?
    sysfs_remove_file(&skobject1->kobj, &s_attr_name);
    sysfs_remove_file(&skobject1->kobj, &s_attr_data);
	// 释放之前创建的 kobject 对象
    kobject_put(&skobject1->kobj);
    PRT("sdriver_demo module exit!\n");
}

module_init(sdriver_demo_init); // 将__init 定义的函数指定为驱动的入口函数
module_exit(sdriver_demo_exit); // 将__exit 定义的函数指定为驱动的出口函数

/* 模块信息(通过 modinfo chrdev_led_demo 查看) */
MODULE_LICENSE("GPL v2");            /* 源码的许可证协议 */
MODULE_AUTHOR("sumu");               /* 字符串常量内容为模块作者说明 */
MODULE_DESCRIPTION("Description");   /* 字符串常量内容为模块功能说明 */
MODULE_ALIAS("module's other name"); /* 字符串常量内容为模块别名 */

这个 demo 是并没有将属性写入到 ktype 中,所以不会自动创建对应的属性文件,需要我们自己调用 sysfs_create_files() 函数创建。

2. 开发板验证

我们编译完,会得到一个 sdriver_demo.ko 文件。接下来开始验证我们写的 demo。

  • (1)一开始的 /sys 目录
image-20250107164518539
  • (2)加载模块
image-20250107164601577

加载模块后,会发现,在/sys 目录生成了 skobject1 目录,在/sys/skobject1 目录下有两个属性文件 skobj_attr_name 和 skobj_attr_data。

  • (3)查看 skobj_attr_name 和 skobj_attr_data 的值
image-20250107164739343
  • (4)修改 skobj_attr_name 和 skobj_attr_data 的值
image-20250107165743466
  • (5)卸载模块
image-20250107165920554

可以看到,卸载模块后,资源释放,两个属性文件以及 kobject 目录全都没了。

二、将属性放入 ktype

其实 kobject 在创建的时候,是会自动帮我们创建属性的,应该是在 populate_dir() 函数中:

c
static int populate_dir(struct kobject *kobj)
{
	struct kobj_type *t = get_ktype(kobj);
	struct attribute *attr;
	int error = 0;
	int i;

	if (t && t->default_attrs) {
		for (i = 0; (attr = t->default_attrs[i]) != NULL; i++) {
			error = sysfs_create_file(kobj, attr);
			if (error)
				break;
		}
	}
	return error;
}

可以看到我们需要把属性给放到 ktype 的 default_attrs 成员中。

1. demo 源码

05_device_model/07_attr_file_demo2

c
#include <linux/init.h> /* module_init module_exit */
#include <linux/kernel.h>
#include <linux/module.h> /* MODULE_LICENSE */

#include <linux/kobject.h>
#include <linux/slab.h>

#include "./timestamp_autogenerated.h"
#include "./version_autogenerated.h"
#include "./sdrv_common.h"

// #undef PRT
// #undef PRTE
#ifndef PRT
#define PRT printk
#endif
#ifndef PRTE
#define PRTE printk
#endif

typedef struct __SKOBJ_{
    char skobj_name[32];
    char name_attr[32];
    int data;
    struct kobject kobj;
}skobj_t;

skobj_t *skobject1 = NULL; // 定义 kobject 指针变量:skobject1

/**
 * @brief  dynamic_kobj_release()
 * @note   定义 kobject 的释放函数
 * @param [in]
 * @param [out]
 * @retval 
 */
static void dynamic_kobj_release(struct kobject *kobj)
{
    skobj_t *pskobj = container_of(kobj, skobj_t, kobj);
    PRT("kobject: (%p) \n", kobj);
    kfree(pskobj);
}

/**
 * @brief  skobj_attr_show()
 * @note   读属性操作函数, cat 属性文件时调用这个函数,会把对应的值显示到终端
 * @param [in]
 * @param [out]
 * @retval 
 */
ssize_t skobj_attr_show(struct kobject *kobj, struct attribute *attr, char *buf)
{
    ssize_t count = 0;
    skobj_t *pskobj = container_of(kobj, skobj_t, kobj);

    if(strcmp(attr->name, "skobj_attr_name") == 0) // skobj_attr_name 属性对应 pskobj-> name 成员
    {
        count = sprintf(buf, "%s\n", pskobj->name_attr);
    }
    else if(strcmp(attr->name, "skobj_attr_data") == 0) // skobj_attr_data 属性对应 pskobj-> data 成员
    {
        count = sprintf(buf, "%d\n", pskobj->data);
    }
    PRT("attr->name=%s count=%d\n", attr->name, count);
    return count;
}

/**
 * @brief  skobj_attr_store()
 * @note   写属性操作文件, echo 属性文件时调用这个函数,用于 echo x 中的 x 写入到属性对应的成员变量中
 * @param [in]
 * @param [out]
 * @retval 
 */
ssize_t skobj_attr_store(struct kobject *kobj, struct attribute *attr, const char *buf, size_t count)
{
    skobj_t *pskobj = container_of(kobj, skobj_t, kobj);

    PRT("attr->name=%s count=%d\n", attr->name, count);
    if(strcmp(attr->name, "skobj_attr_name") == 0)      // skobj_attr_name 属性对应 pskobj-> name 成员
    {
        sscanf(buf, "%s\n", pskobj->name_attr);
    }
    else if(strcmp(attr->name, "skobj_attr_data") == 0) // skobj_attr_data 属性对应 pskobj-> data 成员
    {
        sscanf(buf, "%d\n", &pskobj->data);
    }

    return count;
}

// 自定义的 sysfs_ops 结构体,包含 show 和 store 函数指
struct sysfs_ops attr_sysfs_ops = {
    .show  = skobj_attr_show,
    .store = skobj_attr_store,
};

// 每一个注册的 attribute 结构都是一个属性文件,这里创建两个属性文件
struct attribute s_attr_name = {
    .name = "skobj_attr_name",
    .mode = S_IRWXUGO,
};

struct attribute s_attr_data = {
    .name = "skobj_attr_data",
    .mode = S_IRWXUGO,
};

// 定义一个指针数组,将 attribute 对象全部放入数组中,方便后续赋值给 ktype 的 default_attrs 成员
struct attribute *s_attr[] = {
    &s_attr_name, 
    &s_attr_data, 
    NULL, 
};

// 自定义的 kobj_type 结构体,包含释放函数、默认属性和 sysfs_ops
struct kobj_type stype = {
    .release = dynamic_kobj_release,
    .sysfs_ops = &attr_sysfs_ops,
    .default_attrs = s_attr,
};

/**
 * @brief  sdriver_demo_init()
 * @note   
 * @param [in]
 * @param [out]
 * @retval 
 */
static __init int sdriver_demo_init(void)
{
    int ret = 0;
	printk("*** [%s:%d]Build Time: %s %s, git version:%s ***\n", __FUNCTION__,
           __LINE__, KERNEL_KO_DATE, KERNEL_KO_TIME, KERNEL_KO_VERSION);
    PRT("sdriver_demo module init!\n");
    

    // 创建 kobject 的第二种方法
    // 1.使用 kzalloc 函数分配了一个 kobject 对象的内存
    skobject1 = kzalloc(sizeof(skobj_t), GFP_KERNEL);
    if(!skobject1)
    {
        ret = -1;
        goto err_kzalloc;
    }
    snprintf(skobject1->skobj_name, sizeof(skobject1->skobj_name), "skobject1");

    // 2.初始化并添加到内核中,名为 "skobject1"。
    ret = kobject_init_and_add(&skobject1->kobj, &stype, NULL, "%s", skobject1->skobj_name);
    if(ret < 0)
    {
        PRTE("kobject_init_and_add fail!ret=%d\n", ret);
        goto err_kobject_init_and_add;
    }

    return 0;

err_kobject_init_and_add:
    kfree(skobject1);
err_kzalloc:
	return ret;
}

/**
 * @brief  sdriver_demo_exit()
 * @note   
 * @param [in]
 * @param [out]
 * @retval 
 */
static __exit void sdriver_demo_exit(void)
{
	// 释放之前创建的 kobject 对象
    kobject_put(&skobject1->kobj);
    PRT("sdriver_demo module exit!\n");
}


module_init(sdriver_demo_init); // 将__init 定义的函数指定为驱动的入口函数
module_exit(sdriver_demo_exit); // 将__exit 定义的函数指定为驱动的出口函数

/* 模块信息(通过 modinfo chrdev_led_demo 查看) */
MODULE_LICENSE("GPL v2");            /* 源码的许可证协议 */
MODULE_AUTHOR("sumu");               /* 字符串常量内容为模块作者说明 */
MODULE_DESCRIPTION("Description");   /* 字符串常量内容为模块功能说明 */
MODULE_ALIAS("module's other name"); /* 字符串常量内容为模块别名 */

2. 开发板验证

只是改变了属性文件创建的逻辑,现象和上面完全一样。

三、通过 __ATTR() 宏优化一下

1. 怎么优化?

看一下上面的 show 和 store 函数,这里以 show 函数为例:

image-20250107172843740

由于操作不同属性的时候,都是调用的这个函数,我们要在这里面通过 if 或者 switch 来判断,一两个属性还好,七八个怎么办?是不是可以让 一个属性对应一个 show 和 store,这样每个属性都有自己的 show/store 函数,这样属性再多也没啥问题了。

所以,这个时候我们就可以用的到 struct kobj_attribute 结构体,和 __ATTR() 宏。需要注意以下几点:

  • (1)需要定义 struct sysfs_ops 类型的变量用于操作 kobject 的所有属性,也就是要实现下面两个函数:
c
struct sysfs_ops {
	ssize_t	(*show)(struct kobject *, struct attribute *, char *);
	ssize_t	(*store)(struct kobject *, struct attribute *, const char *, size_t);
};
  • (2)需要定义每个属性的操作函数,就是实现 struct kobj_attribute 中的两个函数:
c
struct kobj_attribute {
	struct attribute attr;
	ssize_t (*show)(struct kobject *kobj, struct kobj_attribute *attr,
			char *buf);
	ssize_t (*store)(struct kobject *kobj, struct kobj_attribute *attr,
			 const char *buf, size_t count);
};

其实就很简单的一个逻辑,之前是靠 if 来区分不同的属性,现在改成通过 container_of()函数在 show/store 函数中获取不同的属性变量的地址,然后调用他们各自的 show/store 函数。具体后面可以看 demo。

2. demo 源码

05_device_model/08_attr_file_demo3

c
#include <linux/init.h> /* module_init module_exit */
#include <linux/kernel.h>
#include <linux/module.h> /* MODULE_LICENSE */

#include <linux/kobject.h>
#include <linux/slab.h>

#include "./timestamp_autogenerated.h"
#include "./version_autogenerated.h"
#include "./sdrv_common.h"

// #undef PRT
// #undef PRTE
#ifndef PRT
#define PRT printk
#endif
#ifndef PRTE
#define PRTE printk
#endif

typedef struct __SKOBJ_{
    char skobj_name[32];
    char name_attr[32];
    int data;
    struct kobject kobj;
}skobj_t;

skobj_t *skobject1 = NULL; // 定义 kobject 指针变量:skobject1

/**
 * @brief  skobj_attr_name_show()
 * @note   读属性操作函数, cat 属性文件时调用这个函数,会把对应的值显示到终端
 * @param [in]
 * @param [out]
 * @retval 
 */
ssize_t skobj_attr_name_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
{
    ssize_t count = 0;
    skobj_t *pskobj = container_of(kobj, skobj_t, kobj);
    count = sprintf(buf, "%s\n", pskobj->name_attr);
    return count;
}

/**
 * @brief  skobj_attr_name_store()
 * @note   写属性操作文件, echo 属性文件时调用这个函数,用于 echo x 中的 x 写入到属性对应的成员变量中
 * @param [in]
 * @param [out]
 * @retval 
 */
ssize_t skobj_attr_name_store(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count)
{
    skobj_t *pskobj = container_of(kobj, skobj_t, kobj);
    sscanf(buf, "%s\n", pskobj->name_attr);
    return count;
}

/**
 * @brief  skobj_attr_data_show()
 * @note   读属性操作函数, cat 属性文件时调用这个函数,会把对应的值显示到终端
 * @param [in]
 * @param [out]
 * @retval 
 */
ssize_t skobj_attr_data_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
{
    ssize_t count = 0;
    skobj_t *pskobj = container_of(kobj, skobj_t, kobj);
    count = sprintf(buf, "%d\n", pskobj->data);
    return count;
}

/**
 * @brief  skobj_attr_data_store()
 * @note   写属性操作文件, echo 属性文件时调用这个函数,用于 echo x 中的 x 写入到属性对应的成员变量中
 * @param [in]
 * @param [out]
 * @retval 
 */
ssize_t skobj_attr_data_store(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count)
{
    skobj_t *pskobj = container_of(kobj, skobj_t, kobj);
    sscanf(buf, "%d\n", &pskobj->data);
    return count;
}

// 定义两个属性,用于管理 name_attr 和 data
// 注意,这里的 mode 赋值的时候不能用这些代表权限的宏,例如 S_IRWXUGO,否则会在展开的时候出现问题
struct kobj_attribute s_attr_name = __ATTR(skobj_attr_name, 0664, skobj_attr_name_show, skobj_attr_name_store);
struct kobj_attribute s_attr_data = __ATTR(skobj_attr_data, 0664, skobj_attr_data_show, skobj_attr_data_store);
// 定义一个指针数组,将 attribute 对象全部放入数组中,方便后续赋值给 ktype 的 default_attrs 成员
struct attribute *s_attr[] = {
    &s_attr_name.attr, 
    &s_attr_data.attr, 
    NULL, 
};

/**
 * @brief  dynamic_kobj_release()
 * @note   定义 kobject 的释放函数
 * @param [in]
 * @param [out]
 * @retval 
 */
static void dynamic_kobj_release(struct kobject *kobj)
{
    skobj_t *pskobj = container_of(kobj, skobj_t, kobj);
    PRT("kobject: (%p) \n", kobj);
    kfree(pskobj);
}

/**
 * @brief  skobj_attr_show()
 * @note   读属性操作函数, cat 属性文件时调用这个函数,会把对应的值显示到终端
 * @param [in]
 * @param [out]
 * @retval 
 */
ssize_t skobj_attr_show(struct kobject *kobj, struct attribute *attr, char *buf)
{
    ssize_t count = 0;
    struct kobj_attribute *pskobj_attr = container_of(attr, struct kobj_attribute, attr);
    count = pskobj_attr->show(kobj, pskobj_attr, buf);
    PRT("attr->name=%s count=%d\n", attr->name, count);
    return count;
}

/**
 * @brief  skobj_attr_store()
 * @note   写属性操作文件, echo 属性文件时调用这个函数,用于 echo x 中的 x 写入到属性对应的成员变量中
 * @param [in]
 * @param [out]
 * @retval 
 */
ssize_t skobj_attr_store(struct kobject *kobj, struct attribute *attr, const char *buf, size_t count)
{
    struct kobj_attribute *pskobj_attr = container_of(attr, struct kobj_attribute, attr);
    PRT("attr->name=%s count=%d\n", attr->name, count);
    return pskobj_attr->store(kobj, pskobj_attr, buf, count);
}

// 自定义的 sysfs_ops 结构体,包含 show 和 store 函数指
struct sysfs_ops attr_sysfs_ops = {
    .show  = skobj_attr_show,
    .store = skobj_attr_store,
};

// 自定义的 kobj_type 结构体,包含释放函数、默认属性和 sysfs_ops
struct kobj_type stype = {
    .release = dynamic_kobj_release,
    .sysfs_ops = &attr_sysfs_ops,
    .default_attrs = s_attr,
};

/**
 * @brief  sdriver_demo_init()
 * @note   
 * @param [in]
 * @param [out]
 * @retval 
 */
static __init int sdriver_demo_init(void)
{
    int ret = 0;
	printk("*** [%s:%d]Build Time: %s %s, git version:%s ***\n", __FUNCTION__,
           __LINE__, KERNEL_KO_DATE, KERNEL_KO_TIME, KERNEL_KO_VERSION);
    PRT("sdriver_demo module init!\n");
    

    // 创建 kobject 的第二种方法
    // 1.使用 kzalloc 函数分配了一个 kobject 对象的内存
    skobject1 = kzalloc(sizeof(skobj_t), GFP_KERNEL);
    if(!skobject1)
    {
        ret = -1;
        goto err_kzalloc;
    }
    snprintf(skobject1->skobj_name, sizeof(skobject1->skobj_name), "skobject1");

    // 2.初始化并添加到内核中,名为 "skobject1"。
    ret = kobject_init_and_add(&skobject1->kobj, &stype, NULL, "%s", skobject1->skobj_name);
    if(ret < 0)
    {
        PRTE("kobject_init_and_add fail!ret=%d\n", ret);
        goto err_kobject_init_and_add;
    }

    return 0;

err_kobject_init_and_add:
    kfree(skobject1);
err_kzalloc:
	return ret;
}

/**
 * @brief  sdriver_demo_exit()
 * @note   
 * @param [in]
 * @param [out]
 * @retval 
 */
static __exit void sdriver_demo_exit(void)
{
	// 释放之前创建的 kobject 对象
    kobject_put(&skobject1->kobj);
    PRT("sdriver_demo module exit!\n");
}


module_init(sdriver_demo_init); // 将__init 定义的函数指定为驱动的入口函数
module_exit(sdriver_demo_exit); // 将__exit 定义的函数指定为驱动的出口函数

/* 模块信息(通过 modinfo chrdev_led_demo 查看) */
MODULE_LICENSE("GPL v2");            /* 源码的许可证协议 */
MODULE_AUTHOR("sumu");               /* 字符串常量内容为模块作者说明 */
MODULE_DESCRIPTION("Description");   /* 字符串常量内容为模块功能说明 */
MODULE_ALIAS("module's other name"); /* 字符串常量内容为模块别名 */

3. 开发板验证

这里还是只是改了属性创建的写法,现象和验证步骤和前面完全一样。

四、默认属性?

1. 有一种创建方式没有指定 ktype?

我们知道 kobject 创建的时候有两种方式,一种是手动申请内存,然后调用 kobject_init_and_add() 函数来创建 kobject:

c
int kobject_init_and_add(struct kobject *kobj, struct kobj_type *ktype,
			 struct kobject *parent, const char *fmt, ...);

可以看到,这里可以直接指定 ktype,这个时候我们可以很方便的给 ktype 添加属性。转头看向另一种创建方式,是自动申请内存,调用 kobject_create_and_add() 函数完成 kobject 的创建:

c
struct kobject *kobject_create_and_add(const char *name, struct kobject *parent);

仔细一看,发现这个函数好像没有 ktyp 参数啊,那它的属性怎么办?通过这种方式创建 kobject 的时候,默认设置 kobj_type 的值为 dynamic_kobj_ktype(在 kobject_create() 函数中设置的):

c
static struct kobj_type dynamic_kobj_ktype = {
	.release	= dynamic_kobj_release,
	.sysfs_ops	= &kobj_sysfs_ops,
};

对于属性的操作,主要是 kobj_sysfs_ops

c
const struct sysfs_ops kobj_sysfs_ops = {
	.show	= kobj_attr_show,
	.store	= kobj_attr_store,
};
EXPORT_SYMBOL_GPL(kobj_sysfs_ops);

可以看到最后调用了 kobj_attr_show()kobj_attr_store(),这两个函数定义如下:

c
/* default kobject attribute operations */
static ssize_t kobj_attr_show(struct kobject *kobj, struct attribute *attr,
			      char *buf)
{
	struct kobj_attribute *kattr;
	ssize_t ret = -EIO;

	kattr = container_of(attr, struct kobj_attribute, attr);
	if (kattr->show)
		ret = kattr->show(kobj, kattr, buf);
	return ret;
}

static ssize_t kobj_attr_store(struct kobject *kobj, struct attribute *attr,
			       const char *buf, size_t count)
{
	struct kobj_attribute *kattr;
	ssize_t ret = -EIO;

	kattr = container_of(attr, struct kobj_attribute, attr);
	if (kattr->store)
		ret = kattr->store(kobj, kattr, buf, count);
	return ret;
}

看到这里,大概就知道了,这里其实 调用了 container_of() 来计算 attr 成员所在的结构体的地址,然后调用里面的 show/store 函数,这是不是和前面 __ATTR() 宏的哪个 demo 很像?只不过这里我们需要手动创建属性文件,并绑定每个属性对应的 show/store 函数。

2. demo 源码

05_device_model/09_attr_file_demo4

c
#include <linux/init.h> /* module_init module_exit */
#include <linux/kernel.h>
#include <linux/module.h> /* MODULE_LICENSE */

#include <linux/kobject.h>
#include <linux/slab.h>

#include "./timestamp_autogenerated.h"
#include "./version_autogenerated.h"
#include "./sdrv_common.h"

// #undef PRT
// #undef PRTE
#ifndef PRT
#define PRT printk
#endif
#ifndef PRTE
#define PRTE printk
#endif

struct kobject *skobject1 = NULL; // 定义 kobject 指针变量:skobject1

/**
 * @brief  skobj_attr_name_show()
 * @note   读属性操作函数, cat 属性文件时调用这个函数,会把对应的值显示到终端
 * @param [in]
 * @param [out]
 * @retval 
 */
ssize_t skobj_attr_name_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
{
    ssize_t count = 0;
    char data[32] = "aaa";
    count = sprintf(buf, "%s\n", data);
    return count;
}

/**
 * @brief  skobj_attr_name_store()
 * @note   写属性操作文件, echo 属性文件时调用这个函数,用于 echo x 中的 x 写入到属性对应的成员变量中
 * @param [in]
 * @param [out]
 * @retval 
 */
ssize_t skobj_attr_name_store(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count)
{
    char data[32] = {0};
    sscanf(buf, "%s\n", data);
    PRT("data=%s\n", data);
    return count;
}

/**
 * @brief  skobj_attr_data_show()
 * @note   读属性操作函数, cat 属性文件时调用这个函数,会把对应的值显示到终端
 * @param [in]
 * @param [out]
 * @retval 
 */
ssize_t skobj_attr_data_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
{
    ssize_t count = 0;
    int a = 8;
    count = sprintf(buf, "%d\n", a);
    return count;
}

/**
 * @brief  skobj_attr_data_store()
 * @note   写属性操作文件, echo 属性文件时调用这个函数,用于 echo x 中的 x 写入到属性对应的成员变量中
 * @param [in]
 * @param [out]
 * @retval 
 */
ssize_t skobj_attr_data_store(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count)
{
    int a = 6;
    sscanf(buf, "%d\n", &a);
    PRT("a = %d\n", a);
    return count;
}

// 定义两个属性,用于管理 name_attr 和 data
// 注意,这里的 mode 赋值的时候不能用这些代表权限的宏,例如 S_IRWXUGO,否则会在展开的时候出现问题
struct kobj_attribute s_attr_name = __ATTR(skobj_attr_name, 0664, skobj_attr_name_show, skobj_attr_name_store);
struct kobj_attribute s_attr_data = __ATTR(skobj_attr_data, 0664, skobj_attr_data_show, skobj_attr_data_store);

/**
 * @brief  sdriver_demo_init()
 * @note   
 * @param [in]
 * @param [out]
 * @retval 
 */
static __init int sdriver_demo_init(void)
{
    int ret = 0;
	printk("*** [%s:%d]Build Time: %s %s, git version:%s ***\n", __FUNCTION__,
           __LINE__, KERNEL_KO_DATE, KERNEL_KO_TIME, KERNEL_KO_VERSION);
    PRT("sdriver_demo module init!\n");
    
    skobject1 = kobject_create_and_add("skobject1", NULL);

    ret = sysfs_create_file(skobject1, &s_attr_name.attr);
    if(ret < 0)
    {
        PRTE("sysfs_create_file s_attr_name fail!ret=%d\n", ret);
        goto err_sysfs_create_file_1;
    }
    ret = sysfs_create_file(skobject1, &s_attr_data.attr);
    if(ret < 0)
    {
        PRTE("sysfs_create_file s_attr_data fail!ret=%d\n", ret);
        goto err_sysfs_create_file_2;
    }

    return 0;

err_sysfs_create_file_2:
    sysfs_remove_file(skobject1, &s_attr_name.attr);
err_sysfs_create_file_1:
    kobject_put(skobject1);
	return ret;
}

/**
 * @brief  sdriver_demo_exit()
 * @note   
 * @param [in]
 * @param [out]
 * @retval 
 */
static __exit void sdriver_demo_exit(void)
{
	// 释放之前创建的 kobject 对象
    kobject_put(skobject1);
    PRT("sdriver_demo module exit!\n");
}


module_init(sdriver_demo_init); // 将__init 定义的函数指定为驱动的入口函数
module_exit(sdriver_demo_exit); // 将__exit 定义的函数指定为驱动的出口函数

/* 模块信息(通过 modinfo chrdev_led_demo 查看) */
MODULE_LICENSE("GPL v2");            /* 源码的许可证协议 */
MODULE_AUTHOR("sumu");               /* 字符串常量内容为模块作者说明 */
MODULE_DESCRIPTION("Description");   /* 字符串常量内容为模块功能说明 */
MODULE_ALIAS("module's other name"); /* 字符串常量内容为模块别名 */

3. 开发板验证

这个 demo 的现象与前面的不同,后面会分析为什么没有办法保持一致。(也许是我太菜了,没有想到好办法?)

  • (1)一开始的 /sys 目录
image-20250108104459756
  • (2)加载模块
image-20250108104653640

加载模块后,会发现,在/sys 目录生成了 skobject1 目录,在/sys/skobject1 目录下有两个属性文件 skobj_attr_name 和 skobj_attr_data。

  • (3)查看 skobj_attr_name 和 skobj_attr_data 的值

由于前面 stow 中数据是写死的,并且没有与内核中某些数据相关联,所以这里显示的值都是固定的:

image-20250108104908429
  • (4)修改 skobj_attr_name 和 skobj_attr_data 的值
image-20250108105043285

会发现这里修改之后,查看的话,内容没变,原因就是上面写死了,并没有与某个变量保持一致,其实还是可以的,定义成全局变量就行了,只是不能像前面的 demo 一样,把 kobject 和自己的属性数据封装到一起,后面会去分析为什么没有封装到一起做。

  • (5)卸载模块
image-20250108105249577

可以看到,卸载模块后,资源释放,两个属性文件以及 kobject 目录全都没了。

4. 为什么不能封装到一起?

前面我们是这样来封装数据:

c
typedef struct __SKOBJ_{
    char skobj_name[32];
    char name_attr[32];
    int data;
    struct kobject kobj;
}skobj_t;

这样我们可以很方便的为自己创建的 kobj 添加属性,并且比较好管理,总好过这样:

c
typedef struct __SKOBJ_{
    char skobj_name[32];
    char name_attr[32];
    int data;
}skobj_t;
skobj_t kobject_attr = {0};
struct kobject *kobj;

然后去对应全局变量把,那么为什么 kobject_create_and_add() 自动创建的 kobject 对象不能像前面几个 demo 一样把属性和对象封装到一起?接下来就来分析一下为什么不行。

前面封装到一起的前提是我们可以通过内部的 kobj 成员,在 show/store 中使用下面的方式获取到这个结构体的地址:

c
ssize_t skobj_attr_name_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
{
    ssize_t count = 0;
    skobj_t *pskobj = container_of(kobj, skobj_t, kobj);
    count = sprintf(buf, "%s\n", pskobj->name_attr);
    return count;
}

container_of()这个函数,在获取结构体的地址的时候,我们需要知道结构体的类型,还需要知道 这个结构体中其中一个成员的地址,这个地址必须是成员的地址,若通过指针成员获取结构体地址的话,必须是指针自己的地址,而不是指针指向的地址。我们可以看一下上一节 __ATTR() 宏优化过的那个 demo 中,kobj 地址的情况:

image-20250108151909652

Tips:注意打印地址的时候,可以用%px,不然可能会打印出来个 ptrval ,好像是因为内核担心暴露地址啥的。

我们来看一下 kobject_create_and_add() 函数:

c
struct kobject *kobject_create_and_add(const char *name, struct kobject *parent);

可以看到,调用它的时候,我们不需要自己申请内存,它的返回值是创建完成的 kobject 的地址。我们要做的就是定义一个指针指向这个地址,这样就可以把 kobj 往后传了。我们来看一下这种情况下 kobj 的地址情况:

image-20250108160633745

接下来对比一下这两种情况:

image-20250108161846238

看一下左边的逻辑,我们到那个 dynamic_kobj_release()函数里面,获取到了 kobj 的地址,但是这里是变量的地址,它不是一个指针,并且 这个地址就是是结构体成员 kobj 的地址,也可以这样理解:我们创建的 kobject 对象和其他的成员在同一个地方:

image-20250108162946044

对于右侧的方式,我们也把 kobject 的地址传递到了 skobj_attr_name_show()函数,这里没有问题,可以正常使用 kobject 对象的,但是需要注意的是,这里传递的是 kobject 的地址,整个地址是存放在一个指针里面,它俩不是同一个东西,按照右侧这种方式,我们封装成结构体,应该是这样的:

c
typedef struct __SKOBJ_{
    char skobj_name[32];
    char name_attr[32];
    int data;
    struct kobject *kobj;
}skobj_t;

就会出现这种情况:

image-20250108163730977

最后,传过去的其实是 kobj 这个成员指针指向的的地址,毫无疑问,它确实是 kobject 对象的地址,但是我们为了获取整个结构体的地址,我们需要的知道的是 结构体成员的地址,而非指针指向的地址。所以,这里就出现了问题,kobj 指针自己的地址是没有继续向下传递的,传递的都是 kobj 指针指向的 kobject 对象的地址。这就意味这无法在 skobj_attr_name_show()函数中通过 kobject 的地址使用 container_of()从而获取整个结构体的地址。

五、创建多个属性的简单方式

1. sysfs_create_group()

多个属性的创建,我们可以使用 sysfs_create_group() 函数。我们需要创建一个属性文件数组,属性文件数组是一个以 struct attribute_group * 类型为元素的数组,以 NULL 结束。每个 struct attribute_group 结构体表示一个属性文件,可以使用&运算符将属性对象(如 struct kobject_attribute) 的.attr 字段传递给属性文件数组。

下面是一个使用 sysfs_create_group() 创建一个组并添加属性文件的简单 demo:

c
struct attribute *attrs[] = {
	&attr1.attr, 
    &attr2.attr, 
    NULL, 
};
const struct attribute_group attr_group = {
	.name = "my_group", 
    .attrs = attrs, 
};

sysfs_create_group(kobj, &attr_group);

我们创建了一个名叫“my_group”的组,并将属性文件 attr1 和 attr2 添加 到该组中,然后使用 sysfs_create_group() 将该组添加到指定的 kobject kobj 中。

2. demo 源码

c
#include <linux/init.h> /* module_init module_exit */
#include <linux/kernel.h>
#include <linux/module.h> /* MODULE_LICENSE */

#include <linux/kobject.h>
#include <linux/slab.h>

#include "./timestamp_autogenerated.h"
#include "./version_autogenerated.h"
#include "./sdrv_common.h"

// #undef PRT
// #undef PRTE
#ifndef PRT
#define PRT printk
#endif
#ifndef PRTE
#define PRTE printk
#endif

typedef struct __SKOBJ_ATTR_VAR_{
    char skobj_name[32];
    char name_attr[32];
    int data;
}skobj_attr_var_t;

struct kobject *skobject1 = NULL; // 定义 kobject 指针变量:skobject1
skobj_attr_var_t skobject1_attr = {0};     // skobject1 的两个属性

/**
 * @brief  skobj_attr_name_show()
 * @note   读属性操作函数, cat 属性文件时调用这个函数,会把对应的值显示到终端
 * @param [in]
 * @param [out]
 * @retval 
 */
ssize_t skobj_attr_name_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
{
    ssize_t count = 0;

    if(strcmp(kobj->name, skobject1_attr.skobj_name) == 0)
    {
        count = sprintf(buf, "%s\n", skobject1_attr.name_attr);
    }
    PRT("kobj->name=%s attr->name=%s count=%d\n", kobj->name, attr->attr.name, count);
    return count;
}

/**
 * @brief  skobj_attr_name_store()
 * @note   写属性操作文件, echo 属性文件时调用这个函数,用于 echo x 中的 x 写入到属性对应的成员变量中
 * @param [in]
 * @param [out]
 * @retval 
 */
ssize_t skobj_attr_name_store(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count)
{

    if(strcmp(kobj->name, skobject1_attr.skobj_name) == 0)
    {
        sscanf(buf, "%s\n", skobject1_attr.name_attr);
    }
    
    PRT("kobj->name=%s attr->name=%s count=%d\n", kobj->name, attr->attr.name, count);

    return count;
}

/**
 * @brief  skobj_attr_data_show()
 * @note   读属性操作函数, cat 属性文件时调用这个函数,会把对应的值显示到终端
 * @param [in]
 * @param [out]
 * @retval 
 */
ssize_t skobj_attr_data_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
{
    ssize_t count = 0;

    if(strcmp(kobj->name, skobject1_attr.skobj_name) == 0)
    {
        count = sprintf(buf, "%d\n", skobject1_attr.data);
    }
    
    PRT("kobj->name=%s attr->name=%s count=%d\n", kobj->name, attr->attr.name, count);

    return count;
}

/**
 * @brief  skobj_attr_data_store()
 * @note   写属性操作文件, echo 属性文件时调用这个函数,用于 echo x 中的 x 写入到属性对应的成员变量中
 * @param [in]
 * @param [out]
 * @retval 
 */
ssize_t skobj_attr_data_store(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count)
{
    if(strcmp(kobj->name, skobject1_attr.skobj_name) == 0)
    {
        sscanf(buf, "%d\n", &skobject1_attr.data);
    }
    
    PRT("kobj->name=%s attr->name=%s count=%d\n", kobj->name, attr->attr.name, count);

    return count;
}

// 定义两个属性,用于管理 name_attr 和 data
// 注意,这里的 mode 赋值的时候不能用这些代表权限的宏,例如 S_IRWXUGO,否则会在展开的时候出现问题
struct kobj_attribute s_attr_name = __ATTR(skobj_attr_name, 0664, skobj_attr_name_show, skobj_attr_name_store);
struct kobj_attribute s_attr_data = __ATTR(skobj_attr_data, 0664, skobj_attr_data_show, skobj_attr_data_store);

struct attribute *s_attr[] = {
    &s_attr_name.attr,
    &s_attr_data.attr,
    NULL,
};

const struct attribute_group s_attr_group = {
    .name = "skobj_attr",
    .attrs = s_attr,
};

/**
 * @brief  sdriver_demo_init()
 * @note   
 * @param [in]
 * @param [out]
 * @retval 
 */
static __init int sdriver_demo_init(void)
{
    int ret = 0;
	printk("*** [%s:%d]Build Time: %s %s, git version:%s ***\n", __FUNCTION__,
           __LINE__, KERNEL_KO_DATE, KERNEL_KO_TIME, KERNEL_KO_VERSION);
    PRT("sdriver_demo module init!\n");
    
    snprintf(skobject1_attr.skobj_name, sizeof(skobject1_attr.skobj_name), "skobject1");
    skobject1 = kobject_create_and_add(skobject1_attr.skobj_name, NULL);

    // 在 kobject "skobject1" 中创建属性组
    ret = sysfs_create_group(skobject1, &s_attr_group);
    if(ret < 0)
    {
        PRTE("sysfs_create_group fail!ret=%d\n", ret);
        goto err_sysfs_create_group;
    }
    return 0;

err_sysfs_create_group:
    kobject_put(skobject1);
	return ret;
}

/**
 * @brief  sdriver_demo_exit()
 * @note   
 * @param [in]
 * @param [out]
 * @retval 
 */
static __exit void sdriver_demo_exit(void)
{
	// 释放之前创建的 kobject 对象
    kobject_put(skobject1);
    PRT("sdriver_demo module exit!\n");
}


module_init(sdriver_demo_init); // 将__init 定义的函数指定为驱动的入口函数
module_exit(sdriver_demo_exit); // 将__exit 定义的函数指定为驱动的出口函数

/* 模块信息(通过 modinfo chrdev_led_demo 查看) */
MODULE_LICENSE("GPL v2");            /* 源码的许可证协议 */
MODULE_AUTHOR("sumu");               /* 字符串常量内容为模块作者说明 */
MODULE_DESCRIPTION("Description");   /* 字符串常量内容为模块功能说明 */
MODULE_ALIAS("module's other name"); /* 字符串常量内容为模块别名 */

3. 开发板验证

我们编译完成后,拷贝到开发板,依次进行以下验证:

  • (1)一开始的 /sys 目录
image-20250108185243172
  • (2)加载模块
image-20250108185515880

加载模块后,会发现,在/sys 目录生成了 skobject1 目录,在/sys/skobject1 目录下有一个 skobj_attr 目录,这个 skobj_attr 目录里面两个属性文件 skobj_attr_name 和 skobj_attr_data。

  • (3)查看/修改 skobj_attr_name 和 skobj_attr_data 的值
image-20250108185904025
  • (5)卸载模块
image-20250108185957935

可以看到,卸载模块后,资源释放,属性文件以及 kobject 目录全都没了。

参考资料:

【1】linux 设备驱动(9)attribute 详解 - Action_er - 博客园

【2】linux 驱动-设备驱动模型(属性文件 kobject )_linux 设备模型 kobject-CSDN 博客

【3】Linux 驱动开发—设备模型框架 kobject 创建属性文件_修改 kobject 创建的文件操作权限-CSDN 博客

【4】内核里 printk 打印指针打印出(____ ptrval ____) - 简书

【5】如何获得正确的 printk 格式占位符 — The Linux Kernel documentation