深入剖析Linux中断机制之三
--Linux对异常和中断的处理
Sailor_forever [email protected] 转载请注明
http://blog.csdn.net/sailor_8318/archive/ 2008/07/09 /2627136.aspx
【摘要】本文详解了Linux内核的中断实现机制。首先介绍了中断的一些基本概念,然后分析了面向对象的Linux中断的组织形式、三种主要数据结构及其之间的关系。随后介绍了Linux处理异常和中断的基本流程,在此基础上分析了中断处理的详细流程,包括保存现场、中断处理、中断退出时的软中断执行及中断返回时的进程切换等问题。最后介绍了中断相关的API,包括中断注册和释放、中断关闭和使能、如何编写中断ISR、共享中断、中断上下文中断状态等。
【关键字】中断,异常,hw_interrupt_type,irq_desc_t,irqaction,asm_do_IRQ,软中断,进程切换,中断注册释放request_irq,free_irq,共享中断,可重入,中断上下文
Linux利用异常来达到两个截然不同的目的:
² 给进程发送一个信号以通报一个反常情况
² 管理硬件资源
对于第一种情况,例如,如果进程执行了一个被0除的操作,CPU则会产生一个“除法错误”异常,并由相应的异常处理程序向当前进程发送一个SIGFPE信号。当前进程接收到这个信号后,就要采取若干必要的步骤,或者从错误中恢复,或者终止执行(如果这个信号没有相应的信号处理程序)。
内核对异常处理程序的调用有一个标准的结构,它由以下三部分组成:
² 在内核栈中保存大多数寄存器的内容(由汇编语言实现)
² 调用C编写的异常处理函数
² 通过ret_from_exception()函数从异常退出。
当一个中断发生时,并不是所有的操作都具有相同的急迫性。事实上,把所有的操作都放进中断处理程序本身并不合适。需要时间长的、非重要的操作应该推后,因为当一个中断处理程序正在运行时,相应的IRQ中断线上再发出的信号就会被忽略。另外中断处理程序不能执行任何阻塞过程,如I/O设备操作。因此,Linux把一个中断要执行的操作分为下面的三类:
² 紧急的(Critical)
这样的操作诸如:中断到来时中断控制器做出应答,对中断控制器或设备控制器重新编程,或者对设备和处理器同时访问的数据结构进行修改。这些操作都是紧急的,应该被很快地执行,也就是说,紧急操作应该在一个中断处理程序内立即执行,而且是在禁用中断的状态下。
² 非紧急的(Noncritical)
这样的操作如修改那些只有处理器才会访问的数据结构(例如,按下一个键后,读扫描码)。这些操作也要很快地完成,因此,它们由中断处理程序立即执行,但在启用中断的状态下。
² 非紧急可延迟的(Noncritical deferrable)
这样的操作如,把一个缓冲区的内容拷贝到一些进程的地址空间(例如,把键盘行缓冲区的内容发送到终端处理程序的进程)。这些操作可能被延迟较长的时间间隔而不影响内核操作,有兴趣的进程会等待需要的数据。
所有的中断处理程序都执行四个基本的操作:
² 在内核栈中保存IRQ的值和寄存器的内容。
² 给与IRQ中断线相连的中断控制器发送一个应答,这将允许在这条中断线上进一步发出中断请求。
² 执行共享这个IRQ的所有设备的中断服务例程(ISR)。
² 跳到ret_to_usr( )的地址后终止。
现在,我们可以从中断请求的发生到CPU的响应,再到中断处理程序的调用和返回,沿着这一思路走一遍,以体会Linux内核对中断的响应及处理。
假定外设的驱动程序都已完成了初始化工作,并且已把相应的中断服务例程挂入到特定的中断请求队列。又假定当前进程正在用户空间运行(随时可以接受中断),且外设已产生了一次中断请求,CPU就在执行完当前指令后来响应该中断。
中断处理系统在Linux中的实现是非常依赖于体系结构的,实现依赖于处理器、所使用的中断控制器的类型、体系结构的设计及机器本身。
设备产生中断,通过总线把电信号发送给中断控制器。如果中断线是激活的,那么中断控制器就会把中断发往处理器。在大多数体系结构中,这个工作就是通过电信号给处理器的特定管脚发送一个信号。除非在处理器上禁止该中断,否则,处理器会立即停止它正在做的事,关闭中断系统,然后跳到内存中预定义的位置开始执行那里的代码。这个预定义的位置是由内核设置的,是中断处理程序的入口点。
对于ARM系统来说,有个专用的IRQ运行模式,有一个统一的入口地址。假定中断发生时CPU运行在用户空间,而中断处理程序属于内核空间,因此,要进行堆栈的切换。也就是说,CPU从TSS中取出内核栈指针,并切换到内核栈(此时栈还为空)。
若当前处于内核空间时,对于ARM系统来说是处于SVC模式,此时产生中断,中断处理完毕后,若是可剥夺内核,则检查是否需要进行进程调度,否则直接返回到被中断的内核空间;若需要进行进程调度,则svc_preempt,进程切换。
190 .align 5
191__irq_svc:
192 svc_entry
197#ifdef CONFIG_PREEMPT
198 get_thread_info tsk
199 ldr r8, [tsk, #TI_PREEMPT] @ get preempt count
200 add r7, r8, #1 @ increment it
201 str r7, [tsk, #TI_PREEMPT]
202#endif
203
204 irq_handler
205#ifdef CONFIG_PREEMPT
206 ldr r0, [tsk, #TI_FLAGS] @ get flags
207 tst r0, #_TIF_NEED_RESCHED
208 blne svc_preempt
209preempt_return:
210 ldr r0, [tsk, #TI_PREEMPT] @ read preempt value
211 str r8, [tsk, #TI_PREEMPT] @ restore preempt count
212 teq r0, r7
213 strne r0, [r0, -r0] @ bug()
214#endif
215 ldr r0, [sp, #S_PSR] @ irqs are already disabled
216 msr spsr_cxsf, r0
221 ldmia sp, {r0 - pc}^ @ load r0 - pc, cpsr
222
223 .ltorg
当前处于用户空间时,对于ARM系统来说是处于USR模式,此时产生中断,中断处理完毕后,无论是否是可剥夺内核,都调转到统一的用户模式出口ret_to_user,其检查是否需要进行进程调度,若需要进行进程调度,则进程切换,否则直接返回到被中断的用户空间。
404 .align 5
405__irq_usr:
406 usr_entry
407
411 get_thread_info tsk
412#ifdef CONFIG_PREEMPT
413 ldr r8, [tsk, #TI_PREEMPT] @ get preempt count
414 add r7, r8, #1 @ increment it
415 str r7, [tsk, #TI_PREEMPT]
416#endif
417
418 irq_handler
419#ifdef CONFIG_PREEMPT
420 ldr r0, [tsk, #TI_PREEMPT]
421 str r8, [tsk, #TI_PREEMPT]
422 teq r0, r7
423 strne r0, [r0, -r0] @ bug()
424#endif
428
429 mov why, #0
430 b ret_to_user
432 .ltorg
105/*
106 * SVC mode handlers
107 */
108
115 .macro svc_entry
116 sub sp, sp, #S_FRAME_SIZE
117 SPFIX( tst sp, #4 )
118 SPFIX( bicne sp, sp, #4 )
119 stmib sp, {r1 - r12}
120
121 ldmia r0, {r1 - r3}
122 add r5, sp, #S_SP @ here for interlock avoidance
123 mov r4, #-1 @ "" "" "" ""
124 add r0, sp, #S_FRAME_SIZE @ "" "" "" ""
125 SPFIX( addne r0, r0, #4 )
126 str r1, [sp] @ save the "real" r0 copied
127 @ from the exception stack
128
129 mov r1, lr
130
131 @
132 @ We are now ready to fill in the remaining blanks on the stack:
133 @
134 @ r0 - sp_svc
135 @ r1 - lr_svc
136 @ r2 - lr_<exception>, already fixed up for correct return/restart
137 @ r3 - spsr_<exception>
138 @ r4 - orig_r0 (see pt_regs definition in ptrace.h)
139 @
140 stmia r5, {r0 - r4}
141 .endm
因为C的调用惯例是要把函数参数放在栈的顶部,因此pt- regs结构包含原始寄存器的值,这些值是以前在汇编入口例程svc_entry中保存在栈中的。
linux+v 2.6.19 /include/asm-arm/arch-at91rm9200/entry-macro.S
18 .macro get_irqnr_and_base, irqnr, irqstat, base, tmp
19 ldr /base, =(AT91_VA_BASE_SYS) @ base virtual address of SYS peripherals
20 ldr /irqnr, [/base, #AT91_AIC_IVR] @ read IRQ vector register: de-asserts nIRQ to processor (and clears interrupt)
21 ldr /irqstat, [/base, #AT91_AIC_ISR] @ read interrupt source number
22 teq /irqstat, #0 @ ISR is 0 when no current interrupt, or spurious interrupt
23 streq /tmp, [/base, #AT91_AIC_EOICR] @ not going to be handled further, then ACK it now.
24 .endm
26/*
27 * Interrupt handling. Preserves r7, r8, r9
28 */
29 .macro irq_handler
301: get_irqnr_and_base r0, r6, r5, lr
31 movne r1, sp
32 @
33 @ routine called with r0 = irq number, r1 = struct pt_regs *
34 @
35 adrne lr, 1b
36 bne asm_do_IRQ
58 .endm
中断号的值也在irq_handler初期得以保存,所以,asm_do_IRQ可以将它提取出来。这个中断处理程序实际上要调用do_IRQ(),而do_IRQ()要调用handle_IRQ_event()函数,最后这个函数才真正地执行中断服务例程(ISR)。下图给出它们的调用关系:
|
|
|
|
|
中断处理函数的调用关系
112asmlinkage void asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
113{
114 struct pt_regs *old_regs = set_irq_regs(regs);
115 struct irqdesc *desc = irq_desc + irq;
116
121 if (irq >= NR_IRQS)
122 desc = &bad_irq_desc;
123
124 irq_enter(); //记录硬件中断状态,便于跟踪中断情况确定是否是中断上下文
125
126 desc_handle_irq(irq, desc);
///////////////////desc_handle_irq
33static inline void desc_handle_irq(unsigned int irq, struct irq_desc *desc)
34{
35 desc->handle_irq(irq, desc); //通常handle_irq指向__do_IRQ
36}
///////////////////desc_handle_irq
130
131 irq_exit(); //中断退出前执行可能的软中断,被中断前是在中断上下文中则直接退出,这保证了软中断不会嵌套
132 set_irq_regs(old_regs);
133}
157 * __do_IRQ - original all in one highlevel IRQ handler
167fastcall unsigned int __do_IRQ(unsigned int irq)
168{
169 struct irq_desc *desc = irq_desc + irq;
170 struct irqaction *action;
171 unsigned int status;
172
173 kstat_this_cpu.irqs[irq]++;
186
187 spin_lock(&desc->lock);
188 if (desc->chip->ack) //首先响应中断,通常实现为关闭本中断线
189 desc->chip->ack(irq);
190
194 status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING);
195 status |= IRQ_PENDING; /* we _want_ to handle it */
196
201 action = NULL;
202 if (likely(!(status & (IRQ_DISABLED | IRQ_INPROGRESS)))) {
203 action = desc->action;
204 status &= ~IRQ_PENDING; /* we commit to handling */
205 status |= IRQ_INPROGRESS; /* we are handling it */
206 }
207 desc->status = status;
208
215 if (unlikely(!action))
216 goto out;
217
218 /*
219 * Edge triggered interrupts need to remember
220 * pending events.
227 */
228 for (;;) {
229 irqreturn_t action_ret;
230
231 spin_unlock(&desc->lock);//解锁,中断处理期间可以响应其他中断,否则再次进入__do_IRQ时会死锁
233 action_ret = handle_IRQ_event(irq, action);
237 spin_lock(&desc->lock);
238 if (likely(!(desc->status & IRQ_PENDING)))
239 break;
240 desc->status &= ~IRQ_PENDING;
241 }
242 desc->status &= ~IRQ_INPROGRESS;
243
244out:
249 desc->chip->end(irq);
250 spin_unlock(&desc->lock);
251
252 return 1;
253}
该函数的实现用到中断线的状态,下面给予具体说明:
#define IRQ_INPROGRESS 1 /* 正在执行这个IRQ的一个处理程序*/
#define IRQ_DISABLED 2 /* 由设备驱动程序已经禁用了这条IRQ中断线 */
#define IRQ_PENDING 4 /* 一个IRQ已经出现在中断线上,且被应答,但还没有为它提供服务 */
#define IRQ_REPLAY 8 /* 当Linux重新发送一个已被删除的IRQ时 */
#define IRQ_WAITING 32 /*当对硬件设备进行探测时,设置这个状态以标记正在被测试的irq */
#define IRQ_LEVEL 64 /* IRQ level triggered */
#define IRQ_MASKED 128 /* IRQ masked - shouldn't be seen again */
#define IRQ_PER_CPU 256 /* IRQ is per CPU */
这8个状态的前5个状态比较常用,因此我们给出了具体解释。
经验表明,应该避免在同一条中断线上的中断嵌套,内核通过IRQ_PENDING标志位的应用保证了这一点。当do_IRQ()执行到for (;;)循环时,desc->status 中的IRQ_PENDING的标志位肯定为0。当CPU执行完handle_IRQ_event()函数返回时,如果这个标志位仍然为0,那么循环就此结束。如果这个标志位变为1,那就说明这条中断线上又有中断产生(对单CPU而言),所以循环又执行一次。通过这种循环方式,就把可能发生在同一中断线上的嵌套循环化解为“串行”。
在循环结束后调用desc->handler->end()函数,具体来说,如果没有设置IRQ_DISABLED标志位,就启用这条中断线。
当执行到for (;;)这个无限循环时,就准备对中断请求队列进行处理,这是由handle_IRQ_event()函数完成的。因为中断请求队列为一临界资源,因此在进入这个函数前要加锁。
handle_IRQ_event执行所有的irqaction链表:
130irqreturn_t handle_IRQ_event(unsigned int irq, struct irqaction *action)
131{
132 irqreturn_t ret, retval = IRQ_NONE;
133 unsigned int status = 0;
134
135 handle_dynamic_tick(action);
136 // 如果没有设置IRQF_DISABLED,则中断处理过程中,打开中断
137 if (!(action->flags & IRQF_DISABLED))
138 local_irq_enable_in_hardirq();
139
140 do {
141 ret = action->handler(irq, action->dev_id);
142 if (ret == IRQ_HANDLED)
143 status |= action->flags;
144 retval |= ret;
145 action = action->next;
146 } while (action);
147
150 local_irq_disable();
151
152 return retval;
153}
这个循环依次调用请求队列中的每个中断服务例程。这里要说明的是,如果设置了IRQF_DISABLED,则中断服务例程在关中断的条件下进行(不包括非屏蔽中断),但通常CPU在穿过中断门时自动关闭中断。但是,关中断时间绝不能太长,否则就可能丢失其它重要的中断。也就是说,中断服务例程应该处理最紧急的事情,而把剩下的事情交给另外一部分来处理。即后半部分(bottom half)来处理,这一部分内容将在下一节进行讨论。
不同的CPU不允许并发地进入同一中断服务例程,否则,那就要求所有的中断服务例程必须是“可重入”的纯代码。可重入代码的设计和实现就复杂多了,因此,Linux在设计内核时巧妙地“避难就易”,以解决问题为主要目标。
中断退出前执行可能的软中断,被中断前是在中断上下文中则直接退出,这保证了软中断不会嵌套
////////////////////////////////////////////////////////////
linux+v 2.6.19 /kernel/softirq.c
285void irq_exit(void)
286{
287 account_system_vtime(current);
288 trace_hardirq_exit();
289 sub_preempt_count(IRQ_EXIT_OFFSET);
290 if (!in_interrupt() && local_softirq_pending())
291 invoke_softirq();
////////////
276#ifdef __ARCH_IRQ_EXIT_IRQS_DISABLED
277# define invoke_softirq() __do_softirq()
278#else
279# define invoke_softirq() do_softirq()
280#endif
////////////
292 preempt_enable_no_resched();
293}
////////////////////////////////////////////////////////////
asm_do_IRQ()这个函数处理所有外设的中断请求后就要返回。返回情况取决于中断前程序是内核态还是用户态以及是否是可剥夺内核。
² 内核态可剥夺内核,只有在preempt_count为0时,schedule()才会被调用,其检查是否需要进行进程切换,需要的话就切换。在schedule()返回之后,或者如果没有挂起的工作,那么原来的寄存器被恢复,内核恢复到被中断的内核代码。
² 内核态不可剥夺内核,则直接返回至被中断的内核代码。
² 中断前处于用户态时,无论是否是可剥夺内核,统一跳转到ret_to_user。
虽然我们这里讨论的是中断的返回,但实际上中断、异常及系统调用的返回是放在一起实现的,因此,我们常常以函数的形式提到下面这三个入口点:
ret_to_user()
终止中断处理程序
ret_slow_syscall ( ) 或者ret_fast_syscall
终止系统调用,即由0x80引起的异常
ret_from_exception( )
终止除了0x80的所有异常
565/*
566 * This is the return code to user mode for abort handlers
567 */
568ENTRY(ret_from_exception)
569 get_thread_info tsk
570 mov why, #0
571 b ret_to_user
57ENTRY(ret_to_user)
58ret_slow_syscall:
由上可知,中断和异常需要返回用户空间时以及系统调用完毕后都需要经过统一的出口ret_slow_syscall,以此决定是否进行进程调度切换等。
linux+v 2.6.19 /arch/arm/kernel/entry-common.S
16 .align 5
17/*
18 * This is the fast syscall return path. We do as little as
19 * possible here, and this includes saving r0 back into the SVC
20 * stack.
21 */
22ret_fast_syscall:
23 disable_irq @ disable interrupts
24 ldr r1, [tsk, #TI_FLAGS]
25 tst r1, #_TIF_WORK_MASK
26 bne fast_work_pending
27
28 @ fast_restore_user_regs
29 ldr r1, [sp, #S_OFF + S_PSR] @ get calling cpsr
30 ldr lr, [sp, #S_OFF + S_PC]! @ get pc
31 msr spsr_cxsf, r1 @ save in spsr_svc
32 ldmdb sp, {r1 - lr}^ @ get calling r1 - lr
33 mov r0, r0
34 add sp, sp, #S_FRAME_SIZE - S_PC
35 movs pc, lr @ return & move spsr_svc into cpsr
36
37/*
38 * Ok, we need to do extra processing, enter the slow path.
39 */
40fast_work_pending:
41 str r0, [sp, #S_R0+S_OFF]! @ returned r0
42work_pending:
43 tst r1, #_TIF_NEED_RESCHED
44 bne work_resched
45 tst r1, #_TIF_NOTIFY_RESUME | _TIF_SIGPENDING
46 beq no_work_pending
47 mov r0, sp @ 'regs'
48 mov r2, why @ 'syscall'
49 bl do_notify_resume
50 b ret_slow_syscall @ Check work again
51
52work_resched:
53 bl schedule
54/*
55 * "slow" syscall return path. "why" tells us if this was a real syscall.
56 */
57ENTRY(ret_to_user)
58ret_slow_syscall:
59 disable_irq @ disable interrupts
60 ldr r1, [tsk, #TI_FLAGS]
61 tst r1, #_TIF_WORK_MASK
62 bne work_pending
63no_work_pending:
64 @ slow_restore_user_regs
65 ldr r1, [sp, #S_PSR] @ get calling cpsr
66 ldr lr, [sp, #S_PC]! @ get pc
67 msr spsr_cxsf, r1 @ save in spsr_svc
68 ldmdb sp, {r0 - lr}^ @ get calling r1 - lr
69 mov r0, r0
70 add sp, sp, #S_FRAME_SIZE - S_PC
71 movs pc, lr @ return & move spsr_svc into cpsr
进入ret_slow_syscall后,首先关中断,也就是说,执行这段代码时CPU不接受任何中断请求。然后,看调度标志是否为非0(tst r1, #_TIF_NEED_RESCHED),如果调度标志为非0,说明需要进行调度,则去调用schedule()函数进行进程调度。