一文完全讀懂 Linux 中斷處理

什麼是中斷

中斷 是爲了解決外部設備完成某些工作後通知 CPU 的一種機制(譬如硬盤完成讀寫操作後通過中斷告知 CPU 已經完成)。早期沒有中斷機制的計算機就不得不通過輪詢來查詢外部設備的狀態,由於輪詢是試探查詢的(也就是說設備不一定是就緒狀態),所以往往要做很多無用的查詢,從而導致效率非常低下。由於中斷是由外部設備主動通知 CPU 的,所以不需要 CPU 進行輪詢去查詢,效率大大提升。

從物理學的角度看,中斷是一種電信號,由硬件設備產生,並直接送入中斷控制器(如 8259A)的輸入引腳上,然後再由中斷控制器向處理器發送相應的信號。處理器一經檢測到該信號,便中斷自己當前正在處理的工作,轉而去處理中斷。此後,處理器會通知 OS 已經產生中斷。這樣,OS 就可以對這個中斷進行適當的處理。不同的設備對應的中斷不同,而每個中斷都通過一個唯一的數字標識,這些值通常被稱爲中斷請求線。

中斷控制器

X86 計算機的 CPU 爲中斷只提供了兩條外接引腳:NMI 和 INTR。其中 NMI 是不可屏蔽中斷,它通常用於電源掉電和物理存儲器奇偶校驗;INTR 是可屏蔽中斷,可以通過設置中斷屏蔽位來進行中斷屏蔽,它主要用於接受外部硬件的中斷信號,這些信號由中斷控制器傳遞給 CPU。

常見的中斷控制器有兩種:

可編程中斷控制器 8259A

傳統的 PIC(Programmable Interrupt Controller,可編程中斷控制器)是由兩片 8259A 風格的外部芯片以 “級聯” 的方式連接在一起。每個芯片可處理多達 8 個不同的 IRQ。因爲從 PIC 的 INT 輸出線連接到主 PIC 的 IRQ2 引腳,所以可用 IRQ 線的個數達到 15 個,如圖下所示。

8259A

高級可編程中斷控制器(APIC)

8259A 只適合單 CPU 的情況,爲了充分挖掘 SMP 體系結構的並行性,能夠把中斷傳遞給系統中的每個 CPU 至關重要。基於此理由,Intel 引入了一種名爲 I/O 高級可編程控制器的新組件,來替代老式的 8259A 可編程中斷控制器。該組件包含兩大組成部分:一是 “本地 APIC”,主要負責傳遞中斷信號到指定的處理器;舉例來說,一臺具有三個處理器的機器,則它必須相對的要有三個本地 APIC。另外一個重要的部分是 I/O APIC,主要是收集來自 I/O 裝置的 Interrupt 信號且在當那些裝置需要中斷時發送信號到本地 APIC,系統中最多可擁有 8 個 I/O APIC。

每個本地 APIC 都有 32 位的寄存器,一個內部時鐘,一個本地定時設備以及爲本地中斷保留的兩條額外的 IRQ 線 LINT0 和 LINT1。所有本地 APIC 都連接到 I/O APIC,形成一個多級 APIC 系統,如圖下所示。

APIC

目前大部分單處理器系統都包含一個 I/O APIC 芯片,可以通過以下兩種方式來對這種芯片進行配置:

辨別一個系統是否正在使用 I/O APIC,可以在命令行輸入如下命令:

# cat /proc/interrupts
           CPU0       
  0:      90504    IO-APIC-edge  timer
  1:        131    IO-APIC-edge  i8042
  8:          4    IO-APIC-edge  rtc
  9:          0    IO-APIC-level  acpi
 12:        111    IO-APIC-edge  i8042
 14:       1862    IO-APIC-edge  ide0
 15:         28    IO-APIC-edge  ide1
177:          9    IO-APIC-level  eth0
185:          0    IO-APIC-level  via82cxxx
...

如果輸出結果中列出了 IO-APIC,說明您的系統正在使用 APIC。如果看到 XT-PIC,意味着您的系統正在使用 8259A 芯片。

中斷分類

中斷可分爲同步(synchronous)中斷和異步(asynchronous)中斷:

根據 Intel 官方資料,同步中斷稱爲異常(exception),異步中斷被稱爲中斷(interrupt)。

