1. 程式人生 > >linux核心設計與實現 —— 中斷和中斷處理(第7章,第8章)

linux核心設計與實現 —— 中斷和中斷處理(第7章,第8章)

中斷和中斷處理

中斷的目的:讓處理器最快地響應外部硬體的請求。

中斷本質上是一種特殊的電訊號,由硬體裝置發向處理器,處理器反映到作業系統中,最後由作業系統處理這個中斷電訊號。

不同的裝置對應的中斷不同。每個中斷都通過一個唯一的數字標記,這個標記通常被稱為中斷請求(IRQ)線。

每個中斷都有一箇中斷處理程式,執行在中斷上下文中。(中斷上下文與程序上下文的區別在於:中斷上下文中的執行程式碼不可阻塞)。

核心必須保證中斷處理程式能夠快速響應,並且在儘可能短的時間內完成執行。

為了在大量的工作與快速執行之間求得一種平衡,核心把處理中斷的工作分為兩部分,上半部和下半部。上半部,一般稱為中斷處理程式,是在接收到一箇中斷後,就立即開始執行,並且禁止一些或全部中斷。下半部,有軟中斷,tasklet和工作佇列,就是執行與中斷處理密切相關但中斷處理程式本身不執行的工作,執行期間可以響應所有的中斷。

上半部 —— 中斷處理程式

1. 中斷處理

在響應一個特定中斷的時候,核心會執行一個函式,該函式叫做中斷處理程式(interrupt handler)或中斷服務例程(interrupt service routine,ISR)。

中斷處理函式

/**
 * irq:處理程式要響應的中斷號
 * dev_id:由註冊中斷處理程式request_irq()的引數dev_id傳遞,常用來區分共享同一中斷處理程式的多個裝置
 */
static irqreturn_t intr_handler(int irq, void *dev_id)

註冊中斷處理函式

/**
 * irq:申請的硬體中斷號
 * handler:向系統註冊的中斷處理函式,當中斷髮生時會觸發該函式,dev_id引數將被傳遞給它
 * irqflags: 中斷處理標誌,上升沿觸發,下降沿觸發等。。。。
 * devname:設定中斷名稱,通常是裝置驅動程式的名稱
 * dev_id:在中斷共享時會用到,一般設定為這個裝置的裝置結構體或者NULL
 */
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long irqflags, const char *devname, void *dev_id)

釋放中斷處理函式

/**
 * irq:申請的硬體中斷號
 * handler:向系統註冊的中斷處理函式,當中斷髮生時會觸發該函式,dev_id引數將被傳遞給它
 * irqflags: 中斷處理標誌,上升沿觸發,下降沿觸發等。。。。
 * devname:設定中斷名稱,通常是裝置驅動程式的名稱
 * dev_id:在中斷共享時會用到,一般設定為這個裝置的裝置結構體或者NULL
 */
void free_irq(unsigned int irq,void *dev_id)

2. 中斷控制

Linux核心提供了一組介面用於操作機器上的中斷狀態。這些介面能夠讓我們禁止當前處理器上的所有中斷,或者遮蔽掉其中的一條中斷線。
一般來說,控制中斷系統的最根本的原因是需要提供同步。通過禁止中斷,可以確保某個中斷處理程式不會搶佔當前的程式碼。此外,禁止中斷還可以禁止核心搶佔。

禁止當前處理器上的中斷,巨集定義

local_irq_disable()

啟用當前處理器上的中斷,巨集定義

local_irq_disable()

禁止當前處理器上的中斷,在禁止前儲存中斷系統的狀態,巨集定義

local_irq_save(flags)

中斷被恢復到給定的狀態,巨集定義

local_irq_save(flags)

禁止指定的中斷線,函式

/**
 * 禁止給定中斷線,並確保該函式返回之前在該中斷線上沒有處理程式在執行
 */
void disable_irq(unsigned int irq);

/**
 * 禁止給定中斷線
 */
void disable_irq_nosync(unsigned int irq);

/**
 * 啟用給定中斷線
 */
void enable_irq(unsigned int irq);

/**
 * 等待一個特定的中斷程式的退出
 */
void synchronize_irq(unsigned int irq);

瞭解中斷系統的狀態,巨集定義

/**
 * 如果本地中斷傳遞被禁止,則返回非0,否則返回0
 */
irqs_disabled()

