ImageVerifierCode 换一换
格式:DOCX , 页数:12 ,大小:27.26KB ,
资源ID:8202941      下载积分:3 金币
快捷下载
登录下载
邮箱/手机:
温馨提示:
快捷下载时,用户名和密码都是您填写的邮箱或者手机号,方便查询和重复下载(系统自动生成)。 如填写123,账号就是123,密码也是123。
特别说明:
请自助下载,系统不会自动发送文件的哦; 如果您已付费,想二次下载,请登录后访问:我的下载记录
支付方式: 支付宝    微信支付   
验证码:   换一换

加入VIP,免费下载
 

温馨提示:由于个人手机设置不同,如果发现不能下载,请复制以下地址【https://www.bdocx.com/down/8202941.html】到电脑端继续下载(重复下载不扣费)。

已注册用户请登录:
账号:
密码:
验证码:   换一换
  忘记密码?
三方登录: 微信登录   QQ登录  

下载须知

1: 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。
2: 试题试卷类文档,如果标题没有明确说明有答案则都视为没有答案,请知晓。
3: 文件的所有权益归上传用户所有。
4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
5. 本站仅提供交流平台,并不能对任何下载内容负责。
6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

版权提示 | 免责声明

本文(Linux设备驱动笔记.docx)为本站会员(b****5)主动上传,冰豆网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。 若此文所含内容侵犯了您的版权或隐私,请立即通知冰豆网(发送邮件至service@bdocx.com或直接QQ联系客服),我们立即给予删除!

Linux设备驱动笔记.docx

1、Linux设备驱动笔记阐述Linux设备文件管理硬件设备有人说Linux已经成为过去式了,这是因为你不懂Linux,如果你想在了解Linux设备文件管理硬件设备,希望本文对你学习Linux设备文件管理硬件设备有帮助。设备管理是操作系统五大管理中最复杂的部分。与Unix系统一样,Linux系统采用设备文件统一管理硬件设备,从而将硬件设备的特性及管理细节对用户隐藏起来,实现用户程序与设备无关性。在Linux系统中,硬件设备分为两种,即块设备和字符设备。1 特别文件用户是通过文件系统与设备接口的,所有设备都作为特别文件,从而在管理上就具有一些共性。(1)每个设备都对应文件系统中的一个索引节点,都有一

2、个文件名。设备的文件名一般由两部分构成,第一部分是主设备号,第二部分是次设备号。主设备号代表设备的类型,可以惟一地确定设备的驱动程序和界面,如hd表示IDE硬盘,sd表示SCSI硬盘,tty表示终端设备等;次设备号代表同类设备中的序号,如hda表示IDE主硬盘,hdb表示IDE从硬盘等。(2)应用程序通常可以通过系统调用open( )打开设备文件,建立起与目标设备的连接。(3)对设备的使用类似于对文件的存取。打开设备文件以后,就可以通过read( )、write( )、ioctl( )等文件操作对目标设备进行操作。(4)设备驱动程序都是系统内核的一部分,它们必须为系统内核或它们的子系统提供一个

3、标准的 接口。例如,一个终端驱动程序必须为Linux内核提供一个文件I/O接口;一个SCSI设备驱动程序应该为SCSI子系统提供一个SCSI设备接口,同 时SCSI子系统也应为内核提供文件I/O和缓冲区。(5)设备驱动程序利用一些标准的内核服务,如内存分配等。另外,大多数Linux设备驱动程序都可以在需要时装入内核,不需要时可以卸载下来。处于应用层的进程通过文件描述字fd与已打开文件的file结构相联系。在文件系统层,按照文件系统的操作规则对该文件进行相应处理。对于一般文件(即磁盘文件),要进行空间的映射从普通文件的逻辑空间映射到设备的逻辑空间,然后在设备驱动层做进一步映射从设备的逻辑空间映射

4、到物理空间(即设备的物理地址空间),进而驱动底层物理设备工作。对于设备文件,则文件的逻辑空间通常就等价于设备的逻辑空间,然后从设备的逻辑空间映射到设备的物理空间,再驱动底层的物理设备工作。2 设备驱动程序和内核之间的接口Linux系统和设备驱动程序之间使用标准的交互接口。无论是字符设备、块设备还是网络设备的驱动程序,当内核请求它们提供服务时,都使用同样的接口。Linux提供了一种全新的机制,就是“可安装模块”。可安装模块是可以在系统运行时动态地 安装和拆卸的内核模块。利用这个机制,可以根据需要在不必对内核重新编译连接的条件下,将可安装模块动态插入运行中的内核,成为其中一个有机组成部分,或 者从

