linux 中斷-很全

轉自Rock3的Linux博客


中斷

tasklet原理

沒有評論

        tasklet是Linux內核中“可延遲執行”機制、或“中斷下半部”的一種。基於軟中斷實現,但比軟中斷靈活,tasklet有的地方翻譯作“任務蕾”,大部分書籍沒找到合適的詞彙去翻譯它。本篇博客主要介紹tasklet的設計原理、使用方法。

        本篇博客耗時8小時。

一、tasklet解決什麼問題?

        先看下tasklet在一些書籍上的介紹:

  • tasklet是I/O驅動程序中實現可延遲函數的首選方法(我猜某個內核版本開始,塊設備從tasklet中獨立出來成爲單獨的軟中斷BLOCK_SOFTIRQ和BLOCK_IOPOLL_SOFTIRQ)——ULK。
  • tasklet和工作隊列是延期執行工作的機制,其實現基於軟中斷,但他們更易於使用,因而更適合與設備驅動程序...tasklet是“小進程”,執行一些迷你任務,對這些人物使用全功能進程可能比較浪費——PLKA。
  • tasklet是並行可執行(但是是鎖密集型的)軟件中斷和舊下半區的一種混合體,這裏既談不上並行性,也談不上性能。引入tasklet是爲了替代原來的下半區。

        下面這段來自於PLKA的話我也很想留在這裏:軟中斷是將操作推遲到未來時刻執行的最有效的方法。但該延期機制處理起來非常複雜。因爲多個處理器可以同時且獨立的處理軟中斷,同一個軟中斷的處理程序可以在幾個CPU上同時運行。對軟中斷的效率來說,這是一個關鍵,多處理器系統上的網絡實現顯然受惠於此。但處理程序的設計必須是完全可重入且線程安全的。另外,臨界區必須用自旋鎖保護(或其他IPC機制),而這需要大量審慎的考慮。

        我自己的理解,由於軟中斷以ksoftirqd的形式與用戶進程共同調度,這將關係到OS整體的性能,因此軟中斷在Linux內核中也僅僅就幾個(網絡、時鐘、調度以及Tasklet等),在內核編譯時確定。軟中斷這種方法顯然不是面向硬件驅動的,而是驅動更上一層:不關心如何從具體的網卡接收數據包,但是從所有的網卡接收的數據包都要經過內核協議棧的處理。而且軟中斷比較“硬”——數量固定、編譯時確定、操作函數必須可重入、需要慎重考慮鎖的問題,不適合驅動直接調用,因此Linux內核爲驅動直接提供了一種使用軟中斷的方法,就是tasklet。

二、tasklet數據結構

        tasklet通過軟中斷實現,軟中斷中有兩種類型屬於tasklet,分別是級別最高的HI_SOFTIRQ和TASKLET_SOFTIRQ。

        Linux內核採用兩個PER_CPU的數組tasklet_vec[]和tasklet_hi_vec[]維護系統種的所有tasklet(kernel/softirq.c),分別維護TASKLET_SOFTIRQ級別和HI_SOFTIRQ級別的tasklet:

1
2
3
4
5
6
7
8
struct tasklet_head
{
    struct tasklet_struct *head;
    struct tasklet_struct **tail;
};
 
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);

    tasklet_vec

        tasklet的核心結構體如下(include/linux/interrupt.h):

1
2
3
4
5
6
7
8
struct tasklet_struct
{
    struct tasklet_struct *next;
    unsigned long state;
    atomic_t count;
    void (*func)(unsigned long);
    unsigned long data;
};

        習慣上稱之爲tasklet描述符,func指針是具體的處理函數指針,data爲可選參數,state表示該tasklet的狀態,分別使用不同的bit表示兩個狀態:TASKLET_STATE_SCHED和TASKLET_STATE_RUN:

  • TASKLET_STATE_SCHED置位表示已經被調度(掛起),也意味着tasklet描述符被插入到了tasklet_vec和tasklet_hi_vec數組的其中一個鏈表中,可以被執行。
  • TASKLET_STATE_RUN置位表示該tasklet正在某個CPU上執行,單個處理器系統上並不校驗該標誌,因爲沒必要檢查特定的tasklet是否正在運行。

        count爲原子計數器,用於禁用已經調度的tasklet,如果該值不爲0,則不予以執行。

三、tasklet操作接口

        tasklet對驅動開放的常用操作包括:

  • 初始化,tasklet_init(),初始化一個tasklet描述符。
  • 調度,tasklet_schedule()和tasklet_hi_schedule(),將taslet置位TASKLET_STATE_SCHED,並嘗試激活所在的軟中斷。
  • 禁用/啓動,tasklet_disable_nosync()、tasklet_disable()、task_enable(),通過count計數器實現。
  • 執行,tasklet_action()和tasklet_hi_action(),具體的執行軟中斷。
  • 殺死,tasklet_kill(),。。。

        tasklet_int()函數實現如下(kernel/softirq.c):

1
2
3
4
5
6
7
8
9
void tasklet_init(struct tasklet_struct *t,
          void (*func)(unsigned long), unsigned long data)
{
    t->next = NULL;
    t->state = 0;
    atomic_set(&t->count, 0);
    t->func = func;
    t->data = data;
}

        tasklet_schedule()函數與tasklet_hi_schedule()函數的實現很類似,這裏只列tasklet_schedule()函數的實現(kernel/softirq.c),都挺明白就不描述了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static inline void tasklet_schedule(struct tasklet_struct *t)
{
    if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
        __tasklet_schedule(t);
}
 
void __tasklet_schedule(struct tasklet_struct *t)
{  
    unsigned long flags;
         
    local_irq_save(flags);
    t->next = NULL;
    *__this_cpu_read(tasklet_vec.tail) = t;
    __this_cpu_write(tasklet_vec.tail, &(t->next));
    raise_softirq_irqoff(TASKLET_SOFTIRQ);
    local_irq_restore(flags);
}

        tasklet_disable()函數、task_enable()函數以及tasklet_disable_nosync()函數(include/linux/interrupt.h),不說了只列代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static inline void tasklet_disable_nosync(struct tasklet_struct *t)
{
    atomic_inc(&t->count);
    smp_mb__after_atomic_inc();
}
 
static inline void tasklet_disable(struct tasklet_struct *t)
{
    tasklet_disable_nosync(t);
    tasklet_unlock_wait(t);
    smp_mb();
}
 
static inline void tasklet_enable(struct tasklet_struct *t)
{
    smp_mb__before_atomic_dec();
    atomic_dec(&t->count);
}

        只列tasklet_action()函數(kernel/softirq.c):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static void tasklet_action(struct softirq_action *a)
{
    struct tasklet_struct *list;
 
    local_irq_disable();
    list = __this_cpu_read(tasklet_vec.head);
    __this_cpu_write(tasklet_vec.head, NULL);
    __this_cpu_write(tasklet_vec.tail, &__get_cpu_var(tasklet_vec).head);
    local_irq_enable();
 
    while (list) {
        struct tasklet_struct *t = list;
 
        list = list->next;
 
        if (tasklet_trylock(t)) {
            if (!atomic_read(&t->count)) {
                if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))
                    BUG();
                t->func(t->data);
                tasklet_unlock(t);
                continue;
            }
            tasklet_unlock(t);
        }
 
        local_irq_disable();
        t->next = NULL;
        *__this_cpu_read(tasklet_vec.tail) = t;
        __this_cpu_write(tasklet_vec.tail, &(t->next));
        __raise_softirq_irqoff(TASKLET_SOFTIRQ);
        local_irq_enable();
    }
}

        tasklet_action()函數在softirq_init()函數中被調用:

1
2
3
4
5
6
void __init softirq_init(void)
{
    ...
    open_softirq(TASKLET_SOFTIRQ, tasklet_action);
    open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}

        tasklet_kill()實現:

1
2
3
4
5
6
7
8
9
10
11
12
13
void tasklet_kill(struct tasklet_struct *t)
{
    if (in_interrupt())
        printk("Attempt to kill tasklet from interrupt\n");
 
    while (test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) {
        do {
            yield();
        } while (test_bit(TASKLET_STATE_SCHED, &t->state));
    }
    tasklet_unlock_wait(t);
    clear_bit(TASKLET_STATE_SCHED, &t->state);
}

        yeild()函數是個值的研究的點。

四、一個tasklet調用例子

        找了一個tasklet的例子看一下(drivers/usb/atm,usb攝像頭),在其自舉函數usbatm_usb_probe()中調用了tasklet_init()初始化了兩個tasklet描述符用於接收和發送的“可延遲操作處理”,但此是並沒有將其加入到tasklet_vec[]或tasklet_hi_vec[]中:

1
2
3
4
    tasklet_init(&instance->rx_channel.tasklet,
usbatm_rx_process, (unsigned long)instance);
    tasklet_init(&instance->tx_channel.tasklet,
usbatm_tx_process, (unsigned long)instance);

        在其發送接口usbatm_atm_send()函數調用tasklet_schedule()函數將所初始化的tasklet加入到當前cpu的tasklet_vec鏈表尾部,並嘗試調用do_softirq_irqoff()執行軟中斷TASKLET_SOFTIRQ:

1
2
3
4
5
6
static int usbatm_atm_send(struct atm_vcc *vcc, struct sk_buff *skb)
{
    ...
    tasklet_schedule(&instance->tx_channel.tasklet);
    ...
}

        在其斷開設備的接口usbatm_usb_disconnect()中調用tasklet_disable()函數和tasklet_enable()函數重新啓動其收發tasklet(具體原因不詳,這個地方可能就是由這個需要,暫時重啓收發tasklet):

