Linux驱动开发实战(一)——设备驱动模型

这篇具有很好参考价值的文章主要介绍了Linux驱动开发实战(一)——设备驱动模型。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。


前言

在早期的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文件系统的关系。
Linux驱动开发实战(一)——设备驱动模型,Linux驱动开发,linux,驱动开发

sysfs文件系统的目录结构

sysfs文件系统中包含了一些重要的目录,这些目录中包含了与设备和驱动等相关的信息,现对其详细介绍如下:
1.sysfs文件系统的目录
sysfs文件系统与其他文件系统一样,由目录、文件、链接组成。与其他文件系统不同的是,sysfs文件系统表示的内容与其他文件系统中的内容不同。另外,sysfs文件系统只存在于内存中,动态的表示着内核的数据结构。
sysfs文件系统挂接了一些子目录,这些目录代表了注册sysfs中的主要子系统。
要查看这些子目录和文件,可以使用ls命令,命令执行如下:
Linux驱动开发实战(一)——设备驱动模型,Linux驱动开发,linux,驱动开发
当设备启动时,设备驱动模型会注册kobject对象,并在sysfs文件系统中产生以上的目录。现对其中的主要目录所包含的信息进行说明。
2. block目录
块目录包含了在系统中发现的每个块设备的子目录,每个块设备对应一个子目录。每个块设备的目录中有各种属性,描述了设备的各种信息。例如设备的大小、设备号等。块设备目录中有一个表示I/O调度器的目录,这个目录中提供了一些属性文件。它们是关于设备请求队列信息和一些可调整的特性。用户和管理员可以用它们优化性能,包括用它们动态改变I/O调度器。块设备的每个分区表示为块设备的子目录,这些目录中包含了分区的读写属性。
3. bus目录
总线目录包含了在内核中注册而得到支持的每个物理总线的子目录,例如ide、pci、scsi、i2c和pnp总线等。使用ls命令可以查看bus目录的结构信息,如下所示:
Linux驱动开发实战(一)——设备驱动模型,Linux驱动开发,linux,驱动开发
ls命令列出了注册到系统中的总线,其中每个目录中的结构都大同小异。这里以usb目录为例,分析其目录的结构关系。使用cd usb命令,进入usb目录,然后使用ls命令列出usb目录中包含的目录和文件,如下所示:
Linux驱动开发实战(一)——设备驱动模型,Linux驱动开发,linux,驱动开发
usb目录中包含了devices和drivers目录。devices目录包含了USB总线下所有设备的列表,这些列表实际上是指向设备目录中相应设备的符号链接。使用ls命令查看如下所示。
Linux驱动开发实战(一)——设备驱动模型,Linux驱动开发,linux,驱动开发
Linux驱动开发实战(一)——设备驱动模型,Linux驱动开发,linux,驱动开发
其中1-0:1.02-0:1.0是USB设备的名字,这些名字由USB协议规范来定义。可以看出devices目录下包含的是符号链接,其指向/sys/devices目录下的相应硬件设备。硬件的设备文件是在/sys/devices/目录及其子目录下,这个链接的目的是为了构建sysfs文件系统的层次结构。
drivers目录包含了USB总线下注册时所有驱动程序的目录。每个驱动目录中有允许查看和操作设备参数的属性文件,和指向该设备所绑定的物理设备的符号链接。
class目录
类目录中的子目录表示每一个注册到内核中的设备类。例如固件类(firmware)、混杂设备类(misc)、图形类(graphics)、声音类(sound)和输入类(input)等。这些类如下所示。
Linux驱动开发实战(一)——设备驱动模型,Linux驱动开发,linux,驱动开发
类对象只包含一些设备的总称,例如网络类包含一切的网络设备,集中在/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设备,则这些设备的关系如下图所示。
Linux驱动开发实战(一)——设备驱动模型,Linux驱动开发,linux,驱动开发
sysfs文件系统,这些设备使用树形目录来表示,如下所示。
Linux驱动开发实战(一)——设备驱动模型,Linux驱动开发,linux,驱动开发
树形结构中每个目录与一个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行,检查kobjktype是否合法,它们都不应该是一个空指针。
  • 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,连接kobjetcentry链表等。
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()函数的路径如下图所示。
Linux驱动开发实战(一)——设备驱动模型,Linux驱动开发,linux,驱动开发
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的指针,如下图所示。
Linux驱动开发实战(一)——设备驱动模型,Linux驱动开发,linux,驱动开发
结合上图解释几个重要的问题。

  • kobject始终代表sysfs文件系统中的一个目录,而不是文件。对kobject_add()函数的调用将在sysfs文件系统中创建一个目录。最底层目录对应于系统中的一个设备、驱动或者其他内容。通常一个目录中包含一个或者多个属性,以文件的方式表示,属性由ktype指向。
  • kobject对象的成员namesysfs文件系统中的目录名。通常使用kobject_set_name()函数来设置。在同一个目录下,不能有相同的目录名。
  • kobjectsysfs文件系统中的位置由parent指针指定。parent指针指向一个kobejct结构体,kobject对应一个目录。
  • kobj_typekobject的属性。一个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_typedefault_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相对应。
