Linux及Linux设备驱动程序设计.docx

上传人:b****5 文档编号:29380880 上传时间:2023-07-22 格式:DOCX 页数:25 大小:155.50KB
下载 相关 举报
Linux及Linux设备驱动程序设计.docx_第1页
第1页 / 共25页
Linux及Linux设备驱动程序设计.docx_第2页
第2页 / 共25页
Linux及Linux设备驱动程序设计.docx_第3页
第3页 / 共25页
Linux及Linux设备驱动程序设计.docx_第4页
第4页 / 共25页
Linux及Linux设备驱动程序设计.docx_第5页
第5页 / 共25页
点击查看更多>>
下载资源
资源描述

Linux及Linux设备驱动程序设计.docx

《Linux及Linux设备驱动程序设计.docx》由会员分享,可在线阅读,更多相关《Linux及Linux设备驱动程序设计.docx(25页珍藏版)》请在冰豆网上搜索。

Linux及Linux设备驱动程序设计.docx

Linux及Linux设备驱动程序设计

 

1.主要功能

因为Linux系统将所有的外围设备都高度抽象成一些字节序列,并且以文件形式来表示这些设备。

所以Linux设备驱动程序被集成在内核中,构成了处理或操作硬件控制器的软件模块。

在实际处理中,将驱动程序作为常驻内存的低级硬件处理程序共享库,设备驱动程序形成了对设备的抽象处理。

也就是说,设备驱动程序是内核中具有高特权级的、常驻内存的、可共享的下层硬件处理例程。

我们知道,仅仅瘵物理设备与计算机系统简单相连,并不能使外部设备为用户提供各种所需要的操作,在系统中还须为各种设备配备相应的动作程序。

除了CPU、内存和少数几个设备外,在程序执行中几乎所有的系统操作最终都要映射到一个物理设备上,对设备的控制操作通常由该设备的特殊可执行代码实现,这些代码就是设备驱动程序。

通俗地讲,驱动程序是用来控制计算机外围设备的程序。

Linux设备驱动程序中包含了如何控制这些设备的技术细节,并通过特定的接口导出一个规范的操作集合,内核模块使用规范的设备接口(即字符设备接口和块设备接口),通过文件系统接口把设备操作导出到用户程序中。

2.功能模块

设备驱动程序是一些函数和数据结构的集合,这些函数和数据结构是用于实现管理设备的一个简单接口。

操作系统内核使用这个接口来请求驱动程序对设备进行I/O操作,甚至,我们可以把设备驱动程序看成一个抽象数据模型,它为计算机中的每个硬件设备都建立了一个通用函数接口。

由于一个设备驱动程序就是一个模块,所以在内核内部用一个file结构来识别设备驱动程序,而且内核使用file_operations结构来访问设备驱动程序,需要理解代码的如下几个部分:

●驱动程序的注册与注销。

●设备的打开与释放。

●设备的读写操作。

●设备的控制操作。

●设备的中断和轮询处理。

 

根据几个部分的代码的划分将设备驱动程序模块划分成几个模块:

如图2-1所示。

图2-1程序模块划分图

2.1.字符设备驱动程序的基本数据结构

2.1.1.file_operations结构

在linux系统中,设备驱动程序所提供的一组入口点用一个结构向系统进行说明,此结构定义为:

#include

structfile_operations{

loff_t(*llseek)(structflie*,loff_t.,int);

ssize_t(*read)(structfile*,char*,size_t,loff_t*);

ssize_t(*write)(structfile*,constchar*,size_t,loff_t*);

int(*ioct)(structinode*,structfile*,unsignedint,unsignedint,unsignedlong);

int(*open)(structinode*,structfile*);

int(*release)(structinode*,structfile*);

};

这里只列出scull代码中实现的一些字段,事实上,该结构的规模随着Linux内核的更新将会不断扩大。

这个结构中的每一个成员名字都对应着一个系统调用。

用户进程利用系统调用在对设备文件进行诸如read/write操作时,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数。

这是Linux设备驱动程序的基本工作原理。

既然是这样,编写设备驱动程序的主要工作就是编写这些子函数,并填充file_operations的各个域。

接下来谈谈file_operations结构的实例化问题。

随着内核不断增加新的功能,file_operations结构已逐渐变得越来越强大。

新增加的操作自然会给设备驱动程序带来移植性问题。

