中断再探

前面简单介绍中断初始化和注册的基本流程,这里接上文留下来的两部分内容,中断处理和中断调试技巧。从内核开发的角度来讲说,前文是科普文,帮助理解接下来的内容,本文没有那么枯燥,可以玩起来。最后的调试技巧是自己总结的,帮助我修复了工作中遇到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_interruptloongson_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<<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_irq2posls7a_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注册。

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报错信息,帮助定位。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章