中斷可分爲 可屏蔽中斷(Maskable interrupt)和 非屏蔽中斷(Nomaskable interrupt)。異常可分爲 故障(fault)、陷阱(trap)、終止(abort)三類。

從廣義上講,中斷可分爲四類:中斷故障陷阱終止。這些類別之間的異同點請參看 表。

表:中斷類別及其行爲

XvXJAu

X86 體系結構的每個中斷都被賦予一個唯一的編號或者向量(8 位無符號整數)。非屏蔽中斷和異常向量是固定的,而可屏蔽中斷向量可以通過對中斷控制器的編程來改變。

中斷處理 - 上半部(硬中斷)

由於 APIC中斷控制器 有點小複雜,所以本文主要通過 8259A中斷控制器 來介紹 Linux 對中斷的處理過程。

中斷處理相關結構

前面說過,8259A中斷控制器 由兩片 8259A 風格的外部芯片以 級聯 的方式連接在一起,每個芯片可處理多達 8 個不同的 IRQ(中斷請求),所以可用 IRQ 線的個數達到 15 個。如下圖:

8259A

在內核中每條 IRQ 線由結構體 irq_desc_t 來描述,irq_desc_t 定義如下:

typedef struct {
    unsigned int status;        /* IRQ status */
    hw_irq_controller *handler;
    struct irqaction *action;   /* IRQ action list */
    unsigned int depth;         /* nested irq disables */
    spinlock_t lock;
} irq_desc_t;

下面介紹一下 irq_desc_t 結構各個字段的作用:

hw_interrupt_type 這個結構與硬件相關,這裏就不作介紹了,我們來看看 irqaction 這個結構:

struct irqaction {
    void (*handler)(int, void *, struct pt_regs *);
    unsigned long flags;
    unsigned long mask;
    const char *name;
    void *dev_id;
    struct irqaction *next;
};

下面說說 irqaction 結構各個字段的作用:

irq_desc_t 結構關係如下圖:

irq_desc_t

註冊中斷處理入口

在內核中,可以通過 setup_irq() 函數來註冊一箇中斷處理入口。setup_irq() 函數代碼如下:

int setup_irq(unsigned int irq, struct irqaction * new)
{
    int shared = 0;
    unsigned long flags;
    struct irqaction *old, **p;
    irq_desc_t *desc = irq_desc + irq;
    ...
    spin_lock_irqsave(&desc->lock,flags);
    p = &desc->action;
    if ((old = *p) != NULL) {
        if (!(old->flags & new->flags & SA_SHIRQ)) {
            spin_unlock_irqrestore(&desc->lock,flags);
            return -EBUSY;
        }

        do {
            p = &old->next;
            old = *p;
        } while (old);
        shared = 1;
    }

    *p = new;

    if (!shared) {
        desc->depth = 0;
        desc->status &= ~(IRQ_DISABLED | IRQ_AUTODETECT | IRQ_WAITING);
        desc->handler->startup(irq);
    }
    spin_unlock_irqrestore(&desc->lock,flags);

    register_irq_proc(irq); // 註冊proc文件系統
    return 0;
}

setup_irq() 函數比較簡單,就是通過 irq 號來查找對應的 irq_desc_t 結構,並把新的 irqaction 連接到 irq_desc_t 結構的 action 鏈表中。要注意的是,如果設備不支持共享 IRQ 線(也即是 flags 字段沒有設置 SA_SHIRQ 標誌),那麼就返回 EBUSY 錯誤。

我們看看 時鐘中斷處理入口 的註冊實例:

static struct irqaction irq0  = { timer_interrupt, SA_INTERRUPT, 0, "timer", NULL, NULL};

void __init time_init(void)
{
    ...
    setup_irq(0, &irq0);
}

可以看到,時鐘中斷處理入口的 IRQ 號爲 0,處理函數爲 timer_interrupt(),並且不支持共享 IRQ 線(flags 字段沒有設置 SA_SHIRQ 標誌)。

處理中斷請求

當一箇中斷髮生時,中斷控制層會發送信號給 CPU,CPU 收到信號會中斷當前的執行,轉而執行中斷處理過程。中斷處理過程首先會保存寄存器的值到棧中,然後調用 do_IRQ() 函數進行進一步的處理,do_IRQ() 函數代碼如下:

asmlinkage unsigned int do_IRQ(struct pt_regs regs)
{
    int irq = regs.orig_eax & 0xff; /* 獲取IRQ號  */
    int cpu = smp_processor_id();
    irq_desc_t *desc = irq_desc + irq;
    struct irqaction * action;
    unsigned int status;

    kstat.irqs[cpu][irq]++;
    spin_lock(&desc->lock);
    desc->handler->ack(irq);

    status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING);
    status |= IRQ_PENDING; /* we _want_ to handle it */

    action = NULL;
    if (!(status & (IRQ_DISABLED | IRQ_INPROGRESS))) { // 當前IRQ不在處理中
        action = desc->action;    // 獲取 action 鏈表
        status &= ~IRQ_PENDING;   // 去除IRQ_PENDING標誌, 這個標誌用於記錄是否在處理IRQ請求的時候又發生了中斷
        status |= IRQ_INPROGRESS; // 設置IRQ_INPROGRESS標誌, 表示正在處理IRQ
    }
    desc->status = status;

    if (!action)  // 如果上一次IRQ還沒完成, 直接退出
        goto out;

    for (;;) {
        spin_unlock(&desc->lock);
        handle_IRQ_event(irq, ®s, action); // 處理IRQ請求
        spin_lock(&desc->lock);
        
        if (!(desc->status & IRQ_PENDING)) // 如果在處理IRQ請求的時候又發生了中斷, 繼續處理IRQ請求
            break;
        desc->status &= ~IRQ_PENDING;
    }
    desc->status &= ~IRQ_INPROGRESS;
out:

    desc->handler->end(irq);
    spin_unlock(&desc->lock);

    if (softirq_active(cpu) & softirq_mask(cpu))
        do_softirq(); // 中斷下半部處理
    return 1;
}

do_IRQ() 函數首先通過 IRQ 號獲取到其對應的 irq_desc_t 結構,注意的是同一個中斷有可能發生多次,所以要判斷當前 IRQ 是否正在被處理當中(判斷 irq_desc_t 結構的 status 字段是否設置了 IRQ_INPROGRESS 標誌),如果不是處理當前,那麼就獲取到 action 鏈表,然後通過調用 handle_IRQ_event() 函數來執行 action 鏈表中的中斷處理函數。

如果在處理中斷的過程中又發生了相同的中斷(irq_desc_t 結構的 status 字段被設置了 IRQ_INPROGRESS 標誌),那麼就繼續對中斷進行處理。處理完中斷後,調用 do_softirq() 函數來對中斷下半部進行處理(下面會說)。

接下來看看 handle_IRQ_event() 函數的實現:

int handle_IRQ_event(unsigned int irq, struct pt_regs * regs, struct irqaction * action)
{
    int status;
    int cpu = smp_processor_id();

    irq_enter(cpu, irq);

    status = 1; /* Force the "do bottom halves" bit */

    if (!(action->flags & SA_INTERRUPT)) // 如果中斷處理能夠在打開中斷的情況下執行, 那麼就打開中斷
        __sti();

    do {
        status |= action->flags;
        action->handler(irq, action->dev_id, regs);
        action = action->next;
    } while (action);
    if (status & SA_SAMPLE_RANDOM)
        add_interrupt_randomness(irq);
    __cli();

    irq_exit(cpu, irq);

    return status;
}

handle_IRQ_event() 函數非常簡單,就是遍歷 action 鏈表並且執行其中的處理函數,比如對於 時鐘中斷 就是調用 timer_interrupt() 函數。這裏要注意的是,如果中斷處理過程能夠開啓中斷的,那麼就把中斷打開(因爲 CPU 接收到中斷信號時會關閉中斷)。

中斷處理 - 下半部(軟中斷)

由於中斷處理一般在關閉中斷的情況下執行,所以中斷處理不能太耗時,否則後續發生的中斷就不能實時地被處理。鑑於這個原因,Linux 把中斷處理分爲兩個部分,上半部 和 下半部上半部 在前面已經介紹過,接下來就介紹一下 下半部 的執行。

一般中斷 上半部 只會做一些最基礎的操作(比如從網卡中複製數據到緩存中),然後對要執行的中斷 下半部 進行標識,標識完調用 do_softirq() 函數進行處理。

softirq 機制

中斷下半部 由 softirq(軟中斷) 機制來實現的,在 Linux 內核中,有一個名爲 softirq_vec 的數組,如下:

static struct softirq_action softirq_vec[32];

其類型爲 softirq_action 結構,定義如下:

struct softirq_action
{
    void    (*action)(struct softirq_action *);
    void    *data;
};

softirq_vec 數組是 softirq 機制的核心,softirq_vec 數組每個元素代表一種軟中斷。但在 Linux 中只定義了四種軟中斷,如下:

enum
{
    HI_SOFTIRQ=0,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    TASKLET_SOFTIRQ
};

HI_SOFTIRQ 是高優先級 tasklet,而 TASKLET_SOFTIRQ 是普通 tasklet,tasklet 是基於 softirq 機制的一種任務隊列(下面會介紹)。NET_TX_SOFTIRQ 和 NET_RX_SOFTIRQ 特定用於網絡子模塊的軟中斷(不作介紹)。

註冊 softirq 處理函數

要註冊一個 softirq 處理函數,可以通過 open_softirq() 函數來進行,代碼如下:

void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
{
    unsigned long flags;
    int i;

    spin_lock_irqsave(&softirq_mask_lock, flags);
    softirq_vec[nr].data = data;
    softirq_vec[nr].action = action;

    for (i=0; i<NR_CPUS; i++)
        softirq_mask(i) |= (1<<nr);
    spin_unlock_irqrestore(&softirq_mask_lock, flags);
}

open_softirq() 函數的主要工作就是向 softirq_vec 數組添加一個 softirq 處理函數。

Linux 在系統初始化時註冊了兩種 softirq 處理函數,分別爲 TASKLET_SOFTIRQ 和 HI_SOFTIRQ

void __init softirq_init()
{
    ...
    open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL);
    open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);
}

處理 softirq

處理 softirq 是通過 do_softirq() 函數實現,代碼如下:

asmlinkage void do_softirq()
{
    int cpu = smp_processor_id();
    __u32 active, mask;

    if (in_interrupt())
        return;

    local_bh_disable();

    local_irq_disable();
    mask = softirq_mask(cpu);
    active = softirq_active(cpu) & mask;

    if (active) {
        struct softirq_action *h;

restart:
        softirq_active(cpu) &= ~active;

        local_irq_enable();

        h = softirq_vec;
        mask &= ~active;

        do {
            if (active & 1)
                h->action(h);
            h++;
            active >>= 1;
        } while (active);

        local_irq_disable();

        active = softirq_active(cpu);
        if ((active &= mask) != 0)
            goto retry;
    }

    local_bh_enable();

    return;

retry:
    goto restart;
}

前面說了 softirq_vec 數組有 32 個元素,每個元素對應一種類型的 softirq,那麼 Linux 怎麼知道哪種 softirq 需要被執行呢?在 Linux 中,每個 CPU 都有一個類型爲 irq_cpustat_t 結構的變量,irq_cpustat_t 結構定義如下:

typedef struct {
    unsigned int __softirq_active;
    unsigned int __softirq_mask;
    ...
} irq_cpustat_t;

其中 __softirq_active 字段表示有哪種 softirq 觸發了(int 類型有 32 個位,每一個位代表一種 softirq),而 __softirq_mask 字段表示哪種 softirq 被屏蔽了。Linux 通過 __softirq_active 這個字段得知哪種 softirq 需要執行(只需要把對應位設置爲 1)。

所以,do_softirq() 函數首先通過 softirq_mask(cpu) 來獲取當前 CPU 對應被屏蔽的 softirq,而 softirq_active(cpu) & mask 就是獲取需要執行的 softirq,然後就通過對比 __softirq_active 字段的各個位來判斷是否要執行該類型的 softirq。

tasklet 機制

前面說了,tasklet 機制是基於 softirq 機制的,tasklet 機制其實就是一個任務隊列,然後通過 softirq 執行。在 Linux 內核中有兩種 tasklet,一種是高優先級 tasklet,一種是普通 tasklet。這兩種 tasklet 的實現基本一致,唯一不同的就是執行的優先級,高優先級 tasklet 會先於普通 tasklet 執行。