每个驱动程序中该结构的实例都是用标准的C语法声明的,一般是将新的操作添加在该结构的末尾,这样对驱动程序简单的进行一次重新编译,这些操作都会被赋予NULL值,因此也就选择为默认的行为。

在新的Linux系统中,内核开发人员转而采用一种“标记化”的初始化格式,这种初始化的方法在Linux设备驱动程序开发中已被广泛采用,它的方法如下:

structfile_operationchr_fops={

read:

scull_read,

write:

scull_write,

ioctl:

scull_ioctl,

release:

scull_release,

};//这里标记的名字来自于scull代码中实现的方法

这种格式允许用名字来对这类结构的字段进行初始化,这种初始化处理并不是标准C的规范,而是对GNU编译器的一种特殊扩展。

这种语法的好处在于,驱动程序在结构的定义发生变化时更具有可移植性,并且使得代码更加紧凑且易读。

标记化的初始方法允许对结构成员进行重新排列。

在某些场合下,将频繁被访问的成员放在相同的硬件缓存上,可以大大提高执行性能。

2.1.2.file结构

file结构主要用于与文件系统对应的设备驱动程序。

当然,其他设备驱动程序也可以使用它。

file结构代表一个打开的文件,它由内核在open时创建,并传递给在该文件上进行操作的所有函数,直到碰到最后的close函数。

在文件的所有实例都被关闭之后,内核会释放这个数据结构。

注意:

file结构与用户空间程序中的File没有任何关联,File在C库中定义且不会出现在内核代码中;而structfile是一个内核结构,它不会出现在用户程序中。

它提供关于被打开的文件的信息,定义为:

#include

Structfile{

mode_tf_mode;//文件模式

loff_tf_pos;//当前读与写的位置

unsignedintf_flags;//文件标志

structfile_operations*f_op;//与文件相关的操作

void*private_data;//跨系统调用时保存状态信息的非常有用的资源

structdentry*f_dentry;//文件对应的目录项结构

};

实际的结构中还有一些其他的字段,但它们对于设备驱动程序并没有什么用处。

由于驱动程序从来都不填写file结构,而只是对别处创建的file结构进行访问,所以可以忽略这些字段。

2.2.字符设备驱动程序的初始化和清除函数

2.2.1初始化函数scull_init()的实现

该函数负责注册字符设备进行初始化,它的主要功能之一是在内核中注册设备驱动程序,具体调用是通过register_chrdev()函数。

register_chrdev()函数定义如下:

#include

#include

intregister_chrdev(unsignedintmajor,const*name,structfile_opertions*fops);

其中major是为设备驱动程序向系统申请的主设备号,如果为0,则系统为此驱动程序动态的分配一个设备号。

Name是设备号,fops是前面定义的file_operations结构的指针。

在注册成功的情况下,如果指定了major,则register_chrdev()函数的返回值为0;如果major值为0,则返回内核分配的主设备号。

若register_chrdev()函数操作成功,设备名就会出现在/proc/devices文件里;在注册失败的情况下,register_chrdev()函数返回值为负数。

字符设备注册以后,还必须在文件系统中为其创建一个代表节点。

该节点可以是在/dev目录中的一个节点,这种节点都是文件节点,且每个节点代表一个具体的设备。

不过要有主设备号和从设备号两个参数才能创建一个节点。

还可以是在devfs设备文件目录下的一个节点,对于这种节点,应根据主设备号给每一种设备都创建一个目录节点,在这个目录下才是代表具体设备的文件节点。

初始化部分一般还负责给设备驱动申请系统资源,包括内存、中断、时钟、I/O端口等。

这些资源也可以在open()子程序或别的地方申请,当这些资源不用时,应该释放它们,以利于资源的共享。

2.1.1.清除函数scull_exit()的实现

该函数负责对设备进行注销,它的主要功能是在内核种注销设备驱动程序,具体调用是通过unregister_chrdev()函数。

unregister_chrdev()函数的定义如下:

#include

#include

intunregister_chrdev(unsignedintmajor,constchar*name);

参数列表包括要释放的主设备号和相应的设备名。

参数中的这个设备名会被内核用来和主设备名参数所对应的已注册设备名进行比较,如果不同,则返回-EINVAL。

如果主设备号超出了所许的范围,则内核同样返回-EINVAL。

此外,注销部分还负责释放设备驱动程序初始化时申请的系统资源。

