您的位置:首页 > 运维架构 > Linux

【嵌入式Linux学习七步曲之第五篇 Linux内核及驱动编程】深入剖析Linux中断机制之三--Linux对异常和中断的处理

2011-09-14 19:49 1396 查看
转载:http://blog.csdn.net/sailor_8318/archive/2008/07/09/2627136.aspx

深入剖析Linux中断机制之三
--Linux对异常和中断的处理
【摘要】本文详解了Linux内核的中断实现机制。首先介绍了中断的一些基本概念,然后分析了面向对象的Linux中断的组织形式、三种主要数据结构及其之间的关系。随后介绍了Linux处理异常和中断的基本流程,在此基础上分析了中断处理的详细流程,包括保存现场、中断处理、中断退出时的软中断执行及中断返回时的进程切换等问题。最后介绍了中断相关的API,包括中断注册和释放、中断关闭和使能、如何编写中断ISR、共享中断、中断上下文中断状态等。

【关键字】中断,异常,hw_interrupt_type,irq_desc_t,irqaction,asm_do_IRQ,软中断,进程切换,中断注册释放request_irq,free_irq,共享中断,可重入,中断上下文



1
Linux对异常和中断的处理

1.1 异常处理

Linux利用异常来达到两个截然不同的目的:

² 给进程发送一个信号以通报一个反常情况

² 管理硬件资源

对于第一种情况,例如,如果进程执行了一个被0除的操作,CPU则会产生一个“除法错误”异常,并由相应的异常处理程序向当前进程发送一个SIGFPE信号。当前进程接收到这个信号后,就要采取若干必要的步骤,或者从错误中恢复,或者终止执行(如果这个信号没有相应的信号处理程序)。

内核对异常处理程序的调用有一个标准的结构,它由以下三部分组成:

² 在内核栈中保存大多数寄存器的内容(由汇编语言实现)

² 调用C编写的异常处理函数

² 通过ret_from_exception()函数从异常退出。

1.2 中断处理

当一个中断发生时,并不是所有的操作都具有相同的急迫性。事实上,把所有的操作都放进中断处理程序本身并不合适。需要时间长的、非重要的操作应该推后,因为当一个中断处理程序正在运行时,相应的IRQ中断线上再发出的信号就会被忽略。另外中断处理程序不能执行任何阻塞过程,如I/O设备操作。因此,Linux把一个中断要执行的操作分为下面的三类:

² 紧急的(Critical)

这样的操作诸如:中断到来时中断控制器做出应答,对中断控制器或设备控制器重新编程,或者对设备和处理器同时访问的数据结构进行修改。这些操作都是紧急的,应该被很快地执行,也就是说,紧急操作应该在一个中断处理程序内立即执行,而且是在禁用中断的状态下。

² 非紧急的(Noncritical)

这样的操作如修改那些只有处理器才会访问的数据结构(例如,按下一个键后,读扫描码)。这些操作也要很快地完成,因此,它们由中断处理程序立即执行,但在启用中断的状态下。

² 非紧急可延迟的(Noncritical deferrable)

这样的操作如,把一个缓冲区的内容拷贝到一些进程的地址空间(例如,把键盘行缓冲区的内容发送到终端处理程序的进程)。这些操作可能被延迟较长的时间间隔而不影响内核操作,有兴趣的进程会等待需要的数据。

所有的中断处理程序都执行四个基本的操作:

² 在内核栈中保存IRQ的值和寄存器的内容。

² 给与IRQ中断线相连的中断控制器发送一个应答,这将允许在这条中断线上进一步发出中断请求。

² 执行共享这个IRQ的所有设备的中断服务例程(ISR)。

² 跳到ret_to_usr( )的地址后终止。

1.3 中断处理程序的执行流程

1.3.1
流程概述

现在,我们可以从中断请求的发生到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

1.3.2
保存现场

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

1.3.3
中断处理

因为C的调用惯例是要把函数参数放在栈的顶部,因此pt- regs结构包含原始寄存器的值,这些值是以前在汇编入口例程svc_entry中保存在栈中的。

linux+v2.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)。下图给出它们的调用关系:



asm_do_IRQ

do_IRQ()

handle_IRQ_event()

中断服务

