操作系統(tǒng)必須有一個快速逞带、異步欺矫、簡單的機(jī)制負(fù)責(zé)對硬件做出迅速響應(yīng)并完成那些時間要求很嚴(yán)格的操作,中斷處理程序很適合用于實(shí)現(xiàn)這些功能展氓;然而對于其他對時間要求相對寬松的任務(wù)穆趴,就應(yīng)該推后到中斷被激活以后再去運(yùn)行。
一遇汞、下半部
下半部的任務(wù)就是執(zhí)行與中斷處理密切相關(guān)但中斷處理程序本身不執(zhí)行的工作未妹,這對系統(tǒng)的響應(yīng)能力和性能都至關(guān)重要。并沒有規(guī)定如何在上半部和下半部劃分工作空入,但有以下經(jīng)驗(yàn):
- 若任務(wù)對時間非常敏感络它,將其放在中斷處理程序中執(zhí)行
- 若任務(wù)和硬件相關(guān),將其放在中斷處理程序中執(zhí)行
- 若任務(wù)要保證不被其他中斷(特別是相同的中斷)中斷歪赢,將其放在中斷處理程序中執(zhí)行
- 其他所有任務(wù)化戳,考慮放在下半部執(zhí)行
通常下半部在中斷處理程序一返回就會馬上執(zhí)行,下半部執(zhí)行的關(guān)鍵在于當(dāng)它們運(yùn)行時允許響應(yīng)所有的中斷埋凯。
當(dāng)前点楼,有三種機(jī)制可以用來實(shí)現(xiàn)將工作推后執(zhí)行:
- 軟中斷
- tasklet
- 工作隊列
二、軟中斷
2.1 軟中斷的實(shí)現(xiàn)
軟中斷是在編譯期間靜態(tài)分配的白对,最多有32個軟中斷掠廓,其結(jié)構(gòu):
struct softirq_action {
void (*action) (struct softirq_action *);
};
static struct softirq_action softirq_vec[NR_SOFTIRQS]; //NR_SOFTIRQS=32
軟中斷處理程序:
void softirq_handler(struct softirq_action *);
一個軟中斷不會搶占另一個軟中斷。唯一可以搶占軟中斷的是中斷處理程序甩恼。不過蟀瞧,其他軟中斷(包括相同類型的軟中斷)可以在其他處理器上同時執(zhí)行。
執(zhí)行軟中斷:一個注冊的軟中斷必須在被標(biāo)記后才會執(zhí)行媳拴。這被稱為觸發(fā)軟中斷黄橘。通常中斷處理程序在返回前標(biāo)記它的軟中斷,使其在稍后被執(zhí)行屈溉。于是在合適的時刻塞关,該軟中斷就會被運(yùn)行:
- 從一個硬件中斷代碼返回時
- 在ksoftirqd內(nèi)核線程中
- 在那些顯示檢查和執(zhí)行待處理的軟中斷的代碼中,如網(wǎng)絡(luò)子系統(tǒng)中
軟中斷最后都在do_softirq()中執(zhí)行子巾,其會循環(huán)遍歷每一個待處理的軟中斷帆赢,調(diào)用它們的處理程序小压。
2.2 使用軟中斷
軟中斷保留給系統(tǒng)中對時間要求最嚴(yán)格以及最重要的下半部使用。目前只有兩個子系統(tǒng)(網(wǎng)絡(luò)和SCSI)直接使用軟中斷椰于。此外怠益,內(nèi)核定時器和tasklet都是建立在軟中斷上。
在編譯期間瘾婿,通過一個枚舉類型來靜態(tài)聲明軟中斷蜻牢,并通過索引值來表示相對優(yōu)先級,索引號小的軟中斷在索引號大的軟中斷之前執(zhí)行偏陪。
注冊軟中斷處理程序:
open_softirq(軟中斷索引號抢呆,處理函數(shù))
例如:
open_softirq(NET_TX_SOFTIRQ, next_tx_action);
open_softirq(NET_RX_SOFTIRQ, next_rx_action);
軟中斷處理程序執(zhí)行的時候,允許響應(yīng)中斷笛谦,但它自己不能休眠抱虐。在一個處理程序運(yùn)行的時候,當(dāng)前處理器上的軟中斷被終止饥脑。但其他的處理器仍可以執(zhí)行別的軟中斷恳邀。實(shí)際上,如果同一個軟中斷在被執(zhí)行的同時再被觸發(fā)了灶轰,那么另一個處理器可以同時處理其處理程序谣沸,這意味著任何共享數(shù)據(jù)都需要嚴(yán)格的鎖保護(hù)。
觸發(fā)軟中斷:將一個軟中斷設(shè)置為掛起狀態(tài)框往,讓它在下次調(diào)用do_softirq()函數(shù)時投入運(yùn)行鳄抒。
raise_softirq(NET_TX_SOFTIRQ);
若中斷被禁止了:
raise_softirq_irqoff(NET_TX_SOFTIRQ);
在中斷處理程序中觸發(fā)軟中斷是最常見的形式。在這種情況下椰弊,中斷處理程序執(zhí)行硬件設(shè)備的相關(guān)操作许溅,然后觸發(fā)相應(yīng)的軟中斷,最后退出秉版。內(nèi)核在執(zhí)行完中斷處理程序后贤重,馬上會調(diào)用do_softirq函數(shù)。于是軟中斷開始執(zhí)行中斷處理程序留給它去完成的剩余任務(wù)清焕。
三并蝗、tasklet
3.1 tasklet實(shí)現(xiàn)
tasklet是通過軟中斷實(shí)現(xiàn)的,由兩類軟中斷代表:HI_SOFTIRQ和TASKLET_SOFTIRQ秸妥。兩者的區(qū)別在于優(yōu)先級滚停。
tasklet的結(jié)構(gòu)體如下:
struct tasklet_struct {
struct tasklet_struct *next; // 鏈表中的下一個tasklet,每個tasklet_struct代表一個不同的tasklet
unsigned long state; // tasklet的狀態(tài)粥惧,0键畴、TASKLET_STATE_SCHED、TASKLET_STATE_RUN
atomic_t count; // 引用計數(shù)器,若不為0起惕,則tasklet被禁止涡贱,不允許運(yùn)行;為0時惹想,tasklet才被激活问词,并設(shè)置為掛起狀態(tài),才能被執(zhí)行
void (*func)(unsigned long); // tasklet處理函數(shù)
unsigned long data; // 給tasklet處理函數(shù)的參數(shù)
};
tasklet由tasklet_schedule()和tasklet_hi_schedule()函數(shù)進(jìn)行調(diào)度嘀粱,接收一個指向tasklet_struct結(jié)構(gòu)的指針作為參數(shù)激挪。
tasklet_schedule()會檢查tasklet的狀態(tài),若可執(zhí)行則調(diào)用_tasklet_schedule(),把需要調(diào)度的tasklet放到tasklet_vec鏈表表頭草穆。觸發(fā)TASKLET_SOFTIRQ軟中斷灌灾,這樣在下一次調(diào)用do_softirq時就會執(zhí)行待處理的tasklet。
do_softirq執(zhí)行對應(yīng)的tasklet_action()悲柱。tasklet_action()會遍歷tasklet_vec鏈表中待處理的tasklet,判斷并標(biāo)志tasklet的狀態(tài)以及count值些己,若tasklet需要執(zhí)行則運(yùn)行其處理函數(shù)tasklet_struct#func豌鸡。
3.2 使用tasklet
聲明tasklet:
靜態(tài)創(chuàng)建tasklet:
DECLARE_TASKLET(name, func, data);//count初始值為0,tasklet處于激活狀態(tài)
DECLARE_TASKLET_DISABLE(name, func, data);//count初始值為1段标,tasklet處于禁止?fàn)顟B(tài)
//創(chuàng)建一個名為my_tasklet涯冠,處理程序?yàn)閙y_tasklet_handler并且被激活的tasklet
//當(dāng)my_tasklet_handler被調(diào)用時dev會被傳遞給它
DECLARE_TASKLET(my_tasklet, my_tasklet_handler, dev);
動態(tài)創(chuàng)建tasklet并初始化:
tasklet_init(t, tasklet_handler, dev)
tasklet處理程序原型如下:
void tasklet_handler(unsigned long data);
tasklet不能休眠。兩個相同的tasklet不會同時運(yùn)行逼庞,但tasklet運(yùn)行時允許響應(yīng)中斷蛇更,所以tasklet與中斷處理程序之間共享數(shù)據(jù)需要做好并發(fā)保護(hù);兩個不通的tasklet可能在兩個處理器同時執(zhí)行赛糟,因此它們之間的共享數(shù)據(jù)也需要做好并發(fā)保護(hù)派任。
調(diào)度tasklet:
tasklet_schedule(&my_tasklet); // 把my_tasklet標(biāo)記為掛起
tasklet被調(diào)度后,只要有機(jī)會就會盡可能早地運(yùn)行:
- 如果它沒得到運(yùn)行機(jī)會之前璧南,有個相同的tasklet被調(diào)度了掌逛,那么它只會運(yùn)行一次;
- 如果它以及開始運(yùn)行了司倚,在另一個處理器上又被調(diào)度了豆混,那么會再運(yùn)行一次。
tasklet_disable(&my_tasklet); //禁止tasklet
tasklet_enable(&my_tasklet); //激活tasklet
tasklet_kill(&my_tasklet); //從掛起的隊列中去掉my_tasklet动知,可能會休眠皿伺,禁止在中斷上下文中使用
當(dāng)大量軟中斷出現(xiàn)時,內(nèi)核會喚醒一組內(nèi)核線程來處理這些負(fù)載盒粮。這些線程以最低優(yōu)先級運(yùn)行(nice值19)鸵鸥,避免跟其他重要任務(wù)搶奪資源。但它們最后肯定會被執(zhí)行拆讯,因此能夠保證在軟中斷負(fù)載重時用戶程序不會因?yàn)榈貌坏教幚砥鲿r間而處于饑餓狀態(tài)脂男,也能保證空閑系統(tǒng)上軟中斷迅速得到處理养叛。
每個處理器都有一個這樣的線程,稱為ksoftirqd/n宰翅,n表示處理器編號弃甥。該線程一旦被初始化,就會陷入循環(huán):若沒有待處理軟中斷汁讼,則調(diào)用schedule讓出時間淆攻;若有則調(diào)用do_softirq來處理。
四嘿架、工作隊列
工作隊列把工作推后瓶珊,交由一個內(nèi)核線程執(zhí)行,這個下半部總是會在進(jìn)程上下文中執(zhí)行耸彪,因此工作隊列執(zhí)行的代碼擁有進(jìn)程上下文的所有優(yōu)勢伞芹。最重要的是工作隊列運(yùn)行重新調(diào)度甚至休眠。
4.1 工作隊列的實(shí)現(xiàn)
工作隊列子系統(tǒng)是一個用于創(chuàng)建內(nèi)核線程的接口蝉娜,通過它創(chuàng)建的進(jìn)程負(fù)責(zé)執(zhí)行由內(nèi)核其他部分放入隊列里的任務(wù)唱较。這些內(nèi)核線程稱為工作線程events/n,n表示處理器編號召川,每個處理器對應(yīng)一個線程南缓。
//工作線程結(jié)構(gòu):
struct workqueue_struct {
struct cpu_workqueue_struct cpu_wq[NR_CPUS]; //每個元素對應(yīng)一個處理器
struct list_head list;
const char *name;
int singlethread;
int freezeable;
int rt;
};
//處理器的結(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)工作隊列結(jié)構(gòu)
task_t *thread; //關(guān)聯(lián)線程
};
// 工作結(jié)構(gòu):
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_fun_t func;
};
工作線程會執(zhí)行worker_thread()函數(shù),在其初始化后會執(zhí)行一個死循環(huán)并開始休眠荧呐,當(dāng)有操作被插入到隊列current_struct時汉形,線程就會被喚醒,以便執(zhí)行這些操作work->func倍阐;當(dāng)沒有剩余操作時概疆,又繼續(xù)休眠。
4.2 使用工作隊列
創(chuàng)建工作:
// 靜態(tài)創(chuàng)建名稱為name收捣,處理函數(shù)為func届案,參數(shù)為data的work_struct結(jié)構(gòu)體:
DECLARE_WORK(name, void (*func)(void *), void *data);
//運(yùn)行時創(chuàng)建work_struct結(jié)構(gòu)體:
INIT_WORK(struct work_struct *work, void (*func)(void *), void *data);
工作隊列處理函數(shù)運(yùn)行在進(jìn)程上下文,原型如下:
void work_handle(void *data);
該函數(shù)默認(rèn)允許響應(yīng)中斷罢艾,并且不持有任何鎖楣颠。如有需要,可以休眠咐蚯。但不能訪問用戶空間童漩。
工作調(diào)度,一旦工作所在處理器上的工作者線程被喚醒春锋,工作就會被執(zhí)行:
schedule_work(&work);
schedule_delayed_work(&work, delay); //延遲執(zhí)行工作
int cancel_delayed_work(struct work_struct *work); //取消延遲執(zhí)行的工作
刷新指定工作隊列矫膨,函數(shù)會一直等待直到隊列中所有對象都被執(zhí)行以后才返回。在等待所有待處理的工作執(zhí)行時,該函數(shù)會進(jìn)入休眠狀態(tài)侧馅,所以只能在進(jìn)程上下文中使用它:
void flush_scheduled_work(void);
創(chuàng)建新的工作隊列和與之相關(guān)的工作者線程:
struct workqueue_struct *create_workqueue(const char *name);
調(diào)度工作到指定的工作隊列:
int queue_work(struct workqueue_struct *wq,
struct work_struct *work);
int queue_delayed_work(struct workqueue_struct *wq,
struct work_struct *work,
unsigned long delay);
刷新指定工作隊列:
flush_workqueue(struct workqueue_struct *wq);