还需要补充一点,新的Linux内核需要在源文件的末尾对驱动模块的初始化和清除函数进行标记:

modue_init(scull_init);

module_exit(scull_exit);

要使用module_init和module_exit,则代码必须包含头文件

这样做的好处是内核中每个初始化和清除函数都有一个唯一的名字,因而给调试驱动程序带来方便。

然而,如果我们仍然使用老的初始化(init_module)和清除函数(cleanup_module)的名字,则不需要使用函数module_init和module_exit进行标记。

实际上,这两个函数对模块所做的唯一事情就是将init_module和cleanup_module定义为给定函数的名字。

2.2.字符设备驱动程序的入口点

2.2.1.open()操作的实现

open()操作的(或称入口点)给驱动程序提供初始化的能力,同时一般还会递增设备的使用计数,防止在文件关闭前模块被卸载出内核。

这个计数值在release()方法中被递减。

在大部分驱动程序中,open()应完成如下工作:

●递增使用计数。

●检查设备特定的错误(诸如设备未就绪和类似的硬件问题)。

●如果设备是首次打开,则对其进行初始化。

●识别次设备号,并且如果有必要,更新f_op指针。

●分配并填写被置于file->private_data中的数据结构。

需要说明的是,早期版本的内核要求模块自己处理有关维护其使用计数的所有工作,常常使用宏定义MOD_INC_USE_COUNT和MOD_DEC_USE_COUNT去维护使用驱动程序的用户数。

而新的内核版本通过file_operations结构中的owner字段维护模块的使用计数。

scull源代码中仅仅实现open()的部分功能和任务。

在一个真正的字符设备驱动程序中,open()操作远远比本实验给出的要复杂。

本实验的目的是编写一个简单的驱动程序,而不是编写一个能够真正驱动硬件的程序。

故后面给出的几个操作是用尽可能简单的方式实现的。

2.2.2.release()操作的实现

release()操作的作用正好与open()相反。

这个设备方法应该完成下面的任务:

●释放由open()分配、保存在file->private_data中的所有内容。

●检查设备特定的错误(诸如设备未就绪和类似的硬件问题)。

●在最后一次关闭操作时关闭设备。

●使用计数减1。

scull_release()的实现源代码很少,也很容易实现,这里省略不谈。

如果在open()期间递增使用计数的话,则不应忘记对其递减,因为如果使用计数不归0,内核就无法卸载模块。

2.2.3.read()操作的实现

read()的功能是从内核地址空间拷贝数据到用户地址空间,这种能力是由下面的内核函数提供的,它用于拷贝任意的一段字节序列,这也是每个read()方法的实现的核心部分:

unsignedlongcopy_to_user(void*to,constvoid*from,unsighedlongcount);

read()方法的原型为:

ssize_tread(structfile*filp,char*buff,size_tcount,loft_t*offp);

参数filp是文件指针,参数buff是指向用户空间的缓冲区,参数count是请求传输的数据长度,参数offp是一个指明用户在文件中进行存取操作的位置的指针。

不同的返回值将有不同的解释:

●返回值等于参数count,说明所请求的字节数传输成功。

●返回值小于参数count,但是为正,则说明只有部分数据传输成功。

●返回值等于0,则表明已经到达了文件尾。

●负值意味着发生了错误,该值指明发生了什么错误,错误码在中的定义。

2.2.4.write()操作的实现

与read()相反,write()的功能是用户地址空间拷贝数据到内核地址空间,这种能力是由下面内核函数提供的:

unsignedlongcopy_from_user(void*to,constvoid*from,unsighedlongcount);

write()方法的原型为:

ssize_twrite(structfile*filp,constchar*buff,size_t*offp);

参数filp是文件指针,参数buff是指向用户空间的缓冲区,参数count是请求传输的数据长度,参数offp是一个指明用户在文件中进行存取操作的位置的指针。

与read()方法的返回值类似,根据不同的值将有不同的解释:

●返回值等于参数count,说明所请求的字节数传输成功。

●返回值小于参数count,但是为正,则说明只有部分数据传输成功。

●返回值等于0,则表明什么都没写入。

●负值意味着发生了错误,与read()方法相同,错误码在中的定义。

2.2.5.llseek()操作的实现

llseek()操作是一个很有用且容易实现的方法,用来修改文件的当前读写位置,并将新位置作为(正的)返回值返回。

