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

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