1
2
3
4
5
6
7
8
9
10
void usbatm_usb_disconnect(struct usb_interface *intf)
{
    ...
    tasklet_disable(&instance->rx_channel.tasklet);
    tasklet_disable(&instance->tx_channel.tasklet);
    ...
    tasklet_enable(&instance->rx_channel.tasklet);
    tasklet_enable(&instance->tx_channel.tasklet);
    ...
}

        在其銷燬接口usbatm_destroy_instance()中調用tasklet_kill()函數,強行將該tasklet踢出調度隊列。

        從上述過程以及tasklet的設計可以看出,tasklet整體是這麼運行的:驅動應該在其硬中斷處理函數的莫爲調用tasklet_schedule()接口激活該taskle,內核經常調用do_softirq()執行軟中斷,通過softirq執行tasket,如下圖所示。圖中灰色部分爲禁止硬中斷部分,爲保護軟中斷pending位圖和tasklet_vec鏈表數組,count的改變均爲原子操作,count確保SMP架構下同時只有一個CPU在執行該tasklet:

tasklet_action

五、tasklet同步

        主要看兩個參數,一個state,一個count。

        state用於校驗在tasklet_action()或tasklet_schedule()時,是否執行該tasklet的handler。state被tasklet_schedule()函數、tasklet_hi_schedule()函數、tasklet_action()函數以及tasklet_kill()函數所修改:

  • tasklet_schedule()函數、tasklet_hi_schedule()函數將state置位TASKLET_STATE_SCHED。
  • tasklet_action()函數將state的TASKLET_STATE_SCHED清除,並設置TASKLET_STATE_RUN。
  • tasklet_kill()函數將state的TASKLET_STATE_SCHED清除。

        tasklet_action()函數在設置TASKLET_STATE_RUN標誌時,使用了tasklet_trylock()、tasklet_unlock()等接口:

​        count用於smp同步,count不爲0,則表示該tasklet正在某CPU上執行,其他CPU則不執行該tasklet,count保證某個tasklet同時只能在一個CPU上執行。count的操作都是原子操作:

  • tasklet_disable()函數/tasklet_disable_nosync()函數將count原子減1。
  • tasklet_enablle()函數將count原子加1。

​        另外,tasklet的操作中還所使用了local_irq_save()/local_irq_disable()等禁止本地中斷的函數,早保護對象被修改完畢後立即使用local_irq_resore()/local_irq_enable()開啓:

  • tasklet_schedule()函數中,用於保護tasklet_vec[]鏈表和軟中斷的pending位圖的更改。因爲硬中斷的激發能導致二者的更改。
  • tasklet_action()函數中,用於保護tasklet_vec[]鏈表和軟中斷的pending位圖的更改。因爲硬中斷的激發能導致二者的更改。

六、總結

        tasklet是一種“可延遲執行”機制中的一種,基於軟中斷實現,主要面向驅動程序。tasklet與軟中斷的區別在於每個CPU上不能同時執行相同的tasklet,tasklet函數本身也不必是可重入的。與軟中斷一樣,爲了保證tasklet和硬中斷之間在同一個CPU上是串行執行的,維護其PER_CPU的鏈表時,需要屏蔽硬中斷。

        不要爲了列代碼而列代碼,不要爲了抄書而抄書。

七、細節

1、原子操作

        tasklet使用taskle_disable()函數和tasklet_enable()函數對count位進行增減操作,以保證SMP架構下,不在不同的CPU上同時運行相同的tasklet。這裏使用了原子操作atomic_inc()和atomic_dec():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static inline void atomic_inc(atomic_t *v)
{
    asm volatile(LOCK_PREFIX "incl %0"
             : "+m" (v->counter));
}
 
static inline void atomic_dec(atomic_t *v)
{
    asm volatile(LOCK_PREFIX "decl %0"
             : "+m" (v->counter));
}
 
#ifdef CONFIG_SMP
#define LOCK_PREFIX_HERE \
        ".pushsection .smp_locks,\"a\"\n"   \
        ".balign 4\n"               \
        ".long 671f - .\n" /* offset */     \
        ".popsection\n"             \
        "671:"
 
#define LOCK_PREFIX LOCK_PREFIX_HERE "\n\tlock; "
 
#else /* ! CONFIG_SMP */
#define LOCK_PREFIX_HERE ""
#define LOCK_PREFIX ""
#endif

2、memory barrier

 

參考資料:

1、Understanding Linux Kernel

2、Professinal Linux Kernel Architecture

Published in 中斷 and tagged tasklet on 2013年11月22日 by rock3

軟中斷原理

    Linux內核中將不太緊急的中斷處理工作留給軟中斷去處理,這部分工作通常稱爲“可延期執行”任務。術語“軟中斷(softirq)”常常表示可延遲函數的所有種類。另一種被廣泛使用的術語是“中斷上下文”:表示內核當前正在執行一箇中斷處理程序或一個可延遲的函數。軟中斷機制目前包含幾種:softirq、tasklet、工作隊列。討厭的是softirq本身也叫軟中斷,本文定位介紹softirq機制,而不包含tasklet和工作隊列。由於本文比較倉促,如有錯誤之處敬請指出。

   本文中使用的內核代碼版本爲3.10.9。

   本篇博客耗時:18小時。

一、軟中斷的目的

    軟中斷,就是一種“可延遲執行”任務的處理方式,一種有別於硬件中斷處理。舉個例子,網卡接收數據包,從網卡產生中斷信號,CPU將網絡數據包拷貝到內核,然後進行協議棧的處理,最後將數據部分傳遞給用戶空間,這個過程都可以說是中斷處理函數需要做的部分,但硬件中斷處理僅僅做從網卡拷貝數據的工作,而協議棧的處理的工作就交給“可延遲執行”部分處理。而軟中斷,正是”可延遲處理“部分的一種機制,之所以叫軟中斷,是因爲類似於硬件中斷的過程,產生中斷信號,維護軟中斷向量,進行中斷處理。

    可延遲函數在執行上一般有四個流程:

  • 初始化,定義一個新的可延遲函數,這個操作通常在內核自身初始化或加載過模塊時進行。
  • 激活,標記一個可延遲函數爲“掛起”狀態(可延遲函數的下一輪調度中可執行),激活可以在任何時後進行(即是正在處理中斷)。
  • 屏蔽,有選擇的屏蔽一個可延遲函數,這樣,即使它被激活,內核也不執行它。
  • 執行,執行一個掛起的可延遲函數和同類型的其他所有掛起的可延遲函數;執行是在特定的時間內進行的。

二、軟中斷內核數據機構

    Linux內核中定義爲數不多的幾種軟中斷:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
enum
{
    HI_SOFTIRQ=0,
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    BLOCK_SOFTIRQ,
    BLOCK_IOPOLL_SOFTIRQ,
    TASKLET_SOFTIRQ,
    SCHED_SOFTIRQ,
    HRTIMER_SOFTIRQ,
    RCU_SOFTIRQ,   
    /* Preferable RCU should always be the last softirq */
 
    NR_SOFTIRQS
};

軟中斷全局數組:

1
2
3
4
5
6
static struct softirq_action softirq_vec[NR_SOFTIRQS]
__cacheline_aligned_in_smp;
struct softirq_action
{
    void    (*action)(struct softirq_action *);
};

    softirq_vec[]數組元素下標就是軟中斷的向量號,內容爲對應的處理函數的指針。

    參數如何傳遞?函數執行時,以softirq_action指針爲參數,怎麼獲得相關的處理數據指針,比如網卡數據接收?

三、軟中斷的操作接口

    軟中斷對應的可延遲函數的四步分別爲:

  • open_softirq(),註冊軟中斷
  • raise_softirq(),激活軟中斷
  • local_bh_disable(),整體屏蔽所有可延遲機制
  • do_softirq(),執行軟中斷

    驅動通過open_softirq()函數初始化softirq_vec[]數組元素,完成對softirq_vec[]數組元素的初始化(kerne/softirq.c)