它的函数原型如下:

loff_t(*llseek)(structfile*filp,loff_t,intwhence)

参数loff_t表示文件读写指针将要偏移的量,参数whence表示文件指针的偏移起点。

文件读写指针常常有三种偏移情况:

●从文件的开始位置偏移。

●从文件的当前位置偏移。

●从文件尾偏移

2.3.设备驱动程序的编译、装卸和卸载

设备驱动程序的编译方式

在Linux系统中,编写完成的设备驱动程序可以用两种方式进行编译:

⑴直接修改系统核心源代码,把设备驱动程序编译进内核里。

这种方式带来的问题是对编写的程序调试不方便,每次调试都重新编译内核,而编译内核是一个很缓慢的过程,同时如果编译的驱动程序出现问题,常常会导致整个Linux内核的崩溃。

这种方式还带了另外一个问题,如果所有的设备驱动程序都直接链入系统内核,将使内核变得非常庞大,有可能使得物理内存不够用(因为内核是常驻内存的)。

⑵设备驱动程序被编译成可加载的模块,由系统管理员动态加载和卸载。

模块化设备驱动程序不会出现第一种方式中的问题,以模块出现的设备驱动程序只会在系统需要时才装载到系统核心中,这时它占用内存;当不需要时,又可随时被卸载,释放内存。

这也可以使开发设备驱动的周期大为缩短,因为无需为了调试程序而重新编译内核了。

当前,Linux系统设备驱动程序主要以模块化的为主,对于经常使用的驱动如网卡驱动常常被直接编译进内核,而像CDROM驱动、USB驱动、I/O驱动等常被编译写成模块。

因此,编写模块化的设备驱动程序是大部分驱动程序开发人员的主要工作。

2.3.1.设备驱动模块的编译

当设计并编写好设备驱动模块后,必须将其编译成一个适合内核装载的对象文件。

由于模块是用C语言来编写完成的,故采用gcc编译器来进行编译。

若需要通知编译程序把这个模块作为内核代码而不是普通的用户代码编译时,就需要向gcc编译器传递参数“D__KERNEL__”;若需要通知编译程序这个文件是一个模块而不是一个普通文件,则需要向gcc编译器传递参数“_DEODULE”;如果需要展开头文件定义的内联函数,则需要使用“-O2”参数;若还需要对装载后的模块进行调试,则需要程序在编译完成这个模块文件后不调用链接程序。

当然,如果在头文件中定义了宏__KERNEL__和MODULE,那么编译时就无需添加这两个参数了。

一般编译模块文件的命令格式如下:

#gcc–o2–g–wall–DMODULE–D__KERNEL__-cscull.c

执行命令后就会得到文件scull.o,该文件就是一个可装载的目标代码文件。

补充一点,如果希望有更高效的开发过程,那么make将是一个很合适的编译助手,它将替程序员省去很多烦心而又容易出错的手动编译程序,使得程序员有更多的精力去编程。

scull代码的编译就是由make工具完成的,避开了使用C编译的又长又烦的命令和参数。

当然,要熟练掌握make工具的使用,还需要进一步学习make相关知识。

2.3.2.设备驱动模块的装载

设备驱动模块的装载方式有两种。

一种是使用insmod命令手工装载模块;另一种是请求装载demandloading(在需要时装载模块),即当用户安装了核心中不存在的文件系统,有必要装载某个模块时,核心将请求内核守护进程kerneld准备装载适当的模块。

该内核守护进程是一个带有超级用户权限的普通用户进程。

此实验中主要采用insmod命令手工装载模块。

系统启动时,kerneld开始执行,并为内核打开一个IPC通道,内核通过向kernel发送消息请求执行各种任务。

kerneld的主要功能是装载和卸载内核模块,kerneld自身并不执行这些任务,它通过某些程序(如insmod)来完成。

kerneld只是内核的处理,只为内核进程进行调度。

insmod程序必须找到请求装载的内核模块(该请求装载的模块一般保存在/lib/modules/kernel-version中或当前目录中)。

这些模块与系统中的其他程序一样是已连接的目标文件,但不同的是它们连接成可重定位的映象(即映象没有被连接到特定的地址上运行,其文件格式是a.out或ELF)。

也就是说,模块在用户空间(使用适当的标志)进行编译,结果产生一个可执行格式的文件。

