Linux 设备模型框架 kobject 和 kset

在 Linux 内核中,设备模型是连接硬件和软件的桥梁。理解 kobject 和 kset 这两个核心概念,对于开发高质量的内核驱动至关重要。本文将深入解析这两个概念,并通过实例代码展示如何在内核模块中使用它们。

一、Linux 设备模型概述

Linux 设备模型的核心目标是:

提供统一的设备管理机制 实现设备与驱动的分离 支持热插拔和电源管理 通过 sysfs 文件系统向用户空间暴露设备层次结构

而这一切的基础,就是kobject和kset。

二、kobject 与 kset 的核心概念

1. kobject

内核中最基本的对象结构 提供引用计数、父子关系和生命周期管理 是所有设备模型对象的基础 对应 sysfs 中的一个目录

2. kset

kobject 的集合,用于组织 kobject 本身也是一个 kobject 提供对象分组和过滤机制 对应 sysfs 中的一个子目录树

3. 关键数据结构

c 复制代码
// include/linux/kobject.h
struct kobject {
    const char      *name;        // 对象名称
    struct list_head    entry;     // 链表节点
    struct kobject      *parent;   // 父对象
    struct kset         *kset;     // 所属kset
    struct kobj_type    *ktype;    // 对象类型
    struct sysfs_dirent *sd;      // sysfs目录项
    struct kref         kref;      // 引用计数
    unsigned int state_initialized:1;  // 初始化状态标志
    ...
};
c 复制代码
// kset定义
struct kset {
    struct list_head list;        // 包含的kobject链表
    spinlock_t list_lock;         // 链表锁
    struct kobject kobj;          // 嵌入的kobject
    const struct kset_uevent_ops *uevent_ops;  // uevent操作
    ...
};

三、kobject 与 kset 的实现源码

下面通过一个完整的内核模块示例,展示如何使用 kobject 和 kset 创建自定义设备模型:

c 复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kobject.h>
#include <linux/sysfs.h>
#include <linux/string.h>
#include <linux/slab.h>


/* 模块参数 */
static int my_param = 42;
module_param(my_param, int, 0644);
MODULE_PARM_DESC(my_param, "A sample parameter");

/* 设备私有数据结构 */
struct my_device {
    struct kobject kobj;         // 嵌入的kobject
    int value;                   // 设备值
    struct mutex lock;           // 并发控制锁
};

static struct my_device *my_dev;

/* kobject属性操作函数 */
static ssize_t value_show(struct kobject *kobj, 
                         struct kobj_attribute *attr,
                         char *buf)
{
    struct my_device *dev = container_of(kobj, struct my_device, kobj);
    mutex_lock(&dev->lock);
    ssize_t ret = sprintf(buf, "%d\n", dev->value);
    mutex_unlock(&dev->lock);
    return ret;
}

static ssize_t value_store(struct kobject *kobj, 
                          struct kobj_attribute *attr,
                          const char *buf, size_t count)
{
    struct my_device *dev = container_of(kobj, struct my_device, kobj);
    int ret;
    long val;
    
    mutex_lock(&dev->lock);
    
    ret = kstrtol(buf, 10, &val);
    if (ret) {
        mutex_unlock(&dev->lock);
        return ret;
    }
    
    dev->value = val;
    mutex_unlock(&dev->lock);
    return count;
}

/* 定义kobject属性 */
static struct kobj_attribute value_attr =
    __ATTR(value, 0664, value_show, value_store);

/* 定义参数属性 */
static ssize_t param_show(struct kobject *kobj, 
                         struct kobj_attribute *attr,
                         char *buf)
{
    return sprintf(buf, "%d\n", my_param);
}

static ssize_t param_store(struct kobject *kobj, 
                          struct kobj_attribute *attr,
                          const char *buf, size_t count)
{
    return kstrtoint(buf, 10, &my_param);
}

static struct kobj_attribute param_attr =
    __ATTR(param, 0664, param_show, param_store);

/* kobject释放函数 */
static void my_kobj_release(struct kobject *kobj)
{
    struct my_device *dev = container_of(kobj, struct my_device, kobj);
    pr_info("My device kobject released\n");
    kfree(dev);
}

/* 不使用groups,改用手动创建属性 */
static struct kobj_type my_ktype = {
    .release = my_kobj_release,
    .sysfs_ops = &kobj_sysfs_ops,
};