1
2
3
4
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
    softirq_vec[nr].action = action;
}

   使用raise_softirq()函數來激活軟中斷,等到下次調度時使用,raise_softirq()函數執行下面的操作:

  • local_irq_save()保存EFLAGS寄存器的IF位。
  • 將per_cpu的pending位圖的相應bit置位,表示處與“掛起”狀態(“掛起”表示可被執行),關於pending位圖的細節請參考“六、一些細節”。
  • 如果當前進程未處於中斷上下文中,則wakeup_softirqd(),嘗試執行所有的“掛起”的softirq。
  • local_irq_restore()保存EFLAGS恢復寄存器的IF位。

    起先,對上述過程,我有兩點不太明白:

  • raise_softirq(nr)函數主要功能是將軟中斷號爲nr的軟中斷“掛起”,也即置位工作,是一個純粹的軟件操作,爲什麼要保存eflags寄存器的硬件中斷的響應標誌?
  • 如果當前進程沒有處於中斷的上下文中(軟中斷、硬中斷),則喚醒ksoftirqd線程。這是什麼意思?爲什麼不單獨使用do_softirq()去做執行軟中斷的事情?

    第一個問題,在“五、同步”一節討論。

    第二個問題,在於兩個接口(do_softirq()和raise_softirq())都可以輪詢softirq函數,這樣做的好處,根據ULK上的說法,爲了儘量不要讓軟中斷像硬中斷一樣中斷用戶進程的操作,而嘗試使用內核線程的方式與用戶進程一同調度,更加公平。經過查找,在x86架構下,單獨調用do_softirq()的地方很少:

    do_softirq

   這也說明了do_softirq()和raise_softirq()的區別:do_softirq()嘗試在當前進程中或者內核softirq_ctx中執行輪詢pending的softirq並執行他們(do_softirq()也嘗試使用ksoftirqd,但是次要的),而raise_softirq()將當前的softirq置位pending,並嘗試在ksoftirqd中輪詢並執行所有狀態爲pending的softirq。這也是爲什麼do_softirq()和raise_softirq()都能輪詢並執行軟中斷數組的原因,內核想在更多的地方執行軟中斷。

    我猜這裏的主要矛盾是硬中斷、軟中斷、用戶進程之間的優先級問題,硬中斷肯定可以中斷軟中斷和用戶進程,而軟中斷和用戶進程之間的優先級應該是軟中斷略高於用戶進程,想象一個socket程序在使用read接口來讀取網絡數據,那麼如果接收並處理網絡數據的軟中斷(NET_RX_SOFTIRQ)優先級沒有用戶進程(當然不一定是read()接口,還有其他用戶進程,如kde什麼的)的高,那麼它將整體弱於用戶進程而執行,那麼將導致read接收數據效率低下,甚至丟包。反過來,如果軟中斷可以任意打斷用戶進程(這樣軟中斷就和硬中斷沒什麼區別了),那麼在接收網絡數據包的同時,會讓KDE桌面顯得很卡(用戶空間驅動)。爲了平衡二者之間的關係,Linux內核發明了raise_softirq()和do_softirq()兩個接口,一個通過ksoftirqd主動觸發軟中斷,一個在不少位置(詳見“四、何時調用軟中斷”)在不同的內核棧裏執行軟中斷。而ksoftirqd與普通用戶進程通過調度器競爭CPU。內核大量的調用軟中斷的方式還是do_softirq(),大量的local_bh_enable()和irq_exit(),伴隨着每次do_IRQ()都會被調用,而raise_softirq()函數往往在硬中斷處理函數中決定是否要調用它。

     do_softirq()函數代碼(64位,32位代碼比64位略微複雜一些):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
asmlinkage void do_softirq(void)
{
    __u32 pending;
    unsigned long flags;
 
    if (in_interrupt())
        return;
 
    local_irq_save(flags);
    pending = local_softirq_pending();
    /* Switch to interrupt stack */
    if (pending) {
        call_softirq();
        WARN_ON_ONCE(softirq_count());
    }  
    local_irq_restore(flags);
}

    call_softirq()爲arch/x86/kernel/entry_64.S中定義,call_softirq()函數指向__do_softirq()函數,__do_softirq()函數在複雜的條件下輪詢softirq_vec[]數組,執行所有掛起的軟中斷處理函數:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
asmlinkage void __do_softirq(void)
{
    struct softirq_action *h;
    __u32 pending;
    unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
    int cpu;
    unsigned long old_flags = current->flags;
    int max_restart = MAX_SOFTIRQ_RESTART;
 
    /*
     * Mask out PF_MEMALLOC s current task context is borrowed for the
     * softirq. A softirq handled such as network RX might set PF_MEMALLOC
     * again if the socket is related to swap
     */
    current->flags &= ~PF_MEMALLOC;
 
    pending = local_softirq_pending();
    account_irq_enter_time(current);
 
    __local_bh_disable(_RET_IP_, SOFTIRQ_OFFSET);
    lockdep_softirq_enter();
 
    cpu = smp_processor_id();
restart:
    /* Reset the pending bitmask before enabling irqs */
    set_softirq_pending(0);
 
    local_irq_enable();
 
    h = softirq_vec;
 
    do {
        if (pending & 1) {
            unsigned int vec_nr = h - softirq_vec;
            int prev_count = preempt_count();
 
            kstat_incr_softirqs_this_cpu(vec_nr);
 
            trace_softirq_entry(vec_nr);
            h->action(h);
            trace_softirq_exit(vec_nr);
            if (unlikely(prev_count != preempt_count())) {
                printk(KERN_ERR "huh, entered softirq %u %s %p"
                       "with preempt_count %08x,"
                       " exited with %08x?\n", vec_nr,
                       softirq_to_name[vec_nr], h->action,
                       prev_count, preempt_count());
                preempt_count() = prev_count;
            }
 
            rcu_bh_qs(cpu);
        }
        h++;
        pending >>= 1;
    } while (pending);
 
    local_irq_disable();
 
    pending = local_softirq_pending();
    if (pending) {
        if (time_before(jiffies, end) && !need_resched() &&
            --max_restart)
            goto restart;
 
        wakeup_softirqd();
    }
 
    lockdep_softirq_exit();
 
    account_irq_exit_time(current);
    __local_bh_enable(SOFTIRQ_OFFSET);
    tsk_restore_flags(current, old_flags, PF_MEMALLOC);
}

    去掉trace部分以及一些狀態和統計數據(lockdep_softirq_enter是更新統計數據)的更新,__do_softirq()函數通過判斷percpu的pending標誌來執行軟中斷數組上所有pending的action,但一下幾點需要注意

  • 在執行do{}while()循環前,local_irq_enable(),執行完畢後local_irq_disable()。先enable,後disable,是因爲do_softirq()中先local_irq_save(),後local_irq_restore(),詳細原因將在“五、同步”中討論。
  • 在進入__do_softirq()函數後,要使用__local_bh_disable(__RET_IP_,SOFTIRQ_OFFSET)來關閉下班部,所有事情做完後,需要通過__local_bh_enable(SOFTIRQ_OFFSET)來激活下班部(“下班部”這個次其實已經過時了,就是激活所有“可延遲執行”機制),這是因爲下班部機制沒必要在同一個CPU上同時執行兩遍。

四、何時調用軟中斷

    Linux內核在以下幾個位置會查看local cpu的軟中斷掩碼位,執行所有pending的軟中斷處理函數,:

  • do_IRQ()執行完硬中斷處理,irq_exit()函數調用invoke_softirq()函數,再調用do_softirq()函數。
  • local_bh_enable()函數執行時,通過__local_bh_enable()函數調用了do_softirq()函數。
  • smp_apic_timer_interrupt()函數執行完畢時,調用了irq_exit()函數。
  • wakeup_softirqd()喚醒ksoftirqd/n時。
  • 其他情況。

    do_IRQ()爲硬中斷處理函數,在do_IRQ()執行完畢desc->handle_irq()後,將通過irq_exit()接口轉入軟中斷處理。這也是硬中斷和軟中斷銜接的位置。(記得desc->action->thread_fn,在handle_irq_event()裏面有處理ONESHOT的情況,看來與軟中斷無關了,這個地方還得再研究一下)。

    五、同步

    軟中斷數組雖然不是PER_CPU的,但是pending位圖卻是PER_CPU的。每個CPU決定自己要將哪個softirq設置爲pending,要將執行哪些softirq(pending狀態的)。兩個CPU執行同時同一個軟中斷也是沒有問題的。有幾個位置需要分析一下,它們分別是:

  • in_interrupt()函數判斷是否處於中斷上下文;
  • __do_softirq()函數執行前的local_irq_disable(),__do_softirq()函數後的local_irq_enable();
  • __do_softirq()函數執行過程中,需要先__local_bh_disable(),執行完畢後__local_bh_enable();
  • __do_softirq()函數中,在具體的循環執行軟中斷處理函數的過程中,先local_irq_enable()開中斷,然後local_irq_disable()關中斷;

local_irq_disable

  • 紫色,主要工作函數
  • 粉色,current->preempt_count的改變點
  • 青色,禁止、開啓本地中斷點
  • 黃色,硬中斷和軟中斷的主要工作
  • 灰色,存取本定pending位圖的位置

    注意,這裏所有的同步操作,都是local的(pending位圖,local_irq,local_bh)軟中斷不管其他CPU上是否正在執行軟中斷,因此不操心其他CPU發生的事情,考慮嵌套的情況:如果在執行do_softirq()函數的某一步,被do_IRQ()函數打斷,do_IRQ()函數在結束時又會調用do_softirq()函數,如果再被打斷,那麼將會形成反覆嵌套:do_IRQ():1->do_softirq():1->do_IRQ():2->do_softirq():2->do_softirq():2 return->do_IRQ():2 return->do softirq():1 return->do_IRQ():1return。先假設不用禁止硬中斷,因爲__do_softirq()函數通過pending位圖來判斷是否執行該action,do_IRQ():2打斷do_softirq():1的點的不同可能出現以下幾種情況:

  • 在__local_bh_disable()前打斷,do_softirq():1等do_IRQ():2返回後繼續執行,但次是pending位圖已經清0,前取出的pending實際上已經發生了變化,沒必要再執行一遍軟中斷,但還是執行了(軟中斷可重入),降低效率。
  • 在__local_bh_disable()後打斷,do_softirq():1等do_IRQ():2返回後繼續執行,由於do_softirq():1已經通過__local_bh_disable()禁止了“可延遲執行”機制,那麼do_softirq():2就直接從in_interrupt()處返回了,do_softirq():2可以正常的執行所有pending的action,但是時間延後了(這也算正常,因爲硬中斷有先級高於軟中斷)。

    爲了避免第一種情況,就要保護PER_CPU的pending位圖與實際執行的action是同步的,要做到這點其實不難,因爲在同一個CPU上代碼串行執行,在這段加個全局標誌就可以,將pending位圖和main work視爲一體,前置標誌,後清標誌。這個標誌在這就是SOFTIRQ_OFFSET,就像下面這樣:

local_irq_disable_myself

    但實際上Linux沒有這麼做,而是在__do_softirq()前後分別屏蔽中斷和開啓中斷,並在__do_softirq()的main work前後作反向操作,這樣做的唯一解釋就是硬中斷中除了do_softirq()本身的操作外,還有破壞非原子操作local_softirq_pending()和set_softirq_pending(0)兩步的操作,那就需要查一下都有在什麼位置設置了pengding位圖的操作了。時鐘中斷就是這樣的,在timer_interrupt(irq_desc[0]->action->handler)中調update_process_timers,最終調用了rasie_softirq()。也就是說,硬中斷有可能改變softirq的pending位圖(其實本來就這麼設計的,是否啓動軟中斷,是硬中斷處理函數說的算):

irq0

 

    按照ULK上的解釋,如果in_interrupt()返回1,則說明當前進程要麼在中斷上下文中調用了do_softirq()函數,要麼當前禁止軟中斷。

    另外,關於pending位圖,經過查找,發現除了raise_softirq()函數會將相應的軟中斷置位以外,沒有發現別的點。(調用or_softirq_pending宏)。

六、一些細節

1、in_interrupt()函數的實現

    軟中斷(raise_softirq()函數和do_softirq()函數)通過in_interrupt()函數來判斷是否處於中斷上下文中(include/linux/preempt_mask.h),如果不處於中斷上下文中,才執行軟中斷輪詢操作。in_interrupt()函數實現如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
* - bit 27 is the PREEMPT_ACTIVE flag
 *
 * PREEMPT_MASK: 0x000000ff
 * SOFTIRQ_MASK: 0x0000ff00
 * HARDIRQ_MASK: 0x03ff0000
 *     NMI_MASK: 0x04000000
 */
 
#define in_interrupt()      (irq_count())
#define irq_count() (preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK \
                 | NMI_MASK))
#define preempt_count() (current_thread_info()->preempt_count)

    而current_thread_info()->preempt_count是進程用於描述被搶佔的屬性,它包含3個計數器,分別用於統計硬中斷、軟中斷、以及搶佔的次數。在下列點被增加:

  • do_IRQ()函數調用irq_enter()函數,再調用__irq_enter()函數,將改變量增加HARDIRQ_OFFSET(0x10000)
  • __local_bh_disable()函數被執行時,也即“可延遲函數”被禁止時,將該變量增加SOFTIRQ_DISABLE_OFFSET(0x200)
  • preempt_disable()函數被執行時,也即內核禁止搶佔時,該變量將被增加1。

    在下列點被減少(是增加點的逆向操作):

  • do_IRQ()函數調用irq_exit()函數,再調用sub_preempt_count()函數,將改變量減少HARDIRQ_OFFSET(0x10000)。
  • __local_bh_enable()函數被執行時,也即“可延遲函數”被激活時,將該變量減少SOFTIRQ_DISABLE_OFFSET(0x200)。
  • preempt_enable()函數被執行時,也即內核激活搶佔時,該變量將被減少1。

    in_interrupt()函數只看irq_count()也即軟中斷、硬中斷以及NMI中斷是否正在處理。也是當前進程是否正處於“中斷上下文”當中,一旦處於中斷上下文當中,說名current進程要麼還有硬中斷沒執行完,要麼還有軟中斷沒執行完,那麼就沒有必要在執行softirq了。當然,改變current_thread_info()->preempt_count計數器的點可能不止這些,這裏就不找那麼複雜了。

2、pending位圖操作

     pending位圖是PER_CPU的,每個CPU都有一個對應的軟中斷掛起位圖,只要相應的位被置位,則說明該softirq可以被執行。pending位圖是irq_stat結構體變量的一個成員(arch/x86/include/asm/hardirq.h):

1
2
3
4
5
6
typedef struct {
    unsigned int __softirq_pending;
    ...
} ____cacheline_aligned irq_cpustat_t;
 
DECLARE_PER_CPU_SHARED_ALIGNED(irq_cpustat_t, irq_stat);

    對pending位圖的基本操作有(arch/x86/include/asm/hardirq.h):

  • local_softirq_pending(),取本地pending值,__do_softirq()函數時會調用。
  • set_softirq_pending(),設置本地pending值,__do_softirq()函數時會調用。
  • or_softirq_pending(),設置本地pending的某一位,raise_softirq()函數會調用。
1
2
3
4
5
#define inc_irq_stat(member)    this_cpu_inc(irq_stat.member)
 
#define set_softirq_pending(x)  \
        this_cpu_write(irq_stat.__softirq_pending, (x))
#define or_softirq_pending(x)   this_cpu_or(irq_stat.__softirq_pending, (x))

3、開關中斷操作

    開關中斷通過以下兩組函數進行:

  • local_irq_enable()和local_irq_disable(),開啓和關閉本地CPU上的中斷處理
  • local_irq_save()和local_irq_restore(),開啓和關閉本地CPU上的中斷處理,並保存和恢復EFLAGS寄存器的IF位。

​    他們的實現如下(省略掉嵌套調用部分,arch/x86/include/asm/irqflags.h),也就是幾個彙編指令:cli(clear interrupt flag)禁止IRQ,sti(set interrupt flag)啓動IRQ,關於c內嵌彙編,詳見這篇帖子”__asm__ __volatile__內嵌彙編用法概述“:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static inline unsigned long native_save_fl(void)
{
    unsigned long flags;
 
    /*
     * "=rm" is safe here, because "pop" adjusts the stack before
     * it evaluates its effective address -- this is part of the
     * documented behavior of the "pop" instruction.
     */
    asm volatile("# __raw_save_flags\n\t"
             "pushf ; pop %0"
             : "=rm" (flags)
             : /* no input */
             : "memory");
 
    return flags;
}
 
static inline void native_restore_fl(unsigned long flags)
{
    asm volatile("push %0 ; popf"
             : /* no output */
             :"g" (flags)
             :"memory", "cc");
}
 
static inline void native_irq_disable(void)
{
    asm volatile("cli": : :"memory");
}
 
static inline void native_irq_enable(void)
{
    asm volatile("sti": : :"memory");
}

4、開關下半部操作

    通過__local_bh_enable()函數和__local_bh_disable()函數來實現開關下半部操作,所謂的下半部機制就是“可延遲操作”機制的總稱。__local_bh_enable()函數和__local_bh_disable()函數僅僅用作current_thread_info()->preempt_count的置位和清除,並不做其他工作(只列一個,kernel/softirq.c):

1
2
3
4
5
6
7
8
9
static void __local_bh_enable(unsigned int cnt)
{
    WARN_ON_ONCE(in_irq());
    WARN_ON_ONCE(!irqs_disabled());
 
    if (softirq_count() == cnt)
        trace_softirqs_on(_RET_IP_);
    sub_preempt_count(cnt);
}

   他們分別被local_bh_enable()函數和local_bh_disable()函數調用,除了置位操作,還嘗試執行do_softirq()函數,這裏就不列了。

 5、內核棧切換

    如果內核棧是8k的,則軟中斷運行於進程的內核棧,如果內核棧是4k的情況,則軟中斷運行於PER_CPU的softirq_ctx棧上,這一點與硬中斷很像。32位有這個操作,但64位沒有,因爲64位都是8K的內核棧。看下32位的do_softirq()函數的實現:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
asmlinkage void do_softirq(void)
{
    unsigned long flags;
    struct thread_info *curctx;
    union irq_ctx *irqctx;
    u32 *isp;
 
    if (in_interrupt())
        return;
 
    local_irq_save(flags);
 
    if (local_softirq_pending()) {
        curctx = current_thread_info();
        irqctx = __this_cpu_read(softirq_ctx);
        irqctx->tinfo.task = curctx->task;
        irqctx->tinfo.previous_esp = current_stack_pointer;
 
        /* build the stack frame on the softirq stack */
        isp = (u32 *) ((char *)irqctx + sizeof(*irqctx));
 
        call_on_stack(__do_softirq, isp);
        /* 
         * Shouldn't happen, we returned above if in_interrupt():
         */
        WARN_ON_ONCE(softirq_count());
    }  
 
    local_irq_restore(flags);
}

    call_on_stack()函數用於切換內核棧:

1
2
3
4
5
6
7
8
9
10
static void call_on_stack(void *func, void *stack)
{
    asm volatile("xchgl %%ebx,%%esp \n"
             "call  *%%edi      \n"
             "movl  %%ebx,%%esp \n"
             : "=b" (stack)
             : "0" (stack),
               "D"(func)
             : "memory", "cc", "edx", "ecx", "eax");
}

七、總結

    Linux內核中,軟中斷是硬中斷的一種輔助處理方式,類似於硬中斷,軟中斷也有軟中斷數組維護,基本操作有註冊、激活、屏蔽、執行等。軟中斷的註冊是靜態的,伴隨內核編譯完畢而完成。軟中斷在幾個位置被執行,其中比較總要的一個位置就是do_IRQ()執行完畢時,irq_exit()函數退出時調用do_softirq(),這也是軟中斷和硬中斷之間的聯繫。

    軟中斷通過PER_CPU的pending位圖來維護它的可執行狀態,當軟中斷被執行後,將清除其pending位圖,在下次硬中斷執行過程中,將決定是否將其pending位圖置位。pengding位圖的同步通過禁止本地中斷來實現。

    如果進程的內核棧爲8k,則軟中斷運行於進程的內核棧,如果內核棧爲4k,則軟中斷運行於內核的單獨的軟中斷處理棧softirq_ctx。

    軟中斷函數的設計必須是可重入的。

八、遺留問題

1、內核棧切換細節?

2、中斷與搶佔有什麼區別?