在用insmod命令转载一个模块时,将会发生如下事件:

⑴新模块(通过内核函数create_module())加入到内核地址空间。

⑵insomd执行一个特权级系统调用get_kernel_syms()函数,以找到内核的输出符号(一个符号表示为符号名和符号值,如地址值)。

⑶create_module()为这个模块分配内存空间,并将新模块添加在内核模块链表的尾部,然后将新模块标记为NUINITIALIZED(模块未初始化)。

⑷通过init_module()系统调用装载模块。

(该模块定义的符号在此时被导出,供其他可能后来装载的模块使用。

⑸insmod为新装载的模块调用init_module()函数,然后将新模块标志为RUNNING(模块正在运行)。

在执行insmod命令后,就可以在/proc/module文件中看到装载的新模块了(为证实其正确性,可在执行insomd命令之前先查看/proc/module文件,执行之后再查看比较)。

2.3.3.设备驱动模块的卸载

当一个模块不再使用时,可以使用rmmod命令卸载该模块,由于无需连接,故此任务比加在模块要简单的多。

但如果请求转载模块的使用计数为0时,kerneld将自动从系统中卸载该模块。

卸载时调用模块cleanup_module()释放分配给该模块的内核资源,并将其标志为DELETED(模块被卸载);同时断开内核模块链表中的连接,修改它所依赖的其他模块的引用,重新分配模块所占的内核存储空间。

测试函数

字符设备驱动程序编译加载后,就可以在/dev目录下创建字符设备文件chrdev(当然,在其他任意目录下创建字符设备文件也可以),使用命令:

#mkond/dev/chrdevcmajorminor

其中“C”表示chrdev是字符设备,“major”是chrdev的主设备号。

当该字符设备驱动程序编译加载后,可在/proc/devices文件中获得主设备号,或者使用命令:

#cat/proc/devices|awk“\\$2==\“chrdev\”{print\\$1}”

获得主设备号,在后面的源代码中将会给出一个测试函数。

 

3.程序流程图

Linux设备驱动程序主要流程图

 

4.程序调试

4.1.用vi编辑器编写程序

编写初始化和注销函数:

scull_init()和scull_exit().

编写5个基本的设备接口函数,包括scull_open()、scull_write()、scull_read()、scull_lseek()和scull_release().

4.2.头文件无法找到

在指定的路径目录下没有找到需要的uaccess.h文件,该指定文件的路径为linux/include/asm/uaccess.h,通过目录查找文件查找没有该文件。

开始时,在当系统的指定目录下查看该最底级的目录,linux/include/asm下,但没有找到。

查阅了一些书,但还是没有找着。

上网检索了一翻后,也没有很好的办法,在网络上有许多人遇到同样的问题,根据网上的回答,分析可能是在系统中的其它目录下面。

我们开始从根目录下一一查找,一级一级深入,最后在目录usr/src/linux/include/下找到的asm相关的目录,在这些目录中有很多的uaccess.h文件。

试着把指定的路径修改后,再编译,经过分析,可能是系统的版本不一样,当前linux的版本为红旗的。

最后,经过老师的帮助查找,更换了服务器后,最终找到指定的文件。

 

4.3.程序运行结果

5.总结

一周的时间虽然短暂,但却让我充分接触了linux的一些实质性的东西,理论指导实践,通过一周的课程设计,利用老师教授的知识完成了linux设备驱动程序的设计。

虽然过程中有很多不会和不熟练的地方,不过在老师的指导与同学的帮助下也慢慢的得到了解决,学会去解决问题这本身也是一个很有意义的过程,从中可以使自己学到更多的知识和技能。

通过本次课程设计,初步了解Linux操作系统中字符设备驱动程序的基本组成,学会编写简单的、模块化的字符设备驱动程序,以及在不重新编译内核的情况下,动态地装卸和卸载以模块形式存在的字符设备驱动程序进行测试,最终了解Linux操作系统是如何管理字符设备的。

在这一次课程设计过程中得到了指导老师的帮助,在此,感谢各位指导老师,谢谢你们!

 

6.附录:

字符设备驱动程序源代码

#ifndef_KERNEL_

#define_KERNEL_

#endif

#ifndefMODULE

#defineMODULE

#endif

#include

#include

MODULE_LICENSE("GPL");

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 工作范文 > 行政公文

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1