小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

linux內(nèi)核分析筆記----中斷上半部與下半部

 rookie 2012-04-12

linux內(nèi)核分析筆記----上半部與下半部(上)     

      嗨,嗨,如果您記性好的話,我在上一篇博客中提到過這樣一件事:中斷處理是分為兩個(gè)部分:中斷處理程序是上半部,它接收到一個(gè)中斷,就立即執(zhí)行,但只做有 嚴(yán)格時(shí)限的工作;而另外被叫做下半部的另外一個(gè)部分主要做被允許能稍后完成的工作。這個(gè)下半部正是今天的重點(diǎn)。

       下半部的任務(wù)就是執(zhí)行與中斷處理密切相關(guān)但中斷處理程序本生身不執(zhí)行的任務(wù)。最好情況當(dāng)然是中斷處理程序把所有的工作都交給下半部執(zhí)行,而自己啥都不做。 因?yàn)槲覀兛偸窍M袛嗵幚沓绦虮M可能快的返回。但是,中斷處理程序注定要完成一部分工作。遺憾的是,并沒有誰嚴(yán)格規(guī)定說什么任務(wù)應(yīng)該在哪個(gè)部分中完成,換 句話說,這個(gè)決定完全由想我們這樣的驅(qū)動(dòng)工程師來做。記住,中斷處理程序會(huì)異步執(zhí)行,并且在最好的情況下它也會(huì)鎖定當(dāng)前的中斷線,因此將中斷處理程序縮短 的越小就越好。當(dāng)然啦,沒有規(guī)則并不是沒有經(jīng)驗(yàn)和教訓(xùn):

1.如果一個(gè)任務(wù)對(duì)時(shí)間非常敏感,感覺告訴我還是將其放在中斷處理程序中執(zhí)行是個(gè)好的選擇。
2.如果一個(gè)任務(wù)和硬件相關(guān),還是將其放在中斷處理程序中執(zhí)行吧。
3.如果一個(gè)任務(wù)要保證不被其他中斷(特別是相同的中斷)打斷,那就將其放在中斷處理程序中吧。
4.其他所有任務(wù),除非你有更好的理由,否則全部丟到下半部執(zhí)行。

       總之,一句話:中斷處理程序要執(zhí)行的越快越好。

       我 們前邊老是說下半部會(huì)在以后執(zhí)行,那么這個(gè)以后是個(gè)什么概念呢?遺憾的說,這個(gè)只是相對(duì)于馬上而言的。下半部并需要指定一個(gè)明確的時(shí)間,只要把這個(gè)任務(wù)推 遲一點(diǎn),讓他們?cè)谙到y(tǒng)不太繁忙并且中斷恢復(fù)后執(zhí)行就可以了。通常下半部在中斷處理程序已返回就會(huì)馬上執(zhí)行,關(guān)鍵在于當(dāng)它們運(yùn)行時(shí),允許相應(yīng)所有的中斷。

       上半部只能通過中斷處理程序來完成,下半部的實(shí)現(xiàn)卻是有很多種方式。這些用來實(shí)現(xiàn)下半部的機(jī)制分別由不同的接口和子系統(tǒng)組成。最早的是“bottom half”,這種機(jī)制也被稱為“BH”,它提供的接口很簡(jiǎn)單,提供了一個(gè)靜態(tài)創(chuàng)建,由32個(gè)bottom half組成的鏈表,上半部通過一個(gè)32位整數(shù)中的一位來標(biāo)識(shí)出哪個(gè)bottom half可執(zhí)行。每個(gè)BH都在全局范圍內(nèi)進(jìn)行同步,即使分屬于不同的處理器,也不允許任何兩個(gè)bottom half同時(shí)執(zhí)行。這種方式使用方便但不夠靈活,簡(jiǎn)單卻有性能瓶頸。以需要更好的方法了。第二種方法,任務(wù)隊(duì)列(task queues).內(nèi)核定義了一組隊(duì)列。其中每個(gè)隊(duì)列都包含一個(gè)由等待調(diào)用的函數(shù)組成鏈表。根據(jù)其所處隊(duì)列的位置,這些函數(shù)會(huì)在某個(gè)時(shí)刻被執(zhí)行,驅(qū)動(dòng)程序可 根據(jù)需要把它們自己的下半部注冊(cè)到合適的隊(duì)列上去。這種方法已經(jīng)不錯(cuò),但仍然不夠靈活,它沒辦法代替整個(gè)BH接口。對(duì)于一些性能要求較高的子系統(tǒng),像網(wǎng)絡(luò) 部分,它也不能勝任。在2.3開發(fā)版中,又引入了軟中斷(softirqs)和tasklet,這里的軟中斷和實(shí)現(xiàn)系統(tǒng)調(diào)用所提到的軟中斷(軟件中斷)不 是同一個(gè)概念。如果無須考慮和過去開發(fā)的驅(qū)動(dòng)程序相兼容的話, 軟中斷和tasklet可以完全代替BH接口。軟中斷是一組靜態(tài)定義的下半部接口,有32個(gè),可以在所有的處理器上同時(shí)執(zhí)行----即使兩個(gè)類型完全相 同。task是一種基于軟中斷實(shí)現(xiàn)的靈活性強(qiáng),動(dòng)態(tài)創(chuàng)建的下半部實(shí)現(xiàn)機(jī)制。兩個(gè)不同類型的tasklet可以在不同的處理器上同時(shí)執(zhí)行,但類型相同的 tasklet不能同時(shí)執(zhí)行。tasklet其實(shí)是一種在性能和易用性之間尋求平衡的產(chǎn)物。軟中斷必須在編譯期間就進(jìn)行靜態(tài)注冊(cè),而tasklet可以通 過代碼進(jìn)行動(dòng)態(tài)注冊(cè)?,F(xiàn)在都是2.6內(nèi)核了,我們說點(diǎn)新鮮的吧,linux2.6內(nèi)核提供了三種不同形式的下半部實(shí)現(xiàn)機(jī)制:軟中斷,tasklets和工 作對(duì)列,這些會(huì)依次介紹到。這時(shí),可能有人會(huì)想到定時(shí)器的概念,定時(shí)器也確實(shí)是這樣,但定時(shí)器提供了精確的推遲時(shí)間,我們這里還不至于這樣,所以先放下, 我們后面再說定時(shí)器。好,下面我開始說說詳細(xì)的各種機(jī)制:

       1.軟中斷   實(shí)際上軟中斷使用的并不多,反而是后面的tasklet比較多,但tasklet是通過軟中斷實(shí)現(xiàn)的,軟中斷的代碼位于/kernel /softirq.c中。軟中斷是在編譯期間靜態(tài)分配的,由softirq_action結(jié)構(gòu)表示,它定義在linux/interrupt.h中:

1
2
3
4
struct softirq_action{
    void (*action)(struct softirq_action *);  //待執(zhí)行的函數(shù)
    void *data; //傳給函數(shù)的參數(shù)
};

kernel/softirq.c中定義了一個(gè)包含有32個(gè)結(jié)構(gòu)體的數(shù)組:

1
static struct softirq_action softirq_vec[32]

       每個(gè)被注冊(cè)的軟中斷都占據(jù)該數(shù)組的一項(xiàng),因此最多可能有32個(gè)軟中斷,這是沒法動(dòng)態(tài)改變的。由于大部分驅(qū)動(dòng)程序都使用tasklet來實(shí)現(xiàn)它們的下半部,所以現(xiàn)在的內(nèi)核中,只用到了6個(gè)。上面的軟中斷結(jié)構(gòu)中,第一項(xiàng)是軟中斷處理程序,原型如下:

1
void softirq_handler(struct softirq_action *)

       當(dāng)內(nèi)核運(yùn)行一個(gè)軟中斷處理程序時(shí),它會(huì)執(zhí)行這個(gè)action函數(shù),其唯一的參數(shù)為指向相應(yīng)softirq_action結(jié)構(gòu)體的指針。例如,如果my_softirq指向softirq_vec數(shù)組的實(shí)現(xiàn),那么內(nèi)核會(huì)用如下的方式調(diào)用軟中斷處理程序中的函數(shù):

1
my_softirq->action(my_softirq)

       一 個(gè)軟中斷不會(huì)搶占另外一個(gè)軟中斷,實(shí)際上,唯一可以搶占軟中斷的是中斷處理程序。不過,其他的軟中斷----甚至是相同類型的軟中斷-----可以在其他 類型的機(jī)器上同時(shí)執(zhí)行。一個(gè)注冊(cè)的軟中斷必須在被標(biāo)記后才能執(zhí)行----觸發(fā)軟中斷(rasing the softirq).通常,中斷處理程序會(huì)在返回前標(biāo)記它的軟中斷,使其在稍后執(zhí)行。在下列地方,待處理的軟中斷會(huì)被檢查和執(zhí)行:

1.處理完一個(gè)硬件中斷以后                   2.在ksoftirqd內(nèi)核線程中                 3.在那些顯示檢查和執(zhí)行待處理的軟中斷的代碼中,如網(wǎng)絡(luò)子系統(tǒng)中。

       軟中斷會(huì)在do_softirq()中執(zhí)行,如果有待處理的軟中斷,do_softirq會(huì)循環(huán)遍歷每一個(gè),調(diào)用他們的處理程序。核心部分如下:

1
2
3
4
5
6
7
8
9
10
11
u32 pending = softirq_pending(cpu);
if(pending){
    struct softirq_action *h = softirq_vec;
    softirq_pending(cpu) = 0;
    do{
      if(pending &1)
        h->action(h);
      h++;
      pending >>=1;
    }while(pending);
}

       上述代碼會(huì)檢查并執(zhí)行所有待處理的軟中斷,softirq_pending(),用它來獲得待處理的軟中斷的32位位圖-----如果第n位被設(shè)置為1, 那么第n位對(duì)應(yīng)類型的軟中斷等待處理,一旦待處理的軟中斷位圖被保存,就可以將實(shí)際的軟中斷位圖清零了。pending &1是判斷pending的第一位是否被置為1.一旦pending為0,就表示沒有待處理的中斷了,因?yàn)閜ending最多可能設(shè)置32位,循 環(huán)最多也只能執(zhí)行32位。軟中斷保留給系統(tǒng)中對(duì)時(shí)間要求最嚴(yán)格以及最重要的下半部使用。所以使用之前還是要想清楚的。下面簡(jiǎn)要的說明如何使用軟中斷:

1.分配索引:在編譯期間,要通過<linux/interrupt.h>中建立的一個(gè)枚舉類型來靜態(tài)地聲明軟中斷。內(nèi)核用這些從0開始的索引來表示一種相對(duì)優(yōu)先級(jí),
  索引號(hào)越小就越先執(zhí)行。所以,可以根據(jù)自己的需要將自己的索引號(hào)放在合適的位置。
2.注冊(cè)處理程序:接著,在運(yùn)行時(shí)通過調(diào)用open_softirq()注冊(cè)中斷處理程序,例如網(wǎng)絡(luò)子系統(tǒng),如下:
1
2
open_softirq(NET_TX_SOFTIRQ,net_tx_action,NULL);
open_softirq(NET_RX_SOFTIRQ,net_rx_action,NULL);
  函數(shù)有三個(gè)參數(shù),軟中斷索引號(hào),處理函數(shù)和data域存放的數(shù)組。軟中斷處理程序執(zhí)行的時(shí)候,允許響應(yīng)中斷,但它自己不能休眠。在一個(gè)處理程序運(yùn)
  行的時(shí)候,當(dāng)前處理器的軟中斷被禁止,但其他的處理器仍可以執(zhí)行別的軟中斷。實(shí)際上,如果一個(gè)軟中斷在它被執(zhí)行的時(shí)候同時(shí)再次被觸發(fā)了,那么
  另外一個(gè)處理器可以同時(shí)運(yùn)行其處理程序。這意味著對(duì)共享數(shù)據(jù)都需要嚴(yán)格的鎖保護(hù)。大部分軟中斷處理程序都通過采取單處理數(shù)據(jù)(僅屬于某一個(gè)處
  理器的數(shù)據(jù))或者其他一些技巧來避免顯示的加鎖,從而提供更出色的性能。
3.觸發(fā)軟中斷:經(jīng)過上面兩項(xiàng),新的軟中斷處理程序就能夠運(yùn)行,raist_softirq(中斷索引號(hào))函數(shù)可以將一個(gè)軟中斷設(shè)置為掛起狀態(tài),從而讓它在下次調(diào)
  用do_softirq()函數(shù)時(shí)投入運(yùn)行。該函數(shù)在觸發(fā)一個(gè)軟中斷之前先要禁止中斷,觸發(fā)后再恢復(fù)回原來的狀態(tài),如果中斷本來就已經(jīng)被禁止了,那么可以調(diào)用另一函數(shù)raise_softirq_irqoff(),這會(huì)帶來一些優(yōu)化效果。

       在中斷處理程序中觸發(fā)軟中斷是最常見的形式,中斷處理程序執(zhí)行硬件設(shè)備的相關(guān)操作,然后觸發(fā)相應(yīng)的軟中斷,最后退出。內(nèi)核在執(zhí)行完中斷處理程序后,馬上就會(huì)調(diào)用do_softirq()函數(shù)。于是,軟中斷就開始執(zhí)行中斷處理程序留給它去完成的剩下任務(wù)。

       2.Tasklets:tasklet 是通過軟中斷實(shí)現(xiàn)的,所以它們本身也是軟中斷。它由兩類軟中斷代表:HI_SOFTIRQ和TASKLET_SOFTIRQ.區(qū)別在于前者會(huì)先于后者執(zhí) 行。Tasklets由tasklet_struct結(jié)構(gòu)表示,每個(gè)結(jié)構(gòu)體單獨(dú)代表一個(gè)tasklet,在linux/interrupt.h中定義:

1
2
3
4
5
6
7
struct tasklet_struct{
    struct tasklet_struct *next;
    unsigned long state;
    atomic_t count;
    void (*func)(unsigned long);
    unsigned long data;
};

       結(jié) 構(gòu)體中func成員是tasklet的處理程序,data是它唯一的參數(shù)。state的取值只能是0,TASKLET_STATE_SCHED(表明 tasklet已經(jīng)被調(diào)度,正準(zhǔn)備投入運(yùn)行)和TASKLET_STATE_RUN(表明該tasklet正在運(yùn)行,它只有在多處理器的系統(tǒng)上才會(huì)作為一 種優(yōu)化來使用,單處理器系統(tǒng)任何時(shí)候都清楚單個(gè)tasklet是不是正在運(yùn)行)之間取值。count是tasklet的引用計(jì)數(shù)器,如果不為0,則 tasklet被禁止,不允許執(zhí)行;只有當(dāng)它為0時(shí),tasklet才被激活,并且被設(shè)置為掛起狀態(tài)時(shí),該tasklet才能夠執(zhí)行。已調(diào)度的 tasklet(等同于被觸發(fā)的軟中斷)存放在兩個(gè)單處理器數(shù)據(jù)結(jié)構(gòu):tasklet_vec(普通tasklet)和task_hi_vec高優(yōu)先級(jí)的 tasklet)中,這兩個(gè)數(shù)據(jù)結(jié)構(gòu)都是由tasklet_struct結(jié)構(gòu)體構(gòu)成的鏈表,鏈表中的每個(gè)tasklet_struct代表一個(gè)不同的 tasklet。Tasklets由tasklet_schedule()和tasklet_hi_schedule()進(jìn)行調(diào)度,它們接收一個(gè)指向 tasklet_struct結(jié)構(gòu)的指針作為參數(shù)。兩個(gè)函數(shù)非常相似(區(qū)別在于一個(gè)使用TASKLET_SOFTIRQ而另外一個(gè)使用 HI_SOFTIRQ).有關(guān)tasklet_schedule()的操作細(xì)節(jié):

1.檢查tasklet的狀態(tài)是否為TASKLET_STATE_SCHED.如果是,說明tasklet已經(jīng)被調(diào)度過了,函數(shù)返回。
2.保存中斷狀態(tài),然后禁止本地中斷。在執(zhí)行tasklet代碼時(shí),這么做能夠保證處理器上的數(shù)據(jù)不會(huì)弄亂。
3.把需要調(diào)度的tasklet加到每個(gè)處理器一個(gè)的tasklet_vec鏈表或task_hi_vec鏈表的表頭上去。
4.喚起TASKLET_SOFTIRQ或HI_SOFTIRQ軟中斷,這樣在下一次調(diào)用do_softirq()時(shí)就會(huì)執(zhí)行該tasklet。
5.恢復(fù)中斷到原狀態(tài)并返回。

       那么作為tasklet處理的核心tasklet_action()和tasklet_hi_action(),具體做了些什么呢:

1.禁止中斷,并為當(dāng)前處理器檢索tasklet_vec或tasklet_hi_vec鏈表。
2.將當(dāng)前處理器上的該鏈表設(shè)置為NULL,達(dá)到清空的效果。
3.運(yùn)行相應(yīng)中斷。
4.循環(huán)遍歷獲得鏈表上的每一個(gè)待處理的tasklet。
5.如果是多處理器系統(tǒng),通過檢查TASKLET_STATE_RUN來判斷這個(gè)tasklet是否正在其他處理器上運(yùn)行。如果它正在運(yùn)行,那么現(xiàn)在就不要執(zhí)行,跳
   到下一個(gè)待處理的tasklet去。
6.如果當(dāng)前這個(gè)tasklet沒有執(zhí)行,將其狀態(tài)設(shè)置為TASKLETLET_STATE_RUN,這樣別的處理器就不會(huì)再去執(zhí)行它了。
7.檢查count值是否為0,確保tasklet沒有被禁止。如果tasklet被禁止,則跳到下一個(gè)掛起的tasklet去。
8.現(xiàn)在可以確定這個(gè)tasklet沒有在其他地方執(zhí)行,并且被我們?cè)O(shè)置為執(zhí)行狀態(tài),這樣它在其他部分就不會(huì)被執(zhí)行,并且引用計(jì)數(shù)器為0,現(xiàn)在可以執(zhí)行
   tasklet的處理程序了。
9.重復(fù)執(zhí)行下一個(gè)tasklet,直至沒有剩余的等待處理的tasklets。

       說了這么多,我們?cè)撛鯓邮褂眠@個(gè)tasklet呢,這個(gè)我在linux設(shè)備驅(qū)動(dòng)理論帖講的太多了。但別急,下邊為了博客完整,我仍然會(huì)大致講講:

       1.聲明自己的tasklet:投其所好,既可以靜態(tài)也可以動(dòng)態(tài)創(chuàng)建,這取決于選擇是想有一個(gè)對(duì)tasklet的直接引用還是間接引用。靜態(tài)創(chuàng)建方法(直接引用),可以使用下列兩個(gè)宏的一個(gè)(在linux/interrupt.h中定義):

1
2
DECLARE_TASKLET(name,func,data)
DECLARE_TASKLET_DISABLED(name,func,data)

       這兩個(gè)宏之間的區(qū)別在于引用計(jì)數(shù)器的初始值不同,前面一個(gè)把創(chuàng)建的tasklet的引用計(jì)數(shù)器設(shè)置為0,使其處于激活狀態(tài),另外一個(gè)將其設(shè)置為1,處于禁止?fàn)顟B(tài)。而動(dòng)態(tài)創(chuàng)建(間接引用)的方式如下:

1
tasklet_init(t,tasklet_handler,dev);

       2.編寫tasklet處理程序:函數(shù)類型是void tasklet_handler(unsigned long data).因?yàn)槭强寇浿袛鄬?shí)現(xiàn),所以tasklet不能休眠,也就是說不能在tasklet中使用信號(hào)量或者其他什么阻塞式的函數(shù)。由于tasklet 運(yùn)行時(shí)允許響應(yīng)中斷,所以必須做好預(yù)防工作,如果新加入的tasklet和中斷處理程序之間共享了某些數(shù)據(jù)額的話。兩個(gè)相同的tasklet絕不能同時(shí)執(zhí) 行,如果新加入的tasklet和其他的tasklet或者軟中斷共享了數(shù)據(jù),就必須要進(jìn)行適當(dāng)?shù)劓i保護(hù)。

       3.調(diào)度自己的tasklet:前邊的工作一做完,接下來就剩下調(diào)度了。通過一下方法實(shí) 現(xiàn):tasklet_schedule(&my_tasklet).在tasklet被調(diào)度以后,只要有合適的機(jī)會(huì)就會(huì)得到運(yùn)行。如果在還沒有得 到運(yùn)行機(jī)會(huì)之前,如果有一個(gè)相同的tasklet又被調(diào)度了,那么它仍然只會(huì)運(yùn)行一次。如果這時(shí)已經(jīng)開始運(yùn)行,那么這個(gè)新的tasklet會(huì)被重新調(diào)度并 再次運(yùn)行。一種優(yōu)化策略是一個(gè)tasklet總在調(diào)度它的處理器上執(zhí)行。調(diào)用tasklet_disable()來禁止某個(gè)指定的tasklet,如果該 tasklet當(dāng)前正在執(zhí)行,這個(gè)函數(shù)會(huì)等到它執(zhí)行完畢再返回。調(diào)用tasklet_disable_nosync()也是來禁止的,只是不用在返回前等 待tasklet執(zhí)行完畢,這么做安全性就不咋嘀了(因?yàn)闆]法估計(jì)該tasklet是否仍在執(zhí)行)。tasklet_enable()激活一個(gè) tasklet??梢允褂胻asklet_kill()函數(shù)從掛起的對(duì)列中去掉一個(gè)tasklet。這個(gè)函數(shù)會(huì)首先等待該tasklet執(zhí)行完畢,然后再 將其移去。當(dāng)然,沒有什么可以阻止其他地方的代碼重新調(diào)度該tasklet。由于該函數(shù)可能會(huì)引起休眠,所以禁止在中斷上下文中使用它。

       接下來的問題,我在前邊說過,對(duì)于軟中斷,內(nèi)核會(huì)選擇幾個(gè)特殊的實(shí)際進(jìn)行處理(常見的是中斷處理程序返回時(shí))。軟中斷被觸發(fā)的頻率有時(shí)會(huì)很好,而且還可能 會(huì)自行重復(fù)觸發(fā),這帶來的結(jié)果就是用戶空間的進(jìn)程無法獲得足夠的處理器時(shí)間,因?yàn)樘幱陴囸I狀態(tài)。同時(shí),如果單純的對(duì)重復(fù)觸發(fā)的軟中斷采取不立即處理的策略 也是無法接受的。兩種極端但完美的情況是什么樣的呢:

1.只要還有被觸發(fā)并等待處理的軟中斷,本次執(zhí)行就要負(fù)責(zé)處理,重新觸發(fā)的軟中斷也在本次執(zhí)行返回前被處理。問題在于,用戶進(jìn)程可能被忽略而使其
   處于饑餓狀態(tài)。
2.選擇不處理重新觸發(fā)的軟中斷。在從中斷返回的時(shí)候,內(nèi)核和平常一樣,也會(huì)檢查所有掛起的軟中斷并處理它們,但是,任何自行重新觸發(fā)的軟中斷都
   不會(huì)馬上處理,它們被放到下一個(gè)軟中斷執(zhí)行時(shí)機(jī)去處理。問題在于新的或重新觸發(fā)的軟中斷必要要等一定的時(shí)間才能被執(zhí)行。

       我現(xiàn)在想的是來個(gè)折衷方案吧,那多好,內(nèi)核開發(fā)者門還真是想到了。內(nèi)核選中的方案是不會(huì)立即處理重新觸發(fā)的軟中斷,作為改進(jìn),當(dāng)大量軟中斷出現(xiàn)的時(shí)候,內(nèi) 核會(huì)喚醒一組內(nèi)核線程來處理這些負(fù)載。這些線程在最低優(yōu)先級(jí)上運(yùn)行(nice值為19)。這種這種方案能夠保證在軟中斷負(fù)擔(dān)很重的時(shí)候用戶程序不會(huì)因?yàn)榈? 不到處理時(shí)間而處理饑餓狀態(tài)。相應(yīng)的,也能保證“過量”的軟中斷終究會(huì)得到處理。最后,在空閑系統(tǒng)上,這個(gè)方案同樣表現(xiàn)良好,軟中斷處理得非常迅速(因?yàn)? 僅存的內(nèi)存線程肯定會(huì)馬上調(diào)度)。為了保證只要有空閑的處理器,它們就會(huì)處理軟中斷,所以給每個(gè)處理器都分配一個(gè)這樣的線程。所有線程的名字都叫做 ksoftirad/n,區(qū)別在于n,它對(duì)應(yīng)的是處理器的編號(hào)。一旦該線程被初始化,它就會(huì)執(zhí)行類似下面這樣的死循環(huán):

1
2
3
4
5
6
7
8
9
10
11
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);
}

       softirq_pending()負(fù)責(zé)發(fā)現(xiàn)是否有待處理的軟中斷。當(dāng)所有需要執(zhí)行的操作都完成以后,該內(nèi)核線程將自己設(shè)置為 TASK_INTERRUPTIBLE狀態(tài),喚起調(diào)度程序選擇其他可執(zhí)行進(jìn)程投入運(yùn)行。最后,只要do_softirq()函數(shù)發(fā)現(xiàn)已經(jīng)執(zhí)行過的內(nèi)核線程 重新觸發(fā)了自己,軟中斷內(nèi)核線程就會(huì)被喚醒。

linux內(nèi)核分析筆記----上半部與下半部(下)

       接著上節(jié)的來,我們?cè)谏瞎?jié)說了軟中斷和tasklet,那這最后就是工作隊(duì)列了哦..

       工作隊(duì)列和前面討論的其他形式都不相同,它可以把工作推后,交由一個(gè)內(nèi)核線程去執(zhí)行----該工作總是會(huì)在進(jìn)程上下文執(zhí)行。這樣,通過工作隊(duì)列執(zhí)行代碼能 占盡進(jìn)程上下文的所有優(yōu)勢(shì),最重要的就是工作隊(duì)列允許重新調(diào)度甚至是睡眠。相比較前邊兩個(gè),這個(gè)選擇起來就很容易了。我說過,前邊兩個(gè)是不允許休眠的,這 個(gè)是允許休眠的,這就很明白了是不?這意味著在你需要獲得大量?jī)?nèi)存的時(shí)候,在你需要獲取信號(hào)量時(shí),在你需要執(zhí)行阻塞式的I/O操作時(shí),它都會(huì)非常有用(先 說話, 這個(gè)不是我說的,是書上這么說的哦)。

       工作隊(duì)列子系統(tǒng)是一個(gè)用于創(chuàng)建內(nèi)核線程的接口,通過它創(chuàng)建的進(jìn)程負(fù)責(zé)執(zhí)行由內(nèi)核其他部分排到隊(duì)列里的任務(wù)。它創(chuàng)建的這些內(nèi)核線程被稱作工作者線程 (worker threads).工作隊(duì)列可以讓你的驅(qū)動(dòng)程序創(chuàng)建一個(gè)專門的工作者線程來處理需要推后的工作。不過,工作隊(duì)列子系統(tǒng)提供了一個(gè)缺省的工作者線程來處理這 些工作。因此,工作隊(duì)列最基本的表現(xiàn)形式就轉(zhuǎn)變成一個(gè)把需要推后執(zhí)行的任務(wù)交給特定的通用線程這樣一種接口。缺省的工作線程叫做event/n.每個(gè)處理 器對(duì)應(yīng)一個(gè)線程,這里的n代表了處理器編號(hào)。除非一個(gè)驅(qū)動(dòng)程序或者子系統(tǒng)必須建立一個(gè)屬于自己的內(nèi)核線程,否則最好還是使用缺省線程。

       1.工作這線程結(jié)構(gòu)用下面的結(jié)構(gòu)表示:

1
2
3
struct workqueue_struct{
    struct cpu_workqueue_struct cpu_wq[NR_CPUS];
}

       結(jié)構(gòu)中數(shù)組的每一項(xiàng)對(duì)應(yīng)系統(tǒng)的一個(gè)CPU.接下來,在看看在kernel/workqueue.c中的核心數(shù)據(jù)結(jié)構(gòu)cpu_workqueue_struct:

1
2
3
4
5
6
7
8
9
10
struct cpu_workqueue_struct{
    spinlock_t lock;
    atomic_t nr_queued;
    struct list_head worklist;
    wait_queue_head_t more_work;
    wait_queue_head_t work_done;
    struct workqueue_struct *wq;
    task_t *thread;
    struct completion exti;
}

       2.表示工作的數(shù)據(jù)結(jié)構(gòu):所有的工作者線程都是用普通的內(nèi)核線程來實(shí)現(xiàn)的,它們都要執(zhí)行worker_thread()函數(shù)。在它初始化完以后,這個(gè)函數(shù) 執(zhí)行一個(gè)死循環(huán)執(zhí)行一個(gè)循環(huán)并開始休眠,當(dāng)有操作被插入到隊(duì)列的時(shí)候,線程就會(huì)被喚醒,以便執(zhí)行這些操作。當(dāng)沒有剩余的時(shí)候,它又會(huì)繼續(xù)休眠。工作有 work_struct(linux/workqueue)結(jié)構(gòu)表示:

1
2
3
4
5
6
7
8
struct work_struct{
    unsigned long pending;
    struct list_head entry;       //連接所有工作的鏈表
    void (*func)(void *);         //處理函數(shù)
    void *data;           //傳遞給處理函數(shù)的參數(shù)
    void *wq_data;
    struct timer_list timer;      //y延遲工作隊(duì)列所用到的定時(shí)器
}

       當(dāng)一個(gè)工作線程被喚醒時(shí),它會(huì)執(zhí)行它的鏈表上的所有工作。工作一旦執(zhí)行完畢,它就將相應(yīng)的work_struct對(duì)象從鏈表上移去,當(dāng)鏈表不再有對(duì)象時(shí),它就繼續(xù)休眠。woker_thread函數(shù)的核心流程如下:

1
2
3
4
5
6
7
8
9
10
11
for(;;){
    set_task_state(current,TASK_INTERRUPTIBLE);
    add_wait_queue(&cwq->more_work,&wait);
    if(list_empty(&cwq->worklist))
        schedule();
    else
        set_task_state(current,TASK_RUNNING);
    remove_wait_queue(&cwq->more_work,&wait);
    if(!list_empty(&cwq->worklist))
        run_workqueue(cwq);
}

       分析一下上面的代碼。首先線程將自己設(shè)置 為休眠狀態(tài)并把自己加入等待隊(duì)列。如果工作對(duì)列是空的,線程調(diào)用schedule()函數(shù)進(jìn)入睡眠狀態(tài)。如果鏈表有對(duì)象,線程就將自己設(shè)為運(yùn)行態(tài),脫離等 待隊(duì)列。然后,再次調(diào)用run_workqueue()執(zhí)行推后的工作。好了,接下來,問題就糾結(jié)在run_workqueue(),它完成實(shí)際推后到此 的工作:

1
2
3
4
5
6
7
8
while(!list_empty(&cwq->worklist)){
    struct work_struct *work = list_entry(cwq->worklist.next,struct work_struct,entry);
    void (*f)(void *) = work->func;
    void *data = work->data;
    list_del_init(cwq->worklist.next);
    clear_bit(0,&work->pending);
    f(data);
}

       該函數(shù)循環(huán)遍歷鏈表上每個(gè)待處理的工作,執(zhí)行鏈表上每個(gè)結(jié)點(diǎn)上的work_struct的func成員函數(shù):

1.當(dāng)鏈表不為空時(shí),選取下一個(gè)節(jié)點(diǎn)對(duì)象。
2.獲取我們希望執(zhí)行的函數(shù)func及其參數(shù)data。
3.把該結(jié)點(diǎn)從鏈表上接下來,將待處理標(biāo)志位pending清0。
4.調(diào)用函數(shù)。
5.重復(fù)執(zhí)行。

       老師說的好:光說不練,不是好漢。現(xiàn)在我們繼續(xù)來看看怎么用吧:

       1.首先,實(shí)際創(chuàng)建一些需要推后完成的工作,可以在編譯時(shí)靜態(tài)地創(chuàng)建該數(shù)據(jù)結(jié)構(gòu):

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

當(dāng)然了,如果愿意,我們當(dāng)然可以在運(yùn)行時(shí)通過指針動(dòng)態(tài)創(chuàng)建一個(gè)工作:

1
INIT_WORK(struct work_struct *work, void (*func)(void *),void *data);

       2.工作隊(duì)列處理函數(shù),會(huì)由一個(gè)工作者線 程執(zhí)行,因此,函數(shù)會(huì)運(yùn)行在進(jìn)程上下文中,默認(rèn)情況下,允許相應(yīng)中斷,并且不持有鎖。如果需要,函數(shù)可以睡眠。需要注意的是,盡管處理函數(shù)運(yùn)行在進(jìn)程上下 文中,但它不能訪問用戶空間,因?yàn)閮?nèi)核線程在用戶空間沒有相應(yīng)的內(nèi)存映射。函數(shù)原型如下:

1
void work_hander(void *data);

       3.對(duì)工作進(jìn)行調(diào)度。前面的準(zhǔn)備工作做完 以后,下面就可以開始調(diào)度了,只需調(diào)用schedule_work(&work).這樣work馬上就會(huì)被調(diào)度,一旦其所在的處理器上的工作者線 程被喚醒,它就會(huì)被執(zhí)行。當(dāng)然如果不想快速執(zhí)行,而是想延遲一段時(shí)間執(zhí)行,按就用 schedule_delay_work(&work,delay);delay是要延遲的時(shí)間節(jié)拍,后面講.

       4.刷新操作。插入隊(duì)列的工作會(huì)在工作者線程下一次被喚醒的時(shí)候執(zhí)行。有時(shí),在繼續(xù)下一步工作之前,你必須保證一些操作已經(jīng)執(zhí)行完畢等等。由于這些原因, 內(nèi)核提供了一個(gè)用于刷新指定工作隊(duì)列的函數(shù):void flush_scheduled_work(void); 這個(gè)函數(shù)會(huì)一直等待,直到隊(duì)列中所有的對(duì)象都被執(zhí)行后才返回。在等待所有待處理的工作執(zhí)行的時(shí)候,該函數(shù)會(huì)進(jìn)入休眠狀態(tài),所以只能在進(jìn)程上下文中使用它。 需要說明的是,該函數(shù)并不取消任何延遲執(zhí)行的工作。取消延遲執(zhí)行的工作應(yīng)該調(diào)用:int cancel_delayed_work(struct work_struct *work);這個(gè)函數(shù)可以取消任何與work_struct 相關(guān)掛起的工作。

       5.創(chuàng)建新的工作隊(duì)列。前邊說過最好使用缺省線程,可如果你堅(jiān)持要使用自己創(chuàng)建的線程,咋辦?這時(shí)你就應(yīng)該創(chuàng)建一個(gè)新的工作隊(duì)列和與之相應(yīng)的工作者線程, 方法很簡(jiǎn)單,用下面的函數(shù):struct workqueue_struct *create_workqueue(const char *name);name是新內(nèi)核線程的名字。這樣就會(huì)創(chuàng)建所有的工作者線程(系統(tǒng)中的每個(gè)處理器都有一個(gè))并且做好所有開始處理工作之前的準(zhǔn)備工作。在創(chuàng) 建之后,就調(diào)用下面的函數(shù)吧:

1
2
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);

       這兩個(gè)函數(shù)和schedule_work()和schedule_delayed_work()相近,唯一的區(qū)別在于它們可以針對(duì)特定的工作隊(duì)列而不是缺省的event隊(duì)列進(jìn)行操作。

       好了,工作隊(duì)列也說完了,我還是結(jié)合前邊一篇,把這三個(gè)地板不實(shí)現(xiàn)的策略比較一下,方便以后選擇.

       首先,tasklet是基于軟中斷實(shí)現(xiàn)的,兩者相近,工作隊(duì)列機(jī)制與它們完全不同,靠?jī)?nèi)核線程來實(shí)現(xiàn)。軟中斷提供的序列化的保障最少,這就要求中斷處理函 數(shù)必須格外小心地采取一些步驟確保共享數(shù)據(jù)的安全,兩個(gè)甚至更多相同類別的軟中斷有可能在不同的處理器上同時(shí)執(zhí)行。如果被考察的代碼本身多線索化的工作做 得非常好,它完全使用單處理器變量,那么軟中斷就是非常好的選擇。對(duì)于時(shí)間要求嚴(yán)格和執(zhí)行效率很高的應(yīng)用來說,它執(zhí)行的也最快。否則選擇tasklets 意義更大。tasklet接口簡(jiǎn)單,而且兩種同種類型的tasklet不能同時(shí)執(zhí)行,所以實(shí)現(xiàn)起來也會(huì)簡(jiǎn)單一些。如果需要把任務(wù)推遲到進(jìn)程上下文中完成, 那你只能選擇工作隊(duì)列了。如果不需要休眠,那軟中斷和tasklet可能更合適。另外就是工作隊(duì)列造成的開銷最大,當(dāng)然這是相對(duì)的,針對(duì)大部分情況,工作 隊(duì)列都能提供足夠的支持。從方便度上考慮就是:工作隊(duì)列,tasklets,最后才是軟中斷。我們?cè)谧鲵?qū)動(dòng)的時(shí)候,關(guān)于這三個(gè)下半部實(shí)現(xiàn),需要考慮兩點(diǎn): 首先,是不是需要一個(gè)可調(diào)度的實(shí)體來執(zhí)行需要推后完成的工作(即休眠的需要),如果有,工作隊(duì)列就是唯一的選擇,否則最好用tasklet。性能如果是最 重要的,那還是軟中斷吧。

       最后,就是一些禁止下半部的相關(guān)部分了,給一個(gè)表:

函數(shù)

描述

void local_bh_disable() 禁止本地處理器的軟中斷和tasklet的處理
void local_bh_enable() 激活本地處理器的軟中斷和tasklet的處理

       這些函數(shù)有可能被嵌套使用----最后被調(diào)用的local_bh_enable()最終激活下半部。函數(shù)通過preempt_count為每個(gè)進(jìn)程維護(hù)一 個(gè)計(jì)數(shù)器。當(dāng)計(jì)數(shù)器變?yōu)?時(shí),下半部才能夠被處理。因?yàn)橄掳氩康奶幚硪呀?jīng)被禁止了,所以local_bh_enable()還需要檢查所有現(xiàn)存的待處理的 下半部并執(zhí)行它們。

       好了,這一次講完了,畫了兩次,我們?cè)谶@兩次中提到了一些同時(shí)發(fā)生的問題,這時(shí)可能存在數(shù)據(jù)共享互斥訪問的問題,這個(gè)就是內(nèi)核同步方面的事情了,我們后面再慢慢說這個(gè)事。


    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多