龙芯中断再探(三)

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