/* 创建kset */
static struct kset *my_kset;

/* kset的uevent操作 */
static int my_uevent_filter(struct kset *kset, struct kobject *kobj)
{
    pr_info("Uevent filter called for %s\n", kobject_name(kobj));
    return 1;  // 允许发送uevent
}

static const char *my_uevent_name(struct kset *kset, 
                                 struct kobject *kobj)
{
    return kobject_name(kobj);
}

static int my_uevent(struct kset *kset, struct kobject *kobj,
                   struct kobj_uevent_env *env)
{
    pr_info("Uevent generated for %s\n", kobject_name(kobj));
    return 0;
}

static struct kset_uevent_ops my_uevent_ops = {
    .filter = my_uevent_filter,
    .name = my_uevent_name,
    .uevent = my_uevent,
};

/* 模块初始化 */
static int __init my_module_init(void)
{
    int ret;
    
    /* 创建kset */
    my_kset = kset_create_and_add("my_kset", &my_uevent_ops, kernel_kobj);
    if (!my_kset) {
        pr_err("Failed to create kset\n");
        return -ENOMEM;
    }
    
    /* 分配并初始化设备结构 */
    my_dev = kzalloc(sizeof(*my_dev), GFP_KERNEL);
    if (!my_dev) {
        pr_err("Failed to allocate device\n");
        kset_unregister(my_kset);
        return -ENOMEM;
    }
    
    /* 初始化kobject */
    kobject_init(&my_dev->kobj, &my_ktype);
    
    /* 设置kobject的父对象为my_kset */
    my_dev->kobj.kset = my_kset;
    
    /* 设置kobject名称 */
    ret = kobject_add(&my_dev->kobj, NULL, "my_device");
    if (ret) {
        pr_err("Failed to add kobject\n");
        kfree(my_dev);
        kset_unregister(my_kset);
        return ret;
    }
    
    /* 手动创建属性文件 */
    ret = sysfs_create_file(&my_dev->kobj, &value_attr.attr);
    if (ret) {
        pr_err("Failed to create value attribute\n");
        goto err_attr;
    }
    
    ret = sysfs_create_file(&my_dev->kobj, &param_attr.attr);
    if (ret) {
        pr_err("Failed to create param attribute\n");
        goto err_param;
    }
    
    /* 初始化设备值 */
    my_dev->value = 0;
    mutex_init(&my_dev->lock);
    
    pr_info("My module initialized\n");
    return 0;
    
err_param:
    sysfs_remove_file(&my_dev->kobj, &value_attr.attr);
err_attr:
    kobject_put(&my_dev->kobj);
    kset_unregister(my_kset);
    return ret;
}

/* 模块退出 */
static void __exit my_module_exit(void)
{
    /* 手动移除属性文件 */
    sysfs_remove_file(&my_dev->kobj, &value_attr.attr);
    sysfs_remove_file(&my_dev->kobj, &param_attr.attr);
    
    /* 释放kobject */
    kobject_put(&my_dev->kobj);
    
    /* 释放kset */
    kset_unregister(my_kset);
    
    pr_info("My module exited\n");
}

module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("cmy");
MODULE_DESCRIPTION("kobject and kset example");

四、源码解析与使用说明

1. kset 的创建与初始化

c 复制代码
my_kset = kset_create_and_add("my_kset", &my_uevent_ops, kernel_kobj);

创建一个名为 "my_kset" 的 kset 父对象设置为kernel_kobj,对应 sysfs 中的/sys/kernel目录 注册 uevent 回调函数,处理热插拔事件

2. kobject 的创建与属性

c 复制代码
kobject_init(&my_dev->kobj, &my_ktype);
ret = kobject_add(&my_dev->kobj, NULL, "my_device");

创建一个名为 "my_device" 的 kobject,作为 my_kset 的子对象 通过__ATTR宏定义两个属性:value和param 属性对应 sysfs 中的文件,可通过读写操作与内核交互

3. sysfs 属性操作

c 复制代码
static ssize_t value_show(struct kobject *kobj, ...)
static ssize_t value_store(struct kobject *kobj, ...)

value_show():处理用户读取value属性的请求 value_store():处理用户写入value属性的请求 通过container_of宏从 kobject 获取设备私有数据

4. 用户空间访问示例

五、kobject 与 kset 的高级应用

1. 设备层次结构组织

可以创建多层 kset 和 kobject,构建复杂的设备树 例如:总线→设备→功能部件

2. 热插拔支持

通过 uevent 机制通知用户空间设备变化 支持动态加载和卸载驱动

3. 电源管理集成

通过 kobject 属性暴露设备电源状态 实现设备的挂起和恢复

六、总线注册与驱动开发

1. 注册自定义总线

在 Linux 设备模型中,总线(Bus)是连接设备(Device)和驱动(Driver)的核心组件,负责管理两者的匹配与通信。以下是注册自定义总线的完整实现:

c 复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kobject.h>
#include <linux/sysfs.h>
#include <linux/device.h>



/* 总线匹配函数 */
static int my_bus_match(struct device *dev, struct device_driver *drv)
{
    pr_info("My bus match: dev=%s, drv=%s\n", dev_name(dev), drv->name);
    
    /* 匹配逻辑示例:检查设备和驱动的名称 */
    if (strncmp(dev_name(dev), drv->name, strlen(drv->name)) == 0)
        return 1;
    
    return 0;
}

/* uevent事件处理 */
static int my_bus_uevent(struct device *dev, struct kobj_uevent_env *env)
{
    add_uevent_var(env, "MY_BUS_EVENT=1");
    return 0;
}

/* 驱动探测函数 */
static int my_bus_probe(struct device *dev)
{
    pr_info("My bus probe: device %s detected\n", dev_name(dev));
    return 0;
}

/* 驱动移除函数 */
static int my_bus_remove(struct device *dev)
{
    pr_info("My bus remove: device %s removed\n", dev_name(dev));
    return 0;
}

/* 驱动关闭函数 */
static void my_bus_shutdown(struct device *dev)
{
    pr_info("My bus shutdown: device %s shutdown\n", dev_name(dev));
}

/* 自定义总线类型 */
struct bus_type my_bus_type = {
    .name       = "my_bus",         // 总线名称
    .dev_groups = NULL,             // 设备属性组
    .match      = my_bus_match,     // 设备与驱动匹配函数
    .uevent     = my_bus_uevent,    // uevent事件处理
    .probe      = my_bus_probe,     // 驱动探测函数
    .remove     = my_bus_remove,    // 驱动移除函数
    .shutdown   = my_bus_shutdown,  // 驱动关闭函数
};

/* 导出总线结构体,使其可被其他模块使用 */
EXPORT_SYMBOL_GPL(my_bus_type);

/* 模块初始化 */
static int __init my_bus_init(void)
{
    int ret;
    
    /* 注册自定义总线 */
    ret = bus_register(&my_bus_type);
    if (ret) {
        pr_err("Failed to register my_bus\n");
        return ret;
    }
    
    pr_info("My bus registered successfully\n");
    return 0;
}

/* 模块退出 */
static void __exit my_bus_exit(void)
{
    /* 注销总线 */
    bus_unregister(&my_bus_type);
    pr_info("My bus unregistered\n");
}