例程1

例程

中断服务

例程2

例程

中断处理函数的调用关系

1.3.3.1
asm_do_IRQ

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)

122desc
= &bad_irq_desc;

123

124irq_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{

35desc->handle_irq(irq,
desc); //通常handle_irq指向__do_IRQ

36}

///////////////////desc_handle_irq

130

131irq_exit();
//中断退出前执行可能的软中断,被中断前是在中断上下文中则直接退出,这保证了软中断不会嵌套

132
set_irq_regs(old_regs);

133}

1.3.3.2
__do_IRQ

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

173kstat_this_cpu.irqs[irq]++;

186

187spin_lock(&desc->lock);

188 if (desc->chip->ack)
//首先响应中断,通常实现为关闭本中断线

189desc->chip->ack(irq);

190

194status
= desc->status
& ~(IRQ_REPLAY |
IRQ_WAITING);

195status
|= IRQ_PENDING; /* we _want_ to handle it */

196

201action
= NULL;

202 if (likely(!(status
& (IRQ_DISABLED |
IRQ_INPROGRESS)))) {

203action
= desc->action;

204
status &= ~IRQ_PENDING; /* we commit to handling */

205status
|= IRQ_INPROGRESS; /* we are handling it */

206 }

207desc->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 (;;) {

229irqreturn_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 }

242desc->status
&= ~IRQ_INPROGRESS;

243

244out:

249
desc->chip->end(irq);

250spin_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标志位,就启用这条中断线。

1.3.3.3
handle_IRQ_event

当执行到for (;;)这个无限循环时,就准备对中断请求队列进行处理,这是由handle_IRQ_event()函数完成的。因为中断请求队列为一临界资源,因此在进入这个函数前要加锁。

handle_IRQ_event执行所有的irqaction链表:

130irqreturn_t
handle_IRQ_event(unsigned int
irq, struct
irqaction *action)

131{

132irqreturn_t
ret,
retval =
IRQ_NONE;

133 unsigned int
status = 0;

134

135handle_dynamic_tick(action);

136 // 如果没有设置IRQF_DISABLED,则中断处理过程中,打开中断

137 if (!(action->flags
& IRQF_DISABLED))

138local_irq_enable_in_hardirq();

139

140 do {

141ret
= action->handler(irq,
action->dev_id);

142 if (ret
== IRQ_HANDLED)

143status
|= action->flags;

144retval
|= ret;

145
action =
action->next;

146 } while (action);

147

150local_irq_disable();

151

152 return
retval;

153}

这个循环依次调用请求队列中的每个中断服务例程。这里要说明的是,如果设置了IRQF_DISABLED,则中断服务例程在关中断的条件下进行(不包括非屏蔽中断),但通常CPU在穿过中断门时自动关闭中断。但是,关中断时间绝不能太长,否则就可能丢失其它重要的中断。也就是说,中断服务例程应该处理最紧急的事情,而把剩下的事情交给另外一部分来处理。即后半部分(bottom
half)来处理,这一部分内容将在下一节进行讨论。

不同的CPU不允许并发地进入同一中断服务例程,否则,那就要求所有的中断服务例程必须是“可重入”的纯代码。可重入代码的设计和实现就复杂多了,因此,Linux在设计内核时巧妙地“避难就易”,以解决问题为主要目标。

1.3.3.4
irq_exit()

中断退出前执行可能的软中断,被中断前是在中断上下文中则直接退出,这保证了软中断不会嵌套

////////////////////////////////////////////////////////////

linux+v2.6.19/kernel/softirq.c

285void
irq_exit(void)

286{

287account_system_vtime(current);

288trace_hardirq_exit();

289sub_preempt_count(IRQ_EXIT_OFFSET);

290 if (!in_interrupt()
&& local_softirq_pending())

291invoke_softirq();

////////////

276#ifdef
__ARCH_IRQ_EXIT_IRQS_DISABLED

277#
defineinvoke_softirq()
__do_softirq()

278#else

279#
defineinvoke_softirq()
do_softirq()

280#endif

////////////

292preempt_enable_no_resched();

293}

////////////////////////////////////////////////////////////

1.3.4
从中断返回

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+v2.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()函数进行进程调度。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