tasklet 本質是一個隊列,通過結構體 tasklet_head 存儲,並且每個 CPU 有一個這樣的隊列,我們來看看結構體 tasklet_head 的定義:

struct tasklet_head
{
    struct tasklet_struct *list;
};

struct tasklet_struct
{
    struct tasklet_struct *next;
    unsigned long state;
    atomic_t count;
    void (*func)(unsigned long);
    unsigned long data;
};

從 tasklet_head 的定義可以知道,tasklet_head 結構是 tasklet_struct 結構隊列的頭部,而 tasklet_struct 結構的 func 字段正式任務要執行的函數指針。Linux 定義了兩種的 tasklet 隊列,分別爲 tasklet_vec 和 tasklet_hi_vec,定義如下:

struct tasklet_head tasklet_vec[NR_CPUS];
struct tasklet_head tasklet_hi_vec[NR_CPUS];

可以看出,tasklet_vec 和 tasklet_hi_vec 都是數組,數組的元素個數爲 CPU 的核心數,也就是每個 CPU 核心都有一個高優先級 tasklet 隊列和一個普通 tasklet 隊列。

調度 tasklet

如果我們有一個 tasklet 需要執行,那麼高優先級 tasklet 可以通過 tasklet_hi_schedule() 函數調度,而普通 tasklet 可以通過 tasklet_schedule() 調度。這兩個函數基本一樣,所以我們只分析其中一個:

static inline void tasklet_hi_schedule(struct tasklet_struct *t)
{
    if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) {
        int cpu = smp_processor_id();
        unsigned long flags;

        local_irq_save(flags);
        t->next = tasklet_hi_vec[cpu].list;
        tasklet_hi_vec[cpu].list = t;
        __cpu_raise_softirq(cpu, HI_SOFTIRQ);
        local_irq_restore(flags);
    }
}

函數參數的類型是 tasklet_struct 結構的指針,表示需要執行的 tasklet 結構。tasklet_hi_schedule() 函數首先判斷這個 tasklet 是否已經被添加到隊列中,如果不是就添加到 tasklet_hi_vec 隊列中,並且通過調用 __cpu_raise_softirq(cpu, HI_SOFTIRQ) 來告訴 softirq 需要執行 HI_SOFTIRQ 類型的 softirq,我們來看看 __cpu_raise_softirq() 函數的實現:

static inline void __cpu_raise_softirq(int cpu, int nr)
{
    softirq_active(cpu) |= (1<<nr);
}

可以看出,__cpu_raise_softirq() 函數就是把 irq_cpustat_t 結構的 __softirq_active 字段的 nr位 設置爲 1。對於 tasklet_hi_schedule() 函數就是把 HI_SOFTIRQ 位(0 位)設置爲 1。

前面我們也介紹過,Linux 在初始化時會註冊兩種 softirq,TASKLET_SOFTIRQ 和 HI_SOFTIRQ

void __init softirq_init()
{
    ...
    open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL);
    open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);
}

所以當把 irq_cpustat_t 結構的 __softirq_active 字段的 HI_SOFTIRQ 位(0 位)設置爲 1 時,softirq 機制就會執行 tasklet_hi_action() 函數,我們來看看 tasklet_hi_action() 函數的實現:

static void tasklet_hi_action(struct softirq_action *a)
{
    int cpu = smp_processor_id();
    struct tasklet_struct *list;

    local_irq_disable();
    list = tasklet_hi_vec[cpu].list;
    tasklet_hi_vec[cpu].list = NULL;
    local_irq_enable();

    while (list != NULL) {
        struct tasklet_struct *t = list;

        list = list->next;

        if (tasklet_trylock(t)) {
            if (atomic_read(&t->count) == 0) {
                clear_bit(TASKLET_STATE_SCHED, &t->state);

                t->func(t->data);  // 調用tasklet處理函數
                tasklet_unlock(t);
                continue;
            }
            tasklet_unlock(t);
        }
        ...
    }
}

tasklet_hi_action() 函數非常簡單,就是遍歷 tasklet_hi_vec 隊列並且執行其中 tasklet 的處理函數。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/sUfPYDe_4Dpz6cWskyZu_w