module_init(my_bus_init);
module_exit(my_bus_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("cmy");
MODULE_DESCRIPTION("My bus driver example");

2. 总线注册过程解析

总线注册的核心步骤如下:

定义总线结构体:

c 复制代码
struct bus_type my_bus_type = {
    .name       = "my_bus",
    .match      = my_bus_match,
    .probe      = my_bus_probe,
    .remove     = my_bus_remove,
    .shutdown   = my_bus_shutdown,
    .uevent     = my_bus_uevent,
};

name:总线名称,用于标识总线 match:关键函数,决定设备与驱动是否匹配 probe:驱动探测函数,设备匹配后调用 remove:驱动移除时调用 uevent:总线相关 uevent 事件处理 注册总线到内核:

c 复制代码
ret = bus_register(&my_bus_type);

该函数会: 创建总线对应的 kset 和 kobject 在 sysfs 中生成/sys/bus/my_bus目录 注册总线的 uevent 处理机制 注销总线:

c 复制代码
bus_unregister(&my_bus_type);

清理总线相关的所有资源,包括 sysfs 节点和注册的回调函数。

3. 在自定义总线下注册驱动

以下是在已注册的总线下开发并注册驱动的示例:

c 复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/device.h>

/* 自定义总线类型(假设已注册) */
extern struct bus_type my_bus_type;

/* 驱动探测函数 */
static int my_driver_probe(struct device *dev)
{
    pr_info("My driver probed: device %s\n", dev_name(dev));
    return 0;
}

/* 驱动移除函数 */
static int my_driver_remove(struct device *dev)
{
    pr_info("My driver removed: device %s\n", dev_name(dev));
    return 0;
}

/* 驱动结构体 */
static struct device_driver my_driver = {
    .name           = "my_device",    // 驱动名称
    .bus            = &my_bus_type,   // 关联的总线
    .probe          = my_driver_probe,// 探测函数
    .remove         = my_driver_remove,// 移除函数
    .shutdown       = NULL,
    .suspend        = NULL,
    .resume         = NULL,
};

/* 模块初始化 */
static int __init my_driver_init(void)
{
    int ret;
    
    /* 在自定义总线下注册驱动 */
    ret = driver_register(&my_driver);
    if (ret) {
        pr_err("Failed to register my_driver\n");
        return ret;
    }
    
    pr_info("My driver registered on my_bus\n");
    return 0;
}

/* 模块退出 */
static void __exit my_driver_exit(void)
{
    /* 注销驱动 */
    driver_unregister(&my_driver);
    pr_info("My driver unregistered my_driver.bus = %x\n", my_driver.bus);
}

module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("cmy");
MODULE_DESCRIPTION("My driver example");

4. 在自定义总线下注册设备

c 复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/device.h>

/* 自定义总线类型(假设已注册) */
extern struct bus_type my_bus_type;


/* 设备释放函数 */
static void my_device_release(struct device *dev)
{
    pr_info("My device released\n");
}

/* 设备结构体 */
static struct device my_device = {
    .init_name      = "my_device",  // 设备初始名称
    .bus            = &my_bus_type, // 关联的总线
    .parent         = NULL,         // 父设备
    .release        = my_device_release, // 释放函数
};


/* 模块初始化 */
static int __init my_device_init(void)
{
    int ret;
    
    /* 在自定义总线下注册设备 */
    ret = device_register(&my_device);
    if (ret) {
        pr_err("Failed to register my_device\n");
        return ret;
    }
    
    pr_info("My device registered on my_bus\n");
    return 0;
}

/* 模块退出 */
static void __exit my_device_exit(void)
{
    /* 注销设备 */
    device_unregister(&my_device);
    pr_info("My device unregistered\n");
}

module_init(my_device_init);
module_exit(my_device_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("cmy");
MODULE_DESCRIPTION("My device example");

5. 总线、设备与驱动的匹配机制

当设备和驱动都注册到同一总线后,总线的match函数会被调用,典型匹配流程:

设备注册时:总线会遍历所有已注册的驱动,调用match函数检查是否匹配 驱动注册时:总线会遍历所有已注册的设备,调用match函数检查是否匹配 匹配成功后:自动调用驱动的probe函数初始化设备

c 复制代码
/* 总线匹配函数 */
static int my_bus_match(struct device *dev, struct device_driver *drv)
{
    pr_info("My bus match: dev=%s, drv=%s\n", dev_name(dev), drv->name);
    
    /* 匹配逻辑示例:检查设备和驱动的名称 */
    if (strncmp(dev_name(dev), drv->name, strlen(drv->name)) == 0)
        return 1;
    
    return 0;
}

6. 查看总线相关信息

sysfs 中的总线目录:

bash 复制代码
ls /sys/bus/my_bus/

查看已注册的设备和驱动:

bash 复制代码
# 设备列表
ls /sys/bus/my_bus/devices/

# 驱动列表
ls /sys/bus/my_bus/drivers/

七、总线注册与 kobject/kset 的关系

每个总线对应一个 kset,位于/sys/bus/bus name 总线的 kset 包含两个子 kset:devices和drivers 设备和驱动注册时,会自动添加到总线的对应 kset 中 总线的 uevent 机制基于 kobject 的 uevent 扩展实现

通过总线机制,Linux 设备模型实现了设备与驱动的分离管理,使得驱动可以动态加载并自动匹配对应的设备,这是热插拔功能的核心基础。

相关推荐
A小辣椒17 小时前
TShark:Wireshark CLI 功能
linux
A小辣椒21 小时前
TShark:基础知识
linux
AlfredZhao1 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5203 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩3 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言