5、内核卸载已安装的模块。设备驱动程序或与设备驱动紧密相关的部分(如文件系统) 都是利用可安装模块实现的。在应用程序界面上,利用内核提供的系统调用来实现可安装模块的动态安装和拆卸。但通常情况下,用户是利用系统提供的插入模块工具和移走模块工具来装卸可安装模块。插入模块的工作主要如下:(1) 打开要安装的模块,把它读到用户空间。这种“模块”就是经过编译但尚未连接的.o文件。(2) 必须把模块内涉及对外访问的符号(函数名或变量名)连接到内核,即把这些符号在内核映像中的地址填入该模块需要访问这些符号的指令及数据结构中。(3) 在内核创建一个module数据结构,并申请所需的系统空间。(4) 最后,把用户空

6、间中完成了连接的模块映像装入内核空间,并在内核中“登记”本模块的有关数据结构(如file_operations结构),其中有指向执行相关操作函数的指针。如前所述,Linux系统是一个动态的操作系统。用户根据工作中的需要,会对系统中设备重新配置,如安装新的打印机、卸载老式终端等。这样,每当Linux系统内核初启时,它都要对硬件配置进行检测,很有可能会检测到不同的物理设备,就需要不同的驱动程序。在构建系统内核时,可以使用配置脚本将设备驱动程序包含在系统内核中。在系统启动时对这些驱动程序初始化,它们可能未找到所控制的设备,而另外的设备驱动程序可以在需要时作为内核模块装入到系统内核中。为了适应设备驱动

7、程序动态连接的特性,设备驱动程序在其初始化时就在系统内核中进行登记。Linux系统利用设备驱动程序的登记表作为内核与驱动程序接口的一部分,这些表中包括指向有关处理程序的指针和其它信息。linux设备驱动(十五)-与硬件通信(1)I/O端口和I/O内存:每种外设都是通过读写寄存器来进行控制。在硬件层,内存区和I/O区域没有概念上的区别: 它们都是通过向在地址总线和控制总线发出电平信号来进行访问,再通过数据总线读写数据。因为外设要与IO总线匹配,而大部分流行的 I/O 总线是基于个人计算机模型(主要是 x86 家族:它为读和写 I/O 端口提供了独立的线路和特殊的 CPU 指令),所以即便那些没有

8、单独I/O 端口地址空间的处理器,在访问外设时也要模拟成读写IO端口。这一功能通常由外围芯片组(PC 中的南北桥)或CPU 中的附加电路实现(嵌入式中的方法)。Linux 在所有的计算机平台上实现了 I/O 端口。但不是所有的设备都将寄存器映射到 I/O 端口。虽然ISA设备普遍使用 I/O 端口,但大部分 PCI 设备则把寄存器映射到某个内存地址区,这种 I/O 内存方法通常是首选的。因为它无需使用特殊的处理器指令,CPU 核访问内存更有效率,且编译器在访问内存时在寄存器分配和寻址模式的选择上有更多自由。(2)I/O寄存器和常规内存:尽管硬件寄存器和内存非常相似,但程序员在访问I/O寄存器的

9、时候必须注意避免由于CPU或编译器不恰当的优化而改变预期的I/O操作(也即对寄存器的地址都声明为volatile)。I/O寄存器和RAM的最主要区别就是I/O操作具有边际效应(其实边际效应就是对I/O寄存器操作,导致高低电平的变化,从而促使硬件进行相对应的行为)。因为存储单元的访问速度对 CPU 性能至关重要,编译器会对源代码进行优化,主要是: 使用高速缓存保存数值 和 重新编排读/写指令顺序。但对I/O 寄存器操作来说,这些优化可能造成致命错误。因此,驱动程序必须确保在操作I/O 寄存器时,不使用高速缓存,且不能重新编排读/写指令顺序。解决的方法:对于硬件自身缓存引起的问题:只要把底层硬件配

