歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux基礎 >> 關於Linux

讀薄《Linux 內核設計與實現》(4) - 中斷與同步

0x00 中斷和中斷處理程序

I 中斷

中斷是一種特殊的電信號,由硬件發向處理器,處理器接收到中斷時,會馬上箱操作系統反映,由操作系統進行處理。中斷隨時可以產生,因此,內核隨時可能因為新到來的中斷而被打斷。

不同的設備對應的中斷不同,每個中斷通過一個唯一的數字標識,這些中斷值通常被稱為中斷請求(IRQ)線。

II 中斷處理程序

中斷處理程序又成為中斷處理例程(ISR),是內核在響應中斷時執行的一個函數

一個中斷處理程序對應一個中斷,一個設備可能發出多種中斷

對於外部設備,中斷處理程序是設備驅動程序的一部分

在 Linux 中,中斷處理程序和 C 函數區別不大,但有自己的規范,主要是運行時需要在中斷上下文中

0x01 中斷處理機制

I 注冊中斷處理程序

驅動程序可以通過request_irq()函數注冊一個中斷處理程序(linux/interrupt.h)

int request_irq(unsigned int irq,
                irqhandler_t handler,
                unsigined long falgs,
                const char *name,
                void *dev)

irq:表示要分配的中斷號

handler:一個指針,指向處理這個中斷的實際中斷處理函數

typedef irqhandler_t(*irq_handler_t)(int, void*);

II 釋放中斷處理程序

卸載驅動程序時,需要注銷響應中斷處理程序,並釋放中斷線。

void free_irq(unsigned int irq, void *dev);

如果指定的中斷線不是共享的,那麼該函數刪除處理程序的同時將禁用這條中斷線;中斷線是共享的,則僅僅刪除 dev 對應的處理程序,而這條中斷線本身只有在刪除了最後一個處理程序時才會被禁用

III 中斷的禁止與激活

local_irq_disable();
local_irq_enable();

IV 上半部與下半部

又想中斷處理程序運行的快,又想中斷處理程序完成的工作多,這兩個目的顯然有所抵觸,所以把中斷處理分為兩個部分:

中斷處理程序是上半部,接收到一個中斷,它就立刻開始執行,但只做有嚴格時限的工作,例如一些只有在中斷被禁止的狀態下才能完成的工作

能夠被允許稍後完成的工作會推遲2到下半部去,此後,在合適的時機,下半部會被開中斷執行

Q1:為什麼要分上半部和下半部? {% endcq %}

中斷程序以異步方式執行,可能打斷重要操作的執行,越快越好中斷處理程序會屏蔽其他同級中斷,所以執行越快越好中斷處理程序往往需要對硬件操作,通常有很高的時限要求中斷處理程序不在進程的上下文中運行,所以不能阻塞

Q2:上半部和下半部如何分開? {% endcq %}

如果一個任務對時間非常敏感,將其放到上半部;如果一個任務和硬件相關,將其放到上半部;如果一個任務要保證不被其它中斷打斷,將其放到上半部;其他的所有任務考慮放到下半部

0x02 下半部

下半部的任務就是執行與終端處理密切相關但中斷處理程序本身不執行的工作。我們期望中斷處理程序將盡量多的工作放到下半部執行,以快速從中斷返回。

I 下半部實現機制

a.軟中斷

此處的軟中斷和系統調用使用的 int 80H 不同,是操作系統支持的一種,在編譯期間靜態分配

軟中斷的實現

定義於 linux/interrupt.h 中:
struct softirq_action{
    void (*action)(struct sfotirq_action*); //待執行的函數
    void *data; //傳遞的參數
}
最多可能32個軟中斷,定義於 kernel/softirq.c
static struct softirq_action softirq_vec[NR_SOFTIRQS];

軟中斷處理程序

void softirq_handler(struct softirq_action*); //傳遞整個結構體

執行軟中斷

一個注冊的軟中斷必須在被標記後才會執行下列地方,待處理的軟中斷會被檢查和執行:

在 ksoftirqd 內核線程中在那些顯式檢查和和執行待處理的軟中斷的代碼中(如網絡子系統)

不管執行的時機,軟中斷都要在do_softirq 中執行

使用軟中斷

分配索引: 通過 linux/interrupt.h 中的一個枚舉類型中聲明一個新的軟中斷

注冊處理程序:在運行時通過調用open_softirq()注冊軟中斷處理程序,有兩個參數,軟中斷和處理函數

觸發軟中斷:raise_softirq()函數可以將一個軟中斷設置為掛起狀態,讓它在下次調用do_softirq()函數時投入運行

b.tasklet

基於軟中斷的實現,但它的接口更簡單,鎖保護要求更低

tasklet 的實現

tasklet 結構體(linux/interrupt.h)
struct tasklet_struct{
    struct tasklet_struct *next;    //鏈表
    unsigned long state;    //tasklet 狀態
    atomic_t count; //引用計數器
    void (*funx)(unsigned long);    //taklet 處理函數
    unsigned long data; //給處理器函數傳遞的參數
}

調度 tasklet

被觸發的軟中斷存放在2個數據結構:tasklet_vec,task_hi_vec,這兩個數據結構都是由task_struct構成的鏈表,由tasklet_schedule()task_hi_schedule()進行調度,調度步驟如下:

(1)檢查 tasklet 狀態,如果為TASK_STATE_SCHED則返回
(2)調用_tasklet_schedule()
(3)保存中斷狀態,禁止本地中斷
(4)把需要調度的 tasklet 加到 tasklet_vectasklet_hi_vec鏈表頭
(5)喚起TASKLET_SOFTIRQHI_SOFTIRQ軟中斷,下一次調用do_softirq()時會執行該 tasklet
(6)恢復中斷

軟中斷處理程序:tasklet_action()task_hi_action()[tasklet 處理的核心]:
(1)禁止中斷(2)將當前處理器上的該鏈表頭設為 NULL (3)允許中斷(4)循環遍歷鏈表上所有待處理的 tasklet (5)如果是多個處理器,檢查TASKLET_STATE_RUN判斷這個 tasklet 是否在其他處理器上運行,如果是,跳到笑一個 tasklet (6)如果否,設置TASKLET_STATE_RUN (7)檢查 count 是否為0,確保 tasklet 沒有被禁止;如果被禁止,跳到下一個 tasklet (8)執行 tasklet 處理程序(9)執行完畢,清除TASKLET_STATE_RUN (10)重復執行下一個 tasklet

使用 tasklet

聲明自己的 tasklet:
靜態:linux/interrupt.h 中的2個宏:
DECLARE_TASKLET(name,func,data);
DECLARE_TASKLET_DIASBLED(name,func,data);
動態:通過一個指針賦給一個動態創建的 tasklet_struct:
tasklet_init(t, takslet_handler, dev);
編寫自己的 tasklet 處理程序
void tasklet_handler(unsigned long data)

注意:不能再 tasklet 中使用信號量或者其他阻塞式函數

調度自己的 tasklet
tasklet_schedule(&my_tasklet);
tasklet_enable(&my_tasklet);
tasklet_disable(&my_tasklet);
ksoftirqd

ksoftirqd 是內核線程,每個處理器都有一個,用於在空閒處理器處理軟中斷

for(;;){
    if(!softirq_pending(cpu))
        schedule();
    set_current_state(TASK_RUNNING);
    while(softirq_pending(cpu)){
        do_softirq();
        if(need_resched())
        schedule();
    }
    set_current_sdtate(TASK_INTERRUPTIBLE);
}

只要有待處理的軟中斷,該線程就會處理

c.工作隊列

工作隊列機制將下半部功能交給內核縣城去執行,有線程上下文,可以睡眠

工作隊列的實現

提供創建工作者線程的接口提供默認的工作者線程處理排到隊列裡的下半部工作提供吧需要推後執行的任務排到隊列裡的接口

處理機制

線程將自己休眠,並加到等待隊列(TASK_INTERRUPTIBLE)如果工作鏈表為空,線程調用schedule(),休眠如果不為空,將自己設為TASK_RUNNING 調用run_workqueue()執行被推後的工作

該函數循環遍歷鏈表上每個待處理的工作:

當鏈表非空,選取下一個節點對象獲取要執行的函數和參數從鏈表上解下該節點,將 pending 位清零調用函數重復執行

工作隊列的使用

創建推後的工作:

靜態:

DECLARE_WORK(name, void(*func)(void*), void *data);

動態:

INIT_WORK(struct work_struct *work, void(*func)(void*), void *data);
工作隊列處理函數
void work_handler(void *data)
對工作的調度
schedule_work(&work);
schedule_delayed_work(&work, delay);
刷新操作
void flush_scheduled_work(void);

3種下半部機制的比較

機制 上下文 順序執行保障 軟中斷 中斷 沒有 tasklet 中斷 同類型不能同時執行 工作隊列 進程 沒有(和進程上下文一樣被調度)

Q:我們要選擇哪種機制? {% endcq %}

如果有休眠的要求,選擇工作隊列;否則,最好使用 tasklet;要是必須專注性能的提高,選擇軟中斷

II 在下半部之間加鎖

如果進程上下文和一個下半部共享數據,在訪問這些數據之前,你需要禁止下半部的處理並得到鎖的使用權

如果中斷上下文和一個下半部共享數據,在訪問數據之前,需要禁止中斷並得到鎖的使用權

0x03 內核同步

I 臨界區

臨界區就是訪問和操作共享資源的代碼段,必須保證原子地執行才能保證安全

II 加鎖

保證在臨界區執行的縣城只有一個

III 造成並發的原因

中斷

軟中斷和 tasklet

內核搶占

睡眠及用戶空間的同步

對稱多處理

IV 死鎖產生條件

要有一個或多個執行線程和一個或多個資源

每一個線程都在等待其中一個資源

所有的資源都被占用

所有縣城都在互相等待,但他們永遠不會釋放已經占有的資源

V 內核同步方法

原子操作

原子整數操作(asm/atomic.h)
atomic_dec_and_test(atmoic_t, *v)
原子位操作(asm/bitops.h)
set_bit(0, &word)

自旋鎖

自旋鎖只能被一個可執行進程持有

若爭用一個被占用的鎖則進程忙等(旋轉)

自旋鎖不能長期被占用,否則效率低

Copyright © Linux教程網 All Rights Reserved