文章目录
前言
在早期的Linux内核中并没有为设备驱动提供统一的设备模型。随着内核的不断扩大及系统更加复杂,编写一个驱动程序越来越困难,所以在Linux2.6内核中添加了一个统一的设备模型。这样,写设备驱动程序就稍微容易一些了。本章将对设备模型进行详细的介绍。
设备驱动模型概述
设备驱动模型比较复杂,Linux系统将设备和驱动归一到设备驱动模型中来管理。设备驱动模型的提出,解决了以前编写驱动程序没有统一方法的局面。设备驱动模型给各种驱动程序提供了很多辅助性的函数,这些函数经过严格测试,可以很大程度上地提高驱动开发人员的工作效率。
设备驱动模型的功能
Linux内核早期的版本为编写驱动程序提供了简单的功能:分配内存、分配I/O地址、分配中断请求等。写好驱动后,直接把程序加入到内核的相关初始化函数中,这是一个非常复杂的过程,所以开发驱动程序并不简单。并且,没有统一的设备驱动模型。几乎每一种设备驱动程序都需要自己完成所有的工作,驱动程序中不免会产生错误和大量的重复代码。
有了设备驱动模型后,现在的情况就不一样了。设备驱动模型提供了硬件的抽象,内核使用该抽象可以完成很多硬件重复的工作。这样很多重复的代码就不需要编写和调试了,编写驱动程序的难度就有所下降。这些抽象包括如下几个方面:
1.电源管理
电源管理一直是内核的一个组成部分,在笔记本和嵌入式系统中更是如此,它们使用电池来供电。简单地说,电源管理就是当系统的某些设备不需要工作时,暂时的以最低电耗的方式挂起设备,以节省系统的电能。电源管理的一个重要功能是:在省电模式下,使系统中的设备以一定的先后顺序挂起;在全速工作模式下,使系统中的设备以一定的先后顺序恢复运行。
例如:一条总线上连接了A、B、C三个设备,只有当A、B、C三个设备都挂起时,总线才能挂起。当A、B、C三个设备中的任何一个恢复以前,总线必须恢复。总之,设备驱动模型使得电源管理子系统能够以正确的顺序遍历系统上的设备。
2.即插即用设备支持
越来越多的设备可以即插即用了,最常用的设备就是U盘,甚至连(移动)硬盘也可以即插即用。这种即插即用机制,使得用户可以根据自己的需要安装和卸载设备。设备驱动模型自动捕捉插拔信号,加载驱动程序,使内核容易与设备进行通信。
3.与用户空间的通信
用户空间程序通过sysfs
虚拟文件系统访问设备的相关信息。这些信息被组织成层次结构,用sysfs
虚拟文件系统来表示。用户通过对sysfs
文件系统的操作,就能控制设备,或者从系统中读出设备的当前信息。
sysfs文件系统
sysfs
文件系统是Linux众多文件系统中的一个。在Linux系统中,每个文件系统都有其特殊的用途。例如ext2
用于快速读写存储文件;ext3
用来记录日志文件。
Linux设备驱动模型由大量的数据结构和算法组成。这些数据结构之间的关系非常的复杂,多数结构之间通过指针相互关联,构成树形或者网状关系。显示这种关系的最好方法是利用一种树形的文件系统,但是这种文件系统需要具有其他文件系统没有的功能,例如显示内核中的一些关于设备、驱动和总线的信息。为了达到这个目的,Linux内核开发者创建了一种新的文件系统,这就是sysfs
文件系统。
1.sys概述
sysfs
文件系统是Linux2.6内核的一个新特性,其是一个只存在于内存中的文件系统。内核通过这个文件系统将信息导出到用户空间中。sysfs文件系统的目录之间的关系非常复杂,各目录与文件之间既有树形结构,又有目录关系。
在内核中,这种关系由设备驱动模型来表示。在sysfs
文件系统中产生的文件大多数是ASCII文件,通常每个文件有一个值,也可叫属性文件。文件的ASCII码特性保证了被导出信息的准确性,而且易于访问,这些特点使sysfs
成为2.6内核最直观,最有用的特性之一。
2.sysfs文件系统与内核结构的关系
sysfs
文件系统是内核对象(kobject
)、属性(kobj_type
)及它们的相互关系的一种表现机制。用户可以从sysfs
文件系统中读出内核的数据,也可以将用户空间的数据写入内核中。这是sysfs
文件系统非常重要的特性,通过这个特性,用户空间的数据就能够传送到内核空间中,从而设置驱动程序的属性和状态。下表揭示了内核中的数据结构与sysfs
文件系统的关系。
sysfs文件系统的目录结构
sysfs
文件系统中包含了一些重要的目录,这些目录中包含了与设备和驱动等相关的信息,现对其详细介绍如下:
1.sysfs文件系统的目录
sysfs
文件系统与其他文件系统一样,由目录、文件、链接组成。与其他文件系统不同的是,sysfs
文件系统表示的内容与其他文件系统中的内容不同。另外,sysfs
文件系统只存在于内存中,动态的表示着内核的数据结构。
sysfs
文件系统挂接了一些子目录,这些目录代表了注册sysfs
中的主要子系统。
要查看这些子目录和文件,可以使用ls
命令,命令执行如下:
当设备启动时,设备驱动模型会注册kobject
对象,并在sysfs
文件系统中产生以上的目录。现对其中的主要目录所包含的信息进行说明。
2. block目录
块目录包含了在系统中发现的每个块设备的子目录,每个块设备对应一个子目录。每个块设备的目录中有各种属性,描述了设备的各种信息。例如设备的大小、设备号等。块设备目录中有一个表示I/O调度器的目录,这个目录中提供了一些属性文件。它们是关于设备请求队列信息和一些可调整的特性。用户和管理员可以用它们优化性能,包括用它们动态改变I/O调度器。块设备的每个分区表示为块设备的子目录,这些目录中包含了分区的读写属性。
3. bus目录
总线目录包含了在内核中注册而得到支持的每个物理总线的子目录,例如ide、pci、scsi、i2c和pnp
总线等。使用ls
命令可以查看bus
目录的结构信息,如下所示:
ls
命令列出了注册到系统中的总线,其中每个目录中的结构都大同小异。这里以usb
目录为例,分析其目录的结构关系。使用cd usb
命令,进入usb
目录,然后使用ls
命令列出usb
目录中包含的目录和文件,如下所示:
usb
目录中包含了devices和drivers
目录。devices
目录包含了USB总线下所有设备的列表,这些列表实际上是指向设备目录中相应设备的符号链接。使用ls
命令查看如下所示。
其中1-0:1.0
和2-0:1.0
是USB设备的名字,这些名字由USB协议规范来定义。可以看出devices
目录下包含的是符号链接,其指向/sys/devices
目录下的相应硬件设备。硬件的设备文件是在/sys/devices/
目录及其子目录下,这个链接的目的是为了构建sysfs
文件系统的层次结构。
drivers
目录包含了USB总线下注册时所有驱动程序的目录。每个驱动目录中有允许查看和操作设备参数的属性文件,和指向该设备所绑定的物理设备的符号链接。
class目录
类目录中的子目录表示每一个注册到内核中的设备类。例如固件类(firmware
)、混杂设备类(misc
)、图形类(graphics
)、声音类(sound
)和输入类(input
)等。这些类如下所示。
类对象只包含一些设备的总称,例如网络类包含一切的网络设备,集中在/sys/class/net
目录下。输入设备类包含一切的输入设备,如鼠标、键盘和触摸板等,它们集中在/sys/class/input
目录下。关于类的详细概述将在后面讲述。
设备驱动模型的核心数据结构
设备驱动模型由几个核心的数据结构组成,分别是kobject、kset和subsystem
。这些结构使设备驱动模型组成了一个层次结构。该层次结构将驱动、设备和总线等联系起来,形成一个完整的设备模型。下面分别对这些结构进行详细的介绍。
kobject结构体
宏观上来说,设备驱动模型是一个设备和驱动组成的层次结构。例如一条总线上挂接了很多设备,总线在Linux中也是一种设备,为了表述清楚,这里将其命名为A。在A总线上挂接了一个USB控制器硬件B,在B上挂接了设备C和D,当然如果C和D是一种可以挂接其他设备的父设备,那么在C和D设备下也可以挂接其他设备,但这里认为它们是普通设备。另外在A总线上还挂接了E和F设备,则这些设备的关系如下图所示。
在sysfs
文件系统,这些设备使用树形目录来表示,如下所示。
树形结构中每个目录与一个kobject
对象相对应,其包含了目录的组织结构和名字等信息。在Linux系统中,kobject
结构体是组成设备驱动模型的基本结构。最初它作为设备的一个引用计数使用,随着系统功能的增加,它的任务也越来越多。kobject
提供了最基本的设备对象管理能力,每一个在内核中注册的kobject
对象都对应于sysfs
文件系统中的一个目录。kobject
结构体的定义如下:
1.kobject
结构体
kobject
结构体的定义如下:
struct kobject{
const char *name; /*kobject的名称*/
struct list_head entry; /*连接下一个kobject结构*/
struct kobject *parent; /*指向父kobject结构体,如果存在父亲*/
struct kset *kset; /*指向kset集合*/
struct kobj_type *ktype; /*指向kobject的类型描述符*/
struct sysfs_dirent *sd; /*对应sysfs的文件目录*/
struct kref kref; /*kobject的引用计数*/
unsigned int state_initialized:1; /*该kobject对象是否初始化的位*/
unsigned int state_in_sysfs:1; /*是否已经加入sysfs中*/
unsigned int state_add_uevent_sent:1;
unsigned int state_remove_uevent_sent:1;
}
下面对kobject
的几个重要成员介绍如下:
- 2行是
kobject
结构体的名称,该名称将显示在sysfs
文件系统中,作为一个目录的名字。 - 6行代表的
kobject
的属性,可以将属性看成sysfs
中的一个属性文件。每个对象都有属性,例如,电源管理需要一个属性表示是否支持挂起;热插拔事件管理需要一个属性来实现设备的状态。因为大部分的同类设备都有相同的属性,因此将这个属性单独组织为一个数据结构kobject_type
,存放在ktype
中。这样就可以灵活地管理属性了。需要注意的是,对于sysfs
中的普通文件读写操作都是都是由kobject->ktype->sysfs_ops
指针来完成的。对于kobj_type
的详细说明将在后面列出。 - 第8行的
kref
字段表示该对象引用的计数,内核通过kref
实现对象引用计数管理。内核提供两个函数kobject_get()、kobject_put()
分别用于增加和减少引用计数,当引用计数为0时,所有该对象使用的资源被释放。下文将对这两个函数详细解释。 - 第9行的
state_initialized
表示kobject
是否已经初始化过,1表示初始化,0表示未初始化。unsigend int state_initialized:1
中的1表示,只用unsigned int
的最低1位表示这个布尔值。 - 第10行的
state_in_sysfs
表示kobject
是否已经注册到sysfs
文件系统中。
2.kobject结构体的初始化函数kobject_init()
对kobject
结构体进行初始化有些复杂。但无论如何,首先应将整个kobject
设置为0,一般使用memset()
函数来完成。如果没有对kobject
置0,那么在以后使用kobject
时,可能发生一些奇怪的错误。对kobject
置0后,可以调用kobject_init()
函数,对其中的成员进行初始化,该函数的代码如下:
void kobject_init(struct kobject *kobj, struct kobj_type *ktype)
{
char *err_str; /*出错时,保存错误字符串提示*/
if(!kobj){
err_str = "invaild kobject pointer!"; /*kobjetc为无效指针*/
goto error;
}
if(!ktype){
err_str = "must have a ktype to be initialized properly!\n";
goto error;
}
if(kobj->state_initialized){ /*如果kobject已经初始化,则出错*/
/*打印错误信息,有时候可以恢复到正常状态*/
printk(KERN_ERR "kobject (%p):tired to init an initialized"
"object, something is seriously wrong.\n",kobj);
dump_stack(); /*以堆栈方式追溯出错信息*/
}
kobject_init_internel(kobj); /*初始化kobject的内部成员变量*/
kobj->ktype = ktype; /*为kobject绑定一个ktype属性*/
return;
error:
printk(KERN_ERR"kobject (%p): %s\n", kobj, err_str);
dump_stack();
}
- 4~11行,检查
kobj
和ktype
是否合法,它们都不应该是一个空指针。 - 12~16行,判断该
kobj
是否已经初始化过了,如果已经初始化,则打印出错信息。 - 18行调用
kobject_init_internel()
函数初始化kobj
结构体的内部成员,该函数将在下面介绍。 - 19行将定义的一个属性结构体
ktype
赋给kobj->ktype
。这是一个kobj_type
结构体,与sysfs
文件的属性有关,将在后面介绍。例如一个喇叭设备在sysfs
目录中注册了一个A目录,该目录对应一个名为A的kobjetc
结构体。即使再普通的喇叭也应该有个音量属性,用来控制和显示音量的大小,这个属性可以在A目录下用一个名为B的属性文件来表示。很显然,如果要控制喇叭的声音大小,应该对B文件进行写操作,将新的音量值写入;如果要查看当前的音量,应该读B文件。所以属性文件B应该是一个可读可写的文件。
3.初始化kobject的内部成员函数kobject_init_internel()
在前面的函数kobject_init()
第18行,调用了kobject_init_internal()
函数初始化kobject
的内部成员。该函数的代码如下:
static void kobject_init_internal(struct kobject *kobj)
{
if(!kobj) /*如果kobj为空,则出错退出*/
return
kref_init(&kobj->kref); /*增加kobjetc的引用计数*/
INIT_LIST_HEAD(&kobj->entry); /*初始化kobject的链表*/
kobj->state_in_sysfs = 0; /*表示kobject还没注册到sysfs中*/
kobj->state_add_uevent_sent = 0;/*始终初始化为0*/
kobj->state_remove_uevent_sent =0; /*始终初始化为0*/
kobj->state_initialized = 1; /*表示该结构体已经初始化了*/
}
该函数主要对kobjetc
的内部成员进行初始化,例如引用计数kref
,连接kobjetc
的entry
链表等。
4.kobject结构体的引用计数操作
kobject_get()
函数是用来增加kobject
的引用计数,引用计数由kobject
结构体的kref
成员表示。主要对象的引用计数大于等于1,对象就必须继续存在。kobject_get()
函数代码如下:
struct kobject *kobject_get(struct kobject *kobj)
{
if(kobj)
kref_get(&kobj->kref); /*增加引用计数*/
return kobj;
}
kobject_get()
函数将增加kobject
的引用计数,并返回指向kobject
的指针。如果当kobject
对象已经在释放的过程中,那么kobject_get()
函数将返回NULL值。
kobject_put()
函数用来减少kobject()
的引用计数,当kobject
的引用计数为0时,系统就将释放该对象和其占用的资源。前面讲的kobject_init()
函数设置了引用计数为1,所以在创建kobject
对象时,就不需要调用kobject_get()
函数增加引用计数了。当删除kobject
对象时,需要调用kobject_put()
函数减少引用计数。该函数的代码如下:
void kobject_put(struct kobject *kobj)
{
if(kobj){
if(!kobj->state_initialized)
/*为初始化kobjet减少引用计数,则出错*/
WARN(1, KERNEL_WARNING "kobject: '%s' (%p): is not"
"initialized, yet kobject_put() is being"
"called.\n", kobject_name(kobj), kobj);
kref_put(&kobj->kref, kobject_release); /*减少引用计数*/
}
}
前面已经说过,当kobject
的引用计数为0时,将释放kobject
对象和其占用的资源。由于每一个kobject
对象所占用的资源都不一样,所以需要驱动开发人员自己实现释放对象资源的函数。该释放函数需要在kobject
的引用计数为0时,被系统自动调用。
kobject_put()
函数的第8行的kref_put()
函数的第二个参数指定了释放函数,该释放函数是kobject_release()
,其由内核实现,其内部调用了kobj_type
结构中自定义的release()
函数。由此可见kobj_type
中的release()
函数是需要驱动开发人员真正实现的释放函数。从kobject_put()
函数到调用自定义的release()
函数的路径如下图所示。
5.设置kobject名字的函数
用来设置kobject.name
的函数有两个,分别是kobject_set_name()
和kobject_rename()
函数,这两个函数的原型如下:
int kobject_set_name(struct kobject *kobj, const char *fmt,...)
int object_rename(struct kobject *kobj, const char *new_name);
第一个函数用来直接设置kobject
结构体的名字。该函数的第一个参数是需要设置名字的kobject
对象,第二个参数是一个用来格式化名字的字符串,与C语言中printf()
函数的对应参数相似。
第2个函数用来当kobject
已经注册到系统后,如果一定要该kobject
结构体的名字时使用。
设备属性kobj_type
每个kobject
对象都有一些属性,这些属性由kobj_type
结构体表示。最开始,内核开发者考虑将属性包含在kobject
结构体中,后来考虑到同类设备会具有相同的属性,所以将属性隔离开来,由kobj_type
表示。kobject
中有指向kobj_type
的指针,如下图所示。
结合上图解释几个重要的问题。
kobject
始终代表sysfs
文件系统中的一个目录,而不是文件。对kobject_add()
函数的调用将在sysfs
文件系统中创建一个目录。最底层目录对应于系统中的一个设备、驱动或者其他内容。通常一个目录中包含一个或者多个属性,以文件的方式表示,属性由ktype
指向。kobject
对象的成员name
是sysfs
文件系统中的目录名。通常使用kobject_set_name()
函数来设置。在同一个目录下,不能有相同的目录名。kobject
在sysfs
文件系统中的位置由parent
指针指定。parent
指针指向一个kobejct
结构体,kobject
对应一个目录。kobj_type
是kobject
的属性。一个kobject
可以有一个或者多个属性。属性用文件来表示,放在kobejct
对应的目录下。atrribute
表示一个属性,其具体定义将在下面介绍。sysfs_ops
表示对属性的操作函数。一个属性只有两种操作,一种是读操作,一种是写操作。
1.属性结构体kobj_type
当创建kobject
结构体的时候,会给kobject
一些默认的属性。这些属性保存在kobj_type
结构体中,该结构体定义如下:
struct kobj_type{
void (*release)(struct kobject *kobj); /*释放kobject和其占用资源的函数*/
struct sysfs_ops *sysfs_ops; /*操作下一个属性数组的方法*/
struct atrribute **default_attrs; /*属性数组*/
}
kobj_type
的default_atrrs
成员保存了属性数组,每一个kobject
对象可以有一个或者多个属性。属性结构体如下:
struct attribute{
const char *name; /*属性的名称*/
struct module *owner; /*指向拥有该属性的模块,已经不常使用*/
mode_t mode; /*属性的读写权限*/
}
在这个结构体中,name
是属性的名字,对应某个目录下的一个文件的名字。owner
指向实现这个属性的模块指针,就是驱动模块的指针。在x86平台上,已经不推荐使用了。mode
是属性的读写权限,也就是sysfs
中文件的学些权限。这些权限在<include/linux/stat.h>
文件中定义。S_IRUGO
表示属性可读;S_IWUGO
表示属性可写。
2.操作结构体sysfs_ops
kobj_type
结构的字段default_attrs
数组说明了一个kobject
都有那些属性,但是并没有说明如何操作这些属性。这个任务要使用kobj_type->sysfs_ops
成员来完成,sysfs_ops
结构体的定义如下:
struct sysfs_ops{
ssize_t (*show)(struct kobject *, struct attribute *, char *);
/*读属性操作函数*/
ssize_t (*store)(struct kobject *, struct attribute *, const char *, size_t);
/*写属性操作函数*/
};
show()
函数用于读取一个属性到用户空间。函数的第1个参数是要读取的kobject
的指针,它对应要读的目录;第2个参数是要读的属性;第3个参数是存放读到的属性的缓存区。当函数调用成功后,会返回实际读取的数据长度,这个长度不能超过PAGE_SIZE
个自己的大小。store()
函数将属性写入内核。函数的第一个参数是与写相关的kobject
的指针,它对应要写的目录;第2个参数是要写的属性;第3个参数是要写入的数据;第4个参数是要写入的参数长度。这个长度不能超过PAGE_SIZE
个字节大小。只有当拥有属性有写权限时,才能调用store()
函数。
说明
:sysfs文件系统约定一个属性不能太长,一般一至两行左右,如果太长,需要把它分为多个属性。
这两个函数比较复杂,下面举一个关于这两个函数的例子。代码如下:
/*该函数用来读取一个属性的名字*/
ssize_t kobject_test_show(struct kobject *kobject, struct attribute *attr, char *buf )
{
printk("call kobject_test_show().\n"); /*调试信息*/
printk("attrname:%s.\n", attr->name); /*打印属性的名字*/
sprintf(buf,"%s\n",attr->name); /*将属性名字存放在buf中,返回用户空间*/
return strlen(attr->name + 2);
}
/*该函数用来写入一个属性的值*/
ssize_t kobject_test_store(struct kobject *kobject , struct attribute *attr, const char *buf, size_t count)
{
printk("call kobject_test_store().\n"); /*调试信息*/
printk("write: %s\n",buf); /*输出要存入的信息*/
/*省略要写入attr中的数据代码,根据具体的逻辑定义*/
return count;
}
kobject_test_show()
函数将kobject
的名字赋给buf
,并返回给用户空间。例如在用户空间使用cat
命令查看属性文件时,会调用kobejct_test_show()
函数,并显示kobject()
的名字。
kobject_test_store()
函数用于将来自用户空间的buf
数据写入内核,此处并没有实际的写入操作,可以根据具体的情况写入一些需要的数据。
3.kobj_type结构体的release()函数
在上面讨论kobj_type
的过程中,遗留了一个重要的函数,就是release()
函数。该函数表示当kobject
的引用计数为0时,将对kobject
采取什么样的操作。对kobject_put()
函数的讲解中,已经对该函数做了铺垫,该函数的原型如下:
void (*release)(struct kobject *kobj);
该函数的存在至少有两个原因:第一,每一个kobject
对象在释放时,可能都有一些不同的操作,所以并没有统一的函数对kobject
及其包含的结构进行释放操作。第二,创建kobject
的代码并不知道什么时候释放kobject
对象。所以kobject
维护了一个引用计数,当计数为0时,则在合适的时候系统会自动调用自定义的release()
函数来释放kobject
对象。一个release()
函数的模块如下:
void kobject_test_release(struct kobject *kobject)
{
printk("kobject_test: kobject_test_release().\n");
struct my_object *myobject = container_of(kobject, struct my_object, kobj);
/*获得my_object对象*/
kfree(myobject); /*释放自定义的my_object对象,其中包含kobject对象*/
}
kobject
一般包含在一个更大的自定义结构中,这里就是my_object
对象。在驱动程序中,为了完成驱动的一些功能,该对象在系统中申请了一些资源,这些资源的释放就在自定义的kobject_test_release()
中完成。
需要注意的是;每一个kobject
对象都有一个release()
方法,此方法会自动在引用计数为0时,被内核调用,不需要程序员来调用。如果在引用计数不为0时调用,就会出现错误。
4.非默认属性
在许多的情况下,kobject
类型的default_attrs
成员定义了kobject
拥有的所有默认属性。但是在特殊情况下,也可以对kobject
添加一些非默认的属性,用来控制kobejct
代表的总线、设备和驱动的行为。例如为驱动的kobject
结构体添加一个属性文件switch
,用来选择驱动的功能。假设驱动有功能A和B,如果switch为A,那么选择驱动A的功能,写switch为B,则选择驱动的B功能。添加非默认属性的函数原型如下:
int sysfs_create_file(struct kobject *kobj, const struct attribute *attr);
如果函数执行成功,则使用attribute
结构中的名字创建一个属性文件,并返回0,否则返回一个负的错误码。这里举一个创建switch
属性的例子,其代码如下:
struct attribute switch_attr = {
.name = "switch", /*属性名*/
.mode = S_IRWXUGO, /*属性为可读可写*/
};
err = sysfs_create_file(kobj, switch_attr); /*创建一个属性文件*/
if(err) /*返回非0,则出错*/
printk(KERN_ERR "sysfs_create_file error");
内核提供了sysfs_remove_file()
函数来删除属性,其函数原型如下:
void sysfs_remove_file(struct kobject *kobj, const struct attribute *attr);
调用该函数成功,将在sysfs
文件系统中删除attr
属性指定的文件。当属性文件删除后,如果用户空间的某一个程序仍然拥有该属性文件的文件描述符,那么利用该文件描述符对属性的操作会出现错误,需要引起开发者的注意。
注册kobject到sysfs中的实例
为了对kobject对象有一个清晰的认识,这里将尽快给读者展示一个完整的实例代码。在讲解这个实例代码之前,需要重点讲解一些到目前为止,我们需要知道的设备驱动结构。
设备驱动模型结构
在Linux设备驱动模型中,设备驱动模型在内核中的关系用kobject
结构体来表示。在用户空间的关系用sysfs
文件系统的结构来表示。如下图,左边是bus
子系统在内核中的关系,使用kobject
结构体来组织。右边是sysfs
文件系统的结构关系,使用目录和文件来表示。左边的kobject
和右边的目录或者文件是一一对应的关系,如果左边有一个kobject
对象,那么右边就对应一个目录。文件表示该kobject
的属性,并不与kobejct
相对应。
kset集合
kobject
通过kset
组织成层次化的结构。kset
是具有相同类型的kobejct
集合,像驱动程序一样放在/sys/drivers
目录下,目录drivers
是一个kset
对象,包含系统中的驱动程序对应的目录,驱动程序的目录由kobject
表示。
1.kset集合
kset结构体的定义如下:
struct kset{
struct list_head list; /*连接所包含的kobject对象的链表首部*/
spinlock_t list_lock; /*维护list链表的自旋锁*/
struct kobject kobj; /*内嵌的kobject结构体,说明kset本身也是一个目录*/
struct kset_uevent_ops *uevent_ops; /*热插拔事件*/
}
- 1行表示一个链表。包含在
kset
中的所有kobject
对象被组织成一个双向循环链表,list
就是这个链表的头部。 - 3行是用来从
list
中添加或者删除kobejct
的自旋锁 - 4行是一个内嵌的
kobject
对象。所有属于这个kset
集合的kobejct
对象的parent
指针,均指向这个内嵌的kobject
对象。另外kset
的引用计数就是内嵌的kobject
对象的引用计数。 - 5行是支持热茶事件的函数集。
2.热插拔事件kset_uevent_ops
一个热插拔事件是从内核空间发送到用户空间的通知,表明系统某些部门的配置已经发生变化。用户空间接收到内核空间的通知后,会调用相应的程序,处理配置的变化。例如,当U盘插入到USB系统时,会产生一个热插拔事件,内核会捕获这个热插拔事件,并调用用户空间的/sbin/hotplug
程序,该程序通过加载驱动程序来响应U盘插入的动作。
在早期的系统中,如果要加入一个新设备,必须要关闭计算机,插入设备,然后再重启,这是一个非常繁琐的过程。现在计算机系统的硬软件已经有能力支持设备的热插拔,这种特性带来的好处是,设备可以即插即用,节省用户的时间。
内核将在什么时候产生热插拔时间呢?当驱动程序将kobject
注册到设备驱动模型时,会产生这些事件。也就是当内核调用kobject_add()
和kobject_del()
函数时,会产生热插拔事件。热插拔事件产生时,内核会根据kobject
的kset
指针找到所属的kset
结构体,执行kset
结构体中uevent_ops
包含的热插拔函数。这些函数的定义如下:
struct kset_uevent_ops{
int (*filter)(struct kset *kset, struct kobject *kobj);
const char *(*name)(struct kset *kset, struct kobject *kobj);
int (*uevent)(struct kset *kset, struct kobject *kobj,
struct kobj_uevent_env *env);
};
- 2行的
filter()
函数是一个过滤函数。通过filter()
函数,内核可以决定是否向用户空间发送事件产生信号。如果filter()
返回0,表示不产生事件;如果filter()
返回1,表示产生事件。例如,在块设备子系统中可以使用该函数决定那些事件应该发送给用户空间。在块设备子系统中至少存在3种类型的kobject
结构体:磁盘、分区和请求队列。用户空间需要对磁盘和分区的改变产生响应,但一般不需要对请求队列的变化产生响应。在把事件发送给用户空间时,可以使用filter()
函数过滤不需要产生的事件。块设备子系统的过滤函数如下:
static int dev_uevent_filter(struct kset *kset, struct kobject *kobj)
{
int ret;
struct kobj_type *ktype = get_ktype(kobj); /*得到kobject属性的类型*/
ret =(ktype == &ktype_block) || (ktype == &ktype_part); /*判断是否磁盘或分区事件*/
return ret; /*0表示过滤,非0表示不过滤*/
}
- 3行的
name()
函数在用户空间的热插拔程序需要知道子系统的名字时被调用。该函数将返回给用户空间程序一个字符串数据。该函数的一个例子是dev_uevent_name()
函数,代码如下:
static const char *dev_uevent_name(struct kset *kset, struct kbject *kobj)
{
struct device *dev = to_dev(kobj);
if(dev->bus)
return dev->bus->name;
if(dev->class)
return dev->class->name;
return NULL;
}
该函数先由kobj
获得device
类型的dev
指针。如果该设备的总线存在,则返回总线的名字,否则返回设备类的名字。
- 任何热插拔程序需要的信息可以通过环境变量来传递。
uevent()
函数可以在热插拔程序执行前,向环境变量中写入值。
kset与kobject的关系
kset
是kobject
的一个集合,用来与kobject
建立层次关系。内核可以将相似的kobject
结构连接在kset
集合中,这些相似的kobject
可能有相似的属性,使用统一的kset
来表示。下图显示了kset
集合和kobject
之间的关系。
kset
集合包含了属于其的kobject
结构体,kset.list
链表用来连接第一个和最后一个kobject
对象。第一个kobject
使用entry
连接kset
集合和第二个kobejct
对象。第二个kobject
对象使用entry
连接第一个kobject
对象和第三个kobject
对象,依次类推,最终形成一个kobject
对象的链表。- 所有
kobject
结构的parent
指针指向kset
包含的kobejct
对象,构成一个父子层次关系。 kobject
的所有kset
指针指向包含它的kset
集合,所以通过kobject
对象很容易就能找到kset
集合。kobject
的kobj_type
指针指向自身的kobj_type
,每一个kobject
都有一个单独的kobj_type
结构。另外在kset
集合中也有一个kobject
结构体,该结构的xxx也指向一个kobj_type
结构体。从前文知道,kobj_type
中定义了一组属性和操作属性的方法。这里需要注意的是,kset
和kobj_type
的优先级要高于kobject
对象中的kobj_type
的优先级。如果两个kobj_type
都存在,那么优先调用kset
中的函数。如果kset
中的kobj_type
为空,才调用各个kobject
结构体自身对应的kobj_type
中的函数。kset
中的kobj
也负责对kset
的引用计数。
kset相关的操作函数
kset
相关的操作函数与kobject
的函数相似,也有初始化、注册和注销扽函数。下面对这些函数进行介绍。
1.初始化函数kset_init()
kset_init()
函数用来初始化kset
对象的成员,其中最重要的是初始化kset.kobj
成员,使用上面介绍过的kobject_init_internal()
函数。
void kset_init(struct kset *k)
{
kobject_init_internal(&k->kobj); /*初始化kset.kobj成员*/
INIT_LIST_HEAD(&k->list); /*初始化连接kobject的链表*/
spin_lock_init(&k->list_lock); /*初始化自旋锁,该锁用于对kobject的添加、删除等操作*/
}
2.注册函数kset_register()
kset_register()
函数用来完成系统对kset
的注册,函数原型如下:
int kset_register(struct kset *k);
3.注销函数kset_unregister()
kset_unregister()
函数用来完成系统对kset
的注销,函数的原型如下:
void kset_unregister(struct kset *k);
4.kset的引用计数
kset
也有引用计数,该引用计数由kset
的kobj
成员来维护。可以使用kset_get()
函数增加引用计数,使用kset_put()
函数减少引用计数。这两个函数的原型如下:
static inline struct kset *kset_get(struct kset *k);
static inline void kset_put(struct kset *k);
注册kobject到sysfs中的实例
对kobject
和kset
有所了解后,本节将讲解一个实例程序,以使读者对这些概念有更清楚的认识。这个实例程序的功能是:在/sys
目录下添加一个名为kobject_test
的目录名,并在该目录下添加一个名为kobject_test_attr
的文件,这个文件就是属性文件。本实例可以通过kobject_test_show()
函数实现显示属性的值;也可以通过kobejct_test_store()
函数向属性中写入一个值。这里实例的完整代码如下:
#include <linux/device.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/string.h>
#include <linux/sysfs.h>
#include <linux/stat.h>
/*释放kobejct结构体的函数*/
void kobject_test_release(struct kobject *kobject);
/*读属性的函数*/
ssize_t kobject_test_show(struct kobject *kobject, struct attribute *attr, char *buf);
/*写属性的函数*/
ssize_t kobject_test_store(struct kobject *kobject, struct attribute *attr,const char *buf, size_t count);
/*定义了一个名为kobject_test,可读可写的属性*/
struct attribute test_attr = {
.name = "kobject_test", /*属性名*/
.mode = S_IRWXUGO, /*属性为可读可写*/
};
/*该kobject只有一个属性*/
static struct attribute *def_attrs[]={
&test_attr,
NULL,
};
struct sysfs_ops obj_test_sysops = {
.show = kobject_test_show, /*属性读函数*/
.store = kobject_test_store, /*属性写函数*/
};
struct kobj_type ktype={
.release = kobject_test_release, /*释放函数*/
.sysfs_ops = &obj_test_sysfs , /*属性操作函数*/
.default_attr = def_attrs, /*默认属性*/
};
void kobject_test_release(struct kobject *kobject)
{
/*这只是一个示例,实际代码要复杂很多*/
printk("kobject_test: kobject_test_release().\n");
}
/*该函数用来读取一个属性的名字*/
ssize_t kobject_test_show(struct kobject *kobject, struct attribute *attr, char *buf)
{
printk("call kobject_test_show().\n"); /*调试信息*/
printk("attrname:%s.\n",attr->name); /*打印属性名*/
sprintf(buf, "%s\n",attr->name); /*将属性名字存放在buf中,返回用户空间*/
return strlen(attr->name)+2;
}
/*该函数用来写入一个值*/
ssize_t kobject_test_store(struct kobject *kobject, struct attribute *attr, const char *buf, size_t count)
{
printk("call kobject_test_store().\n"); /*调试信息*/
printk("write:%s\n",buf); /*输出要存储的信息*/
strcpy(attr->name, buf); /*写一个属性*/
return count;
}
struct kobject kobj; /*要添加的kobject结构*/
static int kobject_test_init()
{
printk("kobject test_init().\n");
kobject_init_and_add(&kobj, &ktype, NULL, "kobject_test"); /*初始化并添加kobject到内核*/
return 0;
}
static int kobject_test_exit()
{
printk("kobject test exit.\n");
kobject_del(&kobj); /*删除kobject*/
return 0;
}
module_init(kobject_test_init);
module_exit(kobject_test_exit);
MODULE_AUTHOR("xxxx");
MODULE_LICENSE("Dual BSD/GPL")
下面对示例的一些扩展知识进行简要介绍。
1.kobject_init_and_add()
函数
加载函数kobject_test_init()
调用kobject_init_and_add()
函数来初始化和添加kobject
到内核中。函数调用成功后将在/sys
目录下新建一个kobject_test
的目录,这样就构建了kobject
的设备层次模型。这个函数主要完成了如下两个功能:
- 调用
kobject_init()
函数对kobject
进行初始化,并将kobject
与kobj_type
关联起来。 - 调用
kobject_add_varg()
函数将对kobject
加入设备驱动层次模型中,并设置一个名字。kobject_init_and_add()
函数的代码如下:
int kobject_init_and_add(struct kobject *kobj, struct kobj_ktype *ktype, struct kobject *parent, const char *fmt, ...)
{
va_list args; /*参数列表*/
int retval; /*返回值*/
kobject_init(kobj, ktype); /*初始化kobject结构体*/
va_start(args, fmt); /*开始解析可变参数列表*/
retval = kobject_add_varg(kobj, parent, fmt, args); /*给kobj添加一些参数*/
va_end(args); /*结束解析参数列表*/
return retval;
}
- 参数说明:第一个参数kobj是指向要初始化的
kobject
结构体;第2个参数ktype
是指向要与kobj
联系的kobj_type
。第3个参数指定kobj
的父kobject
结构体;第4,5个参数是XXXX - 6行的
kobject_init()
函数已经在前面介绍过了。 - 8行调用
kobject_add_varg()
函数向设备驱动模型添加一个kobject
结构体。这个函数比较复杂,将在后面介绍。
2.将kobject加入设备驱动模型中的函数kobject_add_varg()
kobject_add_varg()
函数将kobject
加入驱动设备模型中。函数的第1个参数kobj
是要加入设备驱动模型中的kobject
结构体指针;第2个参数是该kobject
结构体的父结构体,该值为NULL,表示在/sys
目录下创建一个目录,本实例就是这种情况;第3,4个参数与printf()
函数的参数相同,接收一个可变参数,这里用来设置kobject
的名字。kobject_add_vag()
函数的代码如下:
static int kobject_add_vag(struct kobject *kobj, struct kobject *parent, const char *fmt, va_list vargs)
{
int retval; /*返回值*/
retval = kobject_set_name_vargs(kobj, fmt, vargs); /*给kobject赋新的名字*/
if(retval){
printf(KERN_ERR "kobect: can not set name properly!\n");
return retval;
}
kobj->parent = parent; /*设置kobject的父kobject结构体*/
return kobject_add_internal(kobj);
}
- 5~9行,设置将要加入
sysyfs
文件系统中的kobject
的名字。本实例的名字是kobject_test
。将在sysfs
文件系统中加入一个kobject_test
的目录。 - 10行设置
kobject
的父kobject
结构体。也就是kobject_test
的父目录,如果parent
为NULL,那么将在sysfs
文件系统顶层目录中加入kobject_test
目录。表示没有父目录。 - 11行调用
kobject_add_internal()
函数向设备驱动模型中添加kobject
结构体。
3.kobject添加函数kobject_add_internal()
kobject_add_internal()
函数负责向设备驱动模型中添加kobject
结构体,并在sysfs
文件系统中创建一个目录。该函数的代码如下:
static int kobject_add_internal(struct kobject *kobj)
{
int error = 0;
struct kobject *parent;
if(!obj) /*为空,则失败,表示没有需要添加的kobject*/
return -ENOENT;
if(!kobj->name | !kobj->name[0]) {
/*kobject没有名字,不能注册到设备驱动模型中*/
WARN(1, "kobject: (%p): attempted to be registered with empty"
"name!\n", kobj);
return -EINVAL;
}
parent = kobject_get(kobj->parent); /*增加父目录的引用计数*/
if(kobj->kset){ /*是否属于一个kset集合*/
if(!parent) /*如果kobject本身没有父kobject,
则使用kset的kobject作为kobject的父亲*/
parent = kobject_get(&kobj->kset->kobj); /*增加引用计数*/
kobj_kset_join(kobj);
kobj->parent = parent; /*设置父kobject结构*/
}
/*打印调试信息:kobject名字、对象地址、该函数名;父kobject名字;kset集合名字*/
pr_debug("kobject:'%s' (%p): %s : parent: %s , set :'%s'\n",
kobject_name(kobj), kobj, __func__,
parent? kobject_name(parent): "<NULL>");
error = create_dir(kobj); /*创建一个sysfs目录,该目录名字为kobj_name*/
if(error){ /*以下为创建目录失败的函数*/
kobj_kset_leave(kobj);
kobject_put(parent);
kobj->parent = NULL;
/*be noisy on error issues*/
if(error == -EEXIST)
peintk(KERNEL_ERR "%s failed for %s with"
"-EEXIST, don't try to register things with"
"the same name in the same directory.\n",
__func__, kobject_name(kobj));
else
printk(KERN_ERR "%s failed for %s (%d)\n",
__func__, kobject_name(kobj), error);
dump_stack();
}else
kobj->state_in_sysfs = 1; /*创建成功,表示kobject在sysfs中*/
return error;
}
4.删除kobject对象的kobject_del()函数
kobject_del()
函数用来从设备驱动模型中删除一个kobject
对象,本实例中该函数在卸载函数kobject_test_exit()
中调用。具体来说,kobject_del()
函数主要完成以下3个工作:
- 从
sysfs
文件系统中删除kobject
对应的目录,并设置kobject
的状态为没有在sysfs
中。 - 如果
kobject
属于一个kset
集合,则从kset
中删除。 - 减少
kobject
的相关引用计数。kobject_del()
函数代码如下:
void kobject_del(struct kobject *kobj)
{
if(!kobj) /*为空,则退出*/
return;
sysfs_remove_dir(kobj); /*从sysfs文件系统中删除kobj对象*/
kobj->state_in_sysfs = 0; /*表示该kobj没有在sysfs中*/
kobj_kset_leave(kobj); /*如果kobj对象属于一个kset集合,则从集合中删除*/
kobject_put(kobj->parent); /*减少父目录的引用计数*/
kobj->parent = NULL ; /*将父目录设为NULL*/
}
5.释放函数kobject_test_release()
前面已经说过每一个kobject
都有自己的释放函数,本例的释放函数是kobject_test_release()
,该函数除打印一条信息之外,什么也没有做。因为这个例子并不需要做其他工作,在实际的项目中该函数可能较为复杂。
6.读写属性函数
本例有一个test_attr
的属性,该属性的读写函数分别是kobject_test_show()
和kobject_test_strore()
。分别用来向属性test_attr
中读出和写入属性名。
实例测试
使用make
命令编译kobject_test.c
文件,得到kobject_test.ko
模块,然后使用insmod
命令加载该模块。当模块加载后会在/sys
目录中增加一个kobject_test
的目录,如下所示。
进入kobject_test
目录,在该目录下有一个名为kobject_test_attr
属性文件,如下所示。
使用echo
命令和cat
命令可以对这个属性文件进行读写,读写时,内核里调用的分别是kobject_test_show()
和kobject_test_store()
函数。这两个函数分别用来显示和设置属性的名字,测试过程如下:
设备驱动模型的三大组件
设备驱动模型有三大重要组件,分别是总线(bus_type
)、设备(device
)和驱动(driver
)。下面对这三个重要的组件进行分别介绍。
总线
从硬件结构上来讲,物理总线有数据总线和地址总线
。物理总线是处理器与一个或者多个设备之间的通道。在设备驱动模型中,所有设备都通过总线连接,此处的总线与物理总线不同,总线是物理总线的一个抽象,同时还包含一些硬件中不存在的虚拟地址总线。在设备驱动模型中,驱动程序是附属在总线上的。下面将首先介绍总线、设备和驱动之间的关系。
1.总线、设备、驱动关系
在设备驱动模型中,总线、设备和驱动三者之间紧密联系。如下图所示,在/sys
目录下,有一个bus
目录,所有的总线都在bus
目录下有一个新的子目录。一般一个总线目录有一个设备目录、一个驱动目录和一些总线属性文件。设备目录中包含挂接在该总线上的设备,驱动目录包含挂接在总线上的驱动程序。设备和驱动程序之间通过指针互相联系。
如上图所示,总线上的设备链表有3个设备,设备1、设备2和设备3。总线上的驱动链表也有3个驱动程序,驱动1、驱动2和驱动3.其中虚线箭头表示设备与驱动的绑定关系,这个绑定是在总线枚举设备时设置的。这里,设备1与驱动2绑定,设备2与驱动1绑定,设备3与驱动3绑定。
2.总线数据结构bus_type
在Linux设备模型中,总线用bus_type
表示。内核支持的每一条总线都由一个bus_type
对象来描述。
struct bus_type{
const char *name; /*总线支持的名称*/
struct bus_attribute *bus_attrs; /*总线属性和导出到sysfs的方法*/
struct device_attribute *dev_attrs; /*设备属性和导出到sysfs的方法*/
struct driver_attribute *drv_attrs; /*驱动程序属性和导出到sysfs中的方法*/
/*匹配函数,检验参数2中的驱动是都支持参数1中的设备*/
int (*match)(struct device *dev, struct device_driver *drv);
int (*uevent) (struct device *dev, struct kobj_uevent_env *env);
int (*probe)(struct device *dev); /*探测设备*/
int (*remove)(struct device *dev); /*移除设备*/
int (*shutdown)(struct device *dev); /*关闭函数*/
int (*suspend)(struct device *dev, pm_message_t state);
/*改变设备供电状态,使其节能*/
int (*suspend_late)(struct device *dev, pm_message_t state); /*挂起函数*/
int (*resume_early)(struct device *dev); /*唤醒函数*/
int (*resume)(struct device *dev); /*恢复供电状态,使设备正常工作的方法*/
struct dev_pm_ops *pm; /*关于电源管理的操作符*/
struct bus_type_private *p; /*总线私有数据*/
};
- 2行的
name
成员是总线的名字,例如PCI。 - 3~5行分别是三个属性,与kobject对应的属性类似。设备驱动模型的每一个层次都有一个属性。
- 6~15行是总线匹配、探测、电源管理等相关的函数。在具体用到时,将详细解释。
- 16行是
dev_pm_ops
是与电源管理相关的函数集合 - 17行的是
bus_type_private
表示的是总线的私有数据。
3.bus_tye
声明实例
在Linux中,总线不仅是物理总线的抽象,还代表一些虚拟的总线。例如,平台设备总线(platform
)就是虚拟总线。值得注意的是bus_type
中的很少成员需要自己定义,内核复杂完成大部分的功能。例如ac97
声卡的总线定义就非常简单,如果去掉电源管理的函数,那么ac97
总线就只有match()
函数的定义,其总线代码如下:
struct bud_type ac97_bus_type = {
.name = "ac97",
.match = ac97_bus_match,
#ifdef_ CONFIG_PM
.suspend = ac97_bus_suspend,
.resume = ac97_bus_resume,
#endif /*OCNFIG_PM*/
};
4.总线私有数据bus_type_private
总线私有数据结构bus_type_private
包含3个主要的成员。一个kset
的类型的subsys
容器,表示一条总线的主要部分;一个总线上的驱动程序容器drivers_kset
;一个总线上的设备容器devices_kset
。
struct bus_type_private{
struct kset subsys; /*代表该bus子系统,里面的kobj是该bus的主kobj,也就是最顶层*/
struct kset *drivers_kset; /*挂接到该总线上的所有驱动集合*/
struct kset *device_kset; /*挂接到总线上的所有设备集合*/
struct klist klist_devices; /*所有设备的列表,与devices_kset中的list相同*/
struct klist klist_drivers; /*所有驱动程序的列表,与drivers_kset中的list相同*/
struct blocking_notifier_head bus_notifier;
unsigned int drivers_autoprobe:1; /*设置是否在驱动注册时,自动探测(probe)设备*/
struct bus_type *bus; /*回指包含自己的总线*/
};
5.总线注册bus_register()
如果为驱动程序定义了一条新的总线,那么需要调用bus_register()
函数进行注册。这个函数有可能会调用失败,所以有必要检测它的返回值。如果函数调用成功,那么一条新的总线将被添加到系统中。可以在sysyfs
文件系统的/sys/bus
目录下看到它。该函数的代码如下:
int bus_register(struct bus_type *bus)
{
int retval; /*返回值*/
struct bus_type_private *priv; /*总线私有数据*/
priv = kzmalloc(sizeof(struct bus_type_private), GFP_KERNEL);
/*申请一个总线私有数据*/
if(!priv) /*内存不足*/
return -EIOMEM;
priv->bus = bus; /*总线私有数据结构回指的总线*/
bus->p = priv; /*总线私有数据*/
BLOCKING_INIT_NOTIFIER_HEAD(&priv->bus_notifer); /*初始化通知链表*/
retval = kobject_set_name(&priv->subsys.kobj, "%s", bus->name);
/*设置总线的名字,例如PCI*/
if(retval) /*失败则返回*/
goto out;
priv->subsys.kobj.kset = bus_kset;
/*指向其父kset,bus_kset在buses_init()例程中添加*/
priv->subsys.kobj.ktype = &bus_type; /*设置读取总线属性文件的默认方法*/
priv->drivers_autoprobe = 1; /*驱动程序注册时,可以探测(probe)设备*/
retval = kset_register(&priv->subsys); /*注册总线容器priv->subsys*/
if(retval)
goto out;
retval = bus_create_file(bus, &bus_attr_uevent);
/*建立uevent属性文件*/
if(retval)
goto bus_uevnt_fail;
/*创建一个devices_kset容器。也就是在新的总线目录下创建一个devices的目录,其父
目录就是priv->subsys.kobj对应的总线目录*/
priv->device_kset = kset_create_and_add("devices", NULL, &priv->subsys.kobj);
if(!priv->devices_kset){
retval = -ENOMEN;
goto bus_devices_fail;
}
/*创建一个drivers_kset容器。也就是在新的总线目录下创建一个drivers的目录,其父
目录就是priv->subsys.kobj对应的总线目录*/
priv->drivers_kset = kset_create_and_add("drivers", NULL, &priv->subsys.kobj);
if(!priv->drivers_kset){
retval = -ENOMEN;
goto bus_drivers_fail;
}
klist_init(&priv->klist_devices, klist_devices_get, klist_devices_put);
/*初始化设备链表*/
klist_init(&priv->klist_drivers, NULL, NULL);/*初始化驱动程序链表*/
retval = add_probe_file(bus); /*与热插拔相关的探测文件*/
if(retval)
goto bus_probe_file_fail;
retval = bus_add_attrs(bus); /*为总线创建一些属性文件*/
if(retval)
goto bus_attrs_fail;
pr_debug("bus: '%s': registered\n", bus->name);
return 0;
/*错误处理*/
bus_attrs_fail:
kset_unregister(bus->p->drivers_kset)
bus_probe_files_fail:
remove_probe_file(bus);
bus_drivers_fail:
kset_unregister(bus->p->device_kset);
bus_uevent_fail:
kset_unregister(&bus->p->subsys);
kfree(bus->p)
out:
return retval;
bus_register()
函数对bus_type
进行注册,当从系统中删除一条总线时,应该使用bus_unregister()
函数,该函数原型如下:
void bus_unregister(struct bus_type *bus)
总线属性和总线方法
bus_type
中还包含表示总线属性和总线方法的成员。属性使用成员bus_attrs
表示,相对该成员介绍如下:
1.总线的属性bus_attribute
在Linux设备驱动模型中,几乎每一层都有添加属性的函数,bus_type
也不例外。总线属性用bus_attribute
表示,由bus_type
的bus_attr
指针指向。bus_atrribute
属性如以下代码所示:
struct bus_attribute{
struct attribute attr; /*总线属性*/
ssize_t (*show) (struct bus_type *bus, char *buf); /*属性读函数*/
/*属性写函数*/
ssize_t (*store) (struct bus_type *bus, const char *buf, size_t count);
};
bus_attribute
中的attribute
属性与kobject
中的属性结构体是一样的。bus_attribute
总线属性也包含两个显示和设置属性值的函数,分别是show()
和store()
函数。可以使用BUS_ATTR
宏来初始化一个bus_attribute
结构体,该宏的定义如下:
#define BUS_ATTR(name, _mode, _show, _store) \
struct bus_attribute bus_attr_##_name= __ATTR(_name, _mode, _show, _store)
此宏有4个参数,分别是属性名、属性读写模式、显示属性和存储属性。例如定义了一个名为bus_attr_config_time
的属性,可以写成如下形式:
static BUS_ATTR(config_time, 0644, ap_config_time_show, ap_config_time_store);
对该宏进行扩展,就能得到bus_attr_config_time
属性如下的定义:
struct bus_attribute bus_attr_config_time = {
.attr = {.name = config_time, .mode = 0644},
.show = ap_config_time_show,
.store = ap_config_time_store,
}
2.创建和删除总线属性
创建总线属性,需要调用bus_create_file()
函数,该函数的原型如下:
int bus_create_file(struct bus_type *bus, struct bus_attribute *attr);
当不需要某个属性时,可以使用gbus_remove_file()
函数删除该属性,该函数的原型如下:
void bus_remove_file(struct bus_type *bus, struct bus_attribute *attr);
3.总线上的方法
在bus_type
结构体中,定义了许多方法。这些方法都是与总线相关的,例如电源管理,新设备与驱动匹配的方法。这里主要介绍match()
函数和uevent()
函数,其他函数在驱动中几乎不需要使用。match()
函数的原型如下:
int (*match)(struct device *dev, struct device_driver *drv);
当一条总线上的新设备或者新驱动被添加时,会依次或多次调用该函数。如果指定的驱动程序能够适用于指定的设备,那么该函数返回非0值,否则,返回0。当定义一种新总线时,必须实现该函数,以使内核知道怎样匹配设备和驱动程序。一个match()
函数的例子如下:
static bbtv_sub_bus_match(struct device *dev, struct device_driver *drv)
{
struct bttv_sub_driver *sub = to_bttv_sub_drv(drv);/*转换为自定义驱动*/
int len = strlen(sub->wanted); /*取驱动能支持的设备名长度*/
if(0 == strncmp(dev_name(dev), sub->wanted, len))
/*新添加的设备名是否与驱动支持的设备名相同*/
/*如果总线上的驱动支持该设备,则返回1,否则返回0*/
return 1;
return 0;
}
当用户空间产生热插拔事件前,可能需要内核传递一些参数给用户空间程序,这里只能使用环境变量来传递参数。传递环境变量的函数由uevent()
实现。该函数的原型入下:
int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
该函数只有在内核支持热插拔事件(CONFIG_HOTPLUG
)时,才有用,否则该函数被定义为了NULL值。以amba_uevent()
函数为例,该函数只有在支持热插拔时,才被定义。函数体中调用了add_uevent_var()
函数添加了一个新的环境变量,代码如下:
#ifdef CONFIG_HOTPLUG
static int amba_uevent(struct device *dev, struct kobj_uevent_env *env)
{
struct amba_device *pcdev = to_amba_device(dev);
/*由device转换为自定义的设备结构*/
int retval = 0;
/*向env中添加一个新的变量AMBA_ID*/
retval = add_uevent_var(env, "AMBA_ID=%08x", pcdev->perophid);
return retval;
}
#else
#define amba_uevent NULL /*不支持热插拔事件*/
#endif
设备
在Linux设备驱动模型中,每一个设备都由一个device
结构体来描述。device
结构体包含了设备所具有的一些通用信息。对于驱动开发人员来说,当遇到新设备时,需要定义一个新的设备结构体,将device
作为新结构体成员。这样就可以在新结构体中定义新设备的一些信息,而设备通用的信息就使用device
结构体来表示。使用device
结构体的另一个好处是,可以通过device
轻松地将新设备加入设备驱动模型的管理中。下面对device结构体进行简要的介绍。
1.device结构体
device
中的大多函数被内核使用,驱动开发人员不需要关注,这里只对该结构体主要成员进行介绍。该结构体的主要成员如下:
struct device {
struct klist klist_children; /*连接子设备的链表*/
struct device *parent; /*指向父设备的指针*/
struct kobject kobj; /*内嵌的kobject结构体*/
char bus_id[BUS_ID_SIZE]; /*连接到总线上的位置*/
unsigned uevent_suppress:1; /*是否支持热插拔事件*/
const char *init_name; /*设备的初始化名字*/
struct device_type *type; /*设备相关的特殊处理函数*/
struct bus_type *bus; /*指向连接的总线指针*/
struct device_driver *driver; /*指向该设备的驱动程序*/
void *driver_data; /*指向驱动程序私有数据的指针*/
struct dev_pm_info power; /*电源管理信息*/
dev_t devt; /*设备号*/
struct clss *class; /*指向设备所属类*/
struct attribute_group **groups; /*设备的组属性*/
void (*release)(struct device *dev); /*释放设备描述符的回调函数*/
};
- 3行指向父设备,设备的父子关系表示,子设备离开了父设备就不能工作。
- 5行的
bus_id
字段,表示总线上一个设备的名字。例如PCI设备使用了标准的PCI ID格式,其格式为:域编号、总线编号、设备编号和功能编号。 - 8行的
device_type
结构中包含了一个用来对设备操作的函数 - 9行的
bus
指针指向设备所属的总线。 - 10行的
driver
指针指向设备的驱动程序。 - 16行的
release
函数。当指向设备的最后一个引用被删除时,内核会调用该方法。所有向内核注册的device
结构都必须有一个release()
方法,否则内核就会打印出错信息。
2.设备注册和注销
设备必须注册后,才能使用。在注册device
结构之前,至少要设置parent、bus_id、bus和release
成员。常用的注册和注销函数如下代码所示:
int device_register(struct device *dev);
void device_unregister(struct device *dev);
为了使读者对设备注册有一个清楚的认识,下面的代码完成一个简单的设备注册。
static void test_device_release(struct device *dev) /*释放device的函数*/
{
printk(KERN_DEBUG"test_device release().\n");
}
/*设备结构体*/
struct device test_device ={
.bus_id ="test_device"
.release = test_device_release,
.parent=NULL
};
int ret;
ret = device_register(&test_device); /*注册设备结构体*/
if(ret)
printk(KERN_DEBUG"register is error");
这段代码完成一个设备注册,其parent
和bus
成员都是NULL。设备的名字是test_device
。释放函数是test_device_release()
并不做任何实质工作。这段代码调用成功后,会在sysfs
文件系统的/sys/device
目录中,看到一个新的目录test_device
,该目录就对应这里注册的设备。
设备的注销函数是device_unregister()
,该函数的原型如下:
voif device_unregister(struct device *dev);
3.设备属性
每一个设备都可以有一些属性,在sysfs
文件系统中以文件的形式来表示。设备属性的定义如下:
struct device_attribute{
struct attribute attr; /*属性*/
/*显示属性的方法*/
ssize_t (*show)(struct device *dev, struct device_attribute *attr, char *buf);
/*设置属性的方法*/
ssize_t (*store)(struct device *dev, struct device_attribute *attr, const char *buf,
size_t count);
};
在写程序时,可以使用宏DEVICE_ATTR
定义attribute
结构,这个宏的定义如下:
#define DEVICE_ATTR(_name, _mode, _show, _store) \
struct device_attribute dev_attr_##_name = __ATTR(_name, _mode. _show, _store)
该宏使用dev_attr_
作为前缀构造属性名,并传递属性的读写模式,读函数和写函数。另外可以使用下面两个函数对属性文件进行实际的处理。
int device_create_file(struct device *device, struct device_attribute *entry);
void device_remove_file(struct device *dev, struct device_attribute *attr);
device_create_file()
函数用来在device
所在的目录下创建一个属性文件;device_remove_file()
函数用来在device
所在的目录下删除一个属性文件。
驱动
在设备驱动模型中,记录了注册到系统中的所有设备。有些设备可以使用,有些设备不可以使用,原因是设备需要与对应的驱动程序绑定才能使用,本节将重点介绍设备驱动程序。
1.设备驱动device_driver
一个设备对应一个最合适的设备驱动程序。但是,一个设备驱动程序就有可能适用多个设备。设备驱动模型自动地探测新设备的产生,并为其分配最合适的设备驱动程序,这样新设备就能够适用了。驱动程序由以下结构体定义:
struct device_driver{
const char *name; /*设备驱动程序的名字*/
struct bus_type *bus; /*指向驱动属于的总线,总线上有很多的设备*/
struct module *owner; /*设备驱动自身模块*/
const char *mod_name; /*驱动模块的名字*/
/*探测设备的方法,并检测设备驱动可以控制那些设备*/
int (*probe)(struct device *dev);
int (*remove)(struct device *dev); /*移除设备时调用该方法*/
void (*shutdown)(struct device *dev); /*设备关闭时调用的方法*/
int (*suspend)(struct device *dev, pm_message_t state);
/*设备置于低功率状态时所调用的方法*/
int (*resume)(struct device *dev); /*设备恢复正常状态时所调用的方法*/
struct attribute_group **group; /*属性组*/
struct dev_pm_ops *pm; /*用于电源管理*/
struct driver_private *p; /*设备驱动的私有数据*/
};
- 3行的
bus
指针指向驱动所属的总线。 - 7行的
probe()
函数用来探测设备。也就是当总线设备驱动发现一个可能由它处理的设备时,会自动调用probe()
方法。在这个方法中会执行一些硬件初始化工作。 - 8行的
remove()
函数在移除设备时调用。同时,如果驱动程序本身被卸载,那么它所管理的每一个设别都会调用remove()
方法。 - 9~11行是当内核改变设备供电状态时,内核自动调用的函数。
- 12行时驱动所属的属性组,属性组定义了一组驱动共用的属性。
- 14行表示驱动的私有数据,可以用来存储与驱动相关的其他信息。
driver_private
结构体定义如下:
struct driver_private{
struct kobject kobj; /*内嵌的kobject结构,用来构建设备驱动模型的结构*/
struct klist klist_devices; /*该驱动支持的所有设备链表*/
struct klist_node knode_bus; /*该驱动所属总线*/
struct module_kobject *mkobj; /*驱动的模块*/
struct device_driver *driver; /*指向驱动本身*/
}
2.驱动举例
在声明一个device_driver
时,一般需要probe()、remove()、name、bus()、suspend()和resume()
等成员。下面是一个PCI的例子
static struct device_driver aulx00_pcmcia_driver = {
.probe = aulx00_drv_pcmcia_probe,
.remove = aulx00_drv_pcmcia_remove,
.name = "aulx00-pcmcia",
.bus = &platform_bus_type,
.suspend = pcmcia_socket_dev_suspend,
.resume = pcmcia_socket_dev_resume,
};
该驱动程序被挂接在平台总线(platform_bus_type
)上,这是一个很简单的例子。但是在现实中,大多数驱动程序会带有自己特定的设备信息,这些信息不是device_driver
可以全部包含。比较经典的例子是pci_driver
.
struct pci_driver{
struct list_head node;
char *name;
const struct pci_device_id *id_table;
...
struct device_driver driver;
struct pci_dynids dynids;
}
pci_driver
是由device_driver
衍生出来的,pci_driver
中包含了PCI设备特有的信息。
3.驱动程序注册和注销
设备驱动的注册与注销函数如下所示。
int driver_register(struct device_driver *drv);
void driver_unregister(struct device_driver *drv);
driver_register()
函数的功能是向设备驱动程序模型中加入一个新的device_driver
对象。但注册成功后,会在sysfs
文件系统下创建一个新的目录。该函数的代码如下:
int driver_register(struct device_driver *drv)
{
int ret; /*返回值*/
struct device_driver *other;
/*drv和drv所属的bus中主要有一个提供该函数即可,否则也只能调用bus的函数,
而不例会drv的函数。这种方式已经过时,推荐使用bus_type中方法*/
if((drv->bus->probe && drv->probe) ||
(drv->bus->remove && drv->remove) ||
(drv->bus->shutdown && drv->shutdown))
printk(KERN_WARNING"Driver %s needs updating -please use"
"bus_tye methods\n",drv->name);
other = driver_find(drv->name, drv->bus);/*总线中是否已经存在该驱动*/
if(other){
put_driver(other); /*减少驱动引用*/
printk(KERN_ERR "Error: Driver '%s' is already registerd,"
"aborting...\n",drv->name);
return -EEXIST;
}
ret = bus_add_driver(drv); /*将本drv驱动注册等级到drv->bus所在的总线*/
if(ret)
return ret;
ret = driver_add_groups(drv, drv->groups); /*将驱动加到所属组中*/
if(ret)
bus_remove_driver(drv);/*从总线中移除驱动程序*/
return ret;
}
driver_unregister()
函数用来注销驱动程序。该函数首先从驱动组中删除该驱动,然后再从总线中移除该驱动程序,代码如下:
void driver_unregister(struct device_driver *drv)
{
driver_remove_groups(drv, drv->groups); /*从组中移除该驱动*/
bus_remove_driver(drv);
}
4.驱动的属性
驱动的属性可以使用driver_attribute
表示,该结构体的定义如下:
struct driver_attribute{
struct attribute attr;
ssize_t (*show)(struct device_driver *driver, char *buf);
ssize_t (*store)(struct device_driver *driver, const char *buf, size_t count);
};
使用下面的函数可以再驱动所属目录创建和删除一个属性文件。属性文件中的内容可以用来控制驱动的某些特性,这两个函数是:
int driver_create_file(struct device_driver *drv, struct driver_attribute *attr);
void driver_remove_file(struct device_driver *drv, struct driver_attribute *attr);
小结
设备驱动模型是编写Linux驱动程序需要了解的重要知识。设备驱动模型中主要包含三大组件,分别是总线、设备和驱动。这三种结构之间的关系非常复杂,为了使驱动程序对用户进程来说可见的,内核提供了sysfs文件系统来映射设备驱动模型各组件的关系。通过本章的学习会对后面的学习有很大的帮助。