内容提要:
1、核心概念
将重复的部分抽象出来构成固定的框架、将变化的部分隔离出来并将其看成是传给固定框架的参数!
2、对核心概念的阐述
抽象重复、构建层次结构
将驱动的编写由问答题变为填空题
把Kernel看成一个大的黑盒函数,DTB作为该函数的参数,以适应外部设备的千变万化
3、核心细节说明
kobject、ktype、kset的关系
device、bus、class对kobject、ktype、kset的使用
4、更进一步
RTFSC
本文的核心思想是:
Linux的统一设备模型,从某个角度来看,其实就是在设备管理功能中,将重复的、具有共性的部分抽象出来构成固定的框架,同时将变化的、具有具体设备特性的部分分离出来,进而将后者看做是参数传递给先前抽象出来的固定框架,最终完成整体的设备管理功能。
接下来,我将分三个角度来阐述上述观点:
1、借鉴面向对象思想,站在某个抽象层次将重复的概念抽象出来,接着提高抽象层次,同时基于上一层的抽象添加某一类变化构成上一层,依次类推就能构建出面向对象中的继承关系,大大减少重复的逻辑和代码,而减少重复可以说是软件设计中的第一原则。进一步,既然不同抽象层次的共性概念已经被抽象出来了,那么要完全描述该抽象层次的某个事物,只需要填充好那个具体事物的特性就好了,而这些特性就可以看作是抽象出来的共性概念的参数。
何出此言?
我们还是从kobject谈起。
kobject可以看成是所有设备的基类,每个设备都含有一个kobject。其实除了device,设备管理模型中的其他概念,如driver、bus、class等等,都与kobject关系密切。那么kobject到底抽象出来一些什么共性呢?
就我的理解来看,主要包括以下三个方面:
引入引用计数,以便于kobject在引用计数为0时能够动态的释放内存资源,那么有了这个功能后,包含kobject的上层概念如device也应该能够动态的释放内存资源;
引入了parent指针,利用该指针就能够将所有包含kobject的东东构造成一棵大树,利用该树形结构就可以方便的遍历系统中所有这些东东,以便对其进行统一管理,如电源管理等;
kobject跟sysfs相结合,给应用空间提供一个基于文件的用户接口,方便应用层跟驱动的沟通与交流,尤其是方便对驱动程序的调试工作;
谈完kobject,我们来谈谈device。device是比kobject更高一层的概念,除了包含kobject所包含的共性,必然还包含一些其他的共性,那么这些其他的共性又是什么呢?device的核心概念就是对应的各种各样的设备,设备千变万化,但必然也存在一些共性,否则就不能都叫做设备了。譬如:设备大都需要有个初始化的操作(如初始化pin脚,申请各种资源等),大都需要一个驱动程序,大都涉及到电源管理功能,大都基于某一种特定的bus等等。
更进一步,我们基于device来谈谈platform_device。
platform_device是在device基础上,又将一类有共性的设备抽象出来,以方便对这一类设备驱动的开发和使用。那么,这一类设备又有什么共性呢?这个共性就是:可以由CPU
bus直接寻址此类设备,这类设备一般都是各个Soc厂商集成在Soc平台上的各种控制器,作为Linux驱动工程师,平常的大部分工作都是驱动platform设备。
以上,从object到device再到platform_device,就是从最底层一层层提高抽象层次构建出来的,每一层的概念都是抽象出一部分共性,只不过下一层的共性总是上一层共性的子集。下一层的共性加上不同的上一层共性,就抽象出不同的上层概念,如:kobject+设备共性就抽象出device,而如果kobjcet+驱动程序的共性就抽象出device_driver;同理,device+platform设备的共性就抽象出platform_device,而如果device+spi设备的共性就抽象出spi_device。
进一步,既然抽象出了device的共性概念,那么要实现某个具体device的驱动,就只需要将该特定设备的特性填充好就可以了,如对device结构中某些回调函数的实现,在此处该回调函数的实现就可以看作是传给device共性概念的一个参数,每个特定设备的该回调函数的实现都不一样,但是该回调函数代表的共性概念是一样的,共性和特性加在一起,就完成了整个逻辑,从这个方面来说,特定设备的特性就可以看作是传个device共性概念的参数,两者加在一起就完整描述了该特定设备。
2、设备的驱动细节千变万化,但其中的工作逻辑又存在很多的共性,有了Linux的设备管理模型,驱动的编写工作就由先前的问答题变为了简单易行的填空题,何乐而不为呢?
bus、device和device_driver的统一设备模型抽象出来的三组核心概念,构成核心的铁三角关系,目的是描述清楚设备模型的核心逻辑,这其中围绕着一个核心的概念模型,即:
在Linux的系统概念中,每个设备都要挂到一个Bus上才能工作,这个Bus可以是真实存在的Bus如i2c、usb等,也可以是Linux为了方便管理而假象出来的虚拟Bus如platform_bus。Bus一肩挑两端,一端是所有的device列表,一端是所有的device_driver列表,每当有新的device新添加到Bus上时,则触发新device和所有已存在device_driver的match动作;同理,若有新的device_driver添加到Bus上时,也会触发新device_driver和所有已存的device的match动作。match的逻辑也很简单,如果device和device_driver同名,则认为match成功,将会触发device_driver的probe函数进行device的初始化。
怎么样,是不是很简单,其实Linux整个驱动模型就是围绕着这个简单的核心概念逻辑来运转的,不同的特定device只需要给定不同的名称和不同的device_driver的probe函数,就能完成该特定device的初始化工作,在这个过程中,其实就是填空——针对该特定device填写正确的名称和能正常初始化该device的probe函数即可。其他功能如释放设备,也一样由设备模型搭好了整个框架,针对特定device要做的只是填写好其中留下来的空,所以说,有了Linux的设备驱动模型,Linux系统下驱动的编写工作就由问答题变为了简单易行的填空题,大大减少了工程师的工作量和难度,稳定性上也有了进一步的保证。
从这个角度来看,也可以说,这些留下来的该填的空正是传个整个驱动模型框架的参数,不是吗?
3、设备树其实可以看成是传给Kernel的参数,以适应外部成千上万的设备,以不变应万变的。
新版本的Linux版本中,引入了DeviceTree的概念,目的是让各个厂商针对各自的Soc能够写出更优雅的代码,减少各个Soc间的重复部分。可以将Device
Tree看成是各个具体硬件平台整体硬件的一个描述表,如该平台有几颗cpu,有多少内存,有多少usb控制器,mmc控制器,各个mmc控制器用了哪些中断资源,哪些寻址空间等等。其实各个平台涉及到的硬件资源都是固定的,无非都是些cpu,memory,中断,DMA等等,不同的是这些资源的细节实现如cpu的个数,memory的空间大小。如果把这些细节以代码的形式写入到Kernel,那么每个细小的改动都需要修改kernel的源码并重新编译出新的镜像,费时费力不说,而且随着硬件平台的增加,kernel的代码不可避免的会变得臃肿不堪,而且其中大部分都是重复部分,这与kernel设计人员追求的减少重复、优雅简洁的目标无疑是相差甚远的。如果有了Device
Tree,用Device
Tree来描述各个硬件平台的硬件资源,在系统启动时由bootloader将Device
Tree作为一个参数传递给Kernel,Kernel根据该Device
Tree来初始化自己的硬件资源,多么优雅的设计啊。
毕竟是技术文章,我们接下来对一些重要细节做一些梳理。
首先是kobject、ktype、kset三者的细节,在此先列出三者数据结构的定义如下(基于kernel版本:3.14.29):
struct kobject {
const char *name;
struct list_head entry;
struct kobject *parent;
struct kset *kset;
struct kobj_type *ktype;
struct kernfs_node *sd;
struct kref kref;
#ifdef
CONFIG_DEBUG_KOBJECT_RELEASE
struct delayed_work release;
#endif
unsigned int state_initialized:1;
unsigned int state_in_sysfs:1;
unsigned int state_add_uevent_sent:1;
unsigned int state_remove_uevent_sent:1;
unsigned int uevent_suppress:1;
};
name:该Kobject的名称,同时也是sysfs中的目录名称。
entry:用于将Kobject加入到Kset中的list_head。
parent:指向parent kobject,以此形成层次结构(在sysfs就表现为目录结构)。
kset:该kobject属于的Kset。可以为NULL。如果存在,且没有指定parent,则会把Kset作为parent(别忘了Kset是一个特殊的Kobject)。
ktype:该Kobject属于的kobj_type。每个Kobject必须有一个ktype,或者Kernel会提示错误。
sd,该Kobject在sysfs中的表示。
kref,"struct
kref”类型(在include/linux/kref.h中定义)的变量,为一个可用于原子操作的引用计数。
state_initialized,指示该Kobject是否已经初始化,以在Kobject的Init,Put,Add等操作时进行异常校验。
state_in_sysfs,指示该Kobject是否已在sysfs中呈现,以便在自动注销时从sysfs中移除。
state_add_uevent_sent/state_remove_uevent_sent,记录是否已经向用户空间发送ADD
uevent,如果有,且没有发送remove
uevent,则在自动注销时,补发REMOVE
uevent,以便让用户空间正确处理。
uevent_suppress,如果该字段为1,则表示忽略所有上报的uevent事件。
list/list_lock,用于保存该kset下所有的kobject的链表。
kobj,该kset自己的kobject(kset是一个特殊的kobject,也会在sysfs中以目录的形式体现)。
uevent_ops,该kset的uevent操作函数集。当任何Kobject需要上报uevent时,都要调用它所从属的kset的uevent_ops,添加环境变量,或者过滤event(kset可以决定哪些event可以上报)。因此,如果一个kobject不属于任何kset时,是不允许发送uevent的。
release,通过该回调函数,可以将包含该种类型kobject的数据结构的内存空间释放掉。
sysfs_ops,该种类型的Kobject的sysfs文件系统接口。
default_attrs,该种类型的Kobject的atrribute列表(所谓attribute,就是sysfs文件系统中的一个文件)。将会在Kobject添加到内核时,一并注册到sysfs中。
child_ns_type/namespace,和文件系统(sysfs)的命名空间有关。
总结,Ktype以及整个Kobject机制的理解。
Kobject是基本数据类型,每个Kobject都会在"/sys/“文件系统中以目录的形式出现。
Ktype代表Kobject(严格地讲,是包含了Kobject的数据结构)的属性操作集合,同时指定该类Kobject释放资源的方式。
Kset是一个特殊的Kobject(因此它也会在"/sys/“文件系统中以目录的形式出现),它用来集合相似的Kobject(这些Kobject可以是相同属性的,也可以不同属性的),同时用来负责该集合中kobject的uevent功能。
Kobject的核心功能是:保持一个引用计数,当该计数减为0时,自动释放(由本文所讲的kobject模块负责)Kobject所占用的meomry空间。这就决定了Kobject必须是动态分配的(只有这样才能动态释放)。
而Kobject大多数的使用场景,是内嵌在大型的数据结构中(如Kset、device_driver等),因此这些大型的数据结构,也必须是动态分配、动态释放的。那么释放的时机是什么呢?是内嵌的Kobject释放时。但是Kobject的释放是由Kobject模块自动完成的(在引用计数为0时),那么怎么一并释放包含自己的大型数据结构呢?
这时Ktype就派上用场了。我们知道,Ktype中的release回调函数负责释放Kobject(甚至是包含Kobject的数据结构)的内存空间,那么Ktype及其内部函数,是由谁实现呢?是由上层数据结构所在的模块!因为只有它,才清楚Kobject嵌在哪个数据结构中,并通过Kobject指针以及自身的数据结构类型,找到需要释放的上层数据结构的指针,然后释放它。
讲到这里,就清晰多了。所以,每一个内嵌Kobject的数据结构,例如kset、device、device_driver等等,都要实现一个Ktype,并定义其中的回调函数。同理,sysfs相关的操作也一样,必须经过ktype的中转,因为sysfs看到的是Kobject,而真正的文件操作的主体,是内嵌Kobject的上层数据结构!
接下来,我们以device为例,来说明对kobject、kset以及ktype的使用,从这个角度来看,device可以看做是对kobject的应用。
先列出device的结构定义如下:
struct device {
struct device
struct kobject kobj;
const char *init_name;
const struct
#ifdef
CONFIG_PINCTRL
#ifdef
CONFIG_NUMA
int numa_node; #endif
u64 *dma_mask; u64 coherent_dma_mask;
struct list_head dma_pools;
struct dma_coherent_mem *dma_mem; #ifdef
CONFIG_DMA_CMA
struct cma *cma_area; #endif
struct
dev_tdevt;u32
id;
spinlock_tdevres_lock;structlist_headdevres_head;
void (*release)(struct
bool offline_disabled:1;
bool
parent,该设备的父设备,一般是该设备所从属的bus、controller等设备。
p,一个用于struct
device的私有数据结构指针,该指针中会保存子设备链表、用于添加到bus/driver/parent等设备中的链表头等等,具体可查看源代码。
kobj,该数据结构对应的struct
kobject。
init_name,该设备的名称。
type,struct
device_type结构是新版本内核新引入的一个结构,它和struct
device关系,非常类似stuct
kobj_type和struct
kobject之间的关系。
bus,该device属于哪个总线。
driver,该device对应的device
driver。
platform_data,一个指针,用于保存具体的平台相关的数据。具体的driver模块,可以将一些私有的数据,暂存在这里,需要使用的时候,再拿出来,因此设备模型并不关心该指针得实际含义。
power、pm_domain,电源管理相关的逻辑。
pins,"PINCTRL”功能。
numa_node,"NUMA”功能。
dma_mask~archdata,DMA相关的功能。
devt,dev_t是一个32位的整数,它由两个部分(Major和Minor)组成,在需要以设备节点的形式(字符设备和块设备)向用户空间提供接口的设备中,当作设备号使用。在这里,该变量主要用于在sys文件系统中,为每个具有设备号的device,创建/sys/dev
struct
该kset会在系统初始化时分配空间并完成初始化(devices_init(void)),在创建每个具体的设备时(device_initialize(structdevice*dev)),会将该设备所含kobject的kset置为devices_kset。从面向对象角度来看,device是基于kobject构建的一个类,而每个具体设备的实现就是device类的实例化。
根据需求定义出device数据结构,该结构包含kobject,详见:
根据需求定义一个ktype,对于device这个ktype就是device_ktype,定义如下:
该ktype指明了device内存的释放方式,以及device对应attribute的存取方式。
在需要使用device时,用device_create分配device结构所需空间(包含kobject),最终会通过调用device_add--->kobject_add将该device所包含的kobject加入到devices_kset中,即添加到内核中。device_create代码详见:
在需要引用device时,调用get_device来增加device所含kobject的引用计数,调用put_device来减少所含kobject的引用计数。代码详见:
当引用计数减少为0时,会调用device_ktype的release函数,即device_release,用来正确释放device所分配的内存资源。
说到这里,我们明白了device是对kobject的应用,对应的数据结构分别是:
kobject
kset
ktype
device
devices_kset
device_ktype
用此种方式使用kobject得到的device,其对应的核心概念就是设备,实际上在这个实现过程中,我们就是利用了kobject层已经实现的一些共性,然后将其中一些该填的空填上,最终就得到了对device的实现。
同理,bus、class也可以从这个角度来说,将其看做是对kobject的一个使用,其对应关系如下:
kobject
kset
ktype
bus_type
bus_kset
bus_ktype
class
class_kset
class_ktype
最后,要想更进一步了解,就是那句老话,Readthe fucking source
code.
如果懒得下载全套代码,其实可以在线浏览,很多网站都可以做这件事情,而且做好了交叉引用的索引,非常方便,还可以选择不同的版本来浏览,如:http://elixir.free-electrons.com/
Linux浩瀚的设计思想中,到处都隐藏着我们编程的珠玑,差别只在于你付出了多少,又想捡到多少。
参考: