参考文档:
(1)http://blog.chinaunix.net/uid-23769728-id-3079164.html
(2)https://blog.csdn.net/yiyeguzhou100/article/details/49887975
(3)https://blog.csdn.net/ahskx/article/details/50618985
(4)http://blog.chinaunix.net/uid-24153750-id-5113000.html
第四部分:中断的处理过程-C语言部分
中断处理的整体框图
=================================================================================
4.1
按照x86处理器在外部中断发生时的硬件逻辑,在do_IRQ被调用时,处理器已经屏蔽了对外部中断的响应。在图中我们看到中断的处理大体上被分成两部分HARDIRQ和SOFTIRQ,对应到代码层面,do_IRQ()中调用irq_enter函数可以看做hardirq部分的开始,而irq_exit函数的调用则标志着softirq部分的开始;
<arch/x86/kernel/irq.c>
__visible unsigned int __irq_entry do_IRQ(struct pt_regs *regs)
{
struct pt_regs *old_regs = set_irq_regs(regs);
struct irq_desc * desc;
unsigned vector = ~regs->orig_ax; /* 对栈中保存的向量号取反,和原先取反的操作相呼应 */
entering_irq(); /* preempt_count变量的HARDIRQ部分+1,即标识中断上半部处理的开始 */
/* entering_irq() tells RCU that we're not quiescent. Check it. */
RCU_LOCKDEP_WARN(!rcu_is_watching(), "IRQ failed to wake up RCU");
desc = __this_cpu_read(vector_irq[vector]); /* 通过中断向量号获取到对应中断描述符 */
if (!handle_irq(desc, regs)) {
ack_APIC_irq();
if (desc != VECTOR_RETRIGGERED) {
pr_emerg_ratelimited("%s: %d.%d No irq handler for vector\n",__func__,smp_processor_id(),vector);
} else {
__this_cpu_write(vector_irq[vector], VECTOR_UNUSED);
}
}
exiting_irq(); /* 标志着中断上半部处理的结束 */
set_irq_regs(old_regs);
return 1;
}
===========================================================================================
<arch/x86/include/asm/apic.h>
static inline void entering_irq(void)
{
irq_enter(); /* 分析如下 */
exit_idle(); /* 作用不详,待学习 */
}
===========================================================================================
static inline void exiting_irq(void)
{
irq_exit();
}
===========================================================================================
<kernel/softirq.c>
void irq_enter(void)
{
rcu_irq_enter();
if (is_idle_task(current) && !in_interrupt()) { /* 判断1)是否是idle进程,2)是否已经处于中断上下文 */
/* /* 下述具体作用不解,待学习 */
* Prevent raise_softirq from needlessly waking up ksoftirqd
* here, as softirq will be serviced on return from interrupt.
*/
local_bh_disable();
tick_irq_enter();
_local_bh_enable();
}
__irq_enter();
}
===========================================================================================
/*
* Exit an interrupt context. Process softirqs if needed and possible:
*/
void irq_exit(void)
{
#ifndef __ARCH_IRQ_EXIT_IRQS_DISABLED
local_irq_disable();
#else
WARN_ON_ONCE(!irqs_disabled());
#endif
account_irq_exit_time(current);
preempt_count_sub(HARDIRQ_OFFSET); /* HARDIRQ计数-1 */
/* 判断是否处于中断上下文,包括硬中断和软中断;是否有软中断处于pending状态 */
if (!in_interrupt() && local_softirq_pending())
invoke_softirq();
tick_irq_exit();
rcu_irq_exit();
trace_hardirq_exit(); /* must be last! */
}
===========================================================================================
<include/linux/hardirq.h>
#define __irq_enter() \
do { \
account_irq_enter_time(current); \
preempt_count_add(HARDIRQ_OFFSET); \ /* HARDIRQ计数+1 */
trace_hardirq_enter(); \
} while (0)
内核用于标识中断上下文(in_interrupt())的变量preempt_count的布局:
1)irq_enter()函数的核心调用是__irq_enter(),后者的主要作用是在上图的preempt_count变量的HARDIRQ部分+1,即标识一个hardirq的上下文,所以可以认为do_IRQ()调用irq_enter函数 意味着中断处理进入hardirq阶段。
【补充说明】此时处理器响应外部中断的能力依然是被disable掉的(EFLAG.IF=0),因为ISR基本上属于设备驱动程序涉足的领域,内核无法保证在设备驱动程序的ISR中会否将EFLAG.IF置1,所以我们会在内核代码中看到内核开发者为了尽可能避免非受控的ISR部分给系统带来的损害所做的努力。按照现在内核的设计理念,在 hardirq部分最好不要打开中断,所以处在hardirq上下文中的设备驱动程序的ISR应该尽可能快地返回,而将耗时的中断处理善后工作留到softirq部分。因为在softirq部分,内核会使EFLAG.IF=1,所以也意味着softirq部分可以随时被外部中断所打断。
2)irq_enter()的实现中将preempt_count变量的HARDIRQ部分+1,记录硬件中断的次数,这样irq_enter的就可以禁止抢占。preempt_count是进程调度时用到的.也就是系统会根据preempt_count的值来判断是否可以调度以及抢占。只有当preempt_count为0时才可以调度或抢占。
【补充说明】这样的话在do_IRQ执行期间(并且在irq_exit执行preempt_count-1之前),是不允许内核抢占以及调度到其他进程的,这样不论是在irq_enter或者irq_exit函数执行过程中,current宏得到的都是同一个进程,获取的都是同一个进程的preempt_count,即中断前运行的进程。
你可能会奇怪,既然此时的irq中断都是都是被禁止的,为何还要禁止抢占?这是因为要考虑中断嵌套的问题,一旦驱动程序主动通过local_irq_enable打开了IRQ,而此时该中断还没处理完成,新的irq请求到达,这时代码会再次进入irq_enter,在本次嵌套中断返回时,内核不希望进行抢占调度,而是要等到最外层的中断处理完成后才做出调度动作,所以才有了禁止抢占这一处理。
<arch/x86/kernel/irq_64.c>
bool handle_irq(struct irq_desc *desc, struct pt_regs *regs)
{
stack_overflow_check(regs); /* 栈溢出检测,需要配置CONFIG_DEBUG_STACKOVERFLOW */
if (IS_ERR_OR_NULL(desc)) /* 中断描述符有效性判断 */
return false;
generic_handle_irq_desc(desc);
return true;
}
<include/linux/irqdesc.h>
static inline void generic_handle_irq_desc(struct irq_desc *desc)
{
desc->handle_irq(desc);
}
下一步就是desc->handle_irq(desc)在哪里初始化的?
=================================================================================
4.2
此处结合上一章初始化irq部分可知:
<arch/x86/kernel/irqinit.c>
void __init init_IRQ(void)
{
......
x86_init.irqs.intr_init();
}
===========================================================================================
struct x86_init_ops x86_init __initdata = {
......
.irqs = {
.pre_vector_init = init_ISA_irqs,
.intr_init = native_init_IRQ,
.trap_init = x86_init_noop,
},
......
}
===========================================================================================
void __init native_init_IRQ(void)
{
......
/* Execute any quirks before the call gates are initialised: */
x86_init.irqs.pre_vector_init(); //init_ISA_irqs
......
}
===========================================================================================
void __init init_ISA_irqs(void)
{
struct irq_chip *chip = legacy_pic->chip;
int i;
#if defined(CONFIG_X86_64) || defined(CONFIG_X86_LOCAL_APIC)
init_bsp_APIC();
#endif
legacy_pic->init(0); /* 8259A的初始化 */
/* 建立0~15软中断号和irq_chip、desc->handle_irq之间的关系 */
for (i = 0; i < nr_legacy_irqs(); i++)
irq_set_chip_and_handler(i, chip, handle_level_irq);
}
===========================================================================================
/* 其中legacy_pic的初始化如下 */
<arch/x86/kernel/i8259.c>
struct legacy_pic *legacy_pic = &default_legacy_pic;
struct legacy_pic default_legacy_pic = {
.nr_legacy_irqs = NR_IRQS_LEGACY, /* #define NR_IRQS_LEGACY 16*/
.chip = &i8259A_chip,
.mask = mask_8259A_irq,
.unmask = unmask_8259A_irq,
.mask_all = mask_8259A,
.restore_mask = unmask_8259A,
.init = init_8259A,
.probe = probe_8259A,
.irq_pending = i8259A_irq_pending,
.make_irq = make_8259A_irq,
};
===========================================================================================
<arch/x86/include/asm/i8259.h>
static inline int nr_legacy_irqs(void)
{
return legacy_pic->nr_legacy_irqs;
}
===========================================================================================
<include/linux/irq.h>
static inline void irq_set_chip_and_handler(unsigned int irq, struct irq_chip *chip, irq_flow_handler_t handle)
{
irq_set_chip_and_handler_name(irq, chip, handle, NULL);
}
===========================================================================================
<kernel/irq/chip.c>
void irq_set_chip_and_handler_name(unsigned int irq, struct irq_chip *chip, irq_flow_handler_t handle, const char *name)
{
irq_set_chip(irq, chip);
__irq_set_handler(irq, handle, 0, name);
}
===========================================================================================
void __irq_set_handler(unsigned int irq, irq_flow_handler_t handle, int is_chained, const char *name)
{
......
__irq_do_set_handler(desc, handle, is_chained, name);
irq_put_desc_busunlock(desc, flags);
}
===========================================================================================
void __irq_do_set_handler(struct irq_desc *desc, irq_flow_handler_t handle, int is_chained, const char *name)
{
....
desc->handle_irq = handle; //handle 即为handle_level_irq
....
}
由此可见,软中断号0-15的desc->handle_irq()为handle_level_irq(),chip为i8259A_chip。
注意区分软中断号(又叫虚拟中断号virq)和硬件中断向量号,硬件中断向量号和架构相关,比如x86的中断向量号0-255。这里的ISA软中断号0-15对应的硬中断号为0x30-0x3F。
【问题】其他软中断号的desc->handle_irq()是在何处初始化的?
=================================================================================
4.3
handle_level_irq()属于中断流控层
所谓中断流控是指合理并正确地处理连续发生的中断,比如一个中断在处理中,同一个中断再次到达时如何处理,何时应该屏蔽中断,何时打开中断,何时回应中断控制器等一系列的操作。该层实现了与体系和硬件无关的中断流控处理操作,它针对不同的中断电气类型(level,edge…),实现了对应的标准中断流控处理函数,在这些处理函数中,最终会把中断控制权传递到驱动程序注册中断时传入的处理函数或者是中断线程中。目前的通用中断子系统实现了以下这些标准流控回调函数,这些函数都定义在<kernel/irq/chip.c>中,
1)handle_simple_irq 用于简易流控处理;
2)handle_level_irq 用于电平触发中断的流控处理;
3)handle_edge_irq 用于边沿触发中断的流控处理;
4)handle_fasteoi_irq 用于需要响应eoi的中断控制器;
5)handle_percpu_irq 用于只在单一cpu响应的中断;
6)handle_nested_irq 用于处理使用线程的嵌套中断;
以handle_level_irq()为例继续往下分析:
<kernel/irq/chip.c>
/**
* handle_level_irq - Level type irq handler
* @desc: the interrupt description structure for this irq
*
* Level type interrupts are active as long as the hardware line has
* the active level. This may require to mask the interrupt and unmask
* it after the associated handler has acknowledged the device, so the
* interrupt line is back to inactive.
*/
void handle_level_irq(struct irq_desc *desc)
{
raw_spin_lock(&desc->lock); /* 获取自旋锁,用于防止同一个desc->handle_irq的重入 */
mask_ack_irq(desc); /* 向产生中断的硬件发出ACK响应,并暂时屏蔽该中断线,设置IRQD_IRQ_MASKED标志 */
if (!irq_may_run(desc)) /* 判断是否处理该中断,详见如下 */
goto out_unlock;
/*
* 清除desc的相关状态标志位:
* 1)IRQS_REPLAY标志是用来拯救丢失的中断,此标志在check_irq_resend函数中设置。通过中断控制器APIC上的中断信号又一次向cpu发中断,而不是通过外设硬件来重发中断。
* 这里进入到了handle_level_irq函数表示已经收到了中断。不须要重发,所以清除此标志。
* 2)IRQS_WAITING标志表示中断等待处理,这里收到了中断并处理,因此也清除此标志。
* */
desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);
kstat_incr_irqs_this_cpu(desc); /* 增加中断计数统计 */
/*
* If its disabled or no action available keep it masked and get out of here
*/
if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {
desc->istate |= IRQS_PENDING;
goto out_unlock;
}
handle_irq_event(desc); /* 执行中断ISR,详见如下 */
cond_unmask_irq(desc); /* 恢复该中断,对应上面的屏蔽该中断 */
out_unlock:
raw_spin_unlock(&desc->lock);
}
===========================================================================================
static bool irq_may_run(struct irq_desc *desc)
{
unsigned int mask = IRQD_IRQ_INPROGRESS | IRQD_WAKEUP_ARMED; /* 疑问:IRQD_WAKEUP_ARMED标志位的作用?*/
/*
* If the interrupt is not in progress and is not an armed
* wakeup interrupt, proceed.
*/
if (!irqd_has_set(&desc->irq_data, mask))
return true;
/*
* If the interrupt is an armed wakeup source, mark it pending
* and suspended, disable it and notify the pm core about the
* event.
*/
if (irq_pm_check_wakeup(desc))
return false;
/*
* Handle a potential concurrent poll on a different core.
*/
return irq_check_poll(desc); /* 疑问:此处查询中断的轮询,作用不清楚?*/
}
===========================================================================================
<kernel/irq/handle.c>
irqreturn_t handle_irq_event(struct irq_desc *desc)
{
irqreturn_t ret;
desc->istate &= ~IRQS_PENDING; /* 清除IRQS_PENDING标志 */
irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS); /* 设置IRQD_IRQ_INPROGRESS标志,表示正在处理当前中断ISR */
raw_spin_unlock(&desc->lock); /* 疑问:为什么这里又解锁了? */
ret = handle_irq_event_percpu(desc); /* 执行中断ISR,详见如下 */
raw_spin_lock(&desc->lock);
irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS); /* 清除IRQD_IRQ_INPROGRESS标志,表示此次中断ISR处理完毕 */
return ret;
}
===========================================================================================
irqreturn_t handle_irq_event_percpu(struct irq_desc *desc)
{
irqreturn_t retval = IRQ_NONE;
unsigned int flags = 0, irq = desc->irq_data.irq;
struct irqaction *action = desc->action;
do {
irqreturn_t res;
trace_irq_handler_entry(irq, action); /* 疑问:作用不详? */
res = action->handler(irq, action->dev_id); /* 执行action->handler,即request_irq时注册的primary handler */
trace_irq_handler_exit(irq, action, res);
/* 判断全局中断IF是否打开,防止用户注册的primary handler中打开了全局中断 */
if (WARN_ONCE(!irqs_disabled(),"irq %u handler %pF enabled interrupts\n", irq, action->handler))
local_irq_disable();
switch (res) {
/* 返回值res定义见下 */
case IRQ_WAKE_THREAD: /* 线程化的中断通常返回这个值IRQ_WAKE_THREAD */
/*
* Catch drivers which return WAKE_THREAD but
* did not set up a thread function
*/
if (unlikely(!action->thread_fn)) {
warn_no_thread(irq, action);
break;
}
__irq_wake_thread(desc, action); /* 唤醒线程化的中断处理内核线程 */
/* Fall through to add to randomness */
case IRQ_HANDLED:
flags |= action->flags;
break;
default:
break;
}
retval |= res;
action = action->next; /* 如果共享中断,则循环执行action链表上的action */
} while (action);
add_interrupt_randomness(irq, flags); /* 作用不详,暂不关注 */
if (!noirqdebug)
note_interrupt(desc, retval);
return retval;
}
===========================================================================================
<include/linux/irqreturn.h>
/**
* enum irqreturn
* @IRQ_NONE interrupt was not from this device or was not handled
* @IRQ_HANDLED interrupt was handled by this device
* @IRQ_WAKE_THREAD handler requests to wake the handler thread
*/
enum irqreturn {
IRQ_NONE = (0 << 0),
IRQ_HANDLED = (1 << 0),
IRQ_WAKE_THREAD = (1 << 1),
};
通过handle_irq来调用我们提供的中断处理函数、清除中断,对于我们写驱动程序的人,只需要写具体的中断处理函数就可以了,在具体的中断处理函数中我们只需要关系我们需要实现的功能,不需要关系芯片相关的中断使能、中断清除等工作。