Linux Device Driver Part I. kobject/ktype/kset & sysfs
文中代码基于Linux 5.1 rc6版本
Introduction to kobject
Linux Device Model的基本构成元素是kobject,通常每个kobject对应于sysfs(/sys
)中的一个目录,其定义如下:
1 | struct kobject { |
首先,kobject提供引用计数功能(通过kobj->kref
),用户应使用kobject_get
/kobject_put
获取、释放kobject的引用,当引用计数降到0时,则会调用kobj->ktype->release
进行析构工作(注意析构函数定义在ktype
中,因此kobject必须设置其ktype
)。
使用kobject,实际上就是将其初始化并加入sysfs的过程:
- 通过
kobject_init(kobj, ktype)
初始化kobj
对象,此时其引用计数为1,kobj->ktype
设置为传入的ktype
- 通过
kobjct_add(kobj, parent, fmt, ...)
,将kobject加入sysfs- 其在sysfs中的目录名称为
kobj->name
,通过类似于printk(fmt, ...)
的方式设置 - 其父目录如下决定:
- 若传入的
parent
非空,则令kobj->parent = parent
,此时其父目录为parent
对应的目录 - 否则,若
kobj->kset
非空,则令kobj->parent = kobj->kset->kobj
,此时其父目录为kobj->kset
对应的目录 - 两者都不满足,则令
kobj->parent = NULL
,并将其置于/sys
根目录下
- 若传入的
- 其在sysfs中的目录名称为
也可以通过kobject_init_and_add(kobj, ktype, parent, fmt, ...)
一次性完成上述两步操作。
反之,可以通过kobject_del
将kobject目录从sysfs中移除,不过一般不需要手动调用,当引用计数降到0时系统会自动调用kobject_del
。
ktype
上面介绍了如何将kobject初始化并注册为sysfs中的目录,但没有解释kobject目录下的文件从何而来。实际上,它们来自ktype中的属性(attribute),每一个属性就相当于kobject目录下的一个文件。
ktype(struct kobj_type
)的定义如下:
1 | struct kobj_type { |
其中default_attrs
是一个struct attribute*
的数组,代表kobject目录下的文件,每个属性有其文件名和权限。此外,我们还可以使用sysfs_create_file
/sysfs_remove_file
动态地添加、删除新的属性:
1 | struct attribute { |
只读属性通常权限设置为S_IRUGO
即0444,可读写属性通常权限设置为S_IRUGO | S_IWUSR
即0644。
我们可以通过ktype的sysfs_ops
提供对属性的访问,只要实现相应的回调函数即可,sysfs会自动为我们实现open、read、write等系统调用:
1 | struct sysfs_ops { |
为了在show
、store
中区分一个kobject下的不同属性,通常会将struct attribute
嵌入在其他struct中(例如struct device_attribute
),然后在该struct中提供show
、store
函数。这样,我们就可以直接在每个属性上定义回调,而不必在ktype的回调中用一个大switch来实现每个属性。
sysfs的发明就是为了改善procfs中混乱的文件结构和内容,因此每个属性的内容应当只有一行,通常只是一个数值或一个字符串(理论上你也可以不这么做,但并不推荐违反Linux社区的约定)。对于要传输二进制内容的特殊情况,sysfs提供了struct bin_attribute
:
1 | struct bin_attribute { |
我们可以通过sysfs_create_bin_file
/sysfs_remove_bin_file
动态地添加或删除二进制属性,但不能静态地设置,因为静态设置的属性实际上在kobject_add
内部是通过sysfs_create_file
注册的。
这里的show
, store
, read
, write
传入的buffer都只有4K大小,故普通属性最大不能超过4K,二进制属性一次只能读写4K,若文件很大要分多次读写。此外对于二进制属性,上层的驱动要自己设法判断文件已经写入完毕,不会再有下一次4K写入,sysfs层不会给予任何提示。
另外,我们还可以通过sysfs_create_link(kobj, target, name)
,在kobj
目录下创建一个symbolic link,指向target
这个kobject的目录。
sysfs还提供了attribute group功能,实际上就是将一组属性打包成一个struct attribute_group
一次性注册及注销:
1 | struct attribute_group { |
attribute group的特殊之处在于,若为其设置了name
,则该group中的属性都会出现在名为name
的子目录下,而不是作为kobject目录下的文件出现。sysfs提供了sysfs_create_group
、sysfs_update_group
、sysfs_remove_group
、sysfs_add_file_to_group
等函数用于操作attribute group,有兴趣可以查阅include/linux/sysfs.h
查看完整列表。
根据Greg Kroah-Hartman的Presentation,我们应该尽量使用Attribute Group,不要使用单独的Attribute。
kset & uevent
通常kobject都不单独使用,而是嵌入在其他struct中使用,这是一种面向对象的思想,kobject相当于基类的作用。kset就是一种特殊的kobject,它负责容纳一组kobject,以链表的形式组织,其定义如下:
1 | struct kset { |
处于同一个kset中的kobject,其kset
成员都会指向该kset,从而在kobject_add
注册sysfs目录时,kobject的父目录会设置为kset的内嵌kobject(kset->kobj
)。当然,也可以为kobject同时设置parent
和kset
,从而使其父目录不是kset的目录。
我们可以用kset_init
初始化kset,然后用kset_register
将kset注册到sysfs,用kset_unregister
则可以将kset从sysfs中移除。若要将kobject加入kset,或自kset中移除,则分别可以调用kobject_add
和kobject_del
。
kset的作用不仅仅是提供一个kobject的容器,它的根本用途是管理向用户态发送的uevent通知,用户态程序根据uevent可以动态地在/dev
下挂载或移除(即插即用)设备,或是进行其他更复杂的操作。实际上,所有kobject产生的uevent都要经过kset汇总才能发给用户态,如果kobject的祖先节点中没有kset则无法发送uevent。
uevent消息的格式形如NAME1=value1, NAME2=value2, ...
。内核中定义了8种uevent类型,不同的类型对应于不同的ACTION
,例如KOBJ_ADD
对应于ACTION=add
:
1 | enum kobject_action { |
我们可以使用kobject_uevent(kobj, action)
或者kobject_uevent_env(kobj, action, envp)
来发送一个uevent,前者实际上是用后者实现的。例如,在将某设备的kobject注册进sysfs后,就应该调用kobject_uevent(kobj, KOBJ_ADD)
通知用户态该设备已经注册。envp
是一个环境变量数组,用于提供NAME=value
数组,这会被加入到uevent消息中。
kset中的uevent_ops
定义如下:
1 | struct kset_uevent_ops { |
考察kobject_uevent_env
函数,可以发现uevent_ops->filter
的用途是将部分kobject发送的uevent过滤掉:
1 | /* skip the event, if the filter returns zero. */ |
而uevent_ops->name
的用途是将SUBSYSTEM=$name
加入uevent消息,其中name = uevent_ops->name(kset, kobj)
:
1 | /* originating subsystem */ |
最后uevent_ops->uevent
的用途是直接修改构造到一半的uevent消息:
1 | struct kobj_uevent_env *env; |
构造完uevent消息后,有两种方式通知用户态,一种是通过netlink socket将uevent消息广播,另一种是通过直接调用一个用户态程序,将uevent消息作为环境变量传给该程序。
现在主流采用的是netlink方式,对应的用户态程序为udev,它目前已经成为了systemd的一部分。内核的调用链为kobject_uevent_env --> kobject_uevent_net_broadcast --> uevent_net_broadcast_untagged/uevent_net_broadcast_tagged --> netlink_broadcast
,最终使用netlink_broadcast
进行全局广播。内核使用的socket采用NETLINK_KOBJECT_UEVENT
协议,用户态只需要通过socket(AF_NETLINK, *, NETLINK_KOBJECT_UEVENT)
创建socket,即可用此socket接收到内核发出的uevent广播。
另一种方式是通过kobject_uevent_env --> call_usermodehelper_exec
调用用户态程序uevent_helper
,其默认值为CONFIG_UEVENT_HELPER_PATH
,在编译时指定,一般为/sbin/hotplug
,即udev的上一代解决方案hotplug。在嵌入式系统中,常使用mdev取代udev,也采用这种方式传递uevent,其路径为/sbin/mdev
。在kernel运行时,还可以通过sys/kernel/uevent_helper
动态修改uevent_helper
的取值。
Internals
Dir Creation
我们知道namespace是Linux用于实现容器的功能,可以提供名字空间的隔离,例如使两个进程看到不同的进程空间。sysfs必须是namespace aware的,这样才能确保/sys/class/net/eth0
这样的目录对于两个处在不同的network namespace中的进程,会呈现不同的内容。因此,在sysfs中创建目录、文件时,实际上要带上namespace作为参数,这样我们可以为同一个路径注册多次,每次使用不同的namespace。
考察kobject_add --> kobject_add_varg --> kobject_add_internal --> create_dir
,这是创建目录及属性文件的核心函数:
1 | static int create_dir(struct kobject *kobj) |
我们首先试图找到kobj
对应的namespace:
1 | /** |
通常,kobj_ns_ops(kobj)
会返回NULL,于是kobj
没有对应的namespace。若kobj->parent->ktype->child_ns_type
非空,则kobj_ns_ops(kobj)
的返回值为kobj->parent->ktype->child_ns_type(kobj->parent)
,这种情况代表父目录有对应的namespace,自然子目录也应该和同一个namespace绑定。
然后,我们调用sysfs_create_dir_ns
创建kobject对应的目录:
1 | struct kernfs_node *parent, *kn; |
可以发现,真正的工作实际上是在kernfs这一层完成,而sysfs只是kernfs的一个wrapper,它负责的逻辑仅仅是在kobj
没有父节点时,将其注册在/sys
根目录。实际上此前sysfs是一个完整的文件系统实现,到内核3.14版本时,将其核心逻辑抽取出来形成了kernfs,以便复用代码,今后要实现类似的虚拟文件系统时就可以直接利用kernfs快速方便地实现。
create_dir
的下一步工作是调用populate_dir
,将默认属性注册为sysfs中的文件:
1 | /* |
最后,我们检查kobj
是否支持namespace,即kobj->ktype->child_ns_type(kobj)
是否返回有效值,若支持则为该目录启用sysfs的namespace支持。
File Creation
创建目录的过程并不复杂,下面继续考察创建文件的过程,即sysfs_create_file
和sysfs_create_bin_file
,它们最终都会调用到sysfs_add_file_mode_ns
,这个函数实现了创建文件的核心功能:
1 | int sysfs_add_file_mode_ns(struct kernfs_node *parent, |
它最终又是调用了__kernfs_create_file
来创建文件。
1 | kn = __kernfs_create_file(parent, attr->name, mode & 0777, uid, gid, |
注意这里传入的ops
是kernfs层的struct kernfs_ops
,根据是否是二进制属性以及读写权限等,会使用不同的kernfs_ops
:
1 | if (!is_bin) { |
这些回调最终就会调用ktype中的sysfs_ops
回调,以对普通属性的写入为例:
1 | /* kernfs write callback for regular sysfs files */ |
目光回到kernfs,在__kernfs_create_file
中,传入的ops
最终被设置到了kn->attr.ops
:
1 | kn->attr.ops = ops; |
考察kernfs注册的file_operations
:
1 | const struct file_operations kernfs_file_fops = { |
我们还是以写入为例,kernfs_fop_write
中有如下片段:
1 | mutex_lock(&of->mutex); |
其中kernfs_ops
定义如下:
1 | static const struct kernfs_ops *kernfs_ops(struct kernfs_node *kn) |
至此整个调用链已经很清晰了,用户程序访问sysfs时,首先进入kernfs层的file_operations
回调,然后通过kn->attr.ops
调用到sysfs层提供的回调,最终调用到ktype
或bin_attribute
提供的回调。