Linux驱动开发实战(一)——设备驱动模型,Linux驱动开发,linux,驱动开发

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()函数时,会产生热插拔事件。热插拔事件产生时,内核会根据kobjectkset指针找到所属的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的关系

ksetkobject的一个集合,用来与kobject建立层次关系。内核可以将相似的kobject结构连接在kset集合中,这些相似的kobject可能有相似的属性,使用统一的kset来表示。下图显示了kset集合和kobject之间的关系。
Linux驱动开发实战(一)——设备驱动模型,Linux驱动开发,linux,驱动开发

  • kset集合包含了属于其的kobject结构体,kset.list链表用来连接第一个和最后一个kobject对象。第一个kobject使用entry连接kset集合和第二个kobejct对象。第二个kobject对象使用entry连接第一个kobject对象和第三个kobject对象,依次类推,最终形成一个kobject对象的链表。
  • 所有kobject结构的parent指针指向kset包含的kobejct对象,构成一个父子层次关系。
  • kobject的所有kset指针指向包含它的kset集合,所以通过kobject对象很容易就能找到kset集合。
  • kobjectkobj_type指针指向自身的kobj_type,每一个kobject都有一个单独的kobj_type结构。另外在kset集合中也有一个kobject结构体,该结构的xxx也指向一个kobj_type结构体。从前文知道,kobj_type中定义了一组属性和操作属性的方法。这里需要注意的是,ksetkobj_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也有引用计数,该引用计数由ksetkobj成员来维护。可以使用kset_get()函数增加引用计数,使用kset_put()函数减少引用计数。这两个函数的原型如下:

static inline struct kset *kset_get(struct kset *k);
static inline void kset_put(struct kset *k);

注册kobject到sysfs中的实例

kobjectkset有所了解后,本节将讲解一个实例程序,以使读者对这些概念有更清楚的认识。这个实例程序的功能是:在/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进行初始化,并将kobjectkobj_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的目录,如下所示。
Linux驱动开发实战(一)——设备驱动模型,Linux驱动开发,linux,驱动开发
进入kobject_test目录,在该目录下有一个名为kobject_test_attr属性文件,如下所示。
Linux驱动开发实战(一)——设备驱动模型,Linux驱动开发,linux,驱动开发
使用echo命令和cat命令可以对这个属性文件进行读写,读写时,内核里调用的分别是kobject_test_show()kobject_test_store()函数。这两个函数分别用来显示和设置属性的名字,测试过程如下:
Linux驱动开发实战(一)——设备驱动模型,Linux驱动开发,linux,驱动开发

设备驱动模型的三大组件

设备驱动模型有三大重要组件,分别是总线(bus_type)、设备(device)和驱动(driver)。下面对这三个重要的组件进行分别介绍。

总线

从硬件结构上来讲,物理总线有数据总线和地址总线。物理总线是处理器与一个或者多个设备之间的通道。在设备驱动模型中,所有设备都通过总线连接,此处的总线与物理总线不同,总线是物理总线的一个抽象,同时还包含一些硬件中不存在的虚拟地址总线。在设备驱动模型中,驱动程序是附属在总线上的。下面将首先介绍总线、设备和驱动之间的关系。
1.总线、设备、驱动关系
在设备驱动模型中,总线、设备和驱动三者之间紧密联系。如下图所示,在/sys目录下,有一个bus目录,所有的总线都在bus目录下有一个新的子目录。一般一个总线目录有一个设备目录、一个驱动目录和一些总线属性文件。设备目录中包含挂接在该总线上的设备,驱动目录包含挂接在总线上的驱动程序。设备和驱动程序之间通过指针互相联系。
Linux驱动开发实战(一)——设备驱动模型,Linux驱动开发,linux,驱动开发
如上图所示,总线上的设备链表有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_typebus_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");

这段代码完成一个设备注册,其parentbus成员都是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文件系统来映射设备驱动模型各组件的关系。通过本章的学习会对后面的学习有很大的帮助。文章来源地址https://www.toymoban.com/news/detail-604452.html

