Everything you never wanted to know about kobjects, ksets, and ktypes

本文介绍了Linux内核中kobject和kset的概念及其使用方法。kobject作为对象的引用计数器,常嵌入其他结构体中。kset则用于组织多个kobject,形成逻辑上的容器。文章详细解释了kobject的初始化、管理和释放过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

    要理解kobject抽象及其之上的设备驱动模型并不简单,难点之一就在于,没有一个明显的起点。要处理好kobject,需要理解一些别的类型,而这些类型又是相互引用的。为了让事情简单,我们采用多遍的过程,从模糊的概念出发,逐渐增加细节。为此,这里先对一些相关概念进行定义:

—kobject 是一个类型为struct object 的对象。kobject有一个名称和一个引用计数,还有一个指向父kobject的指针(允许kobject分层排布),一个特定的类别,通常还有一个在sysfs虚拟文件系统中的显示。

    kobject本身并不是重点,但它经常嵌入在其它结构中,这些被嵌入的结构里,往往有我们感兴趣的内容。

    每个结构不能有超过一个kobject嵌入其中。如果超过了,对该结构的引用计数会混乱掉,代码会有bug。所以不要这样做。

—ktype是一类嵌入在kobject中的对象。每个使用kobject的结构都需要相应的ktype。ktype管理了在kobject创建和销毁时会发生什么。

—kset是一群kobject。这些kobject可以使用同样的ktype,也可以是属于不同 的ktype。kset是容纳多个kobject的基本容器类型。kset有自己的一个kobject,但你可以安全地忽略它,kset的代码会自动管理这个内部的kobject。

    但你看到一个sysfs目录,下面包含有子目录,通常每个子目录就对应一个处在同一kset下的kobject。

    我们下面看如何创建并管理所有这些类型。我们会使用自底向上的方法,所以我们回到kobject。

 

内嵌的kobject

    内核代码很少创建一个单独的kobject,但后面有一个例外情况。kobject主要用于控制更大的、应用指定的对象的访问。所以,kobject经常内嵌到其它对象结构中。如果你习惯用面向对象的方式来思考,kobject可以看成一个顶层的抽象的类,其它的类从这里继承。kobject中集成了一系列对自己没用的能力,但这些功能在继承的类里很有用。c语言不支持直观地使用继承,所以必须使用其它方式完成——例如结构内嵌。

    例如,UIO设备代码定义了一个表示内存范围的结构:

  1. struct uio_mem  
  2. {  
  3.     struct kobject kobj;  
  4.     unsigned long addr;  
  5.     unsigned long size;  
  6.     int memtype;  
  7.     void __iomem *internal_addr;  
  8. };  

如果你有了一个uio_mem类型的对象,要找到它里面的kobject很简单,只要使用相应的kobj结构成员就行了。但处理kobject的代码往往需要完成相反的过程:给你一个kobject的指针,如何找到指向uio_mem的指针?你需要避免使用一些把戏(例如假设kobject就是uio_mem的第一个结构成员),相反,你应该使用container_of宏,它定义在<linux/kernel.h>中:

  1. container_of(pointer, type, member)  


这里的pointer是指向内嵌kobject的指针,type是kobject所在地结构类型,member是结构类型中kobject成员的名字。container_of()的返回值就是kobject所在的结构类型。假设有一个内嵌在uio_mem中的kobject的指针"kp",可以很容易地把它转化成指向uio_mem结构的指针:

  1. struct uio_mem *u_mem = container_of(kp, struct uio_mem, kobj);  

程序员往往会定义一个更简单的宏来进行从kobject指针到结构指针的回溯。

 

初始化kobject

    创建kobject的代码同样要初始化kobject。可以调用kobject_init()完成kobject内部的初始化。

  1. void kobject_init(struct kobject *kobj, struct kobj_type *ktype);  

    要初始化kobject,需要相应的ktype,因为kobject必须有一个对应的ktype。调用完kobject_init()之后,要调用kobject_add()把kobject注册到sysfs中。

  1. int kobject_add(struct kobject *kobj, struct kobject *parent, const char *fmt, ...);  

   kobject_add设定了kobject的父节点,kobject的名字。如果kobject需要和一个特定的kset关联,在调用kobject_add()之前要先对kobj->kset赋值。如果一个kobject与一个kset关联,那它的父节点可以设为NULL,这样kobject的父节点会在调用kobject_add()时自动设为kset内部的kobject。

   因为kobject的名字是在它调用kobject_add()时设定的,会在sysfs中显示,所以不要手动修改kobject名字,而是调用kobject_rename()。

  1. int kobject_rename(struct kobject *kobj, const char *new_name);  

    kobject_rename 不会加锁,不会检查name是否有效,所以调用者需要提供名称检查和串行化管理。

    有一个叫做kobject_set_name()的汗水,但已经过时了,准备移除。所以不要使用kobject_set_name。

    为了更好地获取kobject的名字,使用kobject_name()。

  1. const char *kobject_name(const struct kobject *kobj);  

    有一个帮助函数,既可以初始化kobject,有可以将其注册到sysfs中,它叫做kobject_init_and_add。

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

 

uevent

    一个kobject被注册到sysfs中后,需要向世界宣布它的创建。这需要调用kobject_uevent()。

  1. int kobject_uevent(struct kobject *kobj, enum kobject_action action);  

    在kobject加入内核时,先使用KOBJ_ADD action。这一调用需要等kobject的属性和子kobject初始化好之后才行,因为调用之后用户空间会立刻检查kobject的属性和子kobject。

    当kobject从内核中删除时,KOBJ_REMOVE action会被自动发出,所以调用者不需要手动发出KOBJ_REMOVE。

 

引用计数

    kobject的一个重要功能就是充当所嵌入结构的引用计数。只要对象的引用还存在,对象就要存在。管理一个kobject引用计数的低层函数如下:

  1. struct kobject *kobject_get(struct kobject *kobj);  
  2. void kobject_put(struct kobject *kobj);  

    调用kobject_get()会增加kobject的引用计数,并返回kobject指针。

    当一个引用要被释放,需要调用kobject_put(),并可能要销毁kobject。注意kobject_init()设置计数值为1,所以创建kobject的代码需要调用kobject_put来释放创建时生成的引用。

    因为kobject是动态的,他们不应该被静态声明或者放在堆栈上,而应该动态创建。内核的更高版本会进行运行时检查,对静态创建的kobject予以警告。

    如果你只想用kobject为你的结构提供一个引用计数器,可以用kref替换,kobject太浪费了。

 

创建简单的kobject

    有时开发者只想在sysfs中创建一个简单的目录,而且不想考虑与kset、show函数、store函数,等一系列细节问题。这是就可以创建一个单独的kobject,用下面的函数创建。

  1. struct kobject *kobject_create_and_add(char *name, struct kobject *parent);  

    这个函数会创建一个kobject,并把它放在sysfs中parent的子目录中。为了创建这个kobject的相关属性,用以下函数。

  1. int sysfs_create_file(struct kobject *kobj, struct attribute *attr);  
  2.   
  3. int sysfs_create_group(struct kobject *kobj, struct attribute_group *grp);  

    两种属性都用到了kobject_create_and_add中创建的kobject。
    具体使用简单kobject的例子可以参见samples/kobject/kobject-example.c。

 

ktype和release函数

    我们还没有讨论当kobject引用计数降为零时会发生什么。创建kobject的代码通常不知道引用计数何时会降为零,如果它知道,那使用kobject就没什么意义了。即使是可以预测的对象生命周期,也会在注册到sysfs文件系统后变得更复杂。因为系统的其它部分也可以获得注册到系统中的kobject的引用。

    我们的目的是在kobject的引用计数降为零之前,决不能释放kobject。这个引用计数并非由创建kobject的代码直接控制,所以要在kobject引用计数降为零时异步地通知创建它的代码。一旦你通过kobject_add()把一个kobject注册到系统中,就决不能直接用kfree()释放它。唯一安全的做法是使用kobject_put()。

    这种释放时对创建代码的异步通知是通过kobject的release函数完成的。通常release函数有如下的形式。

  1. void my_object_release(struct kobject *kobj)  
  2. {  
  3.     struct my_object *mine = container_of(kobj, struct my_object, kobj);  
  4.     /*perform any additional cleanup on this object, then... */  
  5.     kfree(mine);  
  6. }  

    有一件事一定要强调:每个kobject一定要有一个release函数,并且这个kobject在调用release函数前一定要保持存在状态。否则代码就是有缺陷的。注意,如果代码没有提供一个release函数,内核会提醒你的。不要试图用空的release函数来消除警告;如果你那样做,你最终会被kobject的维护者无情地嘲弄。【作者为了提醒这点已经开始不择手段了】

    注意,在release函数中,kobject的名字仍然是有效的,只是绝不能被更改。不然在kobject中会出现内存泄露。
    有趣的是,release函数指针并不存放在kobject中,而是在ktype中。所以我们下面介绍ktype。

  1. struct kobj_type {  
  2.     void (*release)(struct kobject *);  
  3.     struct sysfs_ops *sysfs_ops;  
  4.     struct attribute **default_attrs;  
  5. };     

    ktype用来表示一类kobject的公共内容。每个kobject都要有一个对应的kobj_type结构,在kobject_init()或者kobject_init_and_add()调用时给出。

    kobj_type中的release函数指针,当然就是指向这类kobject使用的release函数。另外两个部分(sysfs_ops和default_attrs)管理这类kobject在sysfs中如何显示,对它们的讨论超出了本文档的范围。

    default_attrs指针是一个默认属性的链表,这些属性会在这类kobject创建时自动创建。

 

kset

    一个kset是一些kobject的集合,这些kobject通过kset关联在一起。一个kset中的kobject并不要求非得使用同样的ktype,但如果不是,必须加以注意。

    一个kset有如下功能: 

         1) 它作为一个容器,包含一组对象。它可以被内核用来跟踪所有的块设备或者所有的PCI设备驱动。

         2) 一个kset在sysfs中变现为一个分层的目录。每个kset都有一个内部kobject,作为其它kobject的父目录,其余各个kobject都是它的子目录。sysfs的顶层目录结构也是这样组成的。

        3) kset可以支持kobject的热插拔,并且影响uevent事件如何被发布到用户空间。

    从面向对象的角度来讲,kset是顶层类容器。kset也有自己内部的kobject,但这个内部kobject是由kset代码管理的,不允许其它用户直接访问。

    kset用一个标准的链表链接它的子kobject。而子kobject又反过来通过结构中的kset域指向kset。在绝大部分情况下,kobject都属于自己所在的kset,把自己的parent域设为所在kset的内部kobject。

    因为kset包括一个内部kobject,kset必须动态创建,不能静态声明。可以用如下函数创建一个新的kset。

  1. struct kset *kset_create_and_add(const char *name, struct kset_uevent_ops *u, struct kobject *parent);  

    当你不再使用一个kset,可以用kset_unregister()释放它。

  1. void kset_unregister(struct kset *kset);  

    在samples/kobject/kset-example.c中有使用kset的例子。

    如果一个kset想要管理其下kobject的uevent操作,可以使用kset_uevent_ops结构。

  1. struct kset_uevent_ops {  
  2.     int (*filter)(struct kset *kset, struct kobject *kobj);  
  3.     const char *(*name)(struct kset *kset, struct kobject *kobj);  
  4.     int (*uevent)(struct kset *kset, struct kobject *kobj, struct kobj_uevent_env *env);  
  5. };  

    filter函数用来阻止kset中某个特定的kobject向用户控件发送uevent。如果函数结果返回0,则不会发送。

    name函数是用来覆盖uevent发送到用户控件的kset名字。默认情况下,这个名字和kset的名字一样,但如果提供了name函数,这个名字会被覆盖。

    uevent函数在uevent将被发送到用户空间时调用,可以在uevent中添加更多的环境变量。

 

    大家或许会怀疑,一个kobject究竟是如何被加入一个kset中的,没有一个函数进行此项操作。其实这项任务是由kobject_add()完成。当一个kobject参数传给kobject_add()时,它的kset域应该指向所属的kset,其余的工作会由kobject_add()完成。

    如果属于一个kset的kobject没有设置parent域,它会被放在kset目录下。但如果一个kobject设置了parent kobject,虽然它仍属于这个kset,但却放在parent kobject的目录下。

 

  
删除kobject

    如果一个kobject成功地注册到内核中,在代码结束对其操作时必须清除该kobject。可以调用kobject_put()。通过调用kobject_put(),系统会自动释放与该kobject有关的所有内存。如果该kobject发送过一个KOBJ_ADD消息,结束时会对应地发一个KOBJ_REMOVE消息,其它的sysfs管理也会被相应地做好。

    如果你需要一个两段式的kobject删除(例如你不希望在删除kobject时进入睡眠),可以调用kobject_del()将kobject从sysfs中注销。这会使kobject不可见,但还没有被清除,对它的引用计数也不变。之后某个时间需要调用kobject_put()结束该kobject相关的内存清理工作。

    kobject_del()可以用来减少对parent object的引用,特别是在循环引用的时候。父对象引用子对象在某些情况下是合法的。为了打破环状的引用,必须显式地调用kobject_del(),这样才能将计数降为零,调用release函数,清除环中的kobject。

    sample/kobject/kset-example.c是一个使用kobject和kset的简单例子。

