龙芯中断再探(三)
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
这两个函数映射出去。先看看 loo
ngson_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<<irq); /* handled by local core */ if ((local_irq & (0x1ULL << irq)) || ls7a_ipi_irq2pos[LS7A_IOAPIC_IRQ_BASE + irq] == (unsigned char)-1) { do_IRQ(LS7A_IOAPIC_IRQ_BASE + irq); continue; } irqd = irq_get_irq_data(LS7A_IOAPIC_IRQ_BASE + irq); cpumask_and(&affinity, irqd->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
是从寄存器读来的。
可以看到不同的主板类型对中断号的映射是不一样的。
02中断调试技巧
这节内容是在上文基础上的一个提升,解 bug
过程中一点不成熟的小经验跟大家分享出来,大佬们见笑。调了很多很多中断错误,回头总结的时候发现中断错误80%都是出在了中断映射部分。上文说过,中断初始化分为两个部分:显示 arch
下调用 irq_set_chip_and_handler
设置中断 high level handler
和驱动中调用 request_irq
注册。
nobody care的中断错误
产生这样的错误是因为,既没有设置 high level handler,
也没有人注册这个中断,但是CPU扫到了该中断。所以告诉内核没有人关注这个中断,这是个异常。一般这种错误产生的原因都是中断被映射错了位置,几乎不可能有程序员在驱动代码中,忘记注册或者是忘记设置 high level handler
了。当然,哪怕是真忘了,这个驱动都注册不成功哪来的中断呢?
感兴趣的大佬可以试试看,故意把中断映射到一个没有人用的中断上,改一下映射函数即可获得。顺便挨个感受硬盘,显卡,声卡, input
等设备中断不工作是什么样的体验,请一定要保证PC上有一个可以正常启动的内核。
好,回到正题,这种错误开发人员第一需要知道得到就是这本来是哪个中断,?这里我没找到一个很好的从正面跟下去的办法,只有一些迂回方法。
-
看
dmesg。
对于内核开发来讲,dmesg
简直是本命啊。有一些驱动是需要和cpu很频繁的交互的,比如说硬盘,如果中断出错了,紧接着,ata驱动就报错了。所以在这个中断错误日志,紧挨着的位置会有一些驱动错误信息打印,这就是一个突破口。假如说dmesg
并没有错误日志、开发人员忽略了日志怎么办呢? -
接下来就是起来看
/proc/interrupt,interrupt
文件中某一个中断没上来或者异常少,那就是它啦。当然这要在内核起来的前提下,就算系统没起来,也是有办法可以看到proc
的。如果说,内核起不来呢? -
这种情况略棘手了,要么就是根据经验判断,因为影响内核启动的中断就那么几个,根据经验加上内核
hang的
位置,很容易定位。运气好可能碰到一个panic
,这就更好判断啦。如果没有这个经验,那就老老实实调试吧。不过别担心,内核起不来的问题一般都挺好调试的。 -
此时,你已经知道是某个中断被错误的映射到某个位置。接下来就简单了,去映射入口看,为什么会被映射错呢。可能是
irq balance
算错了,可能是中断偏移量加错了等等。
bad irq中断错误
这个错误和上面的 nobody care
错误很相似,区别就是: nobody care
错误没设置 high level handler,
而 bad irq
错误设置了。
做个实验,在中断 init
时候,增加一个 irq_set_chip_and_handler
,这里注意里面填充的中断号没有驱动在使用的且不超出范围的。然后在中断映射时候,把某一个特定的中断映射过来。你就能自制中断错误,是不是很有意思嘞。
这个错误用的错误定位方法和上文一样,先看 dmesg
,看 interrupt
和调试。
一些看上去怪怪的问题
这类问题虽然原因类似,但是表现千奇百怪。比如说掉盘,声音响着响着突然不响了,桌面突然卡了等等。
一般来讲,中断问题是比较严重的问题,很多会影响系统起不来,就算系统能起来桌面也进不来。但是有个这样的例外,中断映射错了,错误位置恰好是有一个驱动在用这个中断,这个驱动注册时候又恰好设置了共享中断(不要惊讶,这种情况很多的。 INT
中断线很少,所以大部分驱动注册时候都不会选择独占中断的)。又或者是 irq balance
做的有问题,分散到特定核上会出现中断号计算错误的情况,这样的表现就是刚开始是好的,用着用着驱动死了,过一会又好了这样的怪怪的现象。
这些问题头疼的地方在于,不太可能会怀疑到中断上来。虽然是同一个原因导致的,但是这种错误的表现没有共性。所以当你遇到这样有点怪的问题时,在 menuconfig
尽可能的关掉一些驱动会有帮助。驱动一关,对应中断就不会被注册,这就有可能会得到 bad irq
报错信, 帮助定位。