三 響應中斷
首先在分析源碼之前,讓我們瞭解一些原理性的東西, 我們都知道在處理中斷要保存當前現場狀態,然後才能處理中斷,處理完之後還要把現場狀態恢復過來才能返回到被中斷的地方繼續執行,這裏要說明的是在指令跳轉到中斷向量的地方開始執行之前,CPU幫我們做了哪些事情:
R14_irq = 要執行的下條指令地址 + 4 //這裏的下條指令是相對於被中斷指令的下條。即返回地址
SPSR_irq = CPSR //保存的現場狀態,r0到r12要由我們軟件來保存(如果需要的話)。
CPSR[4:0] = 0b10010 //進入中斷模式
CPSR[5] = 0 //在ARM模式下執行(不是Thumb下)
CPSR[7] = 1 //關掉IRQ中斷, FIQ還是開着
PC = 0Xffff0018 / 0x00000018 //根據異常向量表的位置,跳轉到特定的中斷向量處去執行。
更詳細的關於異常處理的細節可參考<<ARM Architecutre Reference Manual>>
接下來我們在來分析watchdog產生中斷後的處理流程:
當watchdog超時時將會產生中斷,中斷號就是IRQ_WDT,當產生中斷時,系統將從跳轉表中的中斷位置開始運行,對於我們這篇文章來說:是從0xffff0000 + 24處開始運行。 這個地址的指令是:
b vector_irq + stubs_offset
即直接跳轉到 vector_irq 處去運行。這些都在中斷初始化的時候分析過了。
我們來看 vector_irq,它是通過宏vector_stub來定義的:
arch/arm/kernel/entry-armv.S:
/*
* Interrupt dispatcher
*/
vector_stub irq, IRQ_MODE, 4 /*這是個宏定義*/
/*下面這些都是不同模式下的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
來看宏vector_stub
arch/arm/kernel/entry-armv.S:
.macro vector_stub, name, mode, correction=0
.align 5
vector_/name:
.if /correction
sub lr, lr, #/correction
.endif
@
@ Save r0, lr_<exception> (parent PC) and spsr_<exception>
@ (parent CPSR)
@
stmia sp, {r0, lr} @ save r0, lr
mrs lr, spsr
str lr, [sp, #8] @ save spsr
@
@ Prepare for SVC32 mode. IRQs remain disabled.
@
mrs r0, cpsr
eor r0, r0, #(/mode ^ SVC_MODE)
msr spsr_cxsf, r0
@
@ the branch table must immediately follow this code
@
and lr, lr, #0x0f
mov r0, sp
ldr lr, [pc, lr, lsl #2]
movs pc, lr @ branch to handler in SVC mode
.endm
這樣展開後 vector_irq 如下所示:
arch/arm/kernel/entry-armv.S:
vector_irq:
.if 4
@ lr保存的是被打斷指令處地址+8的值,(看上面的分析,由 PC 得到), 這裏-4則就是中斷
@ 處理完後的返回地址, 在中斷處理完後該值會賦給 PC
sub lr, lr, #4
.endif
@
@ Save r0, lr_<exception> (parent PC) and spsr_<exception>
@ (parent CPSR)
@ r0後面會用到所以要保存。
stmia sp, {r0, lr} @ save r0, lr,保存r0,lr到棧上,這裏的棧是中斷模式下的。
mrs lr, spsr @獲取spsr的值,該值保存了被中斷處執行環境的狀態(參考上面的分析)
str lr, [sp, #8] @ save spsr, 保存到棧上
@
@ Prepare for SVC32 mode. IRQs remain disabled.
@
mrs r0, cpsr
eor r0, r0, #( IRQ_MODE ^ SVC_MODE)
msr spsr_cxsf, r0 @把spsr設置成管理模式
@
@ the branch table must immediately follow this code
@
and lr, lr, #0x0f
mov r0, sp
ldr lr, [pc, lr, lsl #2]
movs pc, lr @ branch to handler in SVC mode @ pc = lr, cpsr = spsr
.endm
movs 的目的對象如果是pc的話,則還會把spsr賦值給cpsr,上面我們看到spsr被設成管理模式,因此這條語句過後的代碼也就跑在了管理模式下。
此時的棧情況如下:
可以看到該彙編代碼主要是把被中斷的代碼在執行過程中的狀態(cpsr), 返回地址(lr)等保存在中斷模式下的棧裏,然後進入到管理模式下去執行中斷,同時令r0 = sp,這樣可以在管理模式下找到該地址,進而獲取spsr等信息。該彙編代碼最終根據被中斷的代碼所處的模式跳轉到相應的處理程序中去。
注意管理模式下的棧和中斷模式下的棧不是同一個。同時由於在上面的代碼中棧指針(sp)沒有進行移位,因此即使後面的代碼沒對這個棧進行出棧操作,也不會因爲不斷的產生中斷而導致棧溢出。
下面我們以用戶模式產生中斷爲例,它將跳轉到__irq_usr(看vector_irq的定義)
arch/arm/kernel/entry-armv.S:
__irq_usr:
usr_entry @宏,保存各寄存器,便於返回的時候恢復
get_thread_info tsk @獲取保存當前task信息的地址
#ifdef CONFIG_PREEMPT
ldr r8, [tsk, #TI_PREEMPT] @ get preempt count
add r7, r8, #1 @ increment it
str r7, [tsk, #TI_PREEMPT]
#endif
irq_handler @處理中斷
#ifdef CONFIG_PREEMPT
ldr r0, [tsk, #TI_PREEMPT]
str r8, [tsk, #TI_PREEMPT]
teq r0, r7
strne r0, [r0, -r0]
#endif
mov why, #0
b ret_to_user @返回
先看usr_entry
arch/arm/kernel/entry-armv.S:
.macro usr_entry
sub sp, sp, #S_FRAME_SIZE @棧指針下移,空出一段空間存放各寄存器
stmib sp, {r1 - r12} @保存r1到r12的值
ldmia r0, {r1 - r3} @把r0指向的地址處的值傳給r1到r13,即r1=r0,r2=lr,r3=spsr。
add r0, sp, #S_PC @ here for interlock avoidance @不會描述,看下面的圖吧
mov r4, #-1 @ "" "" "" ""
str r1, [sp] @ save the "real" r0 copied @r1存放的是實際r0的值,這裏就是存儲
@ from the exception stack @r0的值
#if __LINUX_ARM_ARCH__ < 6 && !defined(CONFIG_NEEDS_SYSCALL_FOR_CMPXCHG)
#ifndef CONFIG_MMU
#warning "NPTL on non MMU needs fixing"
#else
@ make sure our user space atomic helper is aborted
cmp r2, #TASK_SIZE
bichs r3, r3, #PSR_Z_BIT
#endif
#endif
@
@ We are now ready to fill in the remaining blanks on the stack:
@
@ r2 - lr_<exception>, already fixed up for correct return/restart
@ r3 - spsr_<exception>
@ r4 - orig_r0 (see pt_regs definition in ptrace.h)
@
@ Also, separately save sp_usr and lr_usr
@
stmia r0, {r2 - r4} @存儲返回地址,現場狀態等(被中斷代碼處的)
stmdb r0, {sp, lr}^ @存儲用戶模式下的sp,lr,好像這裏的順序和規範上倒了一下J
@
@ Enable the alignment trap while in kernel mode
@
alignment_trap r0
@
@ Clear FP to mark the first stack frame
@
zero_fp
.endm
這個宏主要就是保存各個寄存器值到棧上相應的位置,這個宏執行完後的棧如下所示:
S_FRAME_SIZE, S_PC在arch/arm/kernel/Asm-offsets.c:中定義
DEFINE(S_FRAME_SIZE, sizeof(struct pt_regs));
DEFINE(S_PC, offsetof(struct pt_regs, ARM_pc));
include/asm-arm/Ptrace.h:
struct pt_regs {
long uregs[18];
};
#define ARM_pc uregs[15]
呵呵,pt_regs中對應的就是上面棧上的18個寄存器,ARM_pc是pc寄存器存放在這個數組中的偏移。
接着看get_thread_info, 它也是個宏,用來獲取當前線程的地址。在我的一篇linux啓動代碼分析裏曾寫過線程的定義方式:
include/linux/Sched.h:
union thread_union {
struct thread_info thread_info; /*線程屬性*/
unsigned long stack[THREAD_SIZE/sizeof(long)]; /*棧*/
};
由它定義的線程是8K字節對齊的, 並且在這8K的最低地址處存放的就是thread_info對象,即該棧擁有者線程的對象,而get_thread_info就是通過把sp低13位清0(8K邊界)來獲取當前thread_info對象的地址。
arch/arm/kernel/entry-armv.S:
.macro get_thread_info, rd
mov /rd, sp, lsr #13
mov /rd, /rd, lsl #13
.endm
調用該宏後寄存器tsk裏存放的就是當前線程的地址了, tsk是哪個寄存器呢,呵呵我們在看:
arch/arm/kernel/entry-header.S:
tsk .req r9 @ current thread_info
呵呵,tsk只是r9的別名而已, 因此這時r9裏保存的就是當前線程的地址。
我們接着看irq_handler:
arch/arm/kernel/entry-armv.S:
.macro irq_handler
1: get_irqnr_and_base r0, r6, r5, lr @平臺相關,獲取中斷號
movne r1, sp @如果r0(中斷號)不等於0,則r1指向sp所在地址,即pt_regs對象地址(看上圖)
@
@ routine called with r0 = irq number, r1 = struct pt_regs *
@
adrne lr, 1b @ 如果r0(中斷號)不等於0, lr(返回地址)等於標號1處,即
@ get_irqnr_and_base r0, r6, r5, lr的那行,即循環處理所有的中斷。
bne asm_do_IRQ @處理該中斷
#ifdef CONFIG_SMP
/*
* XXX
*
* this macro assumes that irqstat (r6) and base (r5) are
* preserved from get_irqnr_and_base above
*/
test_for_ipi r0, r6, r5, lr
movne r0, sp
adrne lr, 1b
bne do_IPI
#ifdef CONFIG_LOCAL_TIMERS
test_for_ltirq r0, r6, r5, lr
movne r0, sp
adrne lr, 1b
bne do_local_timer
#endif
#endif
.endm
get_irqnr_and_base是平臺相關的,這裏就不列出來了,對於s3c2410,代碼在include/asm-arm/s3c2410/entry-macro.S裏,該宏處理完後,r0 = 中斷號, 接下來r1賦值爲sp地址(pt_regs對象地址), 最後調用c函數asm_do_IRQ, r0, r1作爲參數被傳遞進去。asm_do_IRQ()處理完後將返回到lr指向的地址處即上面彙編部分標號爲1的地址處繼續執行。
我們把__irq_usr的彙編部分分析完後再來分析asm_do_IRQ()等c函數。
Arch/arm/kernel/entry-armv.S:
__irq_usr:
……
……
mov why, #0 @ why = 0, why 是 r8的別名,
b ret_to_user @返回到用戶模式下
我們看ret_to_user
arch/arm/kernel/entry-common.S:
ENTRY(ret_to_user)
ret_slow_syscall:
disable_irq @ disable interrupts @關中斷,
ldr r1, [tsk, #TI_FLAGS] @獲取thread_info中flags域的值
tst r1, #_TIF_WORK_MASK @判斷task是否被阻塞
bne work_pending @根據需要進行進程的切換。
no_work_pending:
@ slow_restore_user_regs
ldr r1, [sp, #S_PSR] @ get calling cpsr 獲取被中斷代碼處的狀態(cpsp)
ldr lr, [sp, #S_PC]! @ get pc 獲取返回地址(被中斷代碼的下條代碼處的地址)
msr spsr_cxsf, r1 @ save in spsr_svc, spsr裏保存好被中斷代碼處的狀態(cpsp)
ldmdb sp, {r0 - lr}^ @ get calling r1 – lr 從棧上獲取用戶態下的r0到lr的值
mov r0, r0
add sp, sp, #S_FRAME_SIZE - S_PC @棧地址恢復,避免多箇中斷後溢出
movs pc, lr @ return & move spsr_svc into cpsr, 返回被中斷代碼處繼續執行,並把spsr賦給cpsp,即恢復被中斷處的現場狀態。這樣CPU又可以從被中斷的地方繼續執行了,而且這個時候所有的寄存器值(r0到r12),包括狀態寄存器值(cpsr)都是源碼被中斷時的值。
我們順便看下work_pending
arch/arm/kernel/entry-common.S:
work_pending:
tst r1, #_TIF_NEED_RESCHED @判斷是否需要調度進程
bne work_resched @進程調度
tst r1, #_TIF_NOTIFY_RESUME | _TIF_SIGPENDING
beq no_work_pending @無需調度,返回
mov r0, sp @ 'regs'
mov r2, why @ 'syscall'
bl do_notify_resume
b ret_slow_syscall @ Check work again
由該彙編可知,如果在用戶模式下產生中斷的話,在返回的時候,會根據需要進行進程調度,而從代碼可知,如果中斷髮生在管理等內核模式下的話是不會進行進程調度的。
Ok, 中斷的流程大體就是這樣的,下面我們就開始分析c函數裏的中斷流程。
先來看asm_do_IRQ
arch/arm/kernel/Irq.c:
/*
* do_IRQ handles all hardware IRQ's. Decoded IRQs should not
* come via this function. Instead, they should provide their
* own 'handler'
*/
asmlinkage void asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
{
struct irqdesc *desc = irq_desc + irq; /*獲取中斷描述符*/
/*
* Some hardware gives randomly wrong interrupts. Rather
* than crashing, do something sensible.
*/
if (irq >= NR_IRQS) /*參數檢查*/
desc = &bad_irq_desc;
irq_enter();
desc_handle_irq(irq, desc, regs); /*中斷處理*/
/* AT91 specific workaround */
irq_finish(irq);
irq_exit();
}
該函數的調用desc_handle_irq()來繼續處理中斷。
include/asm-arm/mach/Irq.h:
/*
* Obsolete inline function for calling irq descriptor handlers.
*/
static inline void desc_handle_irq(unsigned int irq, struct irq_desc *desc,
struct pt_regs *regs)
{
desc->handle_irq(irq, desc, regs);
}
調用中斷描述符的handler_irq函數來處理該中斷,對於IRQ_WDT 就是do_edge_IRQ(前面分析過)。
include/asm-arm/mach/Irq.h:
#define do_edge_IRQ handle_edge_irq
kernel/irq/Chip.c:
/**
* handle_edge_irq - edge type IRQ handler
* @irq: the interrupt number
* @desc: the interrupt description structure for this irq
* @regs: pointer to a register structure
*
* Interrupt occures on the falling and/or rising edge of a hardware
* signal. The occurence is latched into the irq controller hardware
* and must be acked in order to be reenabled. After the ack another
* interrupt can happen on the same source even before the first one
* is handled by the assosiacted event handler. If this happens it
* might be necessary to disable (mask) the interrupt depending on the
* controller hardware. This requires to reenable the interrupt inside
* of the loop which handles the interrupts which have arrived while
* the handler was running. If all pending interrupts are handled, the
* loop is left.
*/
void fastcall
handle_edge_irq(unsigned int irq, struct irq_desc *desc, struct pt_regs *regs)
{
const unsigned int cpu = smp_processor_id();
spin_lock(&desc->lock);
desc->status &= ~(IRQ_REPLAY | IRQ_WAITING);
/*
* If we're currently running this IRQ, or its disabled,
* we shouldn't process the IRQ. Mark it pending, handle
* the necessary masking and go out
*/
/*
* 如果該中斷正在處理或者該中斷被disable掉了的話,就不處理該中斷,並清掉pending
* 寄存器裏的相應位
*/
if (unlikely((desc->status & (IRQ_INPROGRESS | IRQ_DISABLED)) ||
!desc->action)) {
desc->status |= (IRQ_PENDING | IRQ_MASKED);
mask_ack_irq(desc, irq); /*mask該中斷,清pending標誌位*/
goto out_unlock;
}
kstat_cpu(cpu).irqs[irq]++; /*統計中斷數量*/
/* Start handling the irq */
/*開始處理中斷,先清掉pending標誌位*/
desc->chip->ack(irq);
/* Mark the IRQ currently in progress.*/
desc->status |= IRQ_INPROGRESS; /*標上正在處理的標記*/
do {
struct irqaction *action = desc->action; /*獲取該中斷的action*/
irqreturn_t action_ret;
if (unlikely(!action)) {
desc->chip->mask(irq) /*如果沒有註冊action,則mask該中斷*/;
goto out_unlock;
}
/*
* When another irq arrived while we were handling
* one, we could have masked the irq.
* Renable it, if it was not disabled in meantime.
*/
/*
* 如果以前被mask掉的話,在這裏把它打開
*/
if (unlikely((desc->status &
(IRQ_PENDING | IRQ_MASKED | IRQ_DISABLED)) ==
(IRQ_PENDING | IRQ_MASKED))) {
desc->chip->unmask(irq); /*unmask該中斷*/
desc->status &= ~IRQ_MASKED;
}
desc->status &= ~IRQ_PENDING;
spin_unlock(&desc->lock);
action_ret = handle_IRQ_event(irq, regs, action); /*處理中斷事件*/
if (!noirqdebug)
note_interrupt(irq, desc, action_ret, regs);
spin_lock(&desc->lock);
/*如果有IRQ_PENDING狀態,則說明又有中斷產生過,則繼續執行*/
} while ((desc->status & (IRQ_PENDING | IRQ_DISABLED)) == IRQ_PENDING);
desc->status &= ~IRQ_INPROGRESS;
out_unlock:
spin_unlock(&desc->lock);
}
該函數的大體功能都在函數體內解釋出來了,這裏我們對調用的每個函數在進行分析。
先看mask_ack_irq
kernel/irq/Chip.c:
static inline void mask_ack_irq(struct irq_desc *desc, int irq)
{
if (desc->chip->mask_ack) /*對於IRQ_WDT, 該函數沒定義*/
desc->chip->mask_ack(irq);
else {
desc->chip->mask(irq); /*對於IRQ_WDT,該函數就是s3c_irq_mask*/
desc->chip->ack(irq); /*對於IRQ_WDT,該函數就是s3c_irq_ack*/
}
}
可以看到它調用具體平臺相關的mask函數來處理該中斷。
我們來看s3c_irq_mask
arch/arm/mach-s3c2410/Irq.c:
static void
s3c_irq_mask(unsigned int irqno)
{
unsigned long mask;
irqno -= IRQ_EINT0;
mask = __raw_readl(S3C2410_INTMSK);
mask |= 1UL << irqno; /*mask掉對應的中斷號*/
__raw_writel(mask, S3C2410_INTMSK); /*寫MASK寄存器*/
}
改函數僅僅是把MASK寄存器中對應的中斷mask掉,即不再響應該中斷
arch/arm/mach-s3c2410/Irq.c:
static inline void
s3c_irq_ack(unsigned int irqno)
{
unsigned long bitval = 1UL << (irqno - IRQ_EINT0);
/*清除pending寄存器的相應位*/
__raw_writel(bitval, S3C2410_SRCPND);
__raw_writel(bitval, S3C2410_INTPND);
}
由上面這兩個函數可以看出來mask_ack_irq的作用是先mask掉該中斷,並清除pending位,中斷被mask掉後系統就不再響應了, 而pending位被清掉說明系統中該中斷沒有觸發。一般在中斷處理完後都要清pending位, 要不然系統會認爲該中斷又被觸發了。
handle_edge_irq()裏調用的unmask函數,其實就是打開相應的中斷,讓系統響應這個中斷,代碼就不列出來了。
接下來中斷看handle_IRQ_event(),它纔是真正的中斷處理函數。
kernel/irq/handle.c:
/**
* handle_IRQ_event - irq action chain handler
* @irq: the interrupt number
* @regs: pointer to a register structure
* @action: the interrupt action chain for this irq
*
* Handles the action chain of an irq event
*/
irqreturn_t handle_IRQ_event(unsigned int irq, struct pt_regs *regs,
struct irqaction *action)
{
irqreturn_t ret, retval = IRQ_NONE;
unsigned int status = 0;
handle_dynamic_tick(action);
/*下面這個if判斷:當執行action操作時是否可以打開中斷*/
if (!(action->flags & IRQF_DISABLED))
local_irq_enable_in_hardirq(); /*打開中斷*/
do {
/*
* 中斷handler,也就是我們通過request_irq註冊的中斷函數,對於IRQ_WDT就是
* s3c2410wdt_irq
*/
ret = action->handler(irq, action->dev_id, regs);
if (ret == IRQ_HANDLED)
status |= action->flags;
retval |= ret;
action = action->next; /*記得嗎,如果該中斷可以共享的話,它就不爲NULL*/
} while (action);
if (status & IRQF_SAMPLE_RANDOM)
add_interrupt_randomness(irq);
local_irq_disable();
return retval;
}
該函數主要就是調用了action的handler函數,也就是我們用request_irq註冊的中斷例程。這裏需要注意的是:如果我們註冊中斷的時候指明可以共享的話,則必須在我們的中斷例程裏判斷當前產生的中斷是否就是我們自己的中斷,這可以通過傳進來的參數來判斷(該參數就是我們註冊時提供的)。
OK, 到這裏整個中斷的流程就大致分析完了。