static int create_xcdev(struct xdma_pci_dev *xpdev, struct xdma_cdev *xcdev, int bar, struct xdma_engine *engine, enum cdev_type type) { int rv; int minor; struct xdma_dev *xdev = xpdev->xdev; dev_t dev; spin_lock_init(&xcdev->lock); /* new instance? */ if (!xpdev->major) { /* allocate a dynamically allocated char device node */ int rv = alloc_chrdev_region(&dev, XDMA_MINOR_BASE, XDMA_MINOR_COUNT, XDMA_NODE_NAME); if (rv) { pr_err("unable to allocate cdev region %d.\n", rv); return rv; } xpdev->major = MAJOR(dev); } /* * do not register yet, create kobjects and name them, */ xcdev->magic = MAGIC_CHAR; xcdev->cdev.owner = THIS_MODULE; xcdev->xpdev = xpdev; xcdev->xdev = xdev; xcdev->engine = engine; xcdev->bar = bar; rv = config_kobject(xcdev, type); if (rv < 0) return rv; switch (type) { case CHAR_USER: minor = 1024+bar; cdev_bypass_init(xcdev); break; case CHAR_CTRL: /* minor number is type index for non-SGDMA interfaces */ minor = type; cdev_ctrl_init(xcdev); break; case CHAR_XVC: /* minor number is type index for non-SGDMA interfaces */ minor = 100 + bar; cdev_event_init(xcdev); break; case CHAR_XDMA_H2C: minor = 32 + engine->channel; cdev_sgdma_init(xcdev); break; case CHAR_XDMA_C2H: minor = 36 + engine->channel; cdev_sgdma_init(xcdev); break; case CHAR_EVENTS: minor = 10 + bar; cdev_event_init(xcdev); break; case CHAR_BYPASS_H2C: minor = 64 + engine->channel; cdev_bypass_init(xcdev); break; case CHAR_BYPASS_C2H: minor = 68 + engine->channel; cdev_bypass_init(xcdev); break; case CHAR_BYPASS: minor = 100; cdev_bypass_init(xcdev); break; default: pr_info("type 0x%x NOT supported.\n", type); return -EINVAL; } xcdev->cdevno = MKDEV(xpdev->major, minor); /* bring character device live */ rv = cdev_add(&xcdev->cdev, xcdev->cdevno, 1); if (rv < 0) { pr_err("cdev_add failed %d, type 0x%x.\n", rv, type); goto unregister_region; } dbg_init("xcdev 0x%p, %u:%u, %s, type 0x%x.\n", xcdev, xpdev->major, minor, xcdev->cdev.kobj.name, type); /* create device on our class */ if (g_xdma_class) { rv = create_sys_device(xcdev, type); if (rv < 0) goto del_cdev; } return 0; del_cdev: cdev_del(&xcdev->cdev); unregister_region: unregister_chrdev_region(xcdev->cdevno, XDMA_MINOR_COUNT); return rv; } 代码作用分析
05-14
内容概要:本文档为《400_IB Specification Vol 2-Release-2.0-Final-2025-07-31.pdf》,主要描述了InfiniBand架构2.0版本的物理层规范。文档详细规定了链路初始化、配置与训练流程,包括但不限于传输序列(TS1、TS2、TS3)、链路去偏斜、波特率、前向纠错(FEC)支持、链路速度协商及扩展速度选项等。此外,还介绍了链路状态机的不同状态(如禁用、轮询、配置等),以及各状态下应遵循的规则和命令。针对不同数据速率(从SDR到XDR)的链路格式化规则也有详细说明,确保数据包格式和控制符号在多条物理通道上的一致性和正确性。文档还涵盖了链路性能监控和错误检测机制。 适用人群:适用于从事网络硬件设计、开发及维护的技术人员,尤其是那些需要深入了解InfiniBand物理层细节的专业人士。 使用场景及目标:① 设计和实现支持多种数据速率和编码方式的InfiniBand设备;② 开发链路初始化和训练算法,确保链路两端设备能够正确配置并优化通信质量;③ 实现链路性能监控和错误检测,提高系统的可靠性和稳定性。 其他说明:本文档属于InfiniBand贸易协会所有,为专有信息,仅供内部参考和技术交流使用。文档内容详尽,对于理解和实施InfiniBand接口具有重要指导意义。读者应结合相关背景资料进行学习,以确保正确理解和应用规范中的各项技术要求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值