设备模型
1.kobject的全称为kernel object,即内核对象,每一个kobject都会对应到系统/sys/下的一个目录,这些目录的子目录也是一个kobject,以此类推,这些kobject构成树状关系,如下图:
kobject定义在内核源码目录下的/include/linux/kobject.h文件中,如下图:
kset是一组kobject的集合,kset将多个kobject链接了起来,kset同样定义在/include/linux/kobject.h文件中,如下图:
kset通过list成员将多个kobject的entry成员链接起来,形成一个kobject集合(可参考讯为Linux驱动视频第九期P2)。
2.与kobject有关的函数如下(均定义在内核源码目录下的/lib/kobkect.c中):
- struct kobject *kobject_create_and_add(const char *name, struct kobject *parent):该函数用于动态创建一个kobject。其中name为要创建的kobject的名字;parent为创建的kobject的父kobject,传入NULL表示直接在/sys/目录下创建kobject。创建成功会返回指向新kobject的指针,并将该kobject加入到系统/sys/下的层级目录中。
- int kobject_init_and_add(struct kobject *kobj, struct kobj_type *ktype, struct kobject *parent, const char *fmt, ...):该函数用于初始化一个kobject(静态创建)。其中kobj为要初始化的kobject;ktype为关联的kobj_type,定义了kobject的行为(如属性、释放函数等),这里的ktype不能传入NULL;parent为要初始化的kobject的父kobject,传入NULL表示直接在/sys/目录下创建kobject,但如果kobject属于某个kset,传入NULL会在kobject->kset指向的kset对应的目录下创建;fmt为可变参数,用来指定kobject的名字。初始化成功会返回0,并将该kobject加入到系统/sys/下的层级目录中。
- void kobject_put(struct kobject *kobj):该函数用于递减kobject的引用计数,当计数归零时,触发ktype->release释放kobject资源。
这几个函数的具体使用示例如下:
3.与kset有关的函数如下(均定义在内核源码目录下的/lib/kobkect.c中):
- struct kset *kset_create_and_add(const char *name, const struct kset_uevent_ops *uevent_ops, struct kobject *parent_kobj):该函数用于创建一个kset并初始化。其中name为要创建的kset的名字;uevent_ops用于定义kset的事件操作(如filter、name、uevent回调函数);parent为创建的kset的父kobject,传入NULL表示直接在/sys/目录下创建kset。创建成功会返回指向新kset的指针,并将该kset加入到系统/sys/下的层级目录中。
- void kset_unregister(struct kset *k):该函数用于注销先前通过kset_create_and_add()或类似函数创建的kset,并清理相关资源。其中k为要移除的kset。
这几个函数的具体使用示例如下:
4.为了做好设备驱动的管理,并降低驱动开发难度,兼容设备的热拔插和电源管理。Linux对硬件设备进行了分类和归纳,并抽象出来了一套标准的数据结构和接口,这个就是设备模型。设备模型包含总线,设备,驱动和类四个概念:
- 总线:总线是CPU和设备进行信息交互的通道,所有的设备都要连接到总线上,总线包含虚拟总线和外设总线。在设备模型中,使用bus_type来描述总线,stuct bus_type结构体定义在内核源码目录下的/include/linux/device/bus.h中,如下图所示(只截取了部分):
- 设备:将系统中所有硬件设备的共同属性,比如名字,属性,从属关系,类等信息抽象出来就是设备。在设备模型中,使用device来描述设备。struct device结构体定义在内核源码目录下的/include/linux/device.h中,如下图所示(只截取了部分):
- 类:具有相似功能或者属性的设备。在设备模型中,使用class来描述类。struct class结构体定义在内核源码目录下的/include/linux/device/class.h中,如下图所示(只截取了部分):
- 驱动:硬件设备对应的驱动程序。在设备模型中,使用device_driver来描述驱动。stuct device_driver结构体定义在内核源码目录下的/include/linux/device/driver.h中,如下图所示(只截取了部分):
设备控制器(如GPIO控制器、LCD控制器等)直接和CPU连接,CPU可以通过寻址操作访问它们,但设备模型应该具备普适性,因此Linux就虚构了一条patform总线,供这些设备链接。
5.sysfs文件系统是Linux2.6版本引入的虚拟文件系统,sysfs把连接在系统上的设备模型组织成为一个分级的层次视图,并且可以向用户空间导出内核数据结构以及属性。kobject和kset是设备模型的基本框架,在使用kobiect的时候,一般不会单独使用,要嵌入到一个数据结构中,这样就可以把高级对象接入到设备模型里面,例如下面字符设备的结构体:
所以可以把总线、设备等看作是kobject的派生类,kobject是设备模型的基石,一个kobject对应/sys/目录下一个目录。追踪kobject创建的函数调用(下述前面几个函数均定义在内核源码目录下的/lib/kobject.c文件中,最后一个除外),动态创建:kobject_create_and_add->kobject_add->kobject_add_varg->kobject_add_internal->create_dir->sysfs_create_dir_ns(/fs/sysfs/dir.c)或静态创建:kobject_init_and_add->kobject_add_varg->kobject_add_internal->create_dir->sysfs_create_dir_ns(/fs/sysfs/dir.c),其中在sysfs_create_dir_ns函数中会判断传入的parent节点是否为NULL,若为NULL则将其parent指向sysfs_root_kn(代表/sys/对应的节点,全局变量sysfs_root_kn会在内核源码目录下的/fs/sysfs/mount.c文件中的sysfs_init函数中被初始化),所以当创建kobject的时候,若传入parent节点为NULL,会在系统根目录/sys/目录下创建。如下图:
通过查看上述几个函数的具体实现,可知在创建kobject时,有以下规则(可参考讯为Linux驱动视频第九期P5):
- kobject无父目录(parent传入NULL),无kset,则将在sysfs的根目录(即/sys/)下创建目录
- kobject无父目录(parent传入NULL),有kset,则将在kset对应的目录下下创建目录,并将kobject加入kset.list
- kobject有父目录,无kset,则将在parent对应的目录下创建目录
- kobject有父目录,有kset,则将在parent对应的目录下创建目录,并将kobject加入kset.list
6./sys/目录下与设备模型有关的文件夹为/sys/devices/、/sys/bus/、/sys/class/。/sys/devices/目录下是连接到总线的全部设备,从设备级联角度进行展示;/sys/bus/目录下是Linux系统支持并且已经注册的总线,从总线这个角度展示现在有哪些总线以及总线下连接了什么设备和驱动,这下面的所有设备都是/sys/devices/下的设备的软连接;在/sys/class/对设备进行归类,类下的所有设备都是/sys/devices/下的设备的软连接(可参考讯为Linux驱动视频第九期P6)。如下图:
7.引用计数器:当硬件设备插上时,系统会生成一个设备节点,用户在应用空间操作这个设备节点就可以操作设备。将硬件断开时,驱动不会被立刻释放,会等应用程序关闭,再在去释放驱动,Linux系统通过引用计数来实现这一功能。即用kref这个变量记录某个驱动或者某块内存的引用次数,其初始值为1,每引用一次加1,每取消引用一次减1,当计数值为0的时候,自动调用自定义的释放函数进行释放驱动或者内存。struct kref定义在内核源码目录下的/include/linux/kref.h文件中,本质是一个原子变量,如下图:
引用计数器kref一般被嵌入其他结构体中来使用,如在kobject中:
与kref有关的函数有(均定义在/include/linux/kref.h文件中):
- void kref_init(struct kref *kref):该函数将kref的值初始化为1
- unsigned int kref_read(const struct kref *kref):该函数用于读取kref的值
- void kref_get(struct kref *kref):该函数用将kref的值加1
- int kref_put(struct kref *kref, void(*release)(struct kref *kref)):该函数将kref的值减1,若计数值减为0,会调用release函数执行释放操作
如下图:
因为每一个kobject都会对应到系统/sys/下的一个目录,所以对于一个kobject(结构体)中的kref,它的引用计数值应该为1+该kobject下面的直接子kobject数量(即该kobject对应目录下的直接子目录和文件总数,+1表示加上自身),这才能保证在当前kobject中的所有直接子目录或文件被release后该kobject才可能被release(可参考讯为Linux驱动视频第九期P8)。
8.在驱动出口函数中调用kobject_put()函数减少引用计数kref时,kobject_put()会调用kref_put(),这个函数的参数传入了一个release函数kobject_release(),这个被传入的kobject_release()会被调用,kobject_release()又会调用kobject_cleanup(),其定义如下图(定义在/lib/kobject.c中):
可见它会调用kobject->ktype->release去释放kobject,这个release函数是在创建kobject时绑定的。两种方式创建kobject时的函数调用路径分析如下,第一种不用手动申请内存:kobject_create_and_add()->kobject_create()和kobject_add(),其中kobject_create()会调用kzalloc()分配内存创建kobject,并调用kobject_init()初始化kobject,将kobject的ktype成员初始化为dynamic_kobj_ktype,dynamic_kobj_ktype的定义如下图:
可见dynamic_kobj_ktype中的release函数为dynamic_kobj_release,它完成了对在kobject_create()中申请的动态内存的释放,这个release函数会在引用计数器kref的值减至0时被调用。kobject_add()会调用kobject_add_varg()完成/sys/下对应目录的创建等工作。第二种创建kobject的方式需要先手动申请内存创建kobject:kzalloc()->kobject_init_and_add()->kobject_init()和kobject_add_varg(),所以在函数kobject_init_and_add中传入的ktype需要自己定义,定义ktype的release成员(也可以定义为NULL,则释放kobject时什么也不做),且ktype参数不能传入NULL,否则会报错,见本章笔记第2点。
9.一个kobject对应/sys/下的一个目录,在这些目录中可以创建属性(文件),并对这些属性(文件)进行读写操作,属性(文件)的创建和读写与kobj_type结构体中的sysfs_ops、default_attrs两个成员有关系,如下图:
其中sysfs_ops结构体中的show和store成员分别用来定义属性(文件)的读和写操作,default_attrs数组中的每个attribute定义了各个属性(文件)的名字和权限,如下图:
具体实现时需要在show或store函数中根据参数attribute的值去判断当前要读写的是哪个属性(文件),然后执行对应的操作。但要想通过直接定义kobj_type来实现创建属性(文件)的功能,就只能使用kobject_init_and_add去创建kobject,因为kobject_create_and_add在创建kobject时kobj_type是无法自定义的(可参考讯为Linux驱动视频第九期P11)。为了更好地将各个属性与其读写操作联系起来,可以使用linux提供的结构体kobj_attribute以及宏定义__ATTR,然后利用container_of函数根据attribute获取对应的kobj_attribute,进而执行对应的show或store函数,如下图(可参考讯为Linux驱动视频第九期P12):
也可以自定义结构体完成对attribute、show和store的封装。如果想要使用kobject_create_and_add创建kobject(其实kobject_create_and_add函数创建kobject时使用的系统默认的kobj_type中的show和store函数也是利用kobj_attribute结构体完成了对attribute、show和store的封装),并实现属性(文件)相关功能,则可以使用int sysfs_create_file(struct kobject *kobj, const struct attribute *attr);函数添加属性(文件),其中kobj是目标kobject,attr是要添加的属性(文件),但是此函数一次只能添加一个属性(文件)。可以用int sysfs_create_group(struct kobject *kobj, const struct attribute_group *grp)函数一次添加多个属性(文件),其中kobj是目标kobject,grp是要添加的属性(文件)组,struct attribute_group定义如下图(定义在内核源码目录下的文件/kernel/include/linux/sysfs.h中):
若在初始化时没有定义struct attribute_group的name成员,则所有属性(文件)直接被创建在kobject对应的目录下,若指定了name,则所有属性(文件)被创建在“name”目录下,而“name”会被创建在kobject对应的目录下(可参考讯为Linux驱动视频第九期P14)。
10./sys/bus/目录下的每个子目录代表一个总线类型,可以自己注册总线,注册的总线会在/sys/bus/目录下生成对应的子目录。总线对应的结构体为bus_type(参考本章第4点),bus_type结构体中的name成员即为/sys/bus/目录下的子目录名,bus_type中的match成员绑定的函数用于匹配设备和驱动,匹配成功后回去执行bus_type中的probe成员绑定的函数,可以在该probe函数中调用设备对应驱动的结构体device_driver中绑定的probe函数,如下图所示:
总线的注册和注销函数分别是bus_register(&mybus)和bus_unregister(&mybus),可以分别在module_init和module_exit中注册的函数中被调用。与kobject类似,可以在总线对应的目录下创建属性(文件),可以通过调用bus_create_file函数创建(定义在内核源码目录下的/drivers/base/bus.c中):
该函数其实是对前面提到的sysfs_create_file函数的封装,该函数的第一个参数bus就是目标总线,第二个参数attr是将属性(文件)的属性、show、probe操作封装在一起的结构体bus_attribute(与前面的kobj_attribute类似),如下图(定义在内核源码目录下的/include/linux/device/bus.h中):
还可以使用void bus_remove_file(struct bus_type *, struct bus_attribute *)函数删除创建的属性(文件)。
11.使用bus_register注册总线时,在bus_register函数中,会将注册的总线放在名为bus_kset的kset下,bus_kset对应的目录即为/sys/bus/,所以所有注册的总线会放在/sys/bus/目录下。bus_register函数还会为总线默认创建几个kset(文件)和属性(文件),下图截取了部分(更多bus_register函数的分析可参考讯为Linux驱动视频第九期P17):
12.platform总线的注册流程:linux系统在启动时会执行platform_bus_init函数(定义在内核源码目录下的/drivers/base/platform.c中),如下图:
platform_bus_init会调用bus_register注册platform总线,platform_bus_type结构体的定义如下图:
platform_bus_type中的platform_match函数用于匹配platform总线上的platform_device和platform_driver,platform_match函数定义如下图:
从这个函数中就可以看出匹配的优先级为:driver.of_match_table>id_table>driver.name。
13.可以使用int device_register(struct device *dev)函数向总线上注册设备,其中dev为要注册的设备对应的结构体指针,注册的设备会在/sys/devices/目录下的某个目录中生成对应的目录(如果没有指定设备的parent节点则会直接在/sys/devices/目录下生成对应的目录),device_register函数定义如下(在内核源码目录下的/kernel/drivers/base/core.c中定义):
device_initialize函数主要完成一些数据结构的初始化,如下图:
device_add函数用于创建一些软链接(/sys/bus/、/sys/class/等目录下的软链接),并将设备注册到总线上(可参考讯为Linux驱动视频第九期P20),如下图:
void device_unregister(struct device *dev)函数用于注销总线上的设备。设备注册的一个示例如下:
加载这个驱动之后默认会在/sys/devices/目录下生成mydevice子目录。对于platform_device,它的注册过程其实就是对一般的设备注册device_register的包装,如下图:
platform_device_register函数(在内核源码目录下的/kernel/drivers/base/platform.c中定义)也是先调用device_initialize完成了一些初始化工作,然后调用platform_device_add将platform_device设备注册到platform总线上,在platform_device_add函数中会调用device_add函数。对于platform设备,会在/sys/devices/platform/目录下生成对应的设备文件,因为系统在初始化时在调用bus_register函数注册platform总线之前,注册了一个platform_bus设备,之后所有的platform_device设备都会默认被保存到该目录下(见本章第12点中的platform_bus_init函数)。
14.可以使用int driver_register(struct device_driver *drv)函数向总线上注册驱动,其中drv为要注册的设备驱动对应的结构体指针,该函数的定义如下(在内核源码目录下的/kernel/drivers/base/driver.c中定义):
注册成功后会在/sys/bus/注册的总线/drivers/目录下生成对应的目录(注意这里不是软链接)。在驱动和设备匹配成功后,若drivers_autoprobe成员被初始化为1则会自动执行对应的probe函数,这部分代码如下图:
probe函数执行的函数调用链为:driver_register->bus_add_driver->driver_attach->bus_for_each_dev->__driver_attach->device_driver_attach->driver_probe_device->really_probe->对应的probe函数,如下图:
可见会先选择执行bus中定义的probe函数,若bus没定义probe才会去执行驱动中定义的probe函数。可以直接在用户空间通过对属性文件进行写操作去修改这里的drivers_autoprobe变量的值,如下图:
然而不管是先insmod驱动对应的.ko文件还是先insmod设备对应的.ko文件,只要drivers_autoprobe变量的值为1,设备和驱动匹配成功后都会自动取执行对应的probe函数,那是因为在注册设备时也会对drivers_autoprobe的值进行判断,如下图:
此时probe函数执行的函数调用链为:device_register->device_add->bus_probe_device->__device_attach->bus_for_each_drv->__device_attach_driver->driver_probe_device->really_probe->对应的probe函数(可参考讯为Linux驱动视频第九期P26)。对于platform_driver,在注册时platform_driver_register函数(在内核源码目录下的/kernel/drivers/base/platform.c中定义)也是调用driver_register函数完成注册的,如下图: