中斷再探

前面簡單介紹中斷初始化和註冊的基本流程,這裏接上文留下來的兩部分內容,中斷處理和中斷調試技巧。從內核開發的角度來講說,前文是科普文,幫助理解接下來的內容,本文沒有那麼枯燥,可以玩起來。最後的調試技巧是自己總結的,幫助我修復了工作中遇到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報錯信息,幫助定位。

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