3、ksfotirqd是內核進程?內核線程?

4、在SMP架構下,PIC如何將每一次硬件中斷分配給每一個CPU的?

5、軟中斷的可重入意味着什麼?

6、硬件驅動如何與軟中斷掛接?給一個例子

 

 

參考資料:

1、Understanding Linux Kernel

2、Professinal Linux Kernel Architecture

3、內核隨記(二)——內核搶佔與中斷返回

4、淺析Linux的軟中斷的實現

Published in 中斷 and tagged softirq軟中斷 on 2013年11月20日 by rock3

irq_desc操作

     irq_desc[]數組是linux內核中用於維護IRQ資源的管理單元,它存儲了某IRQ號對應的哪些處理函數,屬於哪個PIC管理、來自哪個設備、IRQ自身的屬性、資源等,是內核中斷子系統的一個核心數組,習慣上稱其爲“irq數組”(個人愛好,下標就irq號)。本篇博客着重學習irq_desc[]數組的一些操作的過程和方法,如初始化、中斷處理、中斷號申請、中斷線程等,而對於輔助性的8259A和APIC等設備的初始化過程,不詳細討論,對於某些圖片或代碼,也將其省略掉了。

    本文中出現的irq_desc->和desc->均表示具體的irq數組變量,稱其中的一個個體爲irq_desc[]數組元素,描述個體時也直接時用字符串desc。爲了區別PIC的handle和driver的handle,將前者稱爲中斷處理函數(對應desc->handle_irq,實際上對應handle_xxx_irq()),而將後者稱爲中斷處理操作(對應desc->action)。本文中將以irq_descp[]數組爲操作對象的層稱爲irq層。本文使用的內核代碼版本爲3.10.9。

    一篇好的博客應該是儘量多的說明,配少量的核心代碼,這裏偷懶了,很多部分實際是代碼分析的過程,也沒有省略掉。本篇博客耗時48小時。

一、irq_desc結構和irq_desc[]數組

    irq_desc[]數組,在kernel/irq/irqdesc.c中聲明,用於內核管理中斷請求,例如中斷請求來自哪個設備,使用什麼函數處理,同步資源等:

1
2
3
4
5
6
7
struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = {
    [0 ... NR_IRQS-1] = {
        .handle_irq = handle_bad_irq,
        .depth      = 1,
        .lock       = __RAW_SPIN_LOCK_UNLOCKED(irq_desc->lock),
    }  
};

    整體上,關於irq_desc結構體,如下圖所示:irq_desc

 

    struct irq_desc結構體(以前的版本結構體的名字是irq_desc_t)定義如下所示(簡化過,include/linux/irqdesc.h)。大部分成員都是輔助性的,關鍵的成員是irq_data、handle_irqs、action、depth、lock、istat,所謂irq_desc[]數組的初始化,看其主要成員的初始化的過程,在這裏做簡單的說明:

  • action指針指向具體的設備驅動提供的中斷處理操作,就是所爲的ISR,action本身是一個單向鏈表結構體,由next指針指向下一個操作,因此action實際上是一個操作鏈,可以用於共享IRQ線的情況。
  • handle_irq是irq_desc結構中與PIC相關的中斷處理函數的接口,通常稱作”hard irq handler“。此函數對應了PIC中的handle_xxx_irq()系列函數(xxx代表觸發方式),do_IRQ()就會調用該函數,此函數最終會執行desc->action。
  • irq_data用於描述PIC方法使用的數據,irq_data下面有兩個比較重要的結構:chip和state_use_accessors,前者表示此irq_desc[]元素時用的PIC芯片類型,其中包含對該芯片的基本操作方法的指針;後者表示該chip的狀態和屬性,其中有些用於判斷irq_desc本身應該所處的狀態。
  • lock用於SMP下不同core下的同步。
  • depth表示中斷嵌套深度,也即一箇中斷打斷了幾個其他中斷。
  • istate表示該desc目前的狀態,將在“六、istate狀態”中描述。
1
2
3
4
5
6
7
8
9
10
11
12
13
struct irq_desc {
    struct irq_data     irq_data;
    irq_flow_handler_t  handle_irq;
    ...
    struct irqaction    *action;    /* IRQ action list */
    unsigned int        status_use_accessors;
    unsigned int        core_internal_state__do_not_mess_with_it;
    unsigned int        depth;      /* nested irq disables */
    raw_spinlock_t      lock;
    ...
    struct module       *owner;
    const char      *name;
} ____cacheline_internodealigned_in_smp;

   這裏還是看一下irqaction結構體,action的handler是具體的中斷服務程序,next指針用於指向同一個鏈上的後一個的irqaction,thread_fn用於描述軟中斷處理函數。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef irqreturn_t (*irq_handler_t)(int, void *);
 
struct irqaction {
    irq_handler_t       handler;
    void            *dev_id;
    void __percpu       *percpu_dev_id;
    struct irqaction    *next;
    irq_handler_t       thread_fn;
    struct task_struct  *thread;
    unsigned int        irq;
    unsigned int        flags;
    unsigned long       thread_flags;
    unsigned long       thread_mask;
    const char      *name;
    struct proc_dir_entry   *dir;
} ____cacheline_internodealigned_in_smp;

    這意味着所有的驅動在寫中斷處理函數時,必須以irqreturn_t爲類型:

1
2
3
4
5
6
7
8
// intel e1000
static irqreturn_t e1000_intr(int irq, void *data);
// acpi
static irqreturn_t acpi_irq(int irq, void *dev_id)
// hd
static irqreturn_t hd_interrupt(int irq, void *dev_id)
// ac97
static irqreturn_t atmel_ac97c_interrupt(int irq, void *dev)

    在這裏,很容易產生一個問題,就是驅動程序處理的數據在哪?總要有些數據要處理,是從void參數嗎?那麼這個數據怎麼獲取的?handle_irq_event_percpu()函數裏有具體的action的調用方式:

1
res = action->handler(irq, action->dev_id);

    那麼,void *參數來自action->dev_id,而dev_id是驅動程序註冊時,調用request_irq()函數傳遞給內核的。而這個dev_id通常指向一個device設備,驅動程序就通過該device設備將需要的數據接收上來,並進行處理。

二、irq_desc[]的初始化——8259A

   irq_desc[]數組是內核維護中斷請求資源的核心數組,它必須在合適的時機予以初始化。內核起動後,有步驟的初始化內核各個子系統,init_IRQ()函數主要負責完成內核中斷子系統的主要初始化。irq_desc[]數組伴隨着init_IRQ()函數的執行而完成其一部分的初始化。
   init_IRQ()函數的調用路徑爲main()->...->start_kernel()->init_IRQ()->native_init_IRQ()。init_IRQ()函數與irq_desc[]數組初始化或者IDT、interrupt[]數組的設置有關的函數或過程,關於init_IRQ的內部調用關係,如下圖所示:

init_IRQ

    下面是具體的代碼分析過程:

    從init_IRQ()函數開始分析,init_IRQ在arch/x86/kernel/irqinit.c中定義:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void __init init_IRQ(void)
{
    int i;
 
    /* 
     * We probably need a better place for this, but it works for
     * now ...
     */
    x86_add_irq_domains();
 
    /* 
     * On cpu 0, Assign IRQ0_VECTOR..IRQ15_VECTOR's to IRQ 0..15.
     * If these IRQ's are handled by legacy interrupt-controllers like PIC,
     * then this configuration will likely be static after the boot. If
     * these IRQ's are handled by more mordern controllers like IO-APIC,
     * then this vector space can be freed and re-used dynamically as the
     * irq's migrate etc.
     */
    for (i = 0; i < legacy_pic->nr_legacy_irqs; i++)
        per_cpu(vector_irq, 0)[IRQ0_VECTOR + i] = i;
 
    x86_init.irqs.intr_init();
}

    x86_add_irq_domains()直接略過。這裏的註釋還時很有用的,這裏說開始時使用8259A註冊這些中斷向量號,如果系統使用IO APIC,將覆蓋這些中斷向量號,並且能夠動態的重新使用。vector_irq爲在arch/x86/include/asm/hw_irq.h中定義的per_cpu整形數組,長度爲256,用於描述每個CPU的中斷向量號,即vector_irq[](vector_irq[]元素初始化時被賦值爲-1)中存儲着系統可以使用的中斷向量號。這裏需要注意,vector_irq[]數組時PER_CPU的。

    legacy_pic字面意思爲“遺留的PIC”,就是指8259A,legacy_pic定義在arch/x86/kernel/i8259.c,其中NR_IRQS_LEGACY爲16:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct legacy_pic default_legacy_pic = {
    .nr_legacy_irqs = NR_IRQS_LEGACY,
    .chip  = &i8259A_chip,
    .mask = mask_8259A_irq,
    .unmask = unmask_8259A_irq,
    .mask_all = mask_8259A,
    .restore_mask = unmask_8259A,
    .init = init_8259A,
    .irq_pending = i8259A_irq_pending,
    .make_irq = make_8259A_irq,
};
 