/**
 * 如果在中斷上下文中,則返回非0;如果在程序上下文中,則返回0
 */
in_interrupt()

/**
 * 如果當前正在執行中斷處理程式,則返回非0,否則返回0
 */
in_irq()

下半部 —— 推後的執行的工作

中斷處理程式的上半部在接收到一箇中斷時就立即執行,但只做比較緊急的工作,這些工作都是在所有中斷被禁止的情況下完成的,所以要快,否則其它的中斷就得不到及時的處理。那些耗時又不緊急的工作被推遲到下半部去。

中斷處理程式的下半部分(如果有的話)幾乎做了中斷處理程式所有的事情。它們最大的不同是上半部分不可中斷,而下半部分可中斷

在理想的情況下,最好是中斷處理程式上半部分將所有工作都交給下半部分執行,這樣的話在中斷處理程式上半部分中完成的工作就很少,也就能儘可能快地返回。但是,中斷處理程式上半部分一定要完成一些工作,例如,通過操作硬體對中斷的到達進行確認,還有一些從硬體拷貝資料等對時間比較敏感的工作。剩下的其他工作都可由下半部分執行。

通常下半部在中斷處理程式一返回就會馬上執行。下半部執行的關鍵在於當他們執行的時候,允許響應所有的中斷。
對於上半部分和下半部分之間的劃分沒有嚴格的規則,靠驅動程式開發人員自己的程式設計習慣來劃分,不過還是有一些習慣供參考:

  • 如果該任務對時間比較敏感,將其放在上半部中執行
  • 如果該任務和硬體相關,一般放在上半部中執行
  • 如果該任務要保證不被其他中斷打斷,放在上半部中執行(因為這是系統關中斷)
  • 其他不太緊急的任務, 一般考慮在下半部執行

Linux核心實現下半部的機制主要有軟中斷tasklet工作佇列,下面對他們的實現做簡單介紹。

1. 軟中斷

軟中斷處理程式

軟中斷是一組靜態定義的下半部介面,有32個,可以在所有處理器上同時執行,即使兩個型別相同也可以。
軟中斷保留給系統中對時間要求最嚴格以及最重要的下半部使用。目前,只有兩個子系統(網路和SCSI)直接使用軟中斷。此外,核心定時器和tasklet都是建立在軟中斷上的。
軟中斷由softirq_struct結構表示,它定義在 linux/interrupt.h 中:

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

在 kernel/softirq.c 中定義了一個包含有32個該結構體的陣列:

static struct softirq_action softirq_vec[NR_SOFTIRQS];

每個被註冊的軟中斷都佔據該陣列的一項,因此最多可能有32個軟中斷。
軟中斷處理程式action的函式原型如下:

void softirq_handler(struct softirq_action *)

新增軟中斷

(1) 分配索引
在編譯期間,通過在 linux/interrupt.h 中定義的一個列舉型別來靜態地宣告軟中斷。建立一個新的軟中斷必須在此列舉型別中加入新的項。

(2) 註冊中斷處理程式
接著,在執行時通過呼叫open_softirq()註冊軟中斷處理程式。

/**
 * nr:軟中斷的索引號
 * action:處理函式
 */
void open_softirq(int nr, void (*action)(struct softirq_action *));

(3) 觸發軟中斷
raise_softirq()函式可以將一個軟中斷設定為掛起狀態,讓它在下次呼叫do_softirq()函式時投入執行。

執行軟中斷

一個註冊的軟中斷必須在被標記後才會執行,這就是觸發軟中斷(raising the softirq)。中斷處理程式繪製返回前標記它的軟中斷,使其稍後被執行。在下面情況中,待處理的軟中斷會被檢查和執行:

  • 從一個硬體中斷程式碼處返回時
  • 在ksoftirqd核心執行緒中
  • 在那些顯式檢查和執行待處理的軟中斷的程式碼中,如網路子系統

不管是用什麼辦法喚起,軟中斷都要在do_softirq()中執行,該函式很簡單,如果有待處理的軟中斷,do_softirq()會迴圈遍歷每一個,呼叫它們的處理程式。以下是do_softirq()函式經過簡化後的核心部分:

u32 pending;

pending = local_softirq_pending();
if (pending) {
    struct softirq_action *h;
    set_softirq_pending(0);
    h = softirq_vec;
    do {
        if (pending & 1)
            h->action(h);
        h++;
        pending >>= 1;
    } while (pending);
}

