原文链接:http://blog.chinaunix.net/uid-20543183-id-1930813.html
源码文件:
目录:
一:前言
二:kobject、kset和ktype
三:kobject、kset和ktype的操作
一:前言
Linux设备模型是一个极其复杂的结构体系,在编写驱动程序的时候,通常不会用到这方面的东西,但是,理解这部份内容,对于我们理解linux设备驱动的结构是大有裨益的。我们不但可以在编写程序程序的时候知其然亦知其所以然,又可以学习到一种极其精致的架构设计方法。由于之前已经详细分析了sysfs
文件系统,所以本节的讨论主要集中在设备模型的底层实现上。上层的接口,如pci
、usb
、网络设备都可以看成是底层的封装。
二:kobject、kset和ktype
kobject
,kset
,ktype
这三个结构是设备模型中的下层架构。模型中的每一个元素都对应一个kobject
,kset
和ktype
可以看成是kobject
在层次结构与属性结构方面的扩充。将三者之间的关系用图的方示描述如下:
如上图所示:我们知道,在sysfs
中每一个目录都对应一个kobject
,这些kobject
都有自己的parent
,在没有指定parent
的情况下,都会指向它所属的kset->object
;其次,kset
也内嵌了kobject
,这个kobject
又可以指它上一级的parent
。就这样,构成了一个空间层次关系。
其实,每个对象都有属性,例如,电源管理,执插拨事性管理等等。因为大部份的同类设备都有相同的属性,因此将这个属性隔离开来,存放在ktype
中。这样就可以灵活的管理了。记得在分析sysfs
的时候,对于sysfs
中的普通文件读写操作都是由kobject->ktype->sysfs_ops
来完成的。
经过上面的分析,我们大概了解了kobject
、kset
与ktype
的大概架构与相互之间的关系。下面我们从linux源代码中的分析来详细研究他们的操作。
三:kobject、kset和ktype的操作
为了说明kobject
的操作,先写一个测试模块,代码如下:
加载模块之后,会发现,在/sys
下多了一个eric_test
目录。该目录下有一个叫eric_xiao
的文件。如下所示:
用cat
察看此文件:
再用echo
往里面写点东西;
Dmesg的输出如下:
如上所示,我们看到了kobject
的大概建立过程。
kobject 操作
我们来看一下kobject_init_and_add()
的实现,在这个函数里,包含了对kobject
的大部份操作。
上面的流程主要分为两部份:一部份是kobject
的初始化,在这一部份,它将kobject
与给定的ktype
关联起来,初始化kobject
中的各项结构;另一部份是kobject
的名称设置,空间层次关系的设置,具体表现在sysfs
文件系统中。
对于第一部份,代码比较简单,这里不再赘述。跟踪第二部份,也就是kobject_add_varg()
的实现。
设置好kobject->name
后,转入kobject_add_internal()
,在sysfs中创建空间结构。代码如下:
这段代码比较简单,它主要完成kobject
父结点的判断和选定,然后再调用create_dir()
在sysfs
创建相关信息。该函数代码如下:
我们在上面的示例中看到的/sys
下的eric_test
目录,以及该目录下面的eric_xiao
的这个文件就是这里被创建的。我们先看一下kobject
所表示的目录创建过程,这是在sysfs_create_dir()
中完成的。代码如下:
在这里,我们就要联系之前分析过的sysfs文件系统的研究了。如果不太清楚的,可以再找到那篇文章仔细的研读一下。create_dir()
就是在sysfs
中创建目录的接口,在之前已经详细分析过了。这里不再讲述。
接着看为kobject->ktype
中的属性创建文件,这是在populate_dir()
中完成的。代码如下:
这段代码比较简单,它遍历ktype
中的属性,然后为其建立文件。请注意:文件的操作最后都会回溯到ktype->sysfs_ops
的show
和store
这两个函数中。
kobject
的创建已经分析完了,接着分析怎么将一个kobject
注销掉。注意过程是在kobject_del()
中完成的。代码如下:
该函数会将在sysfs
中的kobject
对应的目录删除。请注意,属性文件是建立在这个目录下面的,只需要将这个目录删除,属性文件也随之删除。
最后,减少相关的引用计数,如果kobject
的引用计数为零。则将其所占空间释放.
kset 操作
kset
的操作与kobject
类似,因为kset
中内嵌了一个kobject
结构,所以,大部份操作都是集中在kset->kobject
上。具体分析一下kset_create_and_add()
这个接口,类似上面分析的kobject
接口,这个接口也包括了kset
的大部分操作。代码如下:
kset_create()
用来创建一个struct kset
结构。代码如下:
我们注意,在这里创建kset
时,为其内嵌的kobject
指定其ktype
结构为kset_ktype
。这个结构的定义如下:
属性文件的读写操作全部都包含在sysfs_ops
成员里,kobj_sysfs_ops
的定义如下:
show
,store
成员对应的函数代码如下所示:
从上面的代码看以看出,会将struct attribute
结构转换为struct kobj_attribte
结构,也就是说struct kobj_attribte
内嵌了一个struct attribute
。实际上,这是和宏__ATTR
配合在一起使用的,经常用于group
中,在这里并不打算研究group
,原理都是一样的,这里列出来只是做个说明而已。
创建好了kset
之后,会调用kset_register()
,这个函数就是kset
操作的核心代码了。如下:
在kset_init()
里会初始化kset
中的其它字段,然后调用kobject_add_internal()
为其内嵌的kobject
结构建立空间层次结构,之后因为添加了kset
,会产生一个事件,这个事件是通过用户空间的hotplug
程序处理的,这就是kset
明显不同于kobject
的地方。详细研究一下这个函数,这对于我们研究hotplug
的深层机理是很有帮助的,它的代码如下:
之后,会调用kobject_uevent_env()
。这个函数中的三个参数含义分别为:引起事件的kobject
,事件类型(add
,remove
,change
,move
,online
,offline
等),第三个参数是要添加的环境变量。
代码篇幅较长,我们效仿情景分析的做法,分段分析如下:
因为对事件的处理函数包含在kobject->kset->uevent_ops
中,要处理事件,就必须要找到上层的一个不为空的kset
。上面的代码就是顺着kobject->parent
找不到一个不为空的kset
,如果不存在这样的kset
,就退出
找到了不为空的kset
,就跟kset->uevent_ops->filter()
匹配,看这个事件是否被过滤。如果没有被过滤掉,就会调用kset->uevent_ops->name()
得到子系统的名称。如果不存在kset->uevent_ops->name()
,就会以kobject->name
做为子系统名称。
接下来,就应该设置为调用hotplug
设置环境变量了。首先,分配一个struct kobj_uevent_env
结构用来存放环境变量的值;然后调用kobject_get_path()
用来获得引起事件的kobject
在sysfs
中的路径;再调用add_uevent_var()
将动作代表的字串、kobject
路径、子系统名称填充到struct kobj_uevent_env
中。如果有指定环境变量,也将其添加进去。 kobject_get_path()
和add_uevent_var()
都比较简单.这里不再详细分析了.请自行查看源代码
在这里还会调用kobject->kset->uevent_ops->uevent()
,让产生事件的kobject
添加环境变量,最后将事件序列添加到环境变量中去。
忽略一段选择编译的代码,再后就是调用用户空间的hotplug
了。添加最后两个环境变量:HOME
和PATH
。然后调用hotplug
,以子系统名称为参数。
现在我们终于知道hotplug
处理程序中的参数和环境变量是怎么来的了.^_^
使用完了kset
,再调用kset_unregister()
将其注销。这个函数很简单,请自行查阅代码.
为了印证一下上面的分析,写一个测试模块。如下:
在这里,定义并注册了二个kset
,第二个kset
的kobj->kset
域指向第一个kset
。这样,当第二个kset
注册或者卸载的时候就会调用第一个kset
中的uevent_ops
的相关操作.kset_p.uevent_ops->filter
函数中,使其返回1
.使其匹配成功。
在kset_p.uevent_ops->name
中,使其返回的子系统名为引起事件的kobject
的名称,即:kset_c
.
最后在kset_p.uevent_ops->uevent
中将环境变量全部打印出来。
下面是dmesg
的输出结果:
输出结果跟我们的分析是吻合的,在这里,值得我们注意的是:注册一个kobject
不会产生事件,只有注册kset
才会。
四:bus、device和device_driver
上面分析了kobject
、kset
、ktype
,这三个结构联合起来一起构成了整个设备模型的基石。而bus
、device
、device_driver
,则是基于kobject
、kset
、ktype
之上的架构。在这里,总线、设备、驱动被有序的组合在一起。bus
、device
、device_driver
三者之间的关系如下图所示:
如上图所示,struct bus_type
的p->drivers_kset
指向注册在上面的驱动程序,它的p->device_kset
上挂着注册在上面的设备。每次有一个新的设备注册到上面,都会去匹配右边的驱动,看是否能匹配上。如果匹配成功,则将设备结构的is_registerd
域置为0
,然后将设备添加到驱动的p->klist_devices
域。同理,每注册一个驱动,都会去匹配左边的设备。如果匹配成功,将则设备加到驱动的p->klist_devices
域,再将设备的is_registerd
置为0
。
这就是linux设备模型用来管理设备和驱动的基本架构,我们来跟踪一下代码来看下详细的操作。
总线注册
注册一个总线的接口为bus_register()
,我们照例分段分析:
首先,先为struct bus_type
的私有区分配空间,然后将其和struct bus_type
关联起来。由于struct bus_type
也要在sysfs
文件中表示一个节点,因此,它也内嵌一个kset
的结构,这就是priv->subsys
。
首先,它为这个kset
的名称赋值为bus
的名称,然后将priv->subsys.kobj.kset
指向bus_kset
,priv->subsys.kobj.ktype
指向bus_ktype
;然后调用kset_reqister()
将priv->subsys
注册。这里涉及到的接口都在之前分析过,注册过后,应该会在bus_kset
所表示的目录下创建一个总线名称的目录,并且用户空间的hotplug
应该会检测到一个add
事件。我们来看一下bus_kset
到底指向的是什么:
从此可以看出,这个bus_kset
在sysfs
中的结点就是/sys/bus
,在这里注册的struct bus_types
就会在/sys/bus/
下面出现。
bus_create_file()
就是在priv->subsys.kobj
的这个kobject
上建立一个普通属性的文件,这个文件的属性对应在bus_attr_uevent
,读写操作对应在priv->subsys.ktype
中,我们到后面才统一分析bus
注册时候的文件创建。
这段代码会在bus
所在的目录下建立两个目录,分别为devices
和drivers
,并初始化挂载设备和驱动的链表。
在这里,会为bus_attr_drivers_probe
, bus_attr_drivers_autoprobe
.注册bus_type
中的属性建立文件
这段代码为出错处理
这段代码中比较繁锁的就是bus_type
对应目录下的属性文件建立,为了直观的说明,将属性文件的建立统一放到一起分析。从上面的代码中可以看,创建属性文件对应的属性分别为:bus_attr_uevent
、bus_attr_drivers_probe
、bus_attr_drivers_autoprobe
。
分别定义如下:
BUS_ATTR
定义如下:
由此可见.上面这三个属性对应的名称为别为uevent
、drivers_probe
、drivers_autoprobe
。也就是说,会在bus_types
目录下生成三个文件,分别为uevent
、probe
、autoprobe
。
根据之前的分析,我们知道在sysfs
文件系统中,对普通属性文件的读写都会回溯到kobject->ktype->sysfs_ops
中.在这里,注意到有:
显然,读写操作就回溯到了bus_ktype
中.定义如下:
show
和store
函数对应的代码为:
从代码可以看出.读写操作又会回溯到bus_attribute
中的show
和store
中.在自定义结构里嵌入struct attribute
,.然后再操作回溯到自定义结构中,这是一种比较高明的架构设计手法.
闲言少叙.我们对应看一下上面三个文件对应的最终操作:uevent
对应的读写操作为:NULL
、bus_uevent_store
。对于这个文件没有读操作,只有写操作,用cat
命令去查看这个文件的时候,可能会返回“设备不存在”的错误。bus_uevent_store()
代码如下:
从这里可以看到,可以在用户空间控制事件的发生,如echo add > event
就会产生一个add
的事件。
probe
文件对应的读写操作为:NULL
、store_drivers_probe
。 store_drivers_probe()
这个函数的代码涉及到struct device
,等分析完struct device
可以自行回过来看下这个函数的实现。实际上,这个函数是将用户输入的设备名称对应的设备与驱动匹配一次。
autoprobe
文件对应的读写操作为show_drivers_autoprobe
, store_drivers_autoprobe
.对应读的代码为:
它将总线对应的drivers_autoprobe
的值输出到用户空间,这个值为1
时,自动将驱动与设备进行匹配,否则,反之。
写操作的代码如下:
写操作就会改变bus->p->drivers_autoprobe
的值,就这样,通过sysfs
就可以控制总线是否要进行自动匹配了。
从这里也可以看出,内核开发者的思维是何等的灵活。我们从sysfs
中找个例子来印证一下:
用ls命令查看:
与上面分析的相吻合
设备注册
设备的注册接口为: device_register()
.
device_initialize()
中有几个很重要的操作,如下:
在这里,它为device
的内嵌kobject
指定了ktype
和kset
。device_kset
的值如下:
即对应sysfs
中的/sys/devices
。device_ktype
中对属性的读写操作同bus
中的类似,被回溯到了struct device_attribute
中的show
和store
。
接着往下看device_add()
的实现.这个函数比较长,分段分析如下:
如果注册device
的时候,没有指定父结点,在kobject_add
将会在/sys/device/
下建立相同名称的目录
忽略notify
部份,这部份不会影响本函数的流程
建立属性为uevent_attr
的属性文件,如果device
中指定了设备号,则建立属性为devt_attr
的属性文件
在这里,不打算讨论class
的部份,dpm
、pm
是选择编译部份,不讨论.device_add_attrs
中涉及到了group
的部分,暂不讨论
bus_add_device()
在对应总线代表目录的device
目录下创建几个到device
的链接,然后调用kobject_uevent()
产生一个add
事件,再调用bus_attach_device()
去匹配已经注册到总线的驱动程序。全部做完之后,将设备挂到父结点的子链表。
出错处理部份.
bus_attach_device()
是一个很重要的函数。它将设备自动与挂在总线上面的驱动进行匹配。代码如下:
从上面的代码我们可以看出。只有在bus->p->drivers_autoprobe
为1
的情况下,才会去自己匹配。这也就是bus
目录下的drivers_probe
文件的作用.然后,将设备挂到总线的设备链表。device_attach()
代码如下:
对于设备自己已经指定驱动的情况,只需要将其直接和驱动绑定即可。如果没有指定驱动,就匹配总线之上的驱动,这是在
完成的。代码如下:
很明显,这个函数就是遍历总线之上的驱动。每遍历一个驱动就调用一次回调函数进行判断,如果回调函数返回不为0
,就说明匹配已经成功了,不需要再匹配剩余的,退出。在这里调用的回调函数是__device_attach()
,在这里,完成了设备与驱动匹配的最核心的动作。代码如下:
转到driver_probe_device()
:
如果设备没有注册到总线之上,即dev->is_registered
不为1
, 就直接返回。然后,再调用总线的match()
函数进行匹配。如果match()
函数返回0
,说明匹配失败,那退出此函数。如果match
函数返回1
,说明初步的检查已经通过了,可以进入really_probe()
再进行细致的检查。如果匹配成功,这个函数会返回1
。此函数比较长而且比较重要,分段列出代码:
先假设驱动和设备是匹配的,为设备结构设置驱动成员,使其指向匹配的驱动,然后再调用driver_sysfs_add()
建立几个符号链接。这几个链接分别为:
1、在驱动目录下建立一个到设备的同名链接;
2、在设备目录下建立一个名为driver到驱动的链接。
然后,再调用总线的probe
函数,如果总线的此函数不存在,就会调用驱动的probe
函数。如果匹配成功,返回0
;如果不成功,就会跳转到probe_failed
。
到这里,设备和驱动已经匹配成功,调用driver_bound()
将其关联起来,在这个函数里会将设备加至驱动的设备链表,这在我们之前分析bus
、device
、driver
中分析到的。相关的代码如下示:
至此,这个匹配过程已经圆满结束了,返回1
这里是匹配不成功的处理,在这里,删除之前建立的几个链接文件,然后将设备的driver
域置空。
从上面的分析可以看到,对应创建的属性文件分别为:uevent_attr
, devt_attr
。它们的定义如下:
uevent_attr
对应的读写函数分别为:show_uevent
、store_uevent
。先分析读操作。它的代码如下:
从代码可以看出,这里会显示出由设备对应的kset
,也就是由devices_kset
所产生的环境变量。例如,在shell中输入如下指令:
输出结果如下:
这就是由devices_kset
所添加的环境变量
写操作对应的代码如下:
从上面的代码可以看出,这个文件的作用是输入一个字符字串,如果字符不合法,就会默认产生一个add
事件。
devt_attr
对应的读写函数为show_dev
、NULL
。写函数为空,也就是说这个属性文件不允许写,只允许读。读操作的代码如下示:
也就是说,会将设备号显示出来.
驱动注册
分析完了bus
、device
,再接着分析driver
。这里我们要分析的最后一个元素了,耐着性子往下看,快要完了^_^
驱动注册的接口为:driver_register()
。代码如下:
如果设备与总线定义了相同的成员的函数,内核是优先使用bus
中定义的,这一点我们在分析device
注册的时候已经分析过。所以,这里打印出警告信息,用来提醒代码编写者。在这里,忽略有关group
的东西,剩余的便只剩下bus_add_driver()
。代码如下:
初始化驱动的driver_private
域,使其内嵌的kobject
的kset
指bus中的drivers_kset
,这样,这个内嵌的kobject
所生成的目录就会存在于bus
对应目录的driver
目录之下。这里还要注意的是,为内嵌kobject
指定的ktype
是driver_ktype
,属性文件的读写操作都回回溯到struct driver_attribute
中,这在之后再分析。
如果总线允许自动进行匹配,就会调用driver_attach()
进行这个自己匹配过程。这个函数跟我们在上面分析的device
自动匹配过程是一样的,请自行分析。最后,将驱动挂到bus
对应的驱动链表。
生成一个属性为driver_attr_uevent
的属性文件
为bus
中的driver
属性生成属性文件
生成属性为driver_attr_unbind
和driver_attr_bind
的属性文件
生成一个add
事件
总的来说,这个函数比较简单,其中涉及到的子函数大部份都在之前分析过。我们接下来分析一下,它所创建的几个属性文件的含义。
如上所述,在这里会创建三个属性文件,对应属性分别为:driver_attr_uevent
,driver_attr_unbind
,driver_attr_bind
。这几个属性的定义如下:
DRIVER_ATTR
宏的定义如下:
对于driver_attr_uevent
,它的读写函数分别为:NULL
,driver_uevent_store
。也就是说这个文件只允许写,不允许读操作。写操作的代码如下示:
很明显,这是一个手动产生事件的过程。用户可间可以写事件到这个文件来产生事件。
对于driver_unbind
,它的读写函数分别为:NULL
,driver_unbind
。这个文件也是不允许读的,写操作代码如下:
从上面的代码可以看出,写入文件的是一个设备名称,这个函数对应操作是将这个设备与驱动的绑定分离开来。
driver_attr_bind
属性对应的读写函数分别为:NULL
,driver_attr_bind
。 即也是不允许写的。从字面意思和上面分析的driver_attr_unbind
操作代码来看,这个属性对应的写函数应该是将写入的设备文件与此驱动绑定起来。我们来看下代码,以证实我们的猜测。代码如下:
果然,和我们猜测的是一样的。
五:小结
在这一节里,分析了设备模型中的最底层的元素和他们之间的关系,也分析了它们建立的几个属性文件的含义。到这里,我们已经可以自己写驱动架构代码了 ^_^