struct legacy_pic *legacy_pic = &default_legacy_pic;

    a) native_inti_IRQ()

    init_IRQ()將vector_irq[]逐個賦值(就賦值了16個,從0x30到0x39)。x86_init爲x86架構初始化時的一個全局變量,記錄了各個子系統(irq,paging,timer,iommu,pci等)初始化使用的具體函數。而實際的x86_init.irqs.intr_init指針指向native_init_IRQ()函數(arch/x86/kernel/irqinit.c):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void __init native_init_IRQ(void)
{
    int i;
 
    /* Execute any quirks before the call gates are initialised: */
    x86_init.irqs.pre_vector_init();
 
    apic_intr_init();
 
    /* 
     * Cover the whole vector space, no vector can escape
     * us. (some of these will be overridden and become
     * 'special' SMP interrupts)
     */
    i = FIRST_EXTERNAL_VECTOR;
    for_each_clear_bit_from(i, used_vectors, NR_VECTORS) {
        /* IA32_SYSCALL_VECTOR could be used in trap_init already. */
        set_intr_gate(i, interrupt[i - FIRST_EXTERNAL_VECTOR]);
    }  
 
    if (!acpi_ioapic && !of_ioapic)
        setup_irq(2, &irq2);
 
#ifdef CONFIG_X86_32
    irq_ctx_init(smp_processor_id());
#endif
}
   x86_init.irqs.pre_vector_init指針指向init_ISA_irqs()函數,主要完成8259A/Local APIC的初始化,apic_intr_init()函數主要完成apic相關的中斷的初始化。接着,native_init_IRQ()函數將調用set_intr_gate()函數設置中斷門,將interrupt[]數組設置的地址設置到相應的中斷門。注意,這裏只是對沒有used_vectors進行set_intr_gate()的賦值,並不是從FIRST_EXTERNAL_VECTOR到NR_VECTORS全部賦值,因爲有些特殊情況會預留(關於used_vectors和vector_irq的關係,詳見“七、中斷向量、鎖和CPU”)。餘下的兩個接口處理了一些特殊情況,這裏不展開了。

    實際上init_IRQ()主要調用了native_init_IRQ(),除了使用set_intr_gate()來初始化Interrupt describptor外,後者主要乾了兩件事:init_ISA_irqs()和apic_intr_init()。先從簡單的看起,apic_intr_init()函數實際上是一系列的set_intr_gate,但不通過interrupt[]數組,也不通過irq_desc[](這就是native_init_IRQ()函數中所爲的“特殊情況”,屬於used_vectors的範圍):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void __init apic_intr_init(void)
{
    smp_intr_init();
 
#ifdef CONFIG_X86_THERMAL_VECTOR
    alloc_intr_gate(THERMAL_APIC_VECTOR, thermal_interrupt);
#endif
    ...
#ifdef CONFIG_HAVE_KVM
    /* IPI for KVM to deliver posted interrupt */
    alloc_intr_gate(POSTED_INTR_VECTOR, kvm_posted_intr_ipi);
#endif
    ...
}

    而smp_intr_init()函數如下執行apic_intr_intr()函數類似的操作,也通過set_intr_gate()函數設置了一些中斷門。

    這些中斷門沒有通過interrupt數組,也沒有irq_desc數組,而是直接使用set_intr_gate()接口將其IDT中的中斷門描述符初始化。而這些中斷在/proc/interrupt中顯示比較特殊,並不以中斷向量號的形式顯示,而是以名字的形式,比如NMI,本身也不連接任何的PIC(截取一部分):

1
2
3
4
5
6
7
8
9
10
[rock3@e4310 linux-stable]$ cat /proc/interrupts
           CPU0       CPU1       CPU2       CPU3      
...
 44:         66         80         77         72   PCI-MSI-edge      snd_hda_intel
 45:   14948296          0          0          0   PCI-MSI-edge      iwlwifi
NMI:       1539      19912      17314      17232   Non-maskable interrupts
LOC:   45133746   42836772   33584448   33666542   Local timer interrupts
SPU:          0          0          0          0   Spurious interrupts
PMI:       1539      19912      17314      17232   Performance monitoring interrupts
IWI:     641572     409182     330064     302186   IRQ work interrupts

    然後看比較複雜的init_ISA_irqs()函數,代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void __init init_ISA_irqs(void)
{
    struct irq_chip *chip = legacy_pic->chip;
    const char *name = chip->name;
    int i;
 
#if defined(CONFIG_X86_64) || defined(CONFIG_X86_LOCAL_APIC)
    init_bsp_APIC();
#endif
    legacy_pic->init(0);
 
    for (i = 0; i < legacy_pic->nr_legacy_irqs; i++)
        irq_set_chip_and_handler_name(i, chip, handle_level_irq, name);
}

    legacy_pic->init指針指向init_8259A()函數,因此init_ISA_irqs執行了init_8259A(0)。irq_set_chip_and_handler_name()函數用於設置irq_desc[]數組的handle_irq、name、chip等成員。因此init_ISA_irqs()函數做了三件事:init_bsp_APIC()、init_8259A()、irq_set_chip_and_handler_name()。此時legacy_pic->nr_legacy_irqs爲16。

    init_bsp_APIC()爲對Local APIC的某種初始化操作,與irq_desc[]數組初始化無關,不討論了。

   init_8259A(0)爲對8259A的某種初始化操作,與Irq_desc[]數組的初始化無關,不討論了。

  irq_set_chip_and_handler_name()函數如下(kernel/irq/chip.c):

1
2
3
4
5
6
7
void
irq_set_chip_and_handler_name(unsigned int irq, struct irq_chip *chip,
                  irq_flow_handler_t handle, const char *name)
{
    irq_set_chip(irq, chip);
    __irq_set_handler(irq, handle, 0, name);
}

    irq_set_chip()將irq_descp[]數組的*action的chip成員,主要是__irq_set_handler()函數(kernel/irq/chip.c),看下__irq_set_handler()函數都設置了irq_desc[]數組的什麼成員:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void
__irq_set_handler(unsigned int irq, irq_flow_handler_t handle, int is_chained,
          const char *name)
{
    unsigned long flags;
    struct irq_desc *desc = irq_get_desc_buslock(irq, &flags, 0);
 
    if (!desc)
        return;
 
    if (!handle) {
        handle = handle_bad_irq;
    } else {
        if (WARN_ON(desc->irq_data.chip == &no_irq_chip))
            goto out;
    }
 
    /* Uninstall? */
    if (handle == handle_bad_irq) {
        if (desc->irq_data.chip != &no_irq_chip)
            mask_ack_irq(desc);
        irq_state_set_disabled(desc);
        desc->depth = 1;
    }
    desc->handle_irq = handle;
    desc->name = name;
 
    if (handle != handle_bad_irq && is_chained) {
        irq_settings_set_noprobe(desc);
        irq_settings_set_norequest(desc);
        irq_settings_set_nothread(desc);
        irq_startup(desc, true);
    }
out:
    irq_put_desc_busunlock(desc, flags);
}

    主要就設置了兩個成員:handle_irq全部設置爲handle_level_irq,name設置爲“XT-PIC”(8259A)。而irq_desc[]數組中的handle_irq成員在do_IRQ()中被調用來執行具體的ISA。這個位置使用了buslock,也即desc->irq_data.chip->irq_bus_lock,而不是desc->lock。buslock用於中斷控制器的操作,desc->lock用於IRQ中斷處理函數的操作。

Linux Kernel中斷機制3——硬件支撐

沒有評論

    中斷(Interrupt)包括中斷和異常兩種類型,異常通常由CPU上執行的指令直接觸發,而中斷是由外設發出的電信號觸發的,但是那麼是否所有的外設都直接接在CPU的中斷PIN腳上觸發中斷?CPU有多少負責中斷額PIN腳?CPU如何區別可屏蔽中斷和非可屏蔽中斷?CPU如何區別Faults、Traps、Aborts?本篇文章主要來搞懂這些問題。

一、中斷控制器

    首先,CPU肯定不會爲允許所有的外設都直接接在其上(否則要總線幹什麼),即使是通知發生中斷這一種功能。應該有專門的中間設備/元器件負責這個工作,這個中間設備就是中斷控制器。我剛剛學習中斷的時候,甚至都不清楚中斷控制器是個硬件還是軟件,是否是CPU的一部分,那麼這個地方先給出兩個中斷控制器的外觀圖片:

Intel-P8259Aich10

82093aa

    以上三張圖片分別是Intel的IP8259A芯片、intel的ICH10南橋芯片以及Intel的S82093AA芯片,他們都有中斷控制器的功能,其中8259A芯片比較老式,目前已經基本淘汰;Intel平臺流行的做法是高級可編程將中斷控制器(APIC)集成到南橋芯片(I/O Controller Hub)中,而較老的S82093AA芯片是最初的APIC的形態。

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

8259A

    8259A除了起到向CPU引入多個外部中斷源的作用外,還有一些基本功能,如中斷分級、中斷屏蔽,中斷管理提(存儲中斷向量)等。

    隨着SMP架構的發展,Intel在2000年左右的時候率先引入一種名爲 高級可編程控制器的新組件(Advanced Programmable Interrupt Controller),來替代老式的 8259A 可編程中斷控制器。APIC包括兩部分:一是“本地 APIC(Local APIC)”,主要負責傳遞中斷信號到指定的處理器,本地APIC通常集成到CPU內部,之所以成爲Local,是相對CPU而言。另外一個重要的部分是 I/O APIC,主要是收集來自 I/O 設備的 Interrupt 信號且將中斷時發送信號到本地 APIC。

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

ioapic

    當然,本地APIC除了接收來自IO APIC的中斷信號,還可以接收其他來源的中斷,比如接在CPU LINT0和LINT1管腳上的中斷、IPI中斷(核間中斷)、APIC定時器產生中斷、性能監視計數器中斷、熱傳感器中斷、APIC內部錯誤中斷等。無論是PIC還是APIC,都通過某種方式與CPU相連(有的時候並不直接相連),這解決兩個問題:

(1)CPU對多個外設的中斷的管理