到了这里,关于Linux驱动开发实战(一)——设备驱动模型的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 深入探讨Linux驱动开发:Linux设备树

    设备树(Device Tree,简称 DT)是一种在嵌入式系统中描述硬件设备的一种数据结构和编程语言。它用于将硬件设备的配置信息以树形结构的方式进行描述,以便操作系统(如 Linux)可以根据这些信息正确地识别、配置和管理硬件设备。 设备树最初被引入到 Linux 内核中,用于解

    2023年04月27日
    浏览(46)
  • Linux设备驱动开发 - 虚拟时钟Clock驱动示例

    By: fulinux E-mail: fulinux@sina.com Blog: https://blog.csdn.net/fulinus 喜欢的盆友欢迎点赞和订阅! 你的喜欢就是我写作的动力! 很多设备里面系统时钟架构极其复杂,让学习Clock驱动的盆友头大。这里我参考S3C2440的clock驱动写了一个virtual clock,即虚拟时钟驱动,分别包含clock的provider和

    2023年04月21日
    浏览(37)
  • Linux -- 字符设备驱动--LED的驱动开发(初级框架)

    看原理图确定引脚,确定引脚输出什么电平才能点亮 / 熄灭 LED 看主芯片手册,确定寄存器操作方法:哪些寄存器?哪些位?地址是? 编写驱动:先写框架,再写硬件操作的代码 注意 :在芯片手册中确定的寄存器地址被称为 物理地址 ,在 Linux 内核中无法直接使用。 需要使

    2024年04月28日
    浏览(31)
  • 【Linux驱动开发】设备树详解(三)设备树Kernel解析

    ​ ​ 活动地址:CSDN21天学习挑战赛 【Linux驱动开发】设备树详解(一)设备树基础介绍 【Linux驱动开发】设备树详解(二)设备树语法详解 【Linux驱动开发】设备树详解(三)设备树Kernel解析   个人主页:董哥聊技术 我是董哥,嵌入式领域新星创作者 创作理念:专注分享

    2023年04月24日
    浏览(49)
  • 嵌入式Linux驱动开发 04:基于设备树的驱动开发

    前面文章 《嵌入式Linux驱动开发 03:平台(platform)总线驱动模型》 引入了资源和驱动分离的概念,这篇文章将在前面基础上更进一步,引入设备树的概念。 在平台总线驱动模型中资源和驱动已经从逻辑上和代码组织上进行了分离,但每次调整资源还是会涉及到内核,所以现

    2024年02月16日
    浏览(66)
  • Linux驱动开发:设备树dts详解

    前言: 掌握设备树是 Linux 驱动开发人员必备的技能!因为在新版本的 Linux 中, ARM 相关的驱动全部采用了设备树(也有支持老式驱动的,比较少),最新出的 CPU 其驱动开发也基本都是基于设备树的,比如 ST 新出的 STM32MP157、NXP 的 I.MX8 系列等。本篇博客核心是系统性的学习设

    2024年02月17日
    浏览(55)
  • 新型LINUX驱动开发 DTS设备树

    1.为什么使用设备树 linux内核3.版本之后才有设备树。 没有设备树之前的板级信息都写在.c文件里面,导致内核臃肿。 因此将板级信息独立成格式,文件名为dts,一个平台对应一个dts。 2.dts dtb dtc dts是设备树源码文件。 dtb是将设备树dts编译以后得到的二进制文件。 dtc是将dt

    2024年02月09日
    浏览(46)
  • 嵌入式Linux系统中的设备驱动开发:从设备树到驱动实现

    大家好,今天给大家介绍 嵌入式Linux系统中的设备驱动开发:从设备树到驱动实现 ,文章末尾附有分享大家一个资料包,差不多150多G。里面学习内容、面经、项目都比较新也比较全! 可进群免费领取。 在嵌入式Linux系统中,设备驱动是连接硬件设备和操作系统之间的桥梁。

    2024年02月19日
    浏览(63)
  • Linux下PCI设备驱动开发详解(二)

    根据上一章的概念,PCI驱动包括PCI通用的驱动,以及根据实际需要设备本身的驱动。 所谓的编写设备驱动,其实就是编写设备本身驱动,因为linux内核的PCI驱动是内核自带的。 为了更好的学习PCI设备驱动,我们需要明白内核具体做了什么,下面我们研究一下,linux PCI通用的驱

    2024年01月19日
    浏览(46)
  • Linux下PCI设备驱动开发详解(一)

    PCI总线是目前应用最广泛的计算机总线标准,而且是一种兼容性最强,功能最全的计算机总线。 而linux作为一种开源的操作系统,同时也为PCI总线与各种新型设备互联成为可能。尤其被现在的异构计算GPU/FPGA、软硬结合新的方向广泛运用。 应用程序位于用户空间,驱动程序位

    2024年02月04日
    浏览(41)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包