前面简单介绍中断初始化和注册的基本流程,这里接上文留下来的两部分内容,中断处理和中断调试技巧。从内核开发的角度来讲说,前文是科普文,帮助理解接下来的内容,本文没有那么枯燥,可以玩起来。最后的调试技巧是自己总结的,帮助我修复了工作中遇到90%的中断错误。如果大佬有更好的技巧,一定分享出来哈。
众所周知,中断处理统一入口是do_IRQ
函数,经过一层层调用真正到处理函数。今天的分享主要集中在调用用 do_IRQ
之前操作。
CPU检测到有中断发生,然后调用handle_int
函数,代码如下:
BUILD_ROLLBACK_PROLOGUE handle_int
NESTED(handle_int, PT_SIZE, sp)
#ifdef CONFIG_TRACE_IRQFLAGS
.set push
.set noat
mfc0 k0, CP0_STATUS
#if defined(CONFIG_CPU_R3000) || defined(CONFIG_CPU_TX39XX)
and k0, ST0_IEP
bnez k0, 1f
mfc0 k0, CP0_EPC
.set noreorder
j k0
rfe
#else
and k0, ST0_IE
bnez k0, 1f
eret
#endif
......
jal plat_irq_dispatch
/* Restore sp */
move sp, s1
j ret_from_irq
#ifdef CONFIG_CPU_MICROMIPS
nop
#endif
END(handle_int)
__INIT
这是mips汇编,语言只是工具。这里做中断处理前保存现场等准备工作。最后有一个跳转,转到plat_irq_dispatch
函数。看,这就是熟悉的领域啦。
asmlinkage void plat_irq_dispatch(void)
{
unsigned int pending;
pending = read_c0_cause() & read_c0_status() & ST0_IM;
/* machine-specific plat_irq_dispatch */
mach_irq_dispatch(pending);
}
函数功能很简单,从寄存器中读出pending,然后传入mach_irq_dispatch
函数。这个寄存器是保存中断状态和来源,经过一系列位移运算得到一个pending。
void mach_irq_dispatch(unsigned int pending)
{
if (pending & CAUSEF_IP7)
do_IRQ(LOONGSON_TIMER_IRQ);
#if defined(CONFIG_SMP)
if (pending & CAUSEF_IP6)
loongson3_ipi_interrupt(NULL);
#endif
if (pending & CAUSEF_IP3)
loongson_pch->irq_dispatch();
if (pending & CAUSEF_IP2)
{
irqs_pci = LOONGSON_INT_ROUTER_ISR(0) & 0xf0;
irq = ffs(irqs_pci)
do_IRQ(irq - 1);
}
if (pending & UNUSED_IPS) {
pr_err("%s : spurious interrupt\n", __func__);
spurious_interrupt();
}
}
前面看到pending是根据中断的状态和来源计算得到的,这个函数以pending为依据做中断映射,也叫中断分发。如果是时钟中断,直接处理。causef_ipX这些表示状态的宏挺重要的,在start_kernel
阶段用到的位置很多,然而我没研究明白具体的含义,orz。mips手册里介绍了,感兴趣的大佬可以研究一下,然后分享出来。大多数中断,会经过loongson3_ipi_interrupt
和loongson_pch->irq_dispatch
这两个函数映射出去。先看看loongson_pch->irq_dispatch
static volatile unsigned long long *irq_status = (volatile unsigned long long *)((LS7A_IOAPIC_INT_STATUS));
void ls7a_irq_dispatch(void)
{
/* read irq status register */
unsigned long long irqs = *irq_status;
while(irqs){
irq = __ffs(irqs);
irqs &= ~(1ULL<common->affinity, cpu_active_mask);
if (cpumask_empty(&affinity)) {
do_IRQ(LS7A_IOAPIC_IRQ_BASE + irq);
continue;
}
irq_cpu[irq] = cpumask_next(irq_cpu[irq], &affinity);
if (irq_cpu[irq] >= nr_cpu_ids)
irq_cpu[irq] = cpumask_first(&affinity);
if (irq_cpu[irq] == cpu) {
do_IRQ(LS7A_IOAPIC_IRQ_BASE + irq);
continue;
}
/* balanced by other cores */
loongson3_send_irq_by_ipi(irq_cpu[irq], (0x1<<(ls7a_ipi_irq2pos[LS7A_IOAPIC_IRQ_BASE + irq])));
}
}
从中断寄存器读取status值,irq_status存储的是中断寄存器的地址。判断中断应该在哪个核上处理,调用do_IRQ
。中断初始化时候,创建了两个数组ls7a_ipi_irq2pos
和ls7a_ipi_pos2irq
,其中ls7a_ipi_irq2pos
用以记录某个中断在某个核上处理次数。这里我们要关注的是中断号是怎么的得来的,函数do_IRQ
的参数即是真正的中断号,是通过中断寄存器中的内容计算得来的。这个计算的过程就是中断映射(或者中断分发)。
下面是另一个中断映射函数——loongson3_ipi_interrupt
:
void loongson3_ipi_interrupt(struct pt_regs *regs)
{
/* Load the ipi register to figure out what we're supposed to do */
action = loongson3_ipi_read32(ipi_status0_regs[cpu_logical_map(cpu)]);
irqs = action >> IPI_IRQ_OFFSET;
/* Clear the ipi register to clear the interrupt */
loongson3_ipi_write32((u32)action, ipi_clear0_regs[cpu_logical_map(cpu)]);
if (action & SMP_RESCHEDULE_YOURSELF)
scheduler_ipi();
if (action & SMP_CALL_FUNCTION) {
irq_enter();
generic_smp_call_function_interrupt();
irq_exit();
}
if (irqs) {
int irq;
switch (loongson_pch->board_type) {
case RS780E:
while ((irq = ffs(irqs))) {
do_IRQ(irq-1);
irqs &= ~(1<<(irq-1));
}
break;
case LS2H:
do_IRQ(irqs);
break;
case LS7A:
while ((irq = ffs(irqs))) {
irq1 = ls7a_ipi_pos2irq[irq-1];
do_IRQ(irq1);
irqs &= ~(1<<(irq-1));
}
break;
default:
break;
}
}
}
从寄存器中读取action,清除ipi寄存器,根据action判断如何处理这个中断。和上文一样,这里我们主要关注中断号是怎么来的。irq是根据irqs计算来的,irqs是action移位得来的,action是从寄存器读来的。可以看到不同的主板类型对中断号的映射是不一样的。
这节内容是在上文基础上的一个提升,解bug过程中一点不成熟的小经验跟大家分享出来,大佬们见笑。
调了很多很多中断错误,回头总结的时候发现中断错误80%都是出在了中断映射部分。上文说过,中断初始化分为两个部分:显示arch下调用irq_set_chip_and_handler
设置中断high level handler和驱动中调用request_irq
注册。
产生这样的错误是因为,既没有设置high level handler,也没有人注册这个中断,但是CPU扫到了该中断。所以告诉内核没有人关注这个中断,这是个异常。一般这种错误产生的原因都是中断被映射错了位置,几乎不可能有程序员在驱动代码中,忘记注册或者是忘记设置high level handler了。当然,哪怕是真忘了,这个驱动都注册不成功哪来的中断呢?
感兴趣的大佬可以试试看,故意把中断映射到一个没有人用的中断上,改一下映射函数即可获得。顺便挨个感受硬盘,显卡,声卡,input等设备中断不工作是什么样的体验,请一定要保证PC上有一个可以正常启动的内核。
好,回到正题,这种错误开发人员第一需要知道得到就是这本来是哪个中断,?这里我没找到一个很好的从正面跟下去的办法,只有一些迂回方法。
/proc/interrupt
,interrupt文件中某一个中断没上来或者异常少,那就是它啦。当然这要在内核起来的前提下,就算系统没起来,也是有办法可以看到proc的。如果说,内核起不来呢?此时,你已经知道是某个中断被错误的映射到某个位置。接下来就简单了,去映射入口看,为什么会被映射错呢。可能是irq balance算错了,可能是中断偏移量加错了等等。
这个错误和上面的nobody care错误很相似,区别就是:nobody care错误没设置high level handler,而bad irq错误设置了。
做个实验,在中断init时候,增加一个irq_set_chip_and_handler
,这里注意里面填充的中断号没有驱动在使用的且不超出范围的。然后在中断映射时候,把某一个特定的中断映射过来。你就能自制中断错误,是不是很有意思嘞。
这个错误用的错误定位方法和上文一样,先看dmesg,看interrupt和调试。
###一些看上去怪怪的问题
这类问题虽然原因类似,但是表现千奇百怪。比如说掉盘,声音响着响着突然不响了,桌面突然卡了等等。
一般来讲,中断问题是比较严重的问题,很多会影响系统起不来,就算系统能起来桌面也进不来。但是有个这样的例外,中断映射错了,错误位置恰好是有一个驱动在用这个中断,这个驱动注册时候又恰好设置了共享中断(不要惊讶,这种情况很多的。INT中断线很少,所以大部分驱动注册时候都不会选择独占中断的)。又或者是irq balance做的有问题,分散到特定核上会出现中断号计算错误的情况,这样的表现就是刚开始是好的,用着用着驱动死了,过一会又好了这样的怪怪的现象。
这些问题头疼的地方在于,不太可能会怀疑到中断上来。虽然是同一个原因导致的,但是这种错误的表现没有共性。所以当你遇到这样有点怪的问题时,在menuconfig尽可能的关掉一些驱动会有帮助。驱动一关,对应中断就不会被注册,这就有可能会得到bad irq报错信息,帮助定位。