(2)多CPU的中斷管理(APIC)

    當然,APIC自有一套硬件邏輯去實現這些功能,Intel也提供了相關的用戶手冊,這裏就不深究了,有興趣可以參考資料3——Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3 Chapter 10 advanced programmable interrupt controller(APIC).

    以下爲ULK中關於IRQ線以及中斷控制器的工作邏輯的描述:

    每個能夠發出中斷請求的硬件設備控制器都有一條名爲IRQ(Interrupt ReQuest)的輸出線。所有現有的IRQ線都與PIC的硬件段路的輸入引腳相連,PIC執行下列動作:

1、監視IRQ線,檢查產生的信號。如果有兩條或兩條以上的IRQ線產生信號,就選擇引腳編號較小的IRQ線。

2、如果一個引發信號出現在IRQ線上:

  • a.把接收到的引發信號轉換成對應的向量。
  • b.把這個向量存放在中斷控制器的一個I/O端口,從而允許CPU通過數據總線讀此向量。
  • c.把引發信號發送哦哦嗯到處理器的INTR引腳,產生一箇中斷。
  • d.等待,知道CPU通過把這個中斷信號寫進PIC的一個I/O端口來確認它;當這種情況發生時,清INTR線。

3、返回到第一步。

二、Intel x86 CPU中斷管腳

    APIC系統主要作用是管理外設產生的異步中斷,而對Intel x86架構下的各種異常,如故障、陷阱以及終止,系統是如何管理的那?這得看Intel手冊:Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3 的Chaper 2 system architecture overview 和Chapter 6 interrupt and exception以及Chapter 10 APIC。

    Intel x86架構提供LINT0和LINT1兩個中斷引腳,他們通常與Local APIC相連,用於接收Local APIC傳遞的中斷信號,另外,當Local APIC被禁用的時候,LINT0和LINT1即被配置爲INTR和NMI管腳,即外部IO中斷管腳和非屏蔽中斷管腳。INTR引腳負責向處理器通知發生了外部中斷,處理器從系統總線上讀出由外部中斷控制器提供的中斷向量(Interrupt Vector)號,例如從8259a提供。NMI中斷使用2號中斷向量。

    通常一個INTR引腳能夠接收64箇中斷源,至於CPU內部怎麼通過LINT0和LINT1進行中斷的處理,就不管了,抑或還有其他的中斷引腳什麼的...

三、Intel x86架構對中斷的支持——中斷向量和IDT表

    Intel採用中斷向量表(Interrupt Describtor Table)的方式去管理中斷中斷 。對於中斷、陷阱、故障以及終止,有一些是Intel自己在設計CPU架構的時候就能夠預知的,例如在執行除零時就會出現異常,在頁式管理機制就可能出現缺頁異常,有一些是Intel無法預估的,比如一個未來設計的設備產生的中斷。對於前者,Intel稱之爲Intel 64 and IA-32 architectures for architecture-defined exceptions and interrupts,即Intel64和IA-32架構定義的架構相關的異常和中斷,姑且簡稱爲架構相關的中斷和異常。

    爲了區別這些中斷和異常,Intel給出了中斷向量(Interrupt Vector),即使用一個數字代表一個特殊的中斷或者異常類型,Intel規定中斷向量號的範圍是0~255,0~31號爲架構相關的異常和中斷,32~255爲User Defined Interrupt(保護模式):

中斷向量號
助記符
描述
類型
0 #DE Divide Error Fault
1 #DB RESERVED Fault/ Trap
2 NMI Interrupt Interrupt
3 #BP Breakpoint(INT 3) Trap
4 #OF Overflow(INTO 0) Trap
5 #BR BOUND Range Exceeded Fault
6 #UD Invalid Opcode (Undefined Opcode) Fault
7 #NM Device Not Available (No Math Coprocessor) Fault
8 #DF Double Fault Abort
9   Coprocessor Segment Overrun (reserved) Fault
顯示第 1 至 10 項結果,共 23 項

    中斷向量是中斷向量表(IDT)的索引,而中斷向量表存在於內存的某個位置,由Intel的寄存器IDTR負責記錄其基址(線性地址)和大小。IDT表中包含了操作系統中註冊的外部IO中斷的處理程序的入口地址,以及其他操作系統實現的架構相關的中斷和異常的處理函數入口地址(這些地址又存放在所謂的gate destribtor中)。INTR和IDT的關係如下圖所示:

IDTRandIDT

    上圖中的Gate for Interrupt #n實際上分爲三類:task gate,interrupt gate,trap gate,結構如下圖所示:

gate_descriptor

    gate在Linux內核(3.11.1)中數據結構如下(32bits):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct desc_struct {
union {
struct {
unsigned int a;
unsigned int b;
};
struct {
u16 limit0;
u16 base0;
unsigned base1: 8, type: 4, s: 1, dpl: 2, p: 1;
unsigned limit: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8;
};
};
} __attribute__((packed));

64bits的gate describtor:

1
2
3
4
5
6
7
8
9
/* 16byte gate */
struct gate_struct64 {
u16 offset_low;
u16 segment;
unsigned ist : 3, zero0 : 5, type : 5, dpl : 2, p : 1;
u16 offset_middle;
u32 offset_high;
u32 zero1;
} __attribute__((packed));

    IDT中的Gate可以以某種方式獲取到具體的中斷/異常處理函數的入口地址,從而可以執行該中斷/異常處理函數:

interrupt_procedure_call

    GDT和LDT分別爲全局描述符表(Global Destribtor Table)和本地描述附表(Local Destribtor Table),其中存儲的內容爲整個操作系統對各segment的描述,GDT、LDT、segment牽扯到內存管理的機制將在《Linux Kernel內存管理》系列中被具體學習,此處關心gate整個概念。

四、中斷過程

    PIC和APIC是用於解決多設備中斷管理問題的硬件設備,而APIC還可以解決SMP架構中斷管理的問題。PIC通常指兩片8259a級聯,而APIC通常包括兩個部分Local APIC和IO APIC,Local APIC通常集成在CPU內部(Intel),外部與IO APIC相連,內部與CPU管腳LINT0和LINT1相連。

    Intel 64和IA-32架構CPU對外提供中斷向量和中斷描述符表(IDT)的機制來處理中斷和異常。中斷按照下列邏輯觸發並被執行(以鍵盤爲例):

1、用戶按下鍵盤按鍵;

2、電平信號變化通知中斷控制器發生了一次中斷;

3、中斷控制器通知CPU此次中斷的中斷向量號;

4、CPU判定是否要處理此次中斷,如果要處理,轉5,否則退出;

5、從IDTR寄存器讀取IDT的基址+中斷向量號,找到對應的中斷處理函數入口地址;

6、CPU按照某種軟件策略執行該中斷處理函數;

五、中斷硬件處理細節(IA32/Intel 64)

對於第四節中斷過程的步驟4、步驟5的一些細節,描述如下(摘自ULK):

當執行了一條指令後,cs和eip這對寄存器包含了下一條要執行的指令的邏輯地址。在處理那條指令之前,控制單元會檢查在運行前一條指令時是否存在已經發生了一箇中斷或者異常。如果發生了一箇中斷或一場,那麼控制單元執行下列操作:

1、確定與中斷或異常關聯的向量i(i的範圍爲0~255)。

2、讀由idtr寄存器指向的IDT表中的第i項(在下面的描述中,我們假定IDT表項中包含的是一箇中斷門或一個陷阱門)。

3、從gdtr寄存器獲得GDT的基地址,並在GDT中查找,以讀取IDT表項中的選擇符標識的段描述符。這個描述符指定中斷或異常處理程序所在段的基地址。

4、確信中斷是授權的(中斷)發生源發出的。首先將當前特權級CPL(存放在cs寄存器的低兩位)與段描述符(存放在GDT中)的描述符特權級DPL比較,如果CPL小於DPL,就產生一個“General Protection”異常,因爲中斷處理程序的特權不能低於引起中斷的程序的特權。對於編程異常,則做進一步的安全檢查:比較CPL與處於IDT中的門描述符的DPL,如果DPL小於CPL,就產生一個“General Protection”異常。這最後一個檢查可以避免用戶程序訪問特殊的陷阱門或中斷門。

5、檢查是否發生了特權級的變化,也就是說,CPL是否不同於所選擇的段描述符的DPL。如果是,控制單元必須開始使用與新的特權級相關的棧。通過執行以下步驟來做到這點:

  • a.讀tr寄存器,以訪問運行進程的TSS段。
  • b.用與新特權級相關的棧段和桟指針的正確值裝載ss和esp寄存器。這些值可以在TSS中找到。
  • c.在新的棧中保存ss和esp以前的值,這些值定義了與舊特權級相關的棧的邏輯地址。

6、如果發生的是“故障(Fault)”,用引起異常的指令地址裝載cs和eip寄存器,從而使得這條指令能再次被執行。

7、在棧中保存eflag、cs及eip的內容。

8、如果硬件產生了一個錯誤碼,則將它保存在棧中。

9、裝載cs和eip寄存器,其值分別是IDT表中的第i項門描述符的段選擇符和偏移量字段。這些值給出了中斷或者異常處理程序的第一條指令的邏輯地址。

控制單元所執行的最後一步就是跳轉到中斷或異常處理程序。還句話說,處理完中斷信號後,控制單元所執行的指令就是被選中處理程序的第一條指令。

中斷或異常被處理後,相應的處理程序必須產生一條iret指令,把控制權轉交給被中斷的進程,這將迫使控制單元:

1、用保存的棧中的值狀態cs、eip或eflag寄存器。如果一個硬件出錯碼曾被壓入棧中,並且在eip內容的上面,那麼,執行iret指令前必須先彈出這個硬件出錯碼。

2、檢查處理程序的CPL是否等於cs中的最低兩位的值(這意味着被中斷的進程與處理程序運行在同一特權級)。如果是,iret終止執行;否則,轉入下一步。

3、從棧中裝載ss和esp寄存器,因此,返回到與舊特權級相關的棧。

4、檢查ds、es、fs以及gs寄存器的內容,如果其中一個寄存器包含的選擇符是一個段描述符,並且DPL值小於CPL,那麼,清相應的段寄存器。控制單元這麼做是爲了禁止用戶態的程序(CPL=3)利用內核以前所用的段寄存器(DPL=0)。如果不清這個寄存器,懷有惡意的用戶態程序就能利用他們來訪問內核地址空間。

存疑

Q1:對門(gate)的概念一直比較含糊,computer sicence + gate,讓人很容易直接反應到門電路——實現與、或、非等邏輯功能的電路,顯然是個硬件。但是在Intel 64和IA-32手冊中,門是這樣被描述的:

The architecture also defines a set of special descriptors called gates (call gates, interrupt gates, trap gates, and task gates). These provide protected gateways to system procedures and handlers that may operate at a different privilege level than application programs and most procedures. For example, a CALL to a call gate can provide access to a procedure in a code segment that is at the same or a numerically lower privilege level ( more privileged) than the current code segment. To access a procedure through a call gate, the calling procedure1 supplies the selector for the call gate. The processor then performs an access rights check on the call gate, comparing the CPL with the privilege level of the call gate and the destination code segment pointed to by the call gate.

架構同時定義了一些列的被稱作門的描述符(call門、interrupt門、trap門、task門)。這些描述符向系統程序和處理函數提供了保護性的網關,這可以讓他們運行在與大多數應用程序不同的權限級別上。例如,通過call gate的一次調用,能夠向代碼段程序同時或週期性的提供比當前的代碼段更低權限數值(更多的權限)。要通過call gate調用一個程序,調用者需要提供當前call gate的selector,接着,處理器就提供了再當前call gate上校驗過的權限,通過比較當前call gate的CPL的權限級別和通過當前call gate指向目標代碼段的權限。

那麼,在Intel架構中,門是一個虛擬的概念,而不是一個硬件,是一個描述符,是Intel對外提供的用於某些特殊權限需要時的程序調用,系統程序向CPU提供門描述符(包含程序代碼入口),而CPU根據權限判定是否允許執行。之所以分爲這麼多門,是因爲權限的不同。不知道理解的對不對哈。

Q2:關於棧的切換的疑問。引發中斷的程序的優先級不能高於中斷處理程序的優先級,否則引發異常,這好理解,但是一旦優先級發生變化時,進行堆棧的切換過程,有些迷惑,主要是對棧的理解:我理解棧是一段連續的內存空間,方便從C程序轉換爲CPU指令的執行(主要是函數嵌套調用,過程調用),由於運行程序的權限不同,分爲內核棧和用戶棧,每個進程都有自己的棧,但中斷處理函數並沒有自己的棧,而是與進程的內核棧共享。中斷處理函數在內核啓動後,即被裝載在屬於內核段的某處內存中,要被壓入具體的進程的內核棧?cs和eip被設置後,壓棧是自動的?慢不慢?類似與call?普通函數調用libc影射的方式?混亂..

參考資料:

1、Linux 內核中斷內幕

2、I/O Controller Hub——wiki

3、Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3

4、Intel® 82093AA I/O Advanced Programmable Interrupt Controller (I/O APIC) Datasheet

5、Intel x86架構之APIC

6、Understanding the Linux Kernel.

Published in 中斷內核 and tagged 8259aAPICgate,門IDT on 2013年9月30日 by rock3

Linux Kernel中斷機制2——中斷分類

沒有評論

《Linux Kernel中斷機制1——中斷概念》一節中描述了中斷的基本概念,其中professional linux kernel architecture中提到中斷可以有異常(exception)和錯誤(error)產生,本節研究中斷的分類和硬件相關的部分。本節主要來源於Understanding the Linux Kernel 3rd.

一、同步中斷和異步中斷

同步中斷和異步中斷的概念經常被提到(understanding the linux kernel 3rd):

同步中斷是當指令執行時由 CPU 控制單元產生,之所以稱爲同步,是因爲只有在一條指令執行完畢後 CPU 纔會發出中斷,而不是發生在代碼指令執行期間,比如系統調用。
異步中斷是指由其他硬件設備依照 CPU 時鐘信號隨機產生,即意味着中斷能夠在指令之間發生,例如鍵盤中斷。

Intel微處理器手冊稱同步中斷爲“異常(Exception)”,稱異步中斷爲“中斷(Interrupt)”,而平時所說的中斷,兩者都包含。

二、中斷和異常

Intel手冊中Interrupt(實際上就是我們平時說的異步中斷)又可以分爲可屏蔽中斷(maskable interrupt)和不可屏蔽中斷(unmaskable interrupt)。可屏蔽中斷通常由外部IO設備發出,處理器根據該中斷是否設置屏蔽位。中斷的執行邏輯如下圖所示:

interrupt

可屏蔽中斷(Maskable interrupts)

所有的I/O設備產生的中斷請求都是可屏蔽中斷,可屏蔽中斷有兩種狀態,masked和unmasked;只要一箇中斷還處於maked狀態,控制單元將忽略它發出的中斷請求。

不可屏蔽中斷(Nonmaskable interrupts)

只有極少數的關鍵事件,像硬件錯誤等,會發出不可屏蔽中斷,不可屏蔽總是被CPU識別

而異常又分爲故障(fault)、陷阱(trap)和終止(abort)三類:

處理器偵測異常(Processor-detected exceptions),當CPU正在執行某條指令時偵測到異常狀況時,產生的異常,這些異常又根據EIP的值分爲3類,CPU的控制單元產生異常時EIP的值被保存在內核棧中

  • 故障(Faults)

能被糾正,一旦發生,程序允許在不影響連貫性的情況下重啓.EIP中保存的值是引起故障的指令,因此當異常處理程序終止時,該指令可以被重新執行。fault

  • 陷阱(Traps)

在陷阱指令執行後立即報告;在內核把控制權返回給程序後,程序還可以不影響連貫性的繼續執行。EIP中保存的值是陷阱指令後應該執行的指令的地址。只有在沒有必要重複執行已經終止的指令時,纔出發陷阱。陷阱的主要功能是調試,在這種情況下,中斷信號的作用是通知debugger一條特殊的指令已經被執行(例如,到斷點位置了)。一旦用戶檢查過debugger提供的數據,她很可能會繼續執行程序的下一條指令。traps

  • 終止(Aborts)

嚴重錯誤發生了,導致控制單元無法將引起異常的指令地址存入EIP,Abort就是被用於報告這些錯誤的,例如硬件故障和系統表中非法或不一致的值。控制單元發送的中斷信號是緊急信號,用來把控制權切換到相應的異常終止處理程序,該異常終止處理程序除了強制受影響的進程終止,別無他法abort

編程異常(Programmed exceptions),由編程者請求發出,通常使用int或者int0/int3指令;into(檢查溢出)和bound(檢查地址邊界)在他們檢測的結果不爲真的時候也會引發編程異常。編程異常被控制單元當作陷阱處理,它們經常稱作software interrupts。這類異常通常有兩類用處:實現系統調用和通知debugger一個特殊的時間發生。

三、總結

中斷,Interrupt在x86架構下,根據發生中斷的時刻處理器上是否執行完畢當前指令,分爲同步中斷(synchronous interrupt)和異步中斷(asynchronous interrupt),Intel通常將同步中斷成爲異常(Exception),而將異步中斷稱爲中斷(Interrupt)。同步中斷多爲CPU上正在執行的指令產生直接產生,而異步中斷來自於I/O設備。如下圖所示:interrupt

Intel的“異常”(Exception)又根據嚴重程度和EIP存儲哪條指令分爲三類:故障(Fault)、陷阱(Trap)和終止(Abort),而這三類異常的處理方法不同:

類別
原因
同步/異步
返回的行爲
中斷(Interrupt) 來自I/O設備的電信號 異步 總是返回到下一條指令
故障(Fault) 潛在可恢復的錯誤 同步 返回到當前指令
陷阱(Trap) 有意的異常 同步 總是返回到下一條指令
終止(Abort) 不可恢復的錯誤 同步 不會返回
顯示第 1 至 4 項結果,共 4 項

四、遺留問題

Q1:異步中斷分爲可屏蔽中斷和非可屏蔽中斷,這種分發來源於Intel 64和IA32技術手冊,也就是說Intel相關架構的CPU在處理外圍硬件設備中斷的時候,這樣區分,並且有硬件上的辦法做到了這一點(猜測有相關的CPU的PIN角負責相關的中斷)。但是,這僅僅是Intel或者x86架構下對常規外設中斷的處理方式,還是計算機系統常用的架構方法那,暫時不得而知(估計是)。

Q2:這裏牽扯到一個問題:Only a few critical events (such as hardware failures) give rise to nonmaskable interrupts. 這裏面give raise to是產生的意思,也就是說非屏蔽中斷事由一些關鍵事件產生的(這裏沒說誰產生的這些關鍵事件,應該也是外圍硬件產生的,比如總線錯誤),這就是說有個機制問題,誰決定某中斷是非可屏蔽中斷或者可屏蔽中斷,是CPU和中斷控制器以及外設的佈線、內核設置或者外圍設備自己?

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