向下之旅(二十四):kobject与sysfs

  2.6内核增加了统一设备模型新特性,设备模型提供了一个独立的机制专门来表示设备,并描述其在系统中的拓扑结构,从而使得系统具有以下优点:

  1.代码重复最小化

  2.提供诸如引用计数这样的统一机制

  3.可以列举系统中的所有设备,观察它们的状态,并且查看它们连接的总线

  4.可以将系统中的全部设备结构以树的形式完整、有效的展示出来,包括所有的总线和内部连接

  5.可以将设备和其对应的驱动联系起来,反之亦然

  6.可以将设备按照类型加以归类,如分类为输入设备,无需理解物理设备的拓扑结构

  7.可以沿设备的叶子向其根的方向依次遍历,以保证能以正确顺序关闭各设备的电源。

  kobject

  设备模型的核心部分就是kobject,它由struct kobject结构体表示,类似于C#或java这些面向对象中的objcet(对象)类。提供了诸如引用计数,名称和父指针等字段,可以创建对象的层次结构。

  

  k_name指针指向kobject名称的起始位置,若名称长于KOBJ_NAME_LEN(20个字节),则动态分配一个足够大的缓冲区来存放kobject的名称,这时k_name指向缓冲区。

  parent指针指向kobjcet的父对象,这便是sysfs的真正面目:一个用户空间的文件系统,用来表示内核中的kobjcet对象的层次结构。

  dentry指针指向dentry结构体,在sysfs中该结构体就表示这个kobject,当然假设该kobject已反映在sysfs中了。

  kobject通常是嵌入到其他结构中的,单独意义并不大,一些更为重要的结构体如struct cdev中才真正需要用到kobjcet结构。

  

  当kobjcet被嵌入到其他结构中,该结构便拥有了kobject提供的标准功能,并且该结构通过kobject的结构体可以成为对象层次架构中的一部份。

  ktype

  ktype用来描述kobjcet所具有的普遍特性。不需要每个kobjcet都分别定义自己的特性,由kobj_type结构体表示:

  

  release指针是当kobjcet引用计数减到零的时候要被调用的析构函数。该函数负责释放所有的kobjcet使用的内存和其他相关清理工作。

  sysfs_ops变量指向sysfs_ops结构体。该结构体描述了sysfs文件读写时的特性。

  default_attrs指向一个attribute结构体数组,这些结构体定义了该kobject相关的默认属性。

  kset

  kset是kobject对象的集合体。可将所有相关的kobject对象,比如“全部的块设备”置于同一位置。具有相相同的ktype的kobject可以分组到不同的ksets中。其结构体如下:

  

  其中ktype指针指向集合(kset)中kobject对象类型(ktype),list连接该集合(kset)中所有的kobject对象。kobj指向的kobject对象代表了该集合的基类。

  subsystem

  subsystem在内核中代表高层概念,它是一个或多个ksets的大集合。ksets包含kobject,subsystems包含ksets,但是在subsystem中ksets之间的关联相比kset中kobject之间的关联要弱很多。在subject中的ksets只可以共享一些较宏观的普遍属性。结构体如下:

  

  subsystem结构体只指向一个kset,但是多个ksets可以通过其subsys指针指向一个subsystem。这种单向关系意味着不可能仅仅通过一个subsystem结构体就找到所有的ksets。

  subsystem中的kset字段指向的是subsystem中的默认kset,它在对象层次结构中起到了粘合剂的作用。

  rwsem字段是一个读写信号量,该信号量用来对subsystem和它所有的ksets进行并发访问保护。所有的ksets必须属于subsystem,因为它们使用该读写信号量去同步访问它们的内部数据。

  管理和操作kobject

  使用kobject的第一步需要先声明和初始化它。kobject通过函数kobject_init进行初始化:

  void kobject_init(struct kobject *kobj);

  该函数唯一的参数就是需要初始化的kobject对象,在调用初始化函数前,kobject必须清空。若kobject未清空,那么只需要调用memset()即可:

  memset(kobj,0,sizeof(*kobj));

  在清零后,就可以安全的初始化parent和kset字段。例如,

  

  初始化后,必须采用kobject_set_name()函数为kobject设置名称:

  int kobject_set_name(struct kobject *kobj, const char * fmt, ...);

  初始化kobject并设置名称后,还需要为它设置kset字段以及可能的ktype字段(可选)。如果kset没有被提供,那么仅需要设置ktype,否则kset中的ktype字段将优先被使用。

  引用计数

  kobject的主要功能之一就是为我们提供了一个统一的引用计数系统。初始化后,引用计数为1,只要计数不为0,那么该对象就继续保留在内存中。任何包含对象引用的代码首先要增加引用计数(获得对象引用),代码结束后减少它的引用计数(释放对象引用)。当计数减少到零时,对象便可以被销毁,同时相关的内存也都被释放。

  增加通过kobject_get()函数:

  struct kobject * kobject)get(struct kobject *kobj);

  减少通过kobject_put()完成:

  void kobject_put(struct kobject *kobj);

  如果对应的kobject的引用计数减少的零,则与该kobject关联的ktype中的析构函数将被调用。

  kobject的引用计数采用kref结构体实现:

  struct kref{

    atomic_t refcount;

  };

  其中唯一的字段是用来存放引用计数的原子变量,采用结构体的原因是便于进行类型检测。在使用kref前,必须通过kref_init()函数来初始化它:

  void kref_init(struct kref *kref){

    atomic_set($kref->refcount,1);

  }

  使用kref_get()函数获取对kref的引用,该函数增加引用计数值,它没有返回值。

  

  使用kref_put()函数减少对kref的引用,当计数为0,则调用release()函数。

  

  sysfs

  sysfs文件系统是一个处于内存中的虚拟文件系统,为我们提供了kobject对象层次结构的视图。以一个简单的文件系统的方式来观察系统中各种设备的拓扑结构。

  sysfs把kobject对象与目录项紧密的联系起来,这点通过kobject对象中的dentry字段实现的。dentry结构体表示目录项,通过连接kobject到指定的目录项上,无疑方便的将kobject映射到该目录上。因此,kobjects其实已经形成了一棵树了——就是对象模型体系。由于kobject被映射到目录项,同时对象层次结构也已经在内存中形成了一棵树,因此sysfs就诞生了。

  

  sysfs的根目录下包含了七个目录:block、bus、class、devices、firmware、module和power。block目录下的每个子目录都对应着系统的一个块设备。bus目录提供了一个系统总线视图。class目录包含了以高层功能逻辑组织起来的系统设备视图。devices目录是系统中设备拓扑结构视图,它直接映射除了内核中设备结构体的组织层次。firmware目录包含了一些诸如ACPI、EDD、EFI等低层子系统的特殊树。power目录包含了系统范围的电源管理数据。最重要的目录是devices,该目录将设备模型导出到用户空间。目录结构就是系统中实际的设备拓扑。其他目录中的很多数据都是将devices目录下的数据加以转换加工而得。

  sysfs中添加和删除kobject

  仅仅初始化kobjcet是不能自动将其导出到sysfs中的,想要把kobject导入sysfs,需要用到函数kobject_add():

  int kobject_add(struct kobject *kobj);

  kobject在sysfs中的位置取决于kobject在对象层次结构中的位置。如果kobject的父指针被设置,那么在sysfs中的kobject将被映射为其父目录下的子目录。如果parent没有设置,那么kobject将被映射为kset->kobj中的子目录。如果给定的kobject中parent或kset字段都没有设置,那么就认为kobject没有父对象,所以就会被映射成sysfs下的根级目录。但这往往不是你所需要的,所以在调用kobject_add()前parent或kset字段应该显示的设置。不管怎样,sysfs中代表kobject的目录名字是由kobj->k_name指定的。

  不过不需要调用kobject_init()和kobject_add(),因为系统提供了函数kobject_register()。

  int kobject_register(struct kobject *kobj)

  该函数既初始化了给定的kobject对象,同时又将其加入到对象层次结构中。

  从sysfs中删除一个kobject对应文件目录。需使用函数kobject_del():

  void kobject_del(struct kobject *kobj);

  类似的,函数kobject_unregister()包含了kobject_del和kobject_put()两者的功能:

  void kobject_unregister(struct kobject *kobj)

  向sysfs中添加文件

  kobject已经被映射为文件目录。所有的对象层次一个不少的映射成sys下的目标结构。sysfs仅仅是一个漂浪的树,但是没有提供实际数据的文件。

  默认属性

  默认的文件集合是通过Kobject和kset的ktype字段提供的。因此所有具有相同类型的kobject在它们对应的sysfs目录下都拥有相同的默认文件集合。kobj_type字段含有一个字段——default_attrs,它是一个attribute结构体数组。这些属性负责将内核数据映射成sysfs中的文件。

  

  其中名称字段提供了该属性的名称,最终出现在sysfs中的文件名就是它。owner字段在存在所属模块的情况下指向其所属module结构体。如果一个模块没有该属性,那么该字段为NULL。mode字段类型为mode_t,它表示了sysfs中该文件的权限。对于只读属性而言,如果是所有人都可读它,那么该字段设为S_IRUGO;如果只限于所有者可读,则该字段设置为S_IRUSR。同样对于可写属性,可能会设置该字段为S_IRUGO|S_IWUSR。sysfs中的所有文件和目录的uid与gid标志均为零。

  虽然default_attrs列出了默认的属性,sysfs_ops字段则描述了如何使用它们。sysfs_ops字段指向了一个定义于文件<linux/sysfs.h>的同名的结构体。

  show()方法在读操作时被调用。它会拷贝由attr提供的属性值到buffer指定的缓冲区中,缓冲区大小为PAGE_SIZE字节;在x86体系中,PAGE_SIZE为4096字节。该函数如果执行成功,则将返回实际写入buffer的字节数。如果失败,则返回负的错误码。

  store()方法在写操作时调用,它会从buffer中读取size大小的字节,并将其存放如attr表示的属性结构体变量中。缓冲区的大小总是为PAGE_SIZE和更小些。该函数如果执行成功,则将返回实际从buffer中读取的字节数。如果失败,则返回负数的错误码。

  创建新属性

  一些特别情况下会碰到特殊的kobject实例,它希望(甚至必须)有自己的属性——也许是通用属性没包含那些需要的数据或者函数。因此使用sysfs_create_file()接口在默认集合上添加新属性:

  int sysfs_create_file(struct kobject *kobj, const struct attribute *attr);

  这个接口通过attr参数指向相应的attribute结构体,而参数kobj则指定了属性所在的kobject对象。在该函数被调用之前,给定的属性将被赋值,如果成功,该函数返回零。否则返回负的错误码。

  kobject中ktype所对应的sysfs_ops操作将负责处理新属性。现有的show()和store()方法必须能够处理新属性。

  除了添加文件外,可能还要创建符号连接。在sysfs中创建一个符号连接方式:

  int sysfs_create_link(struct kobject *kobj, struct kobject *target, char *name);

  该函数创建的符号连接名由name指定,连接则由kobj对应的目录映射到target指定的目录。如果成功,则返回零,如果失败,返回负的错误码。

  删除新属性

  删除一个属性通过函数sysfs_remove_file()完成:

  void sysfs_remove_file(struct kobject *kobj, const struct attribute *attr);

  一旦调用返回,给定的属性将不复存在于给定的kobject目录中。

  由sysfs_create_link()创建的符号连接可通过函数sysfs_remove_link()删除:

  void sysfs_remove_link(struct kobject *kobj, char *name);

  一旦调用返回,给定的连接将不复存在于给定的kobject目录中。

  sysfs约定

  首先sysfs属性应该保证每个文件只导出一个值,该值应该是文本形式而且被映射为简单C类型。其次,在sysfs中要以一个清晰的层次组织数据。最后,记住sysfs提供内核到用户空间的服务,这多少有些用户空间的ABI(应用程序二进制接口)的作用。

  内核事件层

  内核事件层实现了内核到用户的消息通知系统。事件层借助kobject和sysfs得以实现。内核事件层把事件模拟为信号——从明确的kobjects对象发出,所以每个事件源都是一个sysfs路径。如果请求的时间与你的第一个硬盘相关,那么/sys/block/had便是源树。实际上,在内核中我们认为事件都是从幕后的kobject对象产生的。

  每个事件都被赋予了一个动词或动作字符串表示信号。该字符串会以"被修改过"或”未挂载“等词语来描述事件。

  最后,每个事件都有一个可选的负载。相比传递任意一个表示负载的字符串到用户空间而言,内核事件层使用sysfs属相代表负载。

  实现原理:内核事件由内核空间传递到用户空间需要经过netlink。netlink是一个用于传送网络信息的多点传送套接字。使用netlink意味着从用户空间获取内核事件就如同在套接字上堵塞一样易如反掌。方法就是用户空间实现一个系统后台服务用于监听套接字,处理任何读到的信息,并将事件传送到系统栈里。对于这种用户后台服务来说,一个潜在的目的就是将事件融入D-BUS系统。D-BUS系统已经实现了一套系统范围的消息总线,这种总线可帮助内核如同系统中其他组件一样的发出信号。

  在内核中想用户空间发送信号使用函数kobject_uevent():

  int kobject_uevent(struct kobject *kobj,enum kobject_action action, struct attribute *attr);

  第一个参数指定发送该信号的kobject对象。实际的内核事件将包含该kobject映射到sysfs的路径。

  第二个参数指定了描述该信号的“动作”或“动词”。实际的内核事件将包含一个映射成枚举类型kobject_action的字符串。并不是提供一个字符串,而是利用一个枚举变量来提高可重用性和保证类型安全。

  最后一个参数是指向attribute结构体的可选指针。它可看作为一个事件的“负载”。如果事件值不足以描述该事件,那么事件可以指向一个特殊文件,由此文件提供更详细的信息。

  该函数分配内存,所以可以睡眠。但在内核中实现了原子和非原子操作两个版本,其不同在于原子操作使用GFP_ATOMIC标志分配内存。

  

  在可能的情况下,建议使用标准的非原子接口,原子函数与非原子函数两者的参数和行为是一致的。

  使用kobject和属性不但有利于很好的实现基于sysfs的事件,同时也有利于创建新的kobjects对象和属性来表示新对象和数据——它们尚未出现在sysfs中。

 

  参考自:《Linux Kernel Development》.

posted on 2016-04-05 17:17  画家丶  阅读(572)  评论(0编辑  收藏  举报