一步步理解Linux之中斷和異常
作者: gaopenghigh ?,轉載請注明出處。? (原文地址)
中斷和異常的概念
*? 中斷 : 硬件通過中斷來通知內核。中斷是一種電信號,由硬件設備生成,并送入中斷控制器 的輸入引腳中,中斷控制器會給CPU發送一個電信號,CPU檢測到這個信號,就中斷當 前的工作轉而處理中斷。每個中斷都通過一個唯一的數字標志。這些中斷值稱為? 中斷請求(IRQ,Interrupt ReQuest)線 。
*? 異常 : 當CPU執行到由于編程失誤而導致的錯誤指令(比如被0除)的時候,或者在執行期間 出現踢輸情況(如缺葉)而必須靠內核來處理的時候,處理器就產生一個異常。異常 和中斷類似,所以異常也叫“同步中斷(asynchronous interrupt)”。內核對異常的處 理大部分和對中斷的處理一樣。
中斷描述符表
中斷描述符表(Interrupt Descriptor Table, IDT)是一個系統表,它與每一個中斷或異 常向量相聯系,每一個向量在表中有相應的中斷或異常處理程序的入口地址。IDT的地址存 放在
idtr
寄存器中。中斷發生時,內核就從IDT中查詢相應中斷的處理信息。
異常處理
異常處理一般由三個部分組成:
1. 在內核堆棧中保存大多數寄存器的內容(匯編)。
- 用高級的C函數處理異常。
-
通過
ret_from_exception()
函數從異常處理程序退出。
中斷處理
中斷處理一般由四個步驟組成:
- 在內核態堆棧中保存IRQ的值和寄存器的內容。
- 為正在給IRQ線服務的PIC發送一個應答,這將允許PIC進一步發出中斷。
- 執行共享這個IRQ的所有設備的中斷服務例程(ISR)。
-
跳到
ret_from_intr()
的地址。
中斷處理的示意圖如下:
中斷處理程序
在相應一個特定中斷時,內核會執行一個函數,這個函數就叫做? 中斷處理程序(interrupt handler) ,或者叫做? 中斷服務例程(interrupt service routine,ISR) 。
中斷處理程序運行在中斷上下文中,該上下文中的代碼不可以阻塞。要注意,中斷處理程 序執行的代碼不是一個進程,中斷處理程序比一個進程要“輕”。
每個中斷和異常都會引起一個內核控制路徑,而內核控制路徑是可以任意嵌套的。也就是 說,一個中斷處理程序可以被另一個中斷處理程序“中斷”。為了允許這樣的嵌套,中斷處 理程序就必須永不阻塞,換句話說,進程被中斷,在中斷程序運行期間,不能發生進程切 換。這是因為,一個中斷產生時,內核會把當前寄存器的內容保存在內核態堆棧中,這個 內核態堆棧屬于當前進程,嵌套中斷時,上一個中斷執行程序產生的寄存器內容同樣也會 保存在該內核態堆棧,然后從嵌套的下一個中斷恢復時,又從內核態堆棧中取出來放進寄 存器中。
一個內核控制路徑嵌套執行的示例圖如下:
Linux中中斷處理程序是無須重入的。當一條中斷線上的handler正在執行時,這條中斷線 在所有處理器上都會被屏蔽掉。
在/proc/interrupts中可以查看當前系統的中斷統計信息。
IRQ數據結構
每個IRQ都有自己的描述符
irq_desc_t
,描述符中有字段指向PIC對象,有字段指向ISR的 鏈表(因為每個IRQ線上可以注冊多個中斷處理程序)。所有的
irq_desc_t
合起來組成?
irq_desc
數組。示例圖如下:
上半部和下半部的概念
有時候中斷處理需要做的工作很多,而中斷處理程序的性質要求它必須在盡量短的時 間內處理完畢,所以中斷處理的過程可以分為兩部分或者兩半(half)。中斷處理程序屬 于“上半部(top half)”–接受到一個中斷,立刻開始執行,但只做有嚴格時限的工作。 能夠被允許稍微晚一點完成的工作會放到“下半部(bottom half)中去,下半部不會馬上 執行,而是等到一個合適的時機調度執行。也就是說,關鍵而緊急的部分,內核立即執行 ,屬于上半部;其余推遲的部分,內核隨后執行,屬于下半部。
比如說當網卡接收到數據包時,會產生一個中斷,中斷處理程序首要進行的工作是通知硬 件拷貝最新的網絡數據包到內存,然后讀取網卡更多的數據包。這樣網卡緩存就不會溢出 。至于對數據包的處理和其他隨后工作,則放到下半部進行。關于下半部的細節,我們后 面會討論。
注冊中斷處理程序
驅動程序通過
request_irq()
函數注冊一個中斷處理程序:
/* 定義在<linux/interrupt.h>中 */
typedef irqreturn_t (*irq_handler_t)(int, void *);
int request_irq(ussigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev);
參數解釋如下:
-
irq
?要分配的中斷號 -
handler
?是指向中斷處理程序的指針 -
flags
?設置中斷處理程序的一些屬性,可能的值如下:IRQF_DISABLED 在本次中斷處理程序本身期間,禁止所有其他中斷。 IRQF_SAMPLE_RANDOM 這個中斷對內核的隨機數產生源有貢獻。 IRQF_TIMER 該標志是特別為系統定時器的中斷處理準備的。 IRQF_SHARED 表明多個中斷處理程序可以共享這條中斷線。也就是說這 條中斷線上可以注冊多個中斷處理程序,當中斷發生時, 所有注冊到這條中斷線上的handler都會被調用。
-
name
?是與中斷相關設備的ASCII文本表示 -
dev
?類似于一個cookie,內核每次調用中斷處理程序時,都會把這個指針傳遞給它, 指針的值用來表明到底是什么設備產生了這個中斷,當中斷線共享時,這條中斷線上 的handler們就可以通過dev來判斷自己是否需要處理。
釋放中斷處理程序
通過
free_irq
函數注銷相應的中斷處理程序:
void free_irq(unsigned int irq, void *dev);
參數和
request_irq
的參數類似。當一條中斷線上注冊了多個中斷處理程序時,就需要?
dev
來說明想要注銷的是哪一個handler。
下半部(bottom half)
有三種機制來執行下半部的工作:“軟中斷”,“tasklet”和“工作隊列”。
軟中斷是一組靜態定義的下半部接口,有32個,可以在所有處理器上同時執行–即使兩個 類型相同也可以。
tasklet的實現基于軟中斷,但兩個相同類型的tasklet不能同時執行。
工作隊列則是先對要推后執行的工作排隊,稍后在進程上下文中執行它們。
軟中斷(softirq)
軟中斷的實現
軟中斷實在編譯期間靜態分配的,由
softirq_action
結構表示:
/* 在<linux/interrupt.h>中 */
struct softirq_action {
void (*action)(struct softirq_action *);
};
/* kernel/softirq.c中定義了一個包含有32個該結構體的數組 */
static struct softirq_action softirq_vec[NR_SOFTIRQS];
每個被注冊的軟中斷都占據該數組的一項,因此最多可能有32個軟中斷。
當內核運行一個軟中斷處理程序的時候,就會執行
softirq_action
結構中的
action
指 向的函數:
my_softirq->action(my_softirq);
它把自己(整個
softirq_action
結構)的指針作為參數。
軟中斷的觸發
軟中斷在被標記后才會執行,這標記的過程叫做
觸發軟中斷(raising the softirq)
?。通常在中斷處理程序中觸發軟中斷。軟中斷的觸發通過
raise_softirq()
進行。比如
raise_softirq(NET_TX_SOFTIRQ);
觸發網絡子系統的軟中斷。
在下面這些時刻,軟中斷會被檢查和執行:
* 從一個硬件中斷代碼處返回時 * 在ksoftirqd內核線程中(稍后會講到) * 在那些顯式檢查和執行帶處理的軟中斷的代碼中,比如網絡子系統中
軟中斷的執行
軟中斷的狀態通過一個位圖來表示:第n位設置為1,表示第n個類型的軟中斷被觸發,等待 處理。
local_softirq_pending()
宏返回這個位圖。
set_softirq_pending()
宏則可對 位圖進行設置或清零。
軟中斷在
do_softirq()
函數中執行,該函數遍歷每一個軟中斷,如果處于被觸發的狀態 ,則執行其處理程序,該函數的核心部分類似與這樣:
u32 pending;
pending = local_softirq_pending();
if (pending) {
struct softirq_action *h;
set_softirq_pending(0); /* 把位圖清零 */
h = soft_vec;
do {
if (pending & 1)
h-action(h);
h++;
pending >>= 1; /* 位圖向右移1位,原來第二位的現在在第一位 */
} while (pending);
}
需要注意的是,如果同一個軟中斷在它被執行的同時又被觸發了,那么另外一個處理器可 以同時運行其處理程序。這意味著任何共享數據(甚至是僅在軟中斷處理程序內部使用的 全局變量)都需要嚴格的鎖保護。因此,大部分的軟中斷處理程序,都通過采取單處理器 數據或其他的一些技巧來避免顯式地加鎖。
tasklet
tasklet的實現
tasklet基于軟中斷實現,事實上它使用的是
HI_SOFTIRQ
和
TASKLET_SOFTIRQ
這兩個軟 中斷,通過
tasklet_struct
結構表示:
/* 在<linux/interrupt.h>中 */
struct tasklet_struct {
struct tasklet_struct *next; /* 鏈表中的下一個tasklet */
unsigned long state; /* tasklet的狀態 */
atomic_t count; /* 引用計數器 */
void (*func)(unsigned long); /* tasklet處理函數 */
unsigned long data; /* 給tasklet處理函數的參數 */
};
其中,state的值只可以為0,
TASKLET_STATE_SCHED
(表示tasklet已被調度,正在準備 投入運行),和
TASKLET_STATE_RUN
(表示tasklet正在運行)。
tasklet的調度
已經調度的tasklet(相當于觸發了的軟中斷)存放在兩個由
tasklet_struct
結構組成的 鏈表中:
tasklet_vec
和
tasklet_hi_vec
(表示高優先級的tasklet),分別通過
tasklet_schedule()
和
tasklet_hi_schedule()
進行調度。
ksoftirqd
在軟中斷處理程序中有時候會再次觸發軟中斷,這樣就有可能出現大量的軟中斷。這些重 新觸發的軟中斷不會馬上被處理,而是通過內核喚醒的一組內核線程來處理的。
每個處理器都有一組輔助處理軟中斷(包括了tasklet)的內核線程,名字叫做?
ksoftirqd/n
,其中n代表CPU的編號。這些內核線程以最低的優先級運行(nice值19), 這樣就能避免它們和其它重要的任務搶奪資源。這些內核線程會執行類似與下面的循環:
for (;;) {
if (!softirq_pending(cpu))
schedule();
set_current_state(TASK_RUNNING);
while (softirq_pending(cpu)) {
do_softirq();
if (need_resched())
shcedule();
}
set_current_state(TASK_INTERRUPTIBLE);
}
preempt_count字段
在每個進程描述符的
thread_info
結構中有一個32位的字段叫
preempt_count
,它用來 跟蹤內核搶占和內核控制路徑的嵌套。利用
preempt_count
的不同區域表示不同的計數器 和一個標志。
位 描述
0~7 搶占計數器(max value = 255)
8~15 軟中斷計數器(max value = 255)
16~27 硬中斷計數器(max value = 4096)
28 PREEMPT_ACTIVE 標志
- “搶占計數器”記錄顯式禁用本地CPU內核搶占的次數,只有當這個計數器為0時才允許內 核搶占。
- “軟中斷計數器”表示軟中斷被禁用的程度,同樣,值為0時表示軟中斷可以被觸發。
-
“硬中斷計數器”表示本地CPU上中斷處理程序的嵌套數。
irq_enter()
宏遞增它的值,?irq_exit()
宏遞減它的值。
工作隊列
工作隊列(work queue) 是另外一種將工作推后執行的形式,它可以把工作推后,交 由一個內核線程去執行。所以這些工作會在進程上下文中執行,并且運行重新調度和睡眠 。
工作的表示
一個工作用
work_struct
結構體表示:
/* 定義在<linux/workqueue.h>中 */
typedef void (*work_func_t)(struct work_struct *work);
struct work_struct {
atomic_long_t data; /* 執行這個工作時的參數 */
struct list_head entry; /* 工作組成的鏈表 */
work_func_t func; /* 執行這個工作時調用的函數 */
};
這些
work_struct
構成一個鏈表,工作執行完畢時,該工作就會從鏈表中移除。
工作者線程的表示
可以把一些工作放到一個隊列里面,然后創建一個專門的內核線程來執行隊列里的任務, 這些內核線程叫做
工作者線程(worker thread)
。但是大多數情況下不需要自己創建 worker thread,因為內核已經創建了一個默認的,叫做
events/n
,這里的n表示CPU的編 號。
“worker thread”使用
workqueue_struct
結構表示:
struct workqueue_struct {
struct cpu_workqueue_struct cpu_wq[NR_CPUS];
struct list_head list;
const char *name;
int singlethread;
int freezeable;
int rt;
};
一個“worker thread”表示一種類型的工作者線程,默認情況下只有event這一種類型的工 作者線程。然后每一個CPU上又有一個該類型的工作者線程,這就表現為
cpu_wq
數組,該 數組的每一項是
struct cpu_workqueue_struct
結構:
struct cpu_workqueue_struct {
spinlock_t lock; /* 通過自旋鎖保護該結構 */
struct list_head worklist; /* 工作列表 */
wait_queue_head_t more_work;
struct work_struct *current_struct;
struct workqueue_struct *wq; /* 關聯工作隊列結構 */
task_t *thread; /* 關聯線程 */
};
該結構體中的
wq
表明自己是什么類型的worker。
系統調用
什么是系統調用
系統調用(System Call) 就是讓用戶進程與內核進行交互的一組接口,它在用戶進程 和硬件設備之間添加了一個中間層。
以
printf()
為例,應用程序、C庫和內核之間的關系是:
-------------------------------------------------------------------------
printf() ----> C庫中的printf() ----> C庫中的write() ----> write()系統調用
--------------------------------------------------------------------------
| 應用程序 | C庫 | 內核 |
--------------------------------------------------------------------------
每個系統調用被賦予一個獨一無二的系統調用號,系統調用號一旦分配就不能再變更。否 則編譯好的程序就會崩潰。
系統調用處理程序
system_call()
應用程序是通過
軟中斷
來通知內核對系統調用的進行使用的, 事實上是第128號IRQ。 也就是通過引發一個異常來促使系統切換到內核態去執行異常處理程序。此時的異常處理 程序實際上就是系統調用處理程序–
system_call()
。
至于使用的是哪個系統調用,就是通過系統調用號來判斷。在陷入內核空間前,用戶空間 把相應的系統調用號存入
exa
寄存器,
system_call
通過
exa
寄存器得知到底是哪個系 統調用。參數的傳遞也是通過寄存器,如果參數較多,則寄存器里面存的是指向這些參數 的用戶空間地址的指針。
JH, 2013-05-05
參考資料:
- Man pages
- UNIX環境高級編程
- Linux內核設計與實現
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
