中断处理程序
这也是本数据采集卡最为关键的部分。
我们的设备每20ms内设备将 产生一次中断,这也就意味着已经有近1M的数据被准备好了,那么对于正在访问这个设备的应用程序来说,如何在20ms间隔期就将数据给运走呢?同时我们如何保证在这个过程中数据不丢失呢? 我们在实际实现我们的设备中采用两种方案。
方案1: 就象通常的设备文件一样,通过read和write来搬运数据。
在中断产生时设备驱动将数据搬运到缓冲区中,发送信号给用户程序,然后用户程序通过read函数来实现数据的拷贝工作。
方案2: 采用mmap映射,将i/o内存直接映射到进程虚拟空间中去,在中断产生时发送一个信号给应用程序,通知应用程序。
这两种方案的优缺点:
方案1: 数据不容易丢失,但对硬件要求太高,如果硬件达不到要求,那就掉包严重。
方案2: 节省了许多中间环节,也节省了大量的空间,提高了效率,但有可能丢失数据包。
最终公司采用了方案2,在这里先对mmap映射原理做一简单介绍。
我
们知道,Linux内核采用页式映射(其中也经过断式映射,但只是走走过场而已),虚拟地址空间划分成固定大小的“页面”,由MMU在运行是将虚拟地址转
换成实际的物理地址。而mmap与此恰恰相反,为了内核能够使用实际的物理地址,必须将物理地址转换成内核所使用的虚拟地址。这个就是内存映射,这种技术
是现在linux最有趣的功能之一,提供给用户程序直接访问设备内存的能力。当然,并不是所有的设备都能进行mmap抽象,例如,串口设备和其它面向流的
设备就就无法实现这种抽象。还有一个值得注意的问题,mmap映射都是以PAZE_SIZE为单位的。内核只能在页表一级处理虚拟地址;因此,被映射的区
域必须是PAZE_SIZE的整数倍。但抛开这些限制不说,使用mmap有一个最大的优势,这将显著的提高运行速度,可以减少许多次内存的复制工作,这个
在有大量的数据传输的情况下,显然有着极高的应用前景。在我们的数据采集卡驱动程序里,我们知道我们在20ms间隔期就有近1M的数据产生,而CPU在还
需对这些数据进行很复杂的处理,因此,这对我们来说采用mmap是必然的。
我们也尝试过方案1,但系统根本跑不动,处理器处理不过来。我们看源代码:
对方案1:
1.首先在一个中断期间,我们需将数据拷贝到一个我们申请的缓冲区中,同时通知应用程序,函数为:
memcpy_fromio(ptr,tt,READ1);
这是个从I/O内存拷贝数据的批处理函数,类似如前面的readb()函数。
2.应
用程序调用
read()函数从上面提到过的缓冲区中读取数据,请注意,上面我们提到过这个缓冲区是由驱动程序申请的,而驱动程序是模块,运行在内核空间,因此应用程
序不能直接使用这个缓冲区,而必须调用上面提到过的copy_to_user()函数来进行跨空间传递。由以上可知,在这个过程中,我们经历了两次拷贝,
甚至需要申请两个缓冲区。
对方案2:
我们所做的事情就简单多了,只需要实现一个mmap()函数,然后应用程序通过调用这个mmap()函数就可以直接访问这块I/O内存。在应用程序使用情况如下:
io_mmap=(char* )mmap(NULL,0x10000,PROT_READ,MAP_PRIVATE,fd,0);
应用程序此后就可象使用普通指针一样使用io_mmap了,显然这个有着高很多的效率,更高的吞吐量。而对于mmap()函数的具体实现为:
int mylubo_mmap(struct file *filp,struct vm_area_struct * vma)
{
unsigned long offset=vma->vm_pgoff<
if(offset>=__pa(high_memory)||(filp->f_flags&O_SYNC))
vma->vm_flags|=VM_IO;
vma->vm_flags|=VM_RESERVED;
if (remap_page_range(vma, vma->vm_start, offset,
vma->vm_end-vma->vm_start,
vma->vm_page_prot))
return -EAGAIN;
return 0;
}
我们看到这里面只调用了一个remap_page_range(vma, vma->vm_start, offset,
vma->vm_end-vma->vm_start,
vma->vm_page_prot)
函数。
3.2.6中断实现及信号量问题
前
面提到过 设备每20ms就会产生一个中断,我们拟用了9号中断,并且不共享。
在linux处理中断的方法有了较大的改变。早期PC中的中断处理很简单,仅仅涉及到一个处理器和16条中断信号线。而现在硬件可以拥有更多的中断,并且
还可能配有奇特的高级可编程中断控制器,该控制器可以智能的(和可编程)的方式在多个处理器之间分发中断。
Linux下使用中断资源的过程:
安装中断处理程序,
设备驱动程序通过调用request_irq函数来申请中断,通过free_irq
来释放中断。它们的定义为:
#include
int request_irq(unsigned int irq,
void (*handler)(int irq,void dev_id,struct pt_regs *regs),
unsigned long flags,
const char *device,
void *dev_id);
void free_irq(unsigned int irq, void *dev_id);
参数irq表示所要申请的硬件中断号。handler为向系统登记的中断处理子
程序,中断产生时由系统来调用,调用时所带参数irq为中断号,dev_id为申
请时告诉系统的设备标识,regs为中断发生时寄存器内容。device为设备名,
将会出现在/proc/interrupts文件里。flag是申请时的选项,它决定中断处理
程序的一些特性,其中最重要的是中断处理程序是快速处理程序(flag里设置
了SA_INTERRUPT)还是慢速处理程序(不设置SA_INTERRUPT),快速处理程序
运行时,所有中断都被屏蔽,而慢速处理程序运行时,除了正在处理的中断外,
其它中断都没有被屏蔽。在LINUX系统中,中断可以被不同的中断处理程序共享,
这要求每一个共享此中断的处理程序在申请中断时在flags里设置SA_SHIRQ,
这些处理程序之间以dev_id来区分。如果中断由某个处理程序独占,则dev_id
可
以为NULL。request_irq返回0表示成功,返回-INVAL表示irq>15或handler==NULL,返回-EBUSY表示中断
已经被占用且不能共享。对于驱动程序来说,我们可以通过查看/proc/interrupt文件来确定我们是否申请到了irq。也是这个文件,可以告诉我
们设备要申请的中断号是可否用。
但这里还牵涉到一个技术问题,那就是我们如何来检测我们设备使用的是哪个中断号呢?也就是说我们可以自动检测设备irq号吗?linux内核支持
这种自动检测。内核提供了一个自动检测函数probe_irq_on,它只能在非共享模式下工作。这个函数返回一个未分配中断的位掩码。驱动程序必须返回
未掩码,并且将它传递给后面的probe_irq_off函数,调用该函数后,驱动程序要设法安排设备发生一次中断。对probe_irq_off函数,
是在请求设备产生中断后,被驱动程序调用,并使用前面probe_irq_on函数所产生的位掩码作为参数。这个函数返回probe_irq_on之后中
断发生的次数。在内核帮助下自动检测irq号过程:
调用probe_irq_on函数
安排设备产生一次中断
调用probe_irq_off函数 根据probe_irq_off函数的返回值来判断设备所使用的中断号。
当然,除了这种在内核帮助下自动检测irq号,我们还可以手动检测设备的irq。
其原理为:
1.启用所有未被使用的中断号,同时循环的调用request_irq申请所有的这些中断号,并2.在中断处理程序里做好标志
3.设法安排设备产生一次中断
4.根据中断处理程序里的标志来判断中断处理程序是否运行,从而确定中断号
5.释放中断号
中断处理程序的实现:
中断处理程序事实上还是一个普通的函数,只是我们要注意中断处理程序
是
在中断期间运行的,具有最高的优先级别。还有中断处理程序必须要在尽量短的时间内完成,这对于那些需在中断期间内完成耗时的时候是一个问题。在linux
下可以使用tasket和底半部处理来解决这个问题,在这里我们不加以详细的阐述了,我们只具体谈谈我们的中断处理程序的实现。
前面提到过,在方案1里中断处理程序还需将数据拷贝到缓冲区中,而在方案2中这个都不用实现了,那中断处理程序到底都做了什么呢? 事实上我们只作了一件事—那就是发送一个信号给我们的应用程序,通知它来处理数据。具体实现为:
kill_fasync(&(dev->async_queue),SIGIO,POLL_IN);
这是一个进程异步通知函数。那么驱动程序究竟是如何实现异步信号传输的呢?下面我们从内核角度来看的详细操作过程:
当 F_SETOWN被调用时,对filp->f_owner赋值,此外什么也不做。
在执行F_SETFL以启用FASYNC时,调用驱动程序的fasync方法。只要filp->f_flags中的FASYNC标志发生了变化,就会调用该方法,以便把这个变化通知驱动程序,使其能正确响应。文件打开时,FASYNC标志被默认为清除。
当 数据到达时,发送一个信号给所有注册为异步通知的进程。至于发送什么信号由上层应用程序决定,默认为 SIGIO信号。
在这里你也许会问,应用程序如何成为设备的异步访问者呢? 这个就是驱动程序中异步函数的实现过程了。在我的驱动中, fasync方法的实现为:
int mylubo_fasync(int fd,struct file *filp,int mode)
{ mylubo *dev=filp->private_data;
return fasync_helper(fd,filp,mode,&dev->async_queue);
}
关
于应用程序具体如何使用异步传输机制,请看源代码。下面我们来讨论关于信号的传输问题。我们也许会问,驱动程序发送给应用程序后信号会丢失吗?会很及时的
发送给应用程序吗?如果不那样的话,那我们的数据就有可能被洗掉了。这个也就是上面我所说过的使用mmap时,可能会导致数据丢失的根源所在。我曾经对
linux下的信号量机制作过一些研究,下面就是我对信号的理解:
一、 信号本质
信
号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操
作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有
哪些事情发生了。信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。
[url=]二、信号的种类[/url]
可以从两个不同的分类角度对信号进行分类:(1)可靠性方面:可靠信号与不可靠信号;(2)与时间的关系上:实时信号与非实时信号。
1、可靠信号与不可靠信号
"不可靠信号"
Linux信
号机制基本上是从Unix系统中继承过来的。早期Unix系统中的信号机制比较简单和原始,后来在实践中暴露出一些问题,因此,把那些建立在早期机制上的
信号叫做"不可靠信号",信号值小于SIGRTMIN(Red hat
7.2中,SIGRTMIN=32,SIGRTMAX=63)的信号都是不可靠信号。这就是"不可靠信号"的来源。它的主要问题是:
进程每次处理信号后,就将对信号的响应设置为默认动作。在某些情况下,将导致对信号的错误处理;因此,用户如果不希望这样的操作,那么就要在信号处理函数结尾再一次调用signal(),重新安装该信号。
信号可能丢失,后面将对此详细阐述。
因
此,早期unix下的不可靠信号主要指的是进程可能对信号做出错误的反应以及信号可能丢失。
Linux支持不可靠信号,但是对不可靠信号机制做了改进:在调用完信号处理函数后,不必重新调用该信号的安装函数(信号安装函数是在可靠机制上的实
现)。因此,Linux下的不可靠信号问题主要指的是信号可能丢失。
"可靠信号"
随
着时间的发展,实践证明了有必要对信号的原始机制加以改进和扩充。所以,后来出现的各种Unix版本分别在这方面进行了研究,力图实现"可靠信号"。由于
原来定义的信号已有许多应用,不好再做改动,最终只好又新增加了一些信号,并在一开始就把它们定义为可靠信号,这些信号支持排队,不会丢失。同时,信号的
发送和安装也出现了新版本:信号发送函数sigqueue()及信号安装函数sigaction()。POSIX.4对可靠信号机制做了标准化。但
是,POSIX只对可靠信号机制应具有的功能以及信号机制的对外接口做了标准化,对信号机制的实现没有作具体的规定。信号值位于SIGRTMIN和
SIGRTMAX之间的信号都是可靠信号,可靠信号克服了信号可能丢失的问题。Linux在支持新版本的信号安装函数sigation()以及信号发送函
数sigqueue()的同时,仍然支持早期的signal()信号安装函数,支持信号发送函数kill()。
[url=]2[/url]、实时信号与非实时信号
早期Unix系统只定义了32种信号,Ret hat7.2支持64种信号,编号
0-63(SIGRTMIN=31,SIGRTMAX=63),将来可能进一步增加,这需要得到内核的支持。前32
种信号已经有了预定义值,每个信号有了确定的用途及含义,并且每种信号都有各自的缺
省动作。如按键盘的CTRL ^C时,会产生SIGINT信号,对该信号的默认反应就是进程终止。
后32个信号表示实时信号,等同于前面阐述的可靠信号。这保证了发送的多个实时信号都
被接收。实时信号是POSIX标准的一部分,可用于应用进程。非实时信号都不支持排队,
都是不可靠信号;实时信号都支持排队,都是可靠信号。
[url=]三、进程对信号的响应[/url]
进程可以通过三种方式来响应一个信号:(1)忽略信号,即对信号不做任何处理,其
中,有两个信号不能忽略:SIGKILL及SIGSTOP;(2)捕捉信号。定义信号处理函数,当信
号发生时,执行相应的处理函数;(3)执行缺省操作,Linux对每种信号都规定了默认操
作当一个实时信号发送给一个进程时,不管该信号是否已经在进程中注册,都会被再注册
一次,因此,信号不会丢失,因此,实时信号又叫做"可靠信号"。这意味着同一个实时信
号可以在同一个进程的未决信号信息链中占有多个sigqueue结构(进程每收到一个实时信
号,都会为它分配一个结构来登记该信号信息,并把该结构添加在未决信号链尾,即所有
诞生的实时信号都会在目标进程中注册);
当一个非实时信号发送给一个进程时,如果该信号已经在进程中注册,则该信号将被丢弃,
造成信号丢失。因此,非实时信号又叫做"不可靠信号"。这意味着同一个非实时信号在进
程的未决信号信息链中,至多占有一个sigqueue结构(一个非实时信号诞生后,(1)、如
果发现相同的信号已经在目标结构中注册,则不再注册,对于进程来说,相当于不知道本
次信号发生,信号丢失;(2)、如果进程的未决信号中没有相同信号,则在进程中注册自己)。
四 信号的执行时间
1.
在进程中的注销。在目标进程执行过程中,会检测是否有信号等待处理(每次从系统空间返回到用户空间时都做这样的检查)。如果存在未决信号等待处理且该信号
没有被进程阻塞,则在运行相应的信号处理函数前,进程会把信号在未决信号链中占有的结构卸掉。是否将信号从进程未决信号集中删除对于实时与非实时信号是不
同的。对于非实时信号来说,由于在未决信号信息链中最多只占用一个sigqueue结构,因此该结构被释放后,应该把信号在进程未决信号集中删除(信号注
销完毕);而对于实时信号来说,可能在未决信号信息链中占用多个sigqueue结构,因此应该针对占用sigqueue结构的数目区别对待:如果只占用
一个sigqueue结构(进程只收到该信号一次),则应该把信号在进程的未决信号集中删除(信号注销完毕)。否则,不应该在进程的未决信号集中删除该信
号(信号注销完毕)。
进程在执行信号相应处理函数之前,首先要把信号在进程中注销。
2.生命终止。进程注销信号后,立即执行相应的信号处理函数,执行完毕后,信号的本次发送对进程的影响彻底结束。 由以上的分析知,我们应该采用实时信号,以防止信号丢失;而且由于信号是在每次系统执行了从内核空间返回到用户空间都会执行信号的检测,因此采用信号作为传输介质是值得信赖的,一般不会造成很大的延迟。
阅读(1056) | 评论(0) | 转发(0) |