一、设备驱动程序简述
1.1 设备驱动程序分类
1.2 设备驱动程序相关概念
二、设备驱动程序架构
2.1 设备驱动程序基本架构
2.2 相关数据结构
一、设备驱动程序简述
1.1 设备驱动程序分类
在Linux内核源码中,设备驱动程序占有很大比例。虽然驱动程序的增加是Linux源代码的主要增长点,但是驱动程序的结构还是相对稳定的。
系统调用是操作系统内核与应用程序之间的接口,设备驱动程序是操作系统内核与外部硬件之间的接口。由于设备驱动程序能为应用程序屏蔽硬件细节,这样对于Linux系统从应用程序角度来看,硬件设备只是一个设备文件,应用程序可以像操作普通文件一样对硬件设备进行操作。因此,从以上角度来看,设备驱动是内核的一部分,它的功能主要如下所示:
(1)对设备的初始化;
(2)建立内核与硬件的数据交互;
(3)读取应用程序传递给设备文件数据以及回送应用程序请求的数据;
(4)检测与处理设备出现的错误。
Linux系统的设备分为字符设备(char device)、块设备(block device)与网络设备(network device)三种:
(1)字符设备:指存取时没有缓存的设备,典型的字符设备包括鼠标、键盘、串行口等;
(2)块设备:读写都由缓存来支持,并且块设备必须能够随机存取(random access),典型的块设备包括硬盘软盘设备、CD-ROM等;
(3)网络设备:Linux网络系统主要基于BSD Unix的socket机制,在系统与驱动程序之间定义有专门的数据结构(sk_buff)进行数据的传递,系统里支持对发送与接受数据的缓存,并提供流量控制机制与多协议支持。
1.2 设备驱动程序相关概念
下面具体介绍一些与编写驱动程序相关的概念:
1、模块
在Linux系统中,驱动以模块的方式呈现,并能单独作为模块在于内核连接,也可以直接连接进内核,内核连接的模块信息保存在/proc/modules文件中。需要注意的是模块于应用程序的差别体现在以下几点:
(1)模块运行在内核空间,应用程序运行于用户空间;
(2)模块只能使用内核导出的函数,而不能使用其他函数库(比如glibc库)。
(3)模块必须考虑到并发,所以代码必须具有可重入性。
2、资源
模块需要申请资源:I/O端口、I/O内存、DMA通道、中断等,/proc下的ioports、iomem、dma、interrupts列出了已经注册的资源。
3、设备
设备通常采用设备文件方式,存放于/dev下,每个设备都有设备名称、主设备号于次设备号。主设备号标识了对应的驱动程序,驱动程序需要向系统注册一个主设备号;次设备号区分具体设备,有驱动程序管理。
4、中断处理
驱动申请与释放中断信号是通过request_irq与free_irq函数实现的。调用request_irq应该位于设备第一次打开与硬件被告知产生中断之间;调用free_irq应该位于最后一次关闭设备与硬件被告知不再进行中断处理。
5、时钟
在实现驱动程序时,需要时钟信号以实现某些协议的超时处理、非中断机制的硬件轮询等。
二、设备驱动程序架构
2.1 设备驱动程序基本架构
1、块设备文件与字符设备文件
Linux系统对硬件设备支持两种标准接口:块设备文件与字符设备文件。设备由一个主设备与一个次设备号标识,主设备号唯一标识了设备驱动程序类型;次设备号仅由设备驱动程序解释,用于识别I/O请求涉及的设备。
(1)块设备接口
块设备接口仅支持面向块的I/O操作,所有I/O操作都通过在内核地址空间中的I/O缓存区进行,因此可以支持任意长度和任意位置上的I/O请求,即提供随机存取的功能。
(2)字符设备接口
字符设备接口支持面向字符的I/O操作,所以它负责管理自己的缓冲区结构。字符设备接口只支持顺序存取的功能,通常不能进行任意长度的I/O请求,I/O请求的长度必须是设备要求的基本块长度的倍数。
2、设备驱动程序的主要组成部分
(1)自动配置与初始化子程序
自动配置与初始化子程序负责检测所要驱动的设备是否存在或者正常,如果设备正常,则对设备驱动程序需要的软件进行初始化,它仅在初始化的时候被调用一次。
(2)服务于I/O请求的子程序
服务于I/O请求的子程序属于驱动程序的上半部分,这部分是系统调用的结果,即与它与调用的进程属于同一个进程,因此,可以在其中调用sleep()等于进程运行环境有关的函数。
(3)中断服务子程序
终端服务子程序属于驱动程序的下半部分,Linux系统接收硬件中断,再由系统调用中断服务子程序。由于中断可以在任何一个进程运行时产生,因此在中断服务程序被调用的时候,不能依赖任何进程的状态,自然也就不能调用任何与进程运行环境相关的函数。
3、I/O设备存取的入口点
在Linux系统内部,I/O设备的存取是通过一组固定的入口点来进行的,这组入口点是由每个设备的驱动程序提供的,一般来说,块/字符设备驱动程序都能够提供以下几个入口点:
(1)open入口点:打开设备准备I/O操作,对字符设备文件进行打开操作,都会调用设备的open入口点。
(2)close入口点:关闭一个设备,当最后一次使用设备终结后,则调用close子程序。
(3)read入口点:从设备上读取数据,对于有缓冲区的I/O操作,一般从缓冲区里读取数据;对字符设备文件进行读操作将调用read子程序。
(4)write入口点:向设备上写数据,对于有缓冲区的I/O设备,通常把数据写入缓冲区,对字符设备文件进行写操作时将调用write子程序。
(5)ioctl入口点:执行读写之外的操作。
(6)select入口点:检查设备,看设备是否可读或者可写。
2.2 相关数据结构
1、块/字符设备驱动相关的数据结构
具体到Linux系统中,设备驱动程序所提供的I/O设备存取的入口点由一个结构来向系统进行说明,此结构定义为:
其中,(1)struct inode提供了关于特别设备文件/dev/driver的信息,它的定义为:
(2)struct file主要用于与文件系统对应的设备驱动程序,它提供了关于被打开的文件信息,定义为:
在struct file程序中的file_operations里,指出了设备驱动程序所提供的入口点位置,如下表所示:
设备却动程序所提供的入口点,在设备驱动程序初始化的时候向系统进行登记,以便系统在适当时候调用。在Linux系统中,通过调用register_chrdev向系统注册字符型设备驱动程序,register_chrdev的定义为:
其中,major是为设备驱动程序申请的主设备号,如果为0则系统为该驱动程序动态地分配一个主设备号;name是设备名;fops为设备驱动程序所提供的入口点。此函数为0表示成功,返回-EINVAL表示非法地申请了主设备号,如果返回-EBUSY表示所申请地主设备号被其他驱动程使用。如果动态分配主设备号成功,此函数返回所分配地主设备号。如果register_chrdev操作成功,设备名就会出现在/proc/devices文件中。
2、网络设备驱动相关的数据结构
如果使用模块(module)方式加载驱动程序,需要在模块初始化时把设备注册到系统设备表里,不再使用时,把设备从系统中卸除,定义在drivers/net/net_init.h里面地两个函数完成该工作:
dev就是要注册到系统地设备结构指针。
其中最重要的是网络设备的数据结构,其定义在/include/linux/netdevice.h:
Linux网络各层之间数据传送都是通过sk_buff进行的,sk_buff提供一套管理缓冲区的方法,是Linux系统网络高效运行的关键所在,它具有以下特点:
(1)每个sk_buff包含一些控制方法与数据缓冲区,其中控制方法按功能分为两种类型:控制整个buffer链的方法与控制数据缓冲区的方法。
(2)sk_buff组织成双向链表的形式,根据网络应用特点,对链表的操作主要是删除链表头和将元素添加到链表尾。
(3)sk_buff的控制方法都很短小,以尽量减小系统符合。
下表列出了sk_buff常用的操作方法:
3、公用的数据结构
初始化部分通常还负责给设备驱动程序申请系统资源:内存、中断、I/O端口、时钟等,这些系统资源可以在open子程序或者别的地方申请。在Linux系统中,中断处理属于系统核心部分,设备驱动程序通过调用request_irq()函数来申请中断,通过free_irq()函数来释放中断,该函数的定义为:
参数irq表示所申请的硬件中断号,handler为向系统登记的中断处理子程序,产生中断时由系统来调用,dev_id为申请时告诉系统的设备标识,regs为产生中断时的寄存器内容;device为设备名,将会挂载在/proc/interrupts文件中;flag是申请中断时的选项,它控制着中断处理程序的一些特性,比如最常用的选项设置flag为SA_INTERRUPT,则表示快速处理程序,此时所有中断都被屏蔽;而不设置SA_INTERRUPT时,则表示慢速处理程序,除了正在处理的中断外,其他中断都没有被屏蔽。
在Linux系统中,中断可以被不同的中断处理程序共享,着要求每一个共享此中断的处理程序在申请中断时,在flag里设置SA_SHIRQ,这些程序之间以dev_id区分。如果中断由某个处理程序独占,则dev_id可以设置为NULL。
requst_irq返回0表示成功,返回-INVAL表示handler==NULL或者irq>15,返回-EBUSY表示中断已经被占用或不能共享。
作为系统核心的一部分,设备驱动程序在申请与释放内存时不是调用malloc()与free(),调用的而是开kmalloc()与kfree(),它们定义为:
参数len为希望申请的字节数,obj为要释放的内存指针。priority为分配内存的优先级,通常采用GFP_KERNEL。
与中断和内存不同使用一个没有申请的I/O端口不会使CPU产生异常,也就不会导致诸如“segmentation fault”一类的错误发生。任何进程都可以访问任何一个I/O端口,而此时系统无法保证对I/O端口的操作不发生冲突,甚至会因此而使得系统崩溃。因此,在使用I/O端口前,应该检查此I/O端口是否已经被别的程序使用,若没有再把此端口标记为正在使用,再使用完以后释放它,执行这样的操作需要以下几个函数:
调用这些函数的参数有:(1)from表示所申请的I/O端口的起始地址;(2)extent为所要申请的从from开始的端口数;(3)name为设备名,将会出现在/proc/ioports文件中。其中,check_region返回0表示I/O端口空闲,否则表示正在使用。
在申请了I/O端口后,就可以用以下几个函数访问I/O端口:
其中,inb_p与outb_p插入一定的延时以适应某些慢的I/O端口。
在Linux系统中,时钟是由系统接管的,设备驱动程序可以向系统申请时钟,与时钟有关的系统调用如下:
上述几个与时钟有关的系统调用函数中struct timer_list结构体最为重要,它的定义为:
其中,expires是要执行的function的时间,系统核心由一个全局变量JIFFIES,它表示当前时间,通常在调用add_timer()时,jiffies=JIFFIES+num,表示在num个系统最小时间间隔后执行function。系统计时到预定时间就调用function(),并把子程序从定时列表中删除,,function函数的参数d表示timer中的data项。
------------END-----------
领取专属 10元无门槛券
私享最新 技术干货