10、置成在访问I/O区域时禁止硬件缓存即可。对于编译器优化和硬件重新排序引起的问题:对硬件必须以特定顺序执行的操作之间设置内存屏障。Linux提供了以下宏来解决可能的排序问题:#include,void barrier(void) 这个函数通知编译器插入一个内存屏障,但对硬件没影响。编译后的代码会把当前CPU寄存器的所有修改过的数值保存到内存中,需要这些数据时再读出来。对barrier的调用,可阻止在屏障前后的编译器优化,但硬件能完成自己的重新排序。其实中并没有这个函数,因为它是在kernel.h包含的头文件compiler.h中定义的*/#include #define barrier() _m

11、emory_barrier()但在内核中也有如下定义方式:#define barrier() _asm_volatile(:memory)CPU越过内存屏障后,将刷新自已对存储器的缓冲状态。这条语句实际上不生成任何代码,但可使gcc在barrier()之后刷新寄存器对变量的分配。#include voidrmb(void);/*保证任何出现于屏障前的读在执行任何后续的读之前完成*/voidwmb(void);/*保证任何出现于屏障前的写在执行任何后续的写之前完成*/voidmb(void);/*保证任何出现于屏障前的读写操作在执行任何后续的读写操作之前完成*/voidread_barrier_

12、depends(void);/*一种特殊的、弱些的读屏障形式。rmb 阻止屏障前后的所有读指令的重新排序,read_barrier_depends 只阻止依赖于其他读指令返回的数据的读指令的重新排序。区别微小, 且不在所有体系中存在。除非你确切地理解它们的差别, 并确信完整的读屏障会增加系统开销,否则应当始终使用 rmb。*/*以上指令是barrier的超集*/voidsmp_rmb(void);voidsmp_read_barrier_depends(void);voidsmp_wmb(void);voidsmp_mb(void);/*仅当内核为 SMP 系统编译时插入硬件屏障; 否则, 它

13、们都扩展为一个简单的屏障调用。*/这里介绍个小资料1.内核中往往有如下语句:#define _set_task_state(tsk,state_value) do (tsk)-state = state_value; while(0)#define set_task_state(tsk,state_value) set_mb(tsk)-state,state_value)两者区别在于:set_task_state(tsk,state_value)带有一个memory barrier,而_set_task_state却没有。当task的state为RUNNING时,由于scheduler可能会访

14、问这个state,因此此时要改变为其他状态(如INTERRUPTIBLE),则应该用set_task_state来保证其原子性。而当state不为RUNNING时,因为没有人会访问task,所以可以用_set_task_state。但用set_task_state总是安全的,但_set_task_state会比较快。2.在include/asm-i386/system.h中,定义了如下一条语句:#define mb() _asm_ _volatile_ (lock; addl $0,0(%esp): : :memory)分析如下几点:1)set_mb(),mb(),barrier()函数追踪到

15、底,就是_asm_ _volatile_(:memory),而这行代码就是内存屏障。2)_asm_用于指示编译器在此插入汇编语句3)_volatile_用于告诉编译器,严禁将此处的汇编语句与其它的语句重组合优化。即:原原本本按原来的样子处理这这里的汇编。4)memory强制gcc编译器假设RAM所有内存单元均被汇编指令修改,这样cpu中的registers和cache中已缓存的内存单元中的数据将作废。cpu将不得不在需要的时候重新读取内存中的数据。这就阻止了cpu又将registers,cache中的数据用于去优化指令,而避免去访问内存。5):表示这是个空指令。barrier()不用在此插入一

16、条串行化汇编指令。在后文将讨论什么叫串行化指令。6)_asm_,_volatile_,memory在前面已经解释7)lock前缀表示将后面这句汇编语句:addl $0,0(%esp)作为cpu的一个内存屏障。8)addl $0,0(%esp)表示将数值0加到esp寄存器中,而该寄存器指向栈顶的内存单元。加上一个0,esp寄存器的数值依然不变。即这是一条无用的汇编指令。在此利用这条无价值的汇编指令来配合lock指令,在_asm_,_volatile_,memory的作用下,用作cpu的内存屏障。9)set_current_state()和_set_current_state()区别就不难看出。1

