本文分析了DM644x平台arm926ejs中断的流程,参考了网上一些分析arm中断流程的文章。
1.arm硬件中断向量表建立及中断响应都在linux/arch/arm/kernel/entry-armv.S中,故从该文件
开始分析。
linux/arch/arm/kernel/entry-armv.S:
__stubs_end:
.equ __real_stubs_start, .LCvectors + 0x200
.LCvectors:
swi SYS_ERROR0
b __real_stubs_start + (vector_und - __stubs_start)
ldr pc, __real_stubs_start + (.LCvswi - __stubs_start)
b __real_stubs_start + (vector_pabt - __stubs_start)
b __real_stubs_start + (vector_dabt - __stubs_start)
b __real_stubs_start + (vector_addrexcptn - __stubs_start)
b __real_stubs_start + (vector_irq - __stubs_start)
b __real_stubs_start + (vector_fiq - __stubs_start)
ENTRY(__trap_init)
stmfd {r4 - r6, lr}
mov r0, #0xff000000
orr r0, r0, #0x00ff0000 @ high vectors position
adr r1, .LCvectors @ set up the vectors
ldmia r1, {r1, r2, r3, r4, r5, r6, ip, lr}
stmia r0, {r1, r2, r3, r4, r5, r6, ip, lr}
add r2, r0, #0x200
adr r1, __stubs_start @ copy stubs to 0x200
adr r4, __stubs_end
1: ldr r3, [r1], #4
str r3, [r2], #4
cmp r1, r4
blo 1b
add r2, r0, #0x1000 @ top of high vector page
adr r4, __kuser_helper_end @ user helpers to top of page
adr r1, __kuser_helper_start @ going downwards.
1: ldr r3, [r4, #-4]!
str r3, [r2, #-4]!
cmp r4, r1
bhi 1b
LOADREGS(fd, {r4 - r6, pc})
内核解压启动完成一些初始化后,执行init/main.c中的start_kernel()函数,然后到过程便是,
start_kernel()-->setup_arch()-->early_trap_init()-->__trap_init()。
__trap_init()例程首先將7个异常向量从.LCvectors处拷贝到0xffff0000(arm的高向量地址),
然后再把__stubs_start到__stubs_end之间到代码也就是具体的异常处理程序拷贝到相对0xffff0000
偏移0x200处.
现在假设在usr mode发生一个irq,arm硬件将保存当前的pc到lr_irq和当前状态寄存器cpsr到
spsr_irq,经0xffff0018处的中断异常向量跳转至vector_IRQ处的处理程序:
.macro vector_stub, name, sym, correction=0
.align 5
vector_\name:
ldr r13, .LCs\sym
.if \correction
sub lr, lr, #\correction
.endif
str lr, [r13] @ save lr_IRQ
mrs lr, spsr
str lr, [r13, #4] @ save spsr_IRQ
@
@ now branch to the relevant MODE handling routine
@
mrs r13, cpsr
bic r13, r13, #MODE_MASK
orr r13, r13, #MODE_SVC
msr spsr_cxsf, r13 @ switch to SVC_32 mode
/*
lr&15的值是表明发生irq之前cpu所在的空间,
0:在用户空间发生了irq中断
3:在内核空间发生了irq中断
lr<<2=lr*4也就是pc+0和pc+12
分别对应LCtab_irq的.word __irq_usr域和.word __irq_svc域
*/
and lr, lr, #15
ldr lr, [pc, lr, lsl #2]
movs pc, lr @ Changes mode and branches
.endm
__stubs_start:
/*
* Interrupt dispatcher
*/
vector_stub irq, irq, 4 // 这就是vector_irq
.long __irq_usr @ 0 (USR_26 / USR_32)
.long __irq_invalid @ 1 (FIQ_26 / FIQ_32)
.long __irq_invalid @ 2 (IRQ_26 / IRQ_32)
.long __irq_svc @ 3 (SVC_26 / SVC_32)
.long __irq_invalid @ 4
.long __irq_invalid @ 5
.long __irq_invalid @ 6
.long __irq_invalid @ 7
.long __irq_invalid @ 8
.long __irq_invalid @ 9
.long __irq_invalid @ a
.long __irq_invalid @ b
.long __irq_invalid @ c
.long __irq_invalid @ d
.long __irq_invalid @ e
.long __irq_invalid @ f
这段程序在前面已经被拷贝到0xffff0000+0x200后面的地方.这段代码运行时cpu为irq mode,
最后movs pc, lr一句根据cpu被中断时到运行模式跳转入__irq_usr的同时cpu还跳转到到svc mode模式,
所以先临时保存影子寄存器lr_IRQ和spsr_IRQ,也就是pc_usr和cpsr_usr。
从上面的程序可以看出,除了usr和svc mode外,在其他运行模式下发生的irq无效,即不处理.
__irq_usr:
sub sp, sp, #S_FRAME_SIZE
stmia sp, {r0 - r12} @ save r0 - r12
ldr r4, .LCirq
add r8, sp, #S_PC
ldmia r4, {r5 - r7} @ get saved PC, SPSR
#if __LINUX_ARM_ARCH__ < 6
@ make sure our user space atomic helper is aborted
cmp r5, #VIRT_OFFSET
bichs r6, r6, #PSR_Z_BIT
#endif
stmia r8, {r5 - r7} @ save pc, psr, old_r0
stmdb r8, {sp, lr}^
alignment_trap r4, r7, __temp_irq
zero_fp
#ifdef CONFIG_PREEMPT
get_thread_info r8
ldr r9, [r8, #TI_PREEMPT] @ get preempt count
// 增加抢占计数器的值,preempt_count为正数时,禁止抢占
add r7, r9, #1 @ increment it
str r7, [r8, #TI_PREEMPT]
#endif
1: get_irqnr_and_base r0, r6, r5, lr
movne r1, sp
adrsvc ne, lr, 1b
@
@ routine called with r0 = irq number, r1 = struct pt_regs *
@
bne asm_do_IRQ
#ifdef CONFIG_PREEMPT
ldr r0, [r8, #TI_PREEMPT]
teq r0, r7
str r9, [r8, #TI_PREEMPT] // 恢复原值
strne r0, [r0, -r0]
mov tsk, r8
#else
get_thread_info tsk
#endif
mov why, #0
b ret_to_user
dm644x arm cpu中的硬件中断靠中断请求寄存器的位来区分,但由于其有跳转表寄存器(IRQENTRY),
故通过(IRQENTRY/4) - 1获得中断号,get_irqnr_and_base宏代码就是实现这个功能。中断号保存到
r0,sp保存到r1,r0与r1随后作为参数传送给asm_do_IRQ()例程,其中sp已经被压入了中断上现场的各个
寄存器值,也就是pt_reg结构:
.macro get_irqnr_and_base, irqnr, irqstat, base, tmp
/* GIVEN:
* EABASE = 0 ... so IRQNR = (IRQENTRY/4) - 1
* RETURN:
* irqnr: Interrupt number. Zero corresponds
* to bit 0 of the status register
* irqstat, base, and tmp may be considered
* as scratch registers
* Z conditions means no outstanding interrupt
*/
ldr \base, =IO_ADDRESS(DAVINCI_ARM_INTC_BASE)
ldr \tmp, [\base, #0x14]
mov \tmp, \tmp, lsr #2
sub \irqnr, \tmp, #1
cmp \tmp, #0 // 用于判断是否真的有中断发生
.endm
struct pt_regs {
long uregs[17];
};
#define ARM_pc uregs[15]
#define ARM_lr uregs[14]
#define ARM_sp uregs[13]
#define ARM_ip uregs[12]
#define ARM_fp uregs[11]
#define ARM_r10 uregs[10]
#define ARM_r9 uregs[9]
#define ARM_r8 uregs[8]
#define ARM_r7 uregs[7]
#define ARM_r6 uregs[6]
#define ARM_r5 uregs[5]
#define ARM_r4 uregs[4]
#define ARM_r3 uregs[3]
#define ARM_r2 uregs[2]
#define ARM_r1 uregs[1]
#define ARM_r0 uregs[0]
#define ARM_ORIG_r0 uregs[16] -----除了SWI调用,其值一直是-1。
值得注意一下的是adrsvc ne, lr, 1b一句,通过设置asm_do_IRQ()的返回地址lr,实现了对中断的
串行处理。
2.进入asm_do_IRQ()例程。
linux/arch/arm/kernel/irq.c:
asmlinkage notrace void
asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
{
struct irqdesc *desc = irq_desc + irq;
/*
如果打开了追踪内核事件的开关,则
记录硬中断事件,!(user_mode(regs))用于判断是否在内核中。
*/
ltt_ev_irq_entry(irq, !(user_mode(regs)));
/*
* Some hardware gives randomly wrong interrupts. Rather
* than crashing, do something sensible.
*/
if (irq >= NR_IRQS)
desc = &bad_irq_desc;
interrupt_overhead_start();
// 增加抢占计数器的值
irq_enter();
spin_lock(&irq_controller_lock);
/*
该handle在arch/arm/mach-davinci/irq.c中注册,在dm644x平台中为do_edge_IRQ()或
do_level_IRQ(),这两个例程中会调用用户注册的中断例程。
*/
desc->handle(irq, desc, regs);
/*
* Now re-run any pending interrupts.
*/
// 检查是否有的等待处理到中断请求
if (!list_empty(&irq_pending))
do_pending_irqs(regs); // 处理延后的中断请求
spin_unlock(&irq_controller_lock);
// 退出中断上下文并检查是否有软中断发生
irq_exit();
/*
检查延迟中断并记录
*/
latency_check();
// 记录irq进入事件,其实在该平台什么也没做。
ltt_ev_irq_exit();
}
在上面的asm_do_IRQ例程中,调用的desc->handle(irq, desc, regs)在dm644x平台中有两种类型,
do_edge_IRQ()和do_level_IRQ()。下面的分析都是基于asm_do_IRQ例程中的代码。
3.进入desc->handle(),这里假设指向do_edge_IRQ()例程:
linux/arch/arm/kernel/irq.c:
/*
* Most edge-triggered IRQ implementations seem to take a broken
* approach to this. Hence the complexity.
*/
void
do_edge_IRQ(unsigned int irq, struct irqdesc *desc, struct pt_regs *regs)
{
const unsigned int cpu = smp_processor_id();
desc->triggered = 1;
/*
* If we're currently running this IRQ, or its disabled,
* we shouldn't process the IRQ. Instead, turn on the
* hardware masks.
*/
/*
desc->disable_depth为正数时,表明中断被禁止,操作该变量的例程是disable_irq()和
enable_irq(),前者增加disable_depth的值,后者减少disable_depth的值。
*/
if (unlikely(desc->running || desc->disable_depth))
goto running;
/*
* Acknowledge and clear the IRQ, but don't mask it.
*/
// 该ack()在arch/arm/mach-davinci/irq.c中注册,用来清除中断请求寄存器的相应位。
desc->chip->ack(irq);
/*
* Mark the IRQ currently in progress.
*/
desc->running = 1; // 中断正在被处理
kstat_cpu(cpu).irqs[irq]++; //统计irq线发生中断的次数
#ifdef CONFIG_PREEMPT_HARDIRQS
if (desc->action) desc->status |= IRQ_INPROGRESS;
if (redirect_hardirq(desc))
return;
#endif
do {
struct irqaction *action;
action = desc->action;
if (!action) // 检查用户是否注册了中断例程
break;
/*
如果允许打开中断,则调用unmask()例程打开中断(向中断使能寄存器写1),
unmask()例程在arch/arm/mach-davinci/irq.c中注册。
*/
if (desc->pending && !desc->disable_depth) {
desc->pending = 0;
desc->chip->unmask(irq);
}
// 执行用户注册的中断例程,其包含在action结构体的handler中
__do_irq(irq, action, regs);
} while (desc->pending && !desc->disable_depth);//如果又有中断发生,继续执行
desc->status &= ~IRQ_INPROGRESS;
desc->running = 0;
/*
* If we were disabled or freed, shut down the handler.
*/
if (likely(desc->action && !check_irq_lock(desc, irq, regs)))
return;
running:
/*
* We got another IRQ while this one was masked or
* currently running. Delay it.
*/
desc->pending = 1;
desc->status |= IRQ_PENDING;
desc->chip->mask(irq);
desc->chip->ack(irq);
}
4.进入__do_irq例程,其运行用户注册的中断例程.
linux/arch/arm/kernel/irq.c:
static int
__do_irq(unsigned int irq, struct irqaction *action, struct pt_regs *regs)
{
unsigned int status;
int ret, retval = 0;
spin_unlock(&irq_controller_lock);
// 如果硬中断没有嵌套且该中断不是快速中断的话则打开中断,以允许中断嵌套
if (!hardirq_count() || !(action->flags & SA_INTERRUPT))
local_irq_enable();
interrupt_overhead_stop();
status = 0;
// 执行用户注册到中断例程,一个中断线可以被共享,也就是可以注册多个用户中断例程
do {
ret = action->handler(irq, action->dev_id, regs);
if (ret == IRQ_HANDLED)
status |= action->flags;
retval |= ret;
action = action->next;
} while (action);
if (status & SA_SAMPLE_RANDOM)
add_interrupt_randomness(irq);
#ifdef CONFIG_NO_IDLE_HZ
if (timer_update.function && irq != timer_update.skip)
timer_update.function(irq, 0, regs);
#endif
spin_lock_irq(&irq_controller_lock);
return retval;
}
5.进入do_pending_irqs()例程,处理延后的中断请求。
linux/arch/arm/kernel/irq.c:
static void do_pending_irqs(struct pt_regs *regs)
{
struct list_head head, *l, *n;
do {
struct irqdesc *desc;
/*
* First, take the pending interrupts off the list.
* The act of calling the handlers may add some IRQs
* back onto the list.
*/
head = irq_pending; // 获取延后中断列表
INIT_LIST_HEAD(&irq_pending); // 清空延后中断列表
head.next->prev = &head;
head.prev->next = &head;
/*
* Now run each entry. We must delete it from our
* list before calling the handler.
*/
// 获取所有延后的中断描述符结构体指针,并调用中断处理例程desc->handle()
list_for_each_safe(l, n, &head) {
desc = list_entry(l, struct irqdesc, pend);
list_del_init(&desc->pend);
desc->handle(desc - irq_desc, desc, regs);
}
/*
* The list must be empty.
*/
BUG_ON(!list_empty(&head));
} while (!list_empty(&irq_pending));// 该例程在运行的时候可能又产生了新的延后中断
}
6.进入irq_exit()例程,该例程会检查是否有软中断发生。
linux/arch/arm/kernel/irq.c:
// kernel/softirq.c
void irq_exit(void)
{
// 减少硬中断计数器的值
sub_preempt_count(IRQ_EXIT_OFFSET);
// 判断是否在硬中断或软中断上下文中,是否有软中断发生
if (!in_interrupt() && local_softirq_pending())
invoke_softirq(); // 处理软中断
// 减少抢占计数器的值
preempt_enable_no_resched();
}
因为软中断是穿串行处理的,所以不允许发生在软中断上下文中。
7.进入软中断处理例程__do_softirq(),其其实就是invoke_softirq()的#define定义:
linux/kernel/softirq.c:
# define invoke_softirq() __do_softirq()
/*
* We restart softirq processing MAX_SOFTIRQ_RESTART times,
* and we fall back to softirqd after that.
*
* This number has been established via experimentation.
* The two things to balance is latency against fairness -
* we want to handle softirqs as soon as possible, but they
* should not be able to lock up the box.
*/
#define MAX_SOFTIRQ_RESTART 10
asmlinkage void ___do_softirq(void)
{
struct softirq_action *h;
__u32 pending;
int max_restart = MAX_SOFTIRQ_RESTART;
int cpu;
// 把本地cpu软中断的位掩码复制到局部变量pending中
pending = local_softirq_pending();
cpu = smp_processor_id();
restart:
/* Reset the pending bitmask before enabling irqs */
local_softirq_pending() = 0;
local_irq_enable(); // 开中断,允许硬中断
h = softirq_vec; // 获取软中断结构体数组
do {
if (pending & 1) { // 数组下标越小,软中断优先级越高
{
u32 preempt_count = preempt_count();
// 如果打开了追踪内核事件的开关,记录软中断事件
ltt_ev_soft_irq(LTT_EV_SOFT_IRQ_SOFT_IRQ, (h - softirq_vec));
// 执行注册的软中断
h->action(h);
if (preempt_count != preempt_count()) {
print_symbol("softirq preempt bug: exited %s with wrong
preemption count!\n", (unsigned long) h->action);
printk("entered with %08x, exited with %08x.\n",
preempt_count, preempt_count());
preempt_count() = preempt_count;
}
}
rcu_bh_qsctr_inc(cpu);
cond_resched_all(); // 检查是否需要调度程序
}
h++;
pending >>= 1;
} while (pending);
local_irq_disable();
pending = local_softirq_pending();
/*
如果pending不为0,且循环没有达到指定到10次,则继续执行软中断
*/
if (pending && --max_restart)
goto restart;
if (pending) // 如果循环超过了10次,则唤醒内核线程wakeup_softirqd()延后执行
wakeup_softirqd();
}
从asm_do_IRQ()中返回后再用get_irqnr_and_base宏检查是否又有新的中断发生(这就是前面讲到串行
处理),否則在get_thread_info tsk宏中通过sp得到task_struct的地址,并跳到
ret_to_user:
get_thread_info tsk
mov why, #0
b ret_to_user
从以上能看出,linux系统的中断处理过程比较完善,对于一个操作系统来说是比较适合的。但是就是因为
完善,所以整个中断过程比较长,耗费的时间比裸跑的中断多很多,正所谓有利有弊,各取所需。
|