linux核心分析筆記—-上半部與下半部(上)

來源:互聯網
上載者:User

       嗨,嗨,如果您記性好的話,我在上一篇部落格中提到過這樣一件事:中斷處理是分為兩個部分:中斷處理常式是上半部,它接收到一個中斷,就立即執行,但只做有嚴格時限的工作;而另外被叫做下半部的另外一個部分主要做被允許能稍後完成的工作。這個下半部正是今天的重點。

       下半部的任務就是執行與中斷處理密切相關但中斷處理常式本生身不執行的任務。最好情況當然是中斷處理常式把所有的工作都交給下半部執行,而自己啥都不做。因為我們總是希望中斷處理常式儘可能快的返回。但是,中斷處理常式註定要完成一部分工作。遺憾的是,並沒有誰嚴格規定說什麼任務應該在哪個部分中完成,換句話說,這個決定完全由想我們這樣的驅動工程師來做。記住,中斷處理常式會非同步執行,並且在最好的情況下它也會鎖定當前的中斷線,因此將中斷處理常式縮短的越小就越好。當然啦,沒有規則並不是沒有經驗和教訓:

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

       總之,一句話:中斷處理常式要執行的越快越好。

       我們前邊老是說下半部會在以後執行,那麼這個以後是個什麼概念呢?遺憾的說,這個只是相對於馬上而言的。下半部並需要指定一個明確的時間,只要把這個任務延遲一點,讓他們在系統不太繁忙併且中斷恢複後執行就可以了。通常下半部在中斷處理常式已返回就會馬上執行,關鍵在於當它們運行時,允許相應所有的中斷。

       上半部只能通過中斷處理常式來完成,下半部的實現卻是有很多種方式。這些用來實現下半部的機制分別由不同的介面和子系統組成。最早的是“bottom half”,這種機制也被稱為“BH”,它提供的介面很簡單,提供了一個靜態建立,由32個bottom half組成的鏈表,上半部通過一個32位整數中的一位來標識出哪個bottom half可執行。每個BH都在全域範圍內進行同步,即使分屬於不同的處理器,也不允許任何兩個bottom half同時執行。這種方式使用方便但不夠靈活,簡單卻有效能瓶頸。以需要更好的方法了。第二種方法,任務隊列(task queues).核心定義了一組隊列。其中每個隊列都包含一個由等待調用的函數組成鏈表。根據其所處隊列的位置,這些函數會在某個時刻被執行,驅動程式可根據需要把它們自己的下半部註冊到合適的隊列上去。這種方法已經不錯,但仍然不夠靈活,它沒辦法代替整個BH介面。對於一些效能要求較高的子系統,像網路部分,它也不能勝任。在2.3開發版中,又引入了非強制中斷(softirqs)和tasklet,這裡的非強制中斷和實現系統調用所提到的非強制中斷(軟體中斷)不是同一個概念。如果無須考慮和過去開發的驅動程式相相容的話, 非強制中斷和tasklet可以完全代替BH介面。非強制中斷是一組靜態定義的下半部介面,有32個,可以在所有的處理器上同時執行----即使兩個類型完全相同。task是一種基於非強制中斷實現的靈活性強,動態建立的下半部實現機制。兩個不同類型的tasklet可以在不同的處理器上同時執行,但類型相同的tasklet不能同時執行。tasklet其實是一種在效能和易用性之間尋求平衡的產物。非強制中斷必須在編譯期間就進行靜態註冊,而tasklet可以通過代碼進行動態註冊。現在都是2.6核心了,我們說點新鮮的吧,linux2.6核心提供了三種不同形式的下半部實現機制:非強制中斷,tasklets和工作對列,這些會依次介紹到。這時,可能有人會想到定時器的概念,定時器也確實是這樣,但定時器提供了精確的延遲時間,我們這裡還不至於這樣,所以先放下,我們後面再說定時器。好,下面我開始說說詳細的各種機制:

       1.非強制中斷   實際上非強制中斷使用的並不多,反而是後面的tasklet比較多,但tasklet是通過非強制中斷實現的,非強制中斷的代碼位於/kernel/softirq.c中。非強制中斷是在編譯期間靜態分配的,由softirq_action結構表示,它定義在linux/interrupt.h中:

struct softirq_action{void (*action)(struct softirq_action *);  //待執行的函數void *data;//傳給函數的參數};

kernel/softirq.c中定義了一個包含有32個結構體的數組:

static struct softirq_action softirq_vec[32]

       每個被註冊的非強制中斷都佔據該數組的一項,因此最多可能有32個非強制中斷,這是沒法動態改變的。由於大部分驅動程式都使用tasklet來實現它們的下半部,所以現在的核心中,只用到了6個。上面的非強制中斷結構中,第一項是非強制中斷處理常式,原型如下:

void softirq_handler(struct softirq_action *)

       當核心運行一個非強制中斷處理常式時,它會執行這個action函數,其唯一的參數為指向相應softirq_action結構體的指標。例如,如果my_softirq指向softirq_vec數組的實現,那麼核心會用如下的方式調用非強制中斷處理常式中的函數:

my_softirq->action(my_softirq)

       一個非強制中斷不會搶佔另外一個非強制中斷,實際上,唯一可以搶佔非強制中斷的是中斷處理常式。不過,其他的非強制中斷----甚至是相同類型的非強制中斷-----可以在其他類型的機器上同時執行。一個註冊的非強制中斷必須在被標記後才能執行----觸發非強制中斷(rasing the softirq).通常,中斷處理常式會在返回前標記它的非強制中斷,使其在稍後執行。在下列地方,待處理的非強制中斷會被檢查和執行:

1.處理完一個硬體中斷以後                   2.在ksoftirqd核心線程中                 3.在那些顯示檢查和執行待處理的非強制中斷的代碼中,如網路子系統中。

       非強制中斷會在do_softirq()中執行,如果有待處理的非強制中斷,do_softirq會迴圈遍曆每一個,調用他們的處理常式。核心部分如下:

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);}

       上述代碼會檢查並執行所有待處理的非強制中斷,softirq_pending(),用它來獲得待處理的非強制中斷的32位位元影像-----如果第n位被設定為1,那麼第n位對應類型的非強制中斷等待處理,一旦待處理的非強制中斷位元影像被儲存,就可以將實際的非強制中斷位元影像清零了。pending &1是判斷pending的第一位是否被置為1.一旦pending為0,就表示沒有待處理的中斷了,因為pending最多可能設定32位,迴圈最多也只能執行32位。非強制中斷保留給系統中對時間要求最嚴格以及最重要的下半部使用。所以使用之前還是要想清楚的。下面簡要的說明如何使用非強制中斷:

1.分配索引:在編譯期間,要通過<linux/interrupt.h>中建立的一個枚舉類型來靜態地聲明非強制中斷。核心用這些從0開始的索引來表示一種相對優先順序,
  索引號越小就越先執行。所以,可以根據自己的需要將自己的索引號放在合適的位置。
2.註冊處理常式:接著,在運行時通過調用open_softirq()註冊中斷處理常式,例如網路子系統,如下:
open_softirq(NET_TX_SOFTIRQ,net_tx_action,NULL);open_softirq(NET_RX_SOFTIRQ,net_rx_action,NULL);

  函數有三個參數,非強制中斷索引號,處理函數和data域存放的數組。非強制中斷處理常式執行的時候,允許響應中斷,但它自己不能休眠。在一個處理常式運
  行的時候,當前處理器的非強制中斷被禁止,但其他的處理器仍可以執行別的非強制中斷。實際上,如果一個非強制中斷在它被執行的時候同時再次被觸發了,那麼
  另外一個處理器可以同時運行其處理常式。這意味著對共用資料都需要嚴格的鎖保護。大部分非強制中斷處理常式都通過採取單處理資料(僅屬於某一個處
  理器的資料)或者其他一些技巧來避免顯示的加鎖,從而提供更出色的效能。
3.觸發非強制中斷:經過上面兩項,新的非強制中斷處理常式就能夠運行,raist_softirq(中斷索引號)函數可以將一個非強制中斷設定為掛起狀態,從而讓它在下次調
  用do_softirq()函數時投入運行。該函數在觸發一個非強制中斷之前先要禁止中斷,觸發後再恢複回原來的狀態,如果中斷本來就已經被禁止了,那麼可以調用另一函數raise_softirq_irqoff(),這會帶來一些最佳化效果。

       在中斷處理常式中觸發非強制中斷是最常見的形式,中斷處理常式執行硬體裝置的相關操作,然後觸發相應的非強制中斷,最後退出。核心在執行完中斷處理常式後,馬上就會調用do_softirq()函數。於是,非強制中斷就開始執行中斷處理常式留給它去完成的剩下任務。

       2.Tasklets:tasklet是通過非強制中斷實現的,所以它們本身也是非強制中斷。它由兩類非強制中斷代表:HI_SOFTIRQ和TASKLET_SOFTIRQ.區別在於前者會先於後者執行。Tasklets由tasklet_struct結構表示,每個結構體單獨代表一個tasklet,在linux/interrupt.h中定義:

struct tasklet_struct{struct tasklet_struct *next;unsigned long state;atomic_t count;void (*func)(unsigned long);unsigned long data;};

       結構體中func成員是tasklet的處理常式,data是它唯一的參數。state的取值只能是0,TASKLET_STATE_SCHED(表明tasklet已經被調度,正準備投入運行)和TASKLET_STATE_RUN(表明該tasklet正在運行,它只有在多處理器的系統上才會作為一種最佳化來使用,單一處理器系統任何時候都清楚單個tasklet是不是正在運行)之間取值。count是tasklet的引用計數器,如果不為0,則tasklet被禁止,不允許執行;只有當它為0時,tasklet才被啟用,並且被設定為掛起狀態時,該tasklet才能夠執行。已調度的tasklet(等同於被觸發的非強制中斷)存放在兩個單一處理器資料結構:tasklet_vec(普通tasklet)和task_hi_vec高優先順序的tasklet)中,這兩個資料結構都是由tasklet_struct結構體構成的鏈表,鏈表中的每個tasklet_struct代表一個不同的tasklet。Tasklets由tasklet_schedule()和tasklet_hi_schedule()進行調度,它們接收一個指向tasklet_struct結構的指標作為參數。兩個函數非常相似(區別在於一個使用TASKLET_SOFTIRQ而另外一個使用HI_SOFTIRQ).有關tasklet_schedule()的操作細節:

1.檢查tasklet的狀態是否為TASKLET_STATE_SCHED.如果是,說明tasklet已經被調度過了,函數返回。
2.儲存中斷狀態,然後禁止本地中斷。在執行tasklet代碼時,這麼做能夠保證處理器上的資料不會弄亂。
3.把需要調度的tasklet加到每個處理器一個的tasklet_vec鏈表或task_hi_vec鏈表的表頭上去。
4.喚起TASKLET_SOFTIRQ或HI_SOFTIRQ非強制中斷,這樣在下一次調用do_softirq()時就會執行該tasklet。
5.恢複中斷到原狀態並返回。

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

1.禁止中斷,並為當前處理器檢索tasklet_vec或tasklet_hi_vec鏈表。
2.將當前處理器上的該鏈表設定為NULL,達到清空的效果。
3.運行相應中斷。
4.迴圈遍曆獲得鏈表上的每一個待處理的tasklet。
5.如果是多處理器系統,通過檢查TASKLET_STATE_RUN來判斷這個tasklet是否正在其他處理器上運行。如果它正在運行,那麼現在就不要執行,跳
   到下一個待處理的tasklet去。
6.如果當前這個tasklet沒有執行,將其狀態設定為TASKLETLET_STATE_RUN,這樣別的處理器就不會再去執行它了。
7.檢查count值是否為0,確保tasklet沒有被禁止。如果tasklet被禁止,則跳到下一個掛起的tasklet去。
8.現在可以確定這個tasklet沒有在其他地方執行,並且被我們設定為執行狀態,這樣它在其他部分就不會被執行,並且引用計數器為0,現在可以執行
   tasklet的處理常式了。
9.重複執行下一個tasklet,直至沒有剩餘的等待處理的tasklets。

       說了這麼多,我們該怎樣使用這個tasklet呢,這個我在linux裝置驅動理論帖講的太多了。但別急,下邊為了部落格完整,我仍然會大致講講:

       1.聲明自己的tasklet:投其所好,既可以靜態也可以動態建立,這取決於選擇是想有一個對tasklet的直接引用還是間接引用。靜態建立方法(直接引用),可以使用下列兩個宏的一個(在linux/interrupt.h中定義):

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

       這兩個宏之間的區別在於引用計數器的初始值不同,前面一個把建立的tasklet的引用計數器設定為0,使其處於啟用狀態,另外一個將其設定為1,處于禁止狀態。而動態建立(間接引用)的方式如下:

tasklet_init(t,tasklet_handler,dev);

       2.編寫tasklet處理常式:函數類型是void tasklet_handler(unsigned long data).因為是靠非強制中斷實現,所以tasklet不能休眠,也就是說不能在tasklet中使用訊號量或者其他什麼阻塞式的函數。由於tasklet運行時允許響應中斷,所以必須做好預防工作,如果新加入的tasklet和中斷處理常式之間共用了某些資料額的話。兩個相同的tasklet絕不能同時執行,如果新加入的tasklet和其他的tasklet或者非強制中斷共用了資料,就必須要進行適當地鎖保護。

       3.調度自己的tasklet:前邊的工作一做完,接下來就剩下調度了。通過一下方法實現:tasklet_schedule(&my_tasklet).在tasklet被調度以後,只要有合適的機會就會得到運行。如果在還沒有得到運行機會之前,如果有一個相同的tasklet又被調度了,那麼它仍然只會運行一次。如果這時已經開始運行,那麼這個新的tasklet會被重新調度並再次運行。一種最佳化策略是一個tasklet總在調度它的處理器上執行。調用tasklet_disable()來禁止某個指定的tasklet,如果該tasklet當前正在執行,這個函數會等到它執行完畢再返回。調用tasklet_disable_nosync()也是來禁止的,只是不用在返回前等待tasklet執行完畢,這麼做安全性就不咋嘀了(因為沒法估計該tasklet是否仍在執行)。tasklet_enable()啟用一個tasklet。可以使用tasklet_kill()函數從掛起的對列中去掉一個tasklet。這個函數會首先等待該tasklet執行完畢,然後再將其移去。當然,沒有什麼可以阻止其他地方的代碼重新調度該tasklet。由於該函數可能會引起休眠,所以禁止在中斷上下文中使用它。

       接下來的問題,我在前邊說過,對於非強制中斷,核心會選擇幾個特殊的實際進行處理(常見的是中斷處理常式返回時)。非強制中斷被觸發的頻率有時會很好,而且還可能會自行重複觸發,這帶來的結果就是使用者空間的進程無法獲得足夠的處理器時間,因為處於饑餓狀態。同時,如果單純的對重複觸發的非強制中斷採取不立即處理的策略也是無法接受的。兩種極端但完美的情況是什麼樣的呢:

1.只要還有被觸發並等待處理的非強制中斷,本次執行就要負責處理,重新觸發的非強制中斷也在本次執行返回前被處理。問題在於,使用者進程可能被忽略而使其
   處於饑餓狀態。
2.選擇不處理重新觸發的非強制中斷。在從中斷返回的時候,核心和平常一樣,也會檢查所有掛起的非強制中斷並處理它們,但是,任何自行重新觸發的非強制中斷都
   不會馬上處理,它們被放到下一個非強制中斷執行時機去處理。問題在於新的或重新觸發的非強制中斷必要要等一定的時間才能被執行。

       我現在想的是來個折衷方案吧,那多好,核心開發人員門還真是想到了。核心選中的方案是不會立即處理重新觸發的非強制中斷,作為改進,當大量非強制中斷出現的時候,核心會喚醒一組核心線程來處理這些負載。這些線程在最低優先順序上運行(nice值為19)。這種這種方案能夠保證在非強制中斷負擔很重的時候使用者程式不會因為得不到處理時間而處理饑餓狀態。相應的,也能保證“過量”的非強制中斷終究會得到處理。最後,在空閑系統上,這個方案同樣表現良好,非強制中斷處理得非常迅速(因為僅存的記憶體線程肯定會馬上調度)。為了保證只要有閒置處理器,它們就會處理非強制中斷,所以給每個處理器都分配一個這樣的線程。所有線程的名字都叫做ksoftirad/n,區別在於n,它對應的是處理器的編號。一旦該線程被初始化,它就會執行類似下面這樣的死迴圈:

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()負責發現是否有待處理的非強制中斷。當所有需要執行的操作都完成以後,該核心線程將自己設定為TASK_INTERRUPTIBLE狀態,喚起發送器選擇其他可執行進程投入運行。最後,只要do_softirq()函數發現已經執行過的核心線程重新觸發了自己,非強制中斷核心線程就會被喚醒。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.