2. tasklet

tasklet是通過軟中斷實現的,所以它們本身也是軟中斷。tasklet由兩類軟中斷代表:HI_SOFTIRQ和TASKLET_SOFTIRQ,這兩者之間唯一的實際區別在於,HI_SOFTIRQ型別的軟中斷先於TASKLET_SOFTIRQ型別的軟中斷執行。
tasklet由tasklet_struct結構表示,每個結構體單獨代表一個tasklet,它在 linux/interrupt.h 中定義:

struct tasklet_struct {
    struct tasklet_struct *next;  /*連結串列中的下一個tasklet */
    unsigned long state;          /* tasklet的狀態,有三種:0, TASKLET_STATE_SCHED, TASKLET_STATE_RUN */
    atomic_t count;               /* 引用計數器,為0時,tasklet才被啟用 */
    unsigned long data;           /* 給tasklet處理函式的引數 */  
}

排程tasklet

已排程的tasklet存放在兩個單處理器資料結構:tasklet_vec(普通tasklet)和tasklet_hi_vec(高優先順序的tasklet)。這兩個資料結構都是由tasklet_struct結構體構成的連結串列,連結串列中的每個tasklet_struct代表一個不同的tasklet。
tasklet由tasklet_schedule()和tasklet_hi_schedule函式進行排程。
所有的tasklet都通過重複運用HI_SOFTIRQ和TASKLET_SOFTIRQ這兩個軟中斷實現。當一個tasklet被排程時,核心就會喚起這兩個軟中斷函式中的一個。隨後,do_softirq()被執行,呼叫函式tasklet_action()或者tasklet_hi_action()處理tasklet。

使用tasklet

(1) 建立tasklet
可以靜態建立,使用巨集DECLARE_TASKLET或者DECLARE_TASKLET_DISABLED

#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }

#define DECLARE_TASKLET_DISABLED(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }

也可以直接使用結構體struct tasklet_struct動態建立,然後使用函式tasklet_init初始化:

/**
 * t:tasklet_struct結構體
 * func:tasklet處理函式
 * data: tasklet處理函式的傳遞引數
 */
void tasklet_init(struct tasklet_struct *t,
             void (*func)(unsigned long), unsigned long data)

(2) tasklet處理程式
tasklet處理函式必須符合規定的函式型別,如下:

void tasklet_handler(unsigned long data)

(3) 排程tasklet
通過呼叫tasklet_schedule()函式並傳遞給它相應的tasklet_struct的指標,該tasklet就會被排程以便執行:

tasklet_schedule(&my_tasklet);  /* 把my_tasklet標記為掛起 */

3. 中斷處理

對於軟中斷(和tasklet),核心會在幾個特殊的時機執行(注意執行和排程的區別,排程軟中斷只是對軟中斷打上待執行的標記,並沒有真正執行),而在中斷處理程式返回時處理是最常見的。軟中斷的觸發頻率有時可能會很高(例如進行大流量網路通訊期間)。更不利的是,軟中斷的執行函式有時還會排程自身,所以如果軟中斷本身出現的頻率較高,再加上他們又有將自己重新設定為可執行狀態的能力,那麼就會導致使用者空間的程序無法獲得足夠的處理時間,因而處於飢餓狀態。

為了避免使用者程序的飢餓。核心開發者做了一些折中,最終在核心的實現方案中是不會立即處理由軟中斷自身重新觸發的軟中斷(不允許軟中斷巢狀)。而作為改進,核心會喚醒一組核心執行緒來處理這些過多的軟中斷,這些核心執行緒在最低優先順序上執行(nice值是19),這能避免它們跟其他重要的任務搶奪資源,但它們最終肯定會被執行,所以這個方案能夠保證軟中斷負載很重的時候,使用者程序不會因為得不到處理時間而處於飢餓狀態,相應的,也能保證過量的軟中斷終究會得到處理。

每個處理器都有一個這樣的執行緒,來輔助處理軟中斷。所有執行緒的名字都叫做ksoftirqd/n,區別在於n,它對應的是處理器的編號。執行緒執行程式碼類似下面:

for (;;) {
    if (!softirq_pending(cpu))
        schedule();

    set_current_state(TASK_RUNNING);

    while (softirq_pending(cpu)) {
        do_softirq();
        if (need_resched())
            schedule();
    }

    set_current_state(TASK_INTERRUPTIBLE);
}