之前写过中断相关的文章了,详细分析过ARM底层中断实现过程,这篇文章着重从操作系统设计层面理解中断系统。
1.中断
中断:是外围设备产生的异步事件,不同设备,中断号不同;比如在经典PC上,IRQ 0是时钟中断,IRQ 1是键盘中断;对于连接于PCI总线上的设备,中断还可以是动态分配的。
异常:与中断不同,异常的产生必须考虑与处理器时钟同步,比如除零,缺页,软中断等,都是由处理器自身产生的同步事件,异常也可称为同步中断;
本文对异步中断的讨论,大部分也适合于异常(同步中断)。
2.中断处理
每个中断都对应有一个中断处理程序;
ISR是被内核调用来响应中断的,它运行在中断上下文中(也称原子上下文),因此代码是不可阻塞的。
ISR应该快速执行。
3.上半部与下半部
又想ISR运行的快,又想ISR完成的工作量多,这是一个此消彼长的矛盾关系;为了平衡这个矛盾,Linux内核把中断分为两部分:
上半部:接收一个中断,立即开始执行,但只做严格时限的工作;例如中断应答,硬件复位之类,这些工作是在所有中断被禁止的情况下完成的。
下半部:上半部执行完后,在合适的时间,调度执行下半部,下半部可以处理一些耗时的工作,可以休眠。
4.注册中断处理程序
中断处理程序通过request_irq()注册(在linux/interrupt.h定义)
点击(此处)折叠或打开
(1)第一个参数irq:
表示要分配的中断号,这个值通常是预先确定的,但对有的设备来也可以探测获取,或通过编程动态确定。
(2) handler:函数指针,指向这个中断的ISR。
该函数原型如下,接受2个参数,并有irqreturn_t类型的返回值:
typedef irqreturn_t (*irq_handler_t)(int, void *);
(3) flags:中断处理标志,可以用0,也可以用以下一个或多个标志的位掩码
①IRQF_DISABLED: keep irqs disabled when calling the action handler
如果不设置该位,中断处理程序可以与除本身外的其他任何中断同时运行,一般不会设置此位,这个用法是给希望快速执行的轻量级中断,在过去的中断中用以区分“快速”和“慢速”中断。
②IRQF_SAMPLE_RANDOM
此标志表明这个设备的中断对内核熵池有贡献,用以产生随机数。
③IRQF_TIMER:
该标志是特别为系统定时器的中断处理程序而准备的。
④IRQF_SHARED:allow sharing the irq among several devices
必须在给定中断线的所有处理程序中都指定这个标志,否则,一个中断线上只能有一个处理程序;
(4)name:
中断设备的名字,比如PC上键盘中断对应的值是“keyboard”,这个名字会被/proc/irq和/proc/interrupts文件使用。
(5)dev :用于共享中断线
①中断线被共享:dev必须传递唯一信息,当一个中断处理程序需要释放时,dev将提供唯一的标志信息,以便确定要在共享中断线上要删除哪一个处理程序;
②不是共享中断线,可以设为NULL,也可以用来传递设备私有信息。
request_irq()成功执行会返回0,若非0,就表示发生错误,这时指定中断处理程序不会被注册。最常见的错误是-EBUSY,它表示中断线已经在使用,或者注册共享中断时没有指定IRQF_SHARED。
初始化硬件和注册中断处理程序的顺序必须正确,以防止ISR在设备初始化完成之前就开始执行。初始化硬件,包括set_irq_type();
int set_irq_type(unsigned int irq, unsigned int type);
irq:中断号
type:中断类型,可以设为下列标志
IRQT_RISING,IRQT_FALLING,IRQT_BOTHEDGE,IRQT_LOW ,IRQT_HIGH
释放中断处理程序:
卸载驱动程序的时候,要注销相应的中断处理程序,释放中断线
void free_irq(unsigned int irq, void *dev);
如果中断线非共享,该函数删除处理程序,禁止相应中断线。
若为共享中断,free_irq()删除dev相应的处理程序,而这个中断线只有在所有处理程序都被删除后,才会被禁用。
必须在进程上下文中调用free_irq()。
5.编写中断处理程序
(1) 中断处理程序原型:typedef irqreturn_t (*irq_handler_t)(int, irq void * dev);
irq:中断号
dev:通用指针,就是request_irq()注册时的dev。
在共享中断线时,用来区分不同设备;
非共享中断中,一般传入硬件设备私有信息
irqreturn_t:返回值,一般由两个值
IRQ_NONE: 当ISR检测到一个中断,但该中断对应的设备并不是注册ISR时指定的产生源时,返回IRQ_NONE;
IRQ_HANDLED: ISR被正确调用且确实是它所对应的设备产生了中断,返回IRQ_HANDLED.
也可以用宏IRQ_RETVAL(val),val非零时返回IRQ_HANDLED,val为0时返回IRQ_NONE.
利用特殊返回值,讷河可以知道设备发出的是否是一种虚假的中断(为请求)。
中断处理期望完成的工作,扩充的耗时的工作,应该分配给下半部完成。
(2) 中断处理程序特点:
①不能向用户空间发送或接收数据,因为它不在任何进程上下文运行;
②处理程序不能做任何可能休眠的操作;
③不能调用schedule()函数;
④Linux的ISR无须是重入的,当一个给定ISR在执行时,相应的中断线在所有处理器上都会被屏蔽。
⑤通常情况下,其他中断是打开的,所以这些不同中断线上的ISR都可以被处理。
中断的嵌套,还要看CPU硬件提供的支持,Linux只能适应硬件,而不能改变硬件特性。
(3) 共享的中断处理程序:注意点主要有三
①request_irq()的参数flags必须设置为IRQF_SHARED标志;
②对于每个注册的中断处理程序,dev参数必须唯一,且不能为空;
③中断处理程序必须能够区分设备是否真产生了中断,一般由状态寄存器或类似机制实现;
内核收到一个中断后,它将依次调用在该中断线上注册的每一个处理程序,因此每个处理程序都应该能为中断负责,若发现相关设备并未产生中断,应该立即退出。
6.中断上下文
进程上下文,是一种内核所处的操作模式,此时内核代表进程执行,进程以进程上下文的形式连接到内核中,因此进程上下文可以睡眠,也可以调用调度程序;
中断上下文,与进程不相干,它无法被调度,所以不能睡眠(一旦睡眠,则无法返回);
中断上下文具有较为严格的时间限制,中断上下文的代码应当迅速、简洁,尽量不要去处理繁重的工作。中断打断了其他代码的执行(甚至可能是打断了在其他中断线上的另一个中断处理程序),所以ISR应该尽量快速的退出,尽量把工作从ISR中分离出来,放到下半部去执行。
中断处理在32位机上只有4K的中断栈,因此应该尽量节约内核栈空间。
7.中断处理机制的实现
中断处理系统在Linux中的实现,是非常依赖于体系结构的。
当一个中断产生,处理器会跳到一个预先定义好的入口点,每条中断线处理器都会跳转到一个唯一的对应位置。在栈中保存IRQ号,调用do_IRQ()函数。
取出栈顶的参数,计算出中断号后,do_IRQ()对所接收的中断进行应答,禁止这条线上的中断传递。然后调用handle_IRQ_event()来运行中断处理程序。
handle_IRQ_event()函数中,若为共享中断,所有该中断线上的处理程序都被执行一遍。
执行完中断处理程序,返回入口点跳到ret_from_intr()。
这个例程中检查重新调度程序是否正在挂起(need_resched被设置),如果重新调度程序正在挂起,如果内核正返回用户空间,那么schedule()会被调用;如果内核正要返回内核空间(中断了内核本身),只有在preempt_count为0时(不持有锁),schedule()才会被调用,否则抢占内核不安全。
8./proc/interrupts
procfs是一个虚拟文件系统,只存在于内核内存中。对procfs的读写都要通过内核函数实现,/proc/interrupts文件,存放的是系统中与中断相关的统计信息。分别显示中断线,中断发生次数,中断控制器,发生中断名字等。
9.中断控制
Linux内核提供了一组接口,能够禁止当前处理器的中断系统,或屏蔽掉整个机器的一条中断线的能力。
控制中断系统的原因归根结底,是需要提提供同步,通过禁止中断,可以确保某个中断处理程序不会抢占当前的代码,禁止中断还可以禁止内核抢占。
Linux支持多处理器,因此内核代码一般都需要获取某种锁,来防止其他处理器对共享数据的并发访问。锁提供保护机制,防止其他处理器的并发访问;禁止中断提供保护机制,防止来自其他中断处理程序的并发访问;
(1)禁止和激活中断
禁止/激活当前处理器上的本地中断,
点击(此处)折叠或打开
禁止中断之前保存中断系统状态会更安全
禁止本地中断,并保存中断状态
点击(此处)折叠或打开
这一对函数,应当在同一个函数内调用。
(2)禁止指定中断线
点击(此处)折叠或打开
一般来说禁止共享中断线是不合适的,这就禁止了这条线上的所有中断传递。
(3)中断系统状态
点击(此处)折叠或打开