最近幾個月調了很多中斷的bug,啃了很久的源碼。整理了一些東西,大佬們笑納。
離開了架構談中斷都是不深刻的,大佬們肯定玩膩了X86了,今天就以龍芯內核(龍芯官網即可獲得:git://cgit.loongnix.org/kernel/linux-3.10.git)爲例簡單介紹一下哈。中斷在內核中的生命週期主要分爲三個部分:初始化,註冊和中斷處理,剩餘的所有事情都是硬件完成的。這部分打算分享四節內容:中斷初始化、註冊中斷處理以及中斷排錯。這次先分享前兩部分內容。
在一切的前面,先看看中斷相關的數據結構。

0 1

中斷相關數據結構

話不多說,上圖

linux kernel中,對於每一個外設的IRQ都用struct irq_desc來描述,我們稱之中斷描述符(struct irq_desc)。linux kernel中會有一個數據結構保存了關於所有IRQ的中斷描述符信息,我們稱之中斷描述符DB(上圖中的數據結構)。當發生中斷後,首先獲取觸發中斷的HW interupt ID,然後通過irq domain翻譯成IRQ nuber,然後通過IRQ number就可以獲取對應的中斷描述符。調用中斷描述符中的highlevel irq-events handler來進行中斷處理就OK了。而highlevel irq-events handler主要進行下面兩個操作:

(1)調用中斷描述符的底層irq chip driver進行mask,ack等callback函數,進行interrupt flow control。

(2)調用該中斷描述符上的action list中的specific handler(我們用這個術語來區分具體中斷handler和high level的handler)。

接下來看中斷初始化,很多架構中斷初始化代碼都在arch目錄下,龍芯採用mips架構,因此關注點在arch/mips/目錄中,一般架構都是定義好某個中斷號被哪個設備佔用,初始化工作是:申請中斷處理需要的desc結構,每個掃描設備給每個desc填充好handle_irq 和chip結構,給中斷號綁定處理函數。中斷註冊都是在init階段通過兩個接口完成的,爲什麼是兩個接口呢,因爲龍芯一部分中斷是CPU控制,另一部分由橋片控制。由一個宏定義分開,宏定義定義在arch下,名字一般是XXX_IRQ_BASE,XXX是架構名,例如:LS7A_PCH_IRQ_BASE,。中斷號小於該宏是有CPU直接控制的中斷,比如說時鐘中斷,串口中斷等等。大於該宏則是橋片上的,例如:顯卡、網卡、硬盤等等。這也跟龍芯自己的硬件設計有關。

0 2

中斷初始化

這個很容易理解,中斷初始化肯定在內核啓動中,因爲內核非常需要時鐘等中斷。那就先從start_kernel開始看吧,這個函數執行的功能十分冗雜,現在我們只關注irq相關的部分。差不多翻兩頁以後,看到下面這個代碼塊:

 1 if (initcall_debug)
 2            initcall_debug_enable();
 3
 4    context_tracking_init();
 5    /* init some links before init_ISA_irqs() */
 6    early_irq_init();
 7    init_IRQ();
 8    tick_init();
 9    rcu_init_nohz();
10    init_timers();
11    hrtimers_init();
12    softirq_init();
13    timekeeping_init();
14    time_init();
15    printk_safe_init();
16    perf_event_init();
17    profile_init();
18    call_function_init();
19    WARN(!irqs_disabled(), "Interrupts were enabled early\n");
20
21    early_boot_irqs_disabled = false;
22    local_irq_enable();

這是除了start_kenrel一進來就disable_irq之後第一個出現irq字樣的代碼塊了,各種init irq,nice,盤他!先去看看early_init_irq。定義在kernel下。那就是通用的咯,那就應該是初始化前的準備工作,沒關係,先進去看看。

 1int __init early_irq_init(void)
 2{
 3    int i, initcnt, node = first_online_node; 
 4    struct irq_desc *desc;
 5    init_irq_default_affinity();
 6    /* Let arch update nr_irqs and return the nr of preallocated irqs */
 7    initcnt = arch_probe_nr_irqs();
 8    printk(KERN_INFO "NR_IRQS: %d, nr_irqs: %d, preallocated irqs: %d\n",
 9           NR_IRQS, nr_irqs, initcnt);
10
11    if (WARN_ON(nr_irqs > IRQ_BITMAP_BITS))
12            nr_irqs = IRQ_BITMAP_BITS;
13
14    if (WARN_ON(initcnt > IRQ_BITMAP_BITS))
15            initcnt = IRQ_BITMAP_BITS;
16
17    if (initcnt > nr_irqs)
18            nr_irqs = initcnt;
19
20    for (i = 0; i < initcnt; i++)
21    {
22            desc = alloc_desc(i, node, 0, NULL, NULL);
23            set_bit(i, allocated_irqs);
24            irq_insert_desc(i, desc);
25    }
26    return arch_early_irq_init();
27}

好,設置irq affinity標誌,計算中斷數量,分配中斷描述符,中斷描述符插入DB,然後去arch_early_irq_init,再跳進去看看,恩,kernel下的一個空函數。ok,這時候中斷描述符DB已經創建完成了,只是每個desc還是空的。

接下來看init_IRQ——這個看名字就很重要的函數。ctrl+T,arch下的!!開心,一下就過去了。選擇arch/mips/下的定義:

 1void __init init_IRQ(void)   
 2{
 3    int i;      
 4    unsigned int order = get_order(IRQ_STACK_SIZE);
 5    for (i = 0; i < NR_IRQS; i++)
 6            irq_set_noprobe(i);
 7    if (cpu_has_veic)
 8            clear_c0_status(ST0_IM);
 9    arch_init_irq();
10    for_each_possible_cpu(i) {
11            void *s = (void *)__get_free_pages(GFP_KERNEL, order);
12            irq_stack[i] = s;
13            pr_debug("CPU%d IRQ stack at 0x%p - 0x%p\n", i,
14                    irq_stack[i], irq_stack[i] + IRQ_STACK_SIZE);
15    }
16}

挨個設置noprobe,判斷cpu有沒有擴展中斷控制模式(external interrupt controller mode, eic),調arch_init_irq,多CPU挨個申請頁面作爲中斷棧。毫無疑問,註冊流在arch_init_irq裏,就這樣一個一個的跳轉,最後到這裏:

 1void __init mach_init_irq(void)
 2{               
 3    int i;
 4    u64 intenset_addr;
 5    u64 introuter_lpc_addr;
 6    clear_c0_status(ST0_IM | ST0_BEV);
 7    mips_cpu_irq_init();
 8    if (loongson_pch)
 9            loongson_pch->init_irq();
10    /* setup CASCADE irq */
11    setup_irq(LOONGSON_BRIDGE_IRQ, &cascade_irqaction);
12    irq_set_chip_and_handler(LOONGSON_UART_IRQ,
13                    &loongson_irq_chip, handle_level_irq);
14    set_c0_status(STATUSF_IP2 | STATUSF_IP6);
15}

這裏可以看到,init是分兩個過程的,先是mips架構通用的mips_cpu_irq_init,再是調用橋片自己的init_irq。這就是我一開始說的,註冊是分兩步完成的。mips_cpu_irq_init函數負責初始cpu直接控制的中斷,挨個設置chip和handle_irq.

 1static int mips_cpu_intc_map(struct irq_domain *d, unsigned int irq,
 2                          irq_hw_number_t hw)
 3{
 4    static struct irq_chip *chip;
 5
 6    if (hw < 2 && cpu_has_mipsmt) {
 7            /* Software interrupts are used for MT/CMT IPI */
 8            chip = &mips_mt_cpu_irq_controller;
 9    } else {
10            chip = &mips_cpu_irq_controller;
11    }
12
13    if (cpu_has_vint)
14            set_vi_handler(hw, plat_irq_dispatch);
15
16    irq_set_chip_and_handler(irq, chip, handle_percpu_irq);
17
18    return 0;
19 }

初始化流程還是比較簡單,可以看到,在這裏給每一個cpu產生的中斷設置了chip和handle_irq。CPU檢測到中斷觸發直接調用handle_percpu_irq最後到真正的中斷處理函數。接下來看走msi中斷的註冊,這部分稍微複雜一點:

 1void __init ls7a_init_irq(void)
 2{
 3    writeq(0x0ULL, LS7A_INT_EDGE_REG);
 4    writeq(0x0ULL, LS7A_INT_STATUS_REG);
 5    /* Mask all interrupts except LPC (bit 19) */
 6    writeq(0xfffffffffff7ffffULL, LS7A_INT_MASK_REG);
 7    writeq(0xffffffffffffffffULL, LS7A_INT_CLEAR_REG);
 8
 9    /* Enable the LPC interrupt */
10    writel(0x80000000, LS7A_LPC_INT_CTL);
11    /* Clear all 18-bit interrupt bits */
12    writel(0x3ffff, LS7A_LPC_INT_CLR);
13
14    if (pci_msi_enabled())
15            loongson_pch->irq_dispatch = ls7a_msi_irq_dispatch;
16    ....
17
18    init_7a_irq(LS7A_IOAPIC_LPC_OFFSET          , LS7A_IOAPIC_LPC_IRQ          );
19    init_7a_irq(LS7A_IOAPIC_UART0_OFFSET        , LS7A_IOAPIC_UART0_IRQ        );
20    init_7a_irq(LS7A_IOAPIC_I2C0_OFFSET         , LS7A_IOAPIC_I2C0_IRQ         );
21
22      .........
23    }
24}

設置中斷掩碼、中斷清除等寄存器,設置中斷分發函數,調用init_7a_irq爲每個外設中斷設置handle_irq和chip結構。代碼如下:

 1static void init_7a_irq(int dev, int irq) 
 2{
 3    *irq_mask  &= ~(1ULL << dev);
 4    *(volatile unsigned char *)(LS7A_IOAPIC_ROUTE_ENTRY + dev) = USE_7A_INT0;
 5    smp_mb();
 6    irq_set_chip_and_handler(irq, &pch_irq_chip, handle_level_irq);
 7    if(ls3a_msi_enabled) {
 8            *irq_msi_en |= 1ULL << dev;
 9            *(volatile unsigned char *)(irq_msi_vec+dev) = irq;
10            smp_mb();
11    }
12}

這裏就是所有中斷初始化流程了。看到這裏很多人會覺得茫然了,按照上面的流程分析,第一個被初始化到的中斷是MIPS_CPU_IRQ_BASE 也就是56,那前面的中斷號幹嘛去了?前面的中斷號是故意留下來,具體原因先賣個關子 :wink:

未完待續。。。

相關文章