17、0)至于barrier()就很易懂了。3.#include void rmb(void);void wmb(void);void mb(void);这些函数在已编译的指令流中插入硬件内存屏障;具体的插入方法是平台相关的。rmb(读内存屏障)保证了屏障之前的读操作一定会在后来的读操作执行之前完成。wmb 保证写操作不会乱序,mb 指令保证了两者都不会。这些函数都是 barrier函数的超集。解释一下:编译器或现在的处理器常会自作聪明地对指令序列进行一些处理,比如数据缓存,读写指令乱序执行等等。如果优化对象是普通内存,那么一般会提升性能而且不会产生逻辑错误。但如果对I/O操作进行类似优化很可能造成

18、致命错误。所以要使用内存屏障,以强制该语句前后的指令以正确的次序完成。其实在指令序列中放一个wmb的效果是使得指令执行到该处时,把所有缓存的数据写到该写的地方,同时使得wmb前面的写指令一定会在wmb的写指令之前执行。这里有篇文章,分析的很好:典型的应用:writel(dev-registers.addr,io_destination_address);writel(dev-registers.size,io_size);writel(dev-registers.operation,DEV_READ);wmb();/*类似一条分界线,上面的写操作必然会在下面的写操作前完成,但是上面的三个写操作

19、的排序无法保证*/writel(dev-registers.control,DEV_GO);内存屏障影响性能,所以应当只在确实需要它们的地方使用。不同的类型对性能的影响也不同,因此要尽可能地使用需要的特定类型。值得注意的是大部分处理同步的内核原语,例如自旋锁和atomic_t,也可作为内存屏障使用。某些体系允许赋值和内存屏障组合,以提高效率。它们定义如下:#defineset_mb(var,value)dovar=value;mb();while0/*以下宏定义在ARM体系中不存在*/#defineset_wmb(var,value)dovar=value;wmb();while0#defin

20、eset_rmb(var,value)dovar=value;rmb();while0使用do.while来构成宏,使得宏展开后可以作为一个完整的语句。使用I/O端口I/O端口是驱动程序与许多设备之间进行通信的方式。I/O端口分配在尚未取得对这些端口的独占访问之前,不应对这些端口进行操作。内核提供了一个注册用的接口,他允许驱动程序声明自己要操作的端口。接口函数是request_region:#include struct resource *request_region(unsigned long first,unsigned long n,const char *name);这个函数告诉内核

21、,我们要使用起始于first的n个端口,参数name应该是设备的名称。如果分配成功,则返回为NULL值。如果request_region返回NULL,则不能使用这些期望的端口。所有端口分配信息可从/proc/ioports中得到。如果不再需要使用某组I/O端口,则应该使用下面的函数将这些端口释放掉。void release_region(unsigned long start,unsigned long n);下面的函数允许驱动程序检查给定的端口集是否可用:int check_region(unsigned long first,unsigned long n);check_region这个函

22、数并不赞成使用,因为其检查过程不是原子的。但request_region驱动程序可以使用它,这个函数执行了必要的锁定。操作I/O端口当驱动程序请求了要使用的I/O端口范围后,必须读取或写入这些I/O端口。为此,大多数硬件都会把8位、16位、32位的端口区分开来。一般他们不能像访问系统内存那样使用。因此,C语言必须调用不同的函数来访问大小不同的窗口。有些只支持内存映射的I/O寄存器的计算机体系架构通过把I/O端口地址重新映射到内存地址来伪装端口I/O,并且为了易于移植,内核对驱动程序隐藏了这些细节。linux内核头文件中定义了如下一些访问I/O端口的内联函数。unsignedinb(unsign

23、edport);voidoutb(unsignedcharbyte,unsignedport);/*读/写字节端口( 8 位宽 )。port 参数某些平台定义为 unsigned long ,有些为 unsigned short 。 inb 的返回类型也体系而不同。*/unsignedinw(unsignedport);voidoutw(unsignedshortword,unsignedport);/*访问 16位 端口( 一个字宽 )*/unsignedinl(unsignedport);voidoutl(unsignedlongword,unsignedport);/*访问 32位 端口

24、。 longword 声明有的平台为 unsigned long ,有的为 unsigned int。*/注意这里没有定义64为I/O端口操作,即使在64位的体系结构上,端口地址空间也只适用最大32位的数据通路。用户空间访问I/O端口以上函数主要提供给设备驱动使用,但它们也可在用户空间使用,至少在 PC上可以。 GNU C 库在 中定义了它们。如果在用户空间代码中使用必须满足以下条件:(1)程序必须使用 -O 选项编译来强制扩展内联函数。(2)必须用ioperm 和 iopl 系统调用(#include )来获得对端口 I/O 操作的权限。ioperm 为获取单独端口操作权限,而 iopl 为

25、整个 I/O 空间的操作权限。(x86 特有的)(3)程序以 root 来调用 ioperm和 iopl,或是其父进程必须以 root 获得端口操作权限。(x86 特有的)若平台没有 ioperm 和 iopl 系统调用,用户空间可以仍然通过使用 /dev/prot 设备文件访问 I/O 端口。注意:这个文件的定义是体系相关的,并且I/O 端口必须先被注册。串操作除了一次传输一个数据的I/O操作,一些处理器实现了一次传输一个数据序列的特殊指令,序列中的数据单位可以是字节、字或双字,这是所谓的串操作指令。它们完成任务比一个 C 语言循环更快。下列宏定义实现了串I/O,它们有的通过单个机器指令实现

26、;但如果目标处理器没有进行串 I/O 的指令,则通过执行一个紧凑的循环实现。 有的体系的原型如下:voidinsb(unsignedport,void*addr,unsignedlongcount);voidoutsb(unsignedport,void*addr,unsignedlongcount);voidinsw(unsignedport,void*addr,unsignedlongcount);voidoutsw(unsignedport,void*addr,unsignedlongcount);voidinsl(unsignedport,void*addr,unsignedlongc

27、ount);voidoutsl(unsignedport,void*addr,unsignedlongcount);使用时注意: 它们直接将字节流从端口中读取或写入。当端口和主机系统有不同的字节序时,会导致不可预期的结果。 使用 inw 读取端口应在必要时自行转换字节序,以匹配主机字节序。然而串函数不会完成这种交换。暂停式I/O为了匹配低速外设的速度,有时若 I/O 指令后面还紧跟着另一个类似的I/O指令,就必须在 I/O 指令后面插入一个小延时。在这种情况下,可以使用暂停式的I/O函数代替通常的I/O函数,它们的名字以 _p 结尾,如 inb_p、outb_p等等。 这些函数定义被大部分体系

28、支持,尽管它们常常被扩展为与非暂停式I/O 同样的代码。因为如果体系使用一个合理的现代外设总线,就没有必要额外暂停。细节可参考平台的 asm 子目录的 io.h 文件。以下是includeasm-armio.h中的宏定义:#defineoutb_p(val,port)outb(val),(port)#defineoutw_p(val,port)outw(val),(port)#defineoutl_p(val,port)outl(val),(port)#defineinb_p(port)inb(port)#defineinw_p(port)inw(port)#defineinl_p(port)

29、inl(port)#defineoutsb_p(port,from,len)outsb(port,from,len)#defineoutsw_p(port,from,len)outsw(port,from,len)#defineoutsl_p(port,from,len)outsl(port,from,len)#defineinsb_p(port,to,len)insb(port,to,len)#defineinsw_p(port,to,len)insw(port,to,len)#defineinsl_p(port,to,len)insl(port,to,len)由此可见,由于ARM使用内部总

30、线,就没有必要额外暂停,所以暂停式的I/O函数被扩展为与非暂停式I/O 同样的代码。平台相关性由于自身的特性,I/O 指令与处理器密切相关的,非常难以隐藏系统间的不同。所以大部分的关于端口 I/O 的源码是平台依赖的。以下是x86和ARM所使用函数的总结:IA-32 (x86)x86_64这个体系支持所有的以上描述的函数,端口号是 unsigned short 类型。ARM端口映射到内存,支持所有函数。串操作 用C语言实现。端口是 unsigned int 类型。PowerPC和PowerPC64支持所有函数,在32位系统上,端口类型为unsigned char *,在64位系统上,端口类型为unsigned long。x86家族以外的处理器都不为端口提供独立的地址空间。解惑-驱动开发中的I/O地址空间:使用I/O内存除了X86上普遍使用的I/O端口之外,和设备通信的另一种主要机制是通过使用映射到内存

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

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