1. 程式人生 > >自旋 鎖與多核 單核 時間片 搶佔核心 非搶佔核心

自旋 鎖與多核 單核 時間片 搶佔核心 非搶佔核心

(1) linux上的自旋鎖有三種實現:
          1. 在單cpu,不可搶佔核心中,自旋鎖為空操作。
          2. 在單cpu,可搶佔核心中,自旋鎖實現為“禁止核心搶佔”,並不實現“自旋”。
          3. 在多cpu,可搶佔核心中,自旋鎖實現為“禁止核心搶佔” + “自旋”。

(2) 關於搶佔式核心與非搶佔式核心:
          在非搶佔式核心中,如果一個程序在核心態執行,其只有在以下兩種情況會被切換:
          1.  其執行完成(返回使用者空間)
          2.  主動讓出cpu(即主動呼叫schedule或核心中的任務阻塞——這同樣也會導致呼叫schedule)


          在搶佔式核心中,如果一個程序在核心態執行,其只有在以下四種情況會被切換:
          1.  其執行完成(返回使用者空間)
          2.  主動讓出cpu(即主動呼叫schedule或核心中的任務阻塞——這同樣也會導致呼叫schedule)
          3.  當從中斷處理程式正在執行,且返回核心空間之前(此時可搶佔標誌premptcount須為0) 。
          4.  當核心程式碼再一次具有可搶佔性的時候,如解鎖及使能軟中斷等。

禁止核心搶佔只是關閉“可搶佔標誌”,而不是禁止程序切換。顯式使用schedule或程序阻塞(此也會導致呼叫schedule)時,還是會發生程序排程的。


一種死鎖情況:
死鎖是有可能發生的。
死鎖發生在多核的情況,下面來分析一下:
首先,對於多核搶佔與多核非搶佔的情況,在使用自旋鎖時,其情況基本是一致的。
因為在多核搶佔的情況下,使用自旋鎖會禁止核心搶佔,這樣多核搶佔就相當於多核非搶佔的情況。

那下面就只分析多核非搶佔的情況。
假設系統有A,B兩個CPU。
A上正在執行的a程序已獲得自旋鎖,並在臨界區執行。
B上正在執行的b程序企圖獲得自旋鎖,但由於自旋鎖已被佔用,於是b程序在B CPU上“自旋”空轉。

這時,如果在A上的a程序因程式阻塞,而被休眠。接著A會切換執行另一程序c。
若這個程序c也企圖獲取自旋鎖,c程序同樣會因為鎖已被佔用,而在A上“自旋”空轉。
這時候,A上的a程序與c程序就形成了死鎖。a程序需要被c程序佔用的CPU,c程序需要被a程序佔用的鎖。


至於在單cpu核心上不會出現上述情況,因為單cpu上的自旋鎖實際沒有“自旋功能”。

假如持有自旋鎖時程序的時間片用完了的話,是該如何來處理呢? 在單處理機上,spin_lock()就退化成了Preemp_disable(),它就是禁止搶佔,即雖然時間片用完了,但是仍然是不能切換到其它程序去的。因此每個程序有一個preemp_count這個變數,如果這個變數為0的話表示其可以被其它程序所搶佔,如果大於0則不能夠被搶佔,而在呼叫preemp_disable()的時候就會將該程序的preemp_count值加1。使得其不能夠被搶佔。而繼續執行該程序,直到呼叫preemp_enable()將其設為可搶佔,由於時間一般不長,因此不會有影響。 1、自旋鎖作用與基本使用方法? 與其他鎖一樣,自旋鎖也用於保護臨界區,但是自旋鎖主要是用於在SMP上保護臨界區。在SMP上,自旋鎖最多隻能被一個可執行執行緒持有,如果一個執行緒嘗試獲得一個被爭用的自旋鎖,該執行緒將一直旋轉(while迴圈)直到鎖可用;如果鎖未被爭用,請求鎖的執行執行緒將立刻爭用它,並繼續執行。 LINUX下自旋鎖的基本使用方法: 宣告鎖:
spinlock_t lock;
初始化:
lock = SPIN_LOCK_UNLOCKED;
     或者
spin_lock_init(&lock);
加鎖有4個介面,3個會阻塞,1個不阻塞
spin_lock(&lock);//獲取自旋鎖
spin_lock_irq(&lock);//關中斷,獲取自旋鎖,不建議使用
spin_lock_irqsave(&lock, flags);//關中斷,儲存中斷狀態,獲取自旋鎖
spin_trylock(&lock);//與spin_lock一樣,但是獲取不到的時候不阻塞,返回非0
對應的解鎖介面有3個
spin_unlock(&lock);//spin_lock和spin_trylock都用該介面解鎖
spin_unlock_irq(&lock);//不建議使用
spin_unlock_irqrestore(&lock, flags);
另外還提供了一個獲取鎖狀態的介面:
spin_is_locked(&lock);//如果指定的鎖被獲取,返回非0,否則,返回0
2、在SMP和UP上的不同表現? 對於SMP,自旋鎖將在禁止搶佔後,while迴圈自旋直到鎖可用; 對於UP,自旋鎖的行為有點不一樣,具體表現為:
  • 核心不支援搶佔,則spin_lock是個空函式,spin_unlock也是空函式;
  • 核心支援搶佔,則spin_lock關搶佔,spin_unlock開搶佔。
要分析SMP和UP的區別,還得先理解UP下核心支援搶佔與否的區分。 在不支援搶佔的時候,核心排程時機為:
  • 核心程式碼一直要執行到完成(返回使用者空間);
  • 阻塞或主動呼叫schedule();
支援核心搶佔的核心,則在以下情況會發生搶佔:
  • 從中斷處理程式回到核心空間且核心具有搶佔性時;
  • 當核心程式碼再一次具有可搶佔性時;(如時鐘中斷,發現程序時間片已經用完,則發生程序搶佔)
  • 核心顯式呼叫schedule();
  • 核心中的任務阻塞(同樣導致schedule());
從以上區別可以看出,若是核心不支援搶佔,只要臨界區的程式碼不阻塞,就能保證原子性,所以spin_lock/spin_unlock是空函式。 而對於支援搶佔的核心, spin_lock/spin_unlock自然只需要關閉/恢復搶佔功能即可。 3、自旋鎖與上下文 由於自旋鎖不睡眠,既可以用於程序上下文,也可用於非程序上下文,這是它與訊號量相比的一個優勢。 正常來講,如果自旋鎖都是在程序上下文中使用,則建議使用spin_lock/spin_unlock。 若在中斷處理例程中也要使用自旋鎖,則所有爭用同一個鎖的地方應使用spin_lock_irqsave/spin_unlock_irqrestore。 若沒有在中斷而在下半部使用自旋鎖,則所有急用同一個鎖的地方可以使用spin_lock_bh/spin_unlock_bh,這對函式平時用得比較少。 注:自旋鎖用在中斷例程中,若是程序上下文中的核心程式碼使用spin_lock,則在臨界區,可能發生中斷 ,要麼原子性被破壞(UP),要麼造成死鎖(SMP). 4、使用spin_lock()後為什麼不能睡眠? 在UP下,正如前面所說,原子性得不到保證。 而在SMP下,則可能發生死鎖: 假設有3個程序A,B,C,2個CPU稱CPU0,CPU1. CPU0上A程序獲取自旋鎖進入睡眠,排程了B程序,B程序將自旋;  CPU1上排程了C程序也將自旋。 B等著A釋放鎖,A等著B釋放CPU,自旋後的CPU不能發生排程,CPU0和CPU1將一直自旋下去,形成了死鎖。 5、強調:鎖什麼? 使用鎖的時候一定要對症下藥,明確保護的是資料而不是程式碼,這是使用鎖的正確思考方式。經常看別人程式碼的人應該會有所體會,針對程式碼加鎖常常使程式碼難以理解,特別是複雜的程式,往往加鎖沒做好,引起競爭條件。 不防在使用鎖的時候先思考一下自己要保護什麼,然後給對應的鎖取跟資料相關的名稱。比如"int foo;spinlock_t foo_lock;"表示foo由foo_lock加鎖;

/* -------------------------------------------- 認為不錯------------------------------------------*/

中斷處理函式中,在獲取鎖之前,必須先禁止本地中斷。否則,持有鎖的核心程式碼會被中斷處理程式打斷,接著試圖去爭用這個已經被持有的自旋鎖。這樣的結果是,中斷處理函式自旋,等待該鎖重新可用,但是鎖的持有者在該中斷處理程式執行完畢之前不可能執行,這就成為了雙重請求死鎖。注意,需要關閉的只是當前處理器上的中斷。因為中斷髮生在不同的處理器上,即使中斷處理程式在同一鎖上自旋,也不會妨礙鎖的持有者(在不同處理器上)最終釋放

核心提供的禁止中斷同時請求鎖的方法很簡單,如下:

  1. DEFINE_SPINLOCK(mr_lock);  
  2. unsigned long flags;  
  3. spin_lock_irqsave(&mr_lock,flags);  
  4. /*臨界區*/
  5. spin_unlock_irqrestore(&mr_lock,flags);  

函式spin_lock_irqsave():儲存中斷的當前狀態,禁止本地中斷,然後獲取指定的鎖。

函式spin_unlock_reqrestore():對指定的鎖解鎖,讓中斷恢復到加鎖前的狀態。所以即使中斷最初是被禁止的,程式碼也不會錯誤地啟用它們。


自旋鎖可分為用在單核處理器上和用在多核處理器上。

單核處理器: 用在單核處理器上,又可分為兩種: 1.系統不支援核心搶佔 此時自旋鎖什麼也不做,確實也不需要做什麼,因為單核處理器只有一個執行緒在執行,又不支援核心搶佔,因此資源不可能會被其他的執行緒訪問到。 2.系統支援核心搶佔 這種情況下,自旋鎖加鎖僅僅是禁止了核心搶佔,解鎖則是啟用了核心搶佔。 在上述兩種情況下,在獲取自旋鎖後可能會發生中斷,若中斷處理程式去訪問自旋鎖所保護的資源,則會發生死鎖。因此,linux核心又提供了spin_lock_irq()和spin_lock_irqsave(),這兩個函式會在獲取自旋鎖的同時(同時禁止核心搶佔),禁止本地外部可遮蔽中斷,從而保證自旋鎖的原子操作。 多核處理器: 多核處理器意味著有多個執行緒可以同時在不同的處理器上並行執行。舉個例子: 四核處理器,若A處理器上的執行緒1獲取了鎖,B、C兩個處理器恰好這個時候也要訪問這個鎖保護的資源,因此他倆CPU就一直自旋忙等待。D並不需要這個資源,因此它可以正常處理其他事情。 自旋鎖的幾個特點: 1.被自旋鎖保護的臨界區程式碼執行時不能睡眠。單核處理器下,獲取到鎖的執行緒睡眠,若恰好此時CPU排程的另一個執行執行緒也需要獲取這個鎖,則會造成死鎖;多核處理器下,若想獲取鎖的執行緒在同一個處理器下,同樣會造成死鎖,若位於另外的處理器,則會長時間佔用CPU等待睡眠的執行緒釋放鎖,從而浪費CPU資源。 2.被自旋鎖保護的臨界區程式碼執行時不能被其他中斷打斷。原因同上類似。 3.被自旋鎖保護的臨界區程式碼在執行時,核心不能被搶佔,亦同上類似。

為何自旋鎖會自旋? 

假設:求鎖的程序 由於請求自旋鎖 而使自己在 程序執行就緒佇列上 優先順序變高了 普通程序就沒有出頭之日

如果這個程序是實時程序,將會一直忙等待持有鎖
比如SCHED_FIFO級別的程序不受時間片限制,可以一直執行下去。只要它不阻塞或主動顯示放棄cpu,普通程序就沒有出頭之日

轉:http://blog.sina.com.cn/s/blog_98ee3a930102wg2x.html

2個排程器

可以用兩種方法來啟用排程

  • 一種是直接的, 比如程序打算睡眠或出於其他原因放棄CPU

  • 另一種是通過週期性的機制, 以固定的頻率執行, 不時的檢測是否有必要

因此當前linux的排程程式由兩個排程器組成:主排程器週期性排程器(兩者又統稱為通用排程器(generic scheduler)核心排程器(core scheduler))

並且每個排程器包括兩個內容:排程框架(其實質就是兩個函式框架)及排程器類

6種排程策略

linux核心目前實現了6中排程策略(即排程演算法), 用於對不同型別的程序進行排程, 或者支援某些特殊的功能

  • SCHED_NORMAL和SCHED_BATCH排程普通的非實時程序

  • SCHED_FIFO和SCHED_RR和SCHED_DEADLINE則採用不同的排程策略排程實時程序

  • SCHED_IDLE則在系統空閒時呼叫idle程序.

5個排程器類

而依據其排程策略的不同實現了5個排程器類, 一個排程器類可以用一種種或者多種排程策略排程某一類程序, 也可以用於特殊情況或者排程特殊功能的程序.

其所屬程序的優先順序順序為

stop_sched_class -> dl_sched_class -> rt_sched_class -> fair_sched_class -> idle_sched_class
  • 1

3個排程實體

排程器不限於排程程序, 還可以排程更大的實體, 比如實現組排程.

這種一般性要求排程器不直接操作程序, 而是處理可排程實體, 因此需要一個通用的資料結構描述這個排程實體,即seched_entity結構, 其實際上就代表了一個排程物件,可以為一個程序,也可以為一個程序組.

linux中針對當前可排程的實時和非實時程序, 定義了型別為seched_entity的3個排程實體

  • sched_dl_entity 採用EDF演算法排程的實時排程實體

  • sched_rt_entity 採用Roound-Robin或者FIFO演算法排程的實時排程實體

  • sched_entity 採用CFS演算法排程的普通非實時程序的排程實體

1.2 主排程器與核心/使用者搶佔

1.2.1 排程過程中關閉核心搶佔

我們在上一篇linux核心主排程器schedule(文章連結, CSDNGithub)中在分析主排程器的時候, 我們會發現核心在進行排程之前都會通過preempt_disable關閉核心搶佔, 而在完成排程工作後, 又會重新開啟核心搶佔

    do {
        preempt_disable();                                  /*  關閉核心搶佔  */
        __schedule(false);                                  /*  完成排程  */
        sched_preempt_enable_no_resched();                  /*  開啟核心搶佔  */

    } while (need_resched());   /*  如果該程序被其他程序設定了TIF_NEED_RESCHED標誌,則函式重新執行進行排程    */
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

這個很容易理解, 我們在核心完成排程器過程中, 這時候如果發生了核心搶佔, 我們的排程會被中斷, 而排程卻還沒有完成, 這樣會丟失我們排程的資訊.

1.2.2 排程完成檢查need_resched看是否需要重新排程

而同樣我們可以看到, 在排程完成後, 核心會去判斷need_resched條件, 如果這個時候為真, 核心會重新程序一次排程.

這個的原因, 我們在前一篇部落格中, 也已經說的很明白了,

核心在thread_info的flag中設定了一個標識來標誌程序是否需要重新排程, 即重新排程need_resched標識TIF_NEED_RESCHED, 核心在即將返回使用者空間時會檢查標識TIF_NEED_RESCHED標誌程序是否需要重新排程,如果設定了,就會發生排程, 這被稱為使用者搶佔

2 非搶佔式和可搶佔式核心

為了簡化問題,我使用嵌入式實時系統uC/OS作為例子

首先要指出的是,uC/OS只有核心態,沒有使用者態,這和Linux不一樣

多工系統中, 核心負責管理各個任務, 或者說為每個任務分配CPU時間, 並且負責任務之間的通訊.

核心提供的基本服務是任務切換. 排程(Scheduler),英文還有一詞叫dispatcher, 也是排程的意思.

這是核心的主要職責之一, 就是要決定該輪到哪個任務運行了. 多數實時核心是基於優先順序排程法的, 每個任務根據其重要程度的不同被賦予一定的優先順序. 基於優先順序的排程法指,CPU總是讓處在就緒態的優先順序最高的任務先執行. 然而, 究竟何時讓高優先順序任務掌握CPU的使用權, 有兩種不同的情況, 這要看用的是什麼型別的核心, 是不可剝奪型的還是可剝奪型核心

2.1 非搶佔式核心

非搶佔式核心是由任務主動放棄CPU的使用權

非搶佔式排程法也稱作合作型多工, 各個任務彼此合作共享一個CPU. 非同步事件還是由中斷服務來處理. 中斷服務可以使一個高優先順序的任務由掛起狀態變為就緒狀態.

但中斷服務以後控制權還是回到原來被中斷了的那個任務, 直到該任務主動放棄CPU的使用權時,那個高優先順序的任務才能獲得CPU的使用權。非搶佔式核心如下圖所示.

非搶佔式核心

非搶佔式核心的優點有

  • 中斷響應快(與搶佔式核心比較);

  • 允許使用不可重入函式;

  • 幾乎不需要使用訊號量保護共享資料, 執行的任務佔有CPU,不必擔心被別的任務搶佔。這不是絕對的,在印表機的使用上,仍需要滿足互斥條件。

非搶佔式核心的缺點有

  • 任務響應時間慢。高優先順序的任務已經進入就緒態,但還不能執行,要等到當前執行著的任務釋放CPU

  • 非搶佔式核心的任務級響應時間是不確定的,不知道什麼時候最高優先順序的任務才能拿到CPU的控制權,完全取決於應用程式什麼時候釋放CPU

2.2 搶佔式核心

使用搶佔式核心可以保證系統響應時間. 最高優先順序的任務一旦就緒, 總能得到CPU的使用權。當一個執行著的任務使一個比它優先順序高的任務進入了就緒態, 當前任務的CPU使用權就會被剝奪,或者說被掛起了,那個高優先順序的任務立刻得到了CPU的控制權。如果是中斷服務子程式使一個高優先順序的任務進入就緒態,中斷完成時,中斷了的任務被掛起,優先順序高的那個任務開始執行。

搶佔式核心如下圖所示

搶佔式核心

搶佔式核心的優點有

  • 使用搶佔式核心,最高優先順序的任務什麼時候可以執行,可以得到CPU的使用權是可知的。使用搶佔式核心使得任務級響應時間得以最優化。 
    搶佔式核心的缺點有:

  • 不能直接使用不可重入型函式。呼叫不可重入函式時,要滿足互斥條件,這點可以使用互斥型訊號量來實現。如果呼叫不可重入型函式時,低優先順序的任務CPU的使用權被高優先順序任務剝奪,不可重入型函式中的資料有可能被破壞。

3 linux使用者搶佔

3.1 linux使用者搶佔

當核心即將返回使用者空間時, 核心會檢查need_resched是否設定, 如果設定, 則呼叫schedule(),此時,發生使用者搶佔.

3.2 need_resched標識

核心如何檢查一個程序是否需要被排程呢?

核心在即將返回使用者空間時檢查程序是否需要重新排程,如果設定了,就會發生排程, 這被稱為使用者搶佔, 因此核心在thread_info的flag中設定了一個標識來標誌程序是否需要重新排程, 即重新排程need_resched標識TIF_NEED_RESCHED

並提供了一些設定可檢測的函式

函式 描述 定義
set_tsk_need_resched 設定指定程序中的need_resched標誌
clear_tsk_need_resched 清除指定程序中的need_resched標誌
test_tsk_need_resched 檢查指定程序need_resched標誌

而我們核心中排程時常用的need_resched()函式檢查程序是否需要被重新排程其實就是通過test_tsk_need_resched實現的, 其定義如下所示

// http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.6#L3093
static __always_inline bool need_resched(void)
{
    return unlikely(tif_need_resched());
}

// http://lxr.free-electrons.com/source/include/linux/thread_info.h?v=4.6#L106
#define tif_need_resched() test_thread_flag(TIF_NEED_RESCHED)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

3.3 使用者搶佔的發生時機(什麼時候需要重新排程need_resched)

一般來說,使用者搶佔發生幾下情況:

  • 從系統呼叫返回使用者空間;

  • 從中斷(異常)處理程式返回使用者空間

從這裡我們可以看到, 使用者搶佔是發生在使用者空間的搶佔現象.

更詳細的觸發條件如下所示, 其實不外乎就是前面所說的兩種情況: 從系統呼叫或者中斷返回使用者空間

  1. 時鐘中斷處理例程檢查當前任務的時間片,當任務的時間片消耗完時,scheduler_tick()函式就會設定need_resched標誌;

  2. 訊號量、等到佇列、completion等機制喚醒時都是基於waitqueue的,而waitqueue的喚醒函式為default_wake_function,其呼叫try_to_wake_up將被喚醒的任務更改為就緒狀態並設定need_resched標誌。

  3. 設定使用者程序的nice值時,可能會使高優先順序的任務進入就緒狀態;

  4. 改變任務的優先順序時,可能會使高優先順序的任務進入就緒狀態;

  5. 新建一個任務時,可能會使高優先順序的任務進入就緒狀態;

  6. 對CPU(SMP)進行負載均衡時,當前任務可能需要放到另外一個CPU上執行

4 linux核心搶佔

4.1 核心搶佔的概念

對比使用者搶佔, 顧名思義, 核心搶佔就是指一個在核心態執行的程序, 可能在執行核心函式期間被另一個程序取代.

4.2 為什麼linux需要核心搶佔

linux系統中, 程序在系統呼叫後返回使用者態之前, 或者是核心中某些特定的點上, 都會呼叫排程器. 這確保除了一些明確指定的情況之外, 核心是無法中斷的, 這不同於使用者程序.

如果核心處於相對耗時的操作中, 比如檔案系統或者記憶體管理相關的任務, 這種行為可能會帶來問題. 這種情況下, 核心代替特定的程序執行相當長的時間, 而其他程序無法執行, 無法排程, 這就造成了系統的延遲增加, 使用者體驗到”緩慢”的響應. 比如如果多媒體應用長時間無法得到CPU, 則可能發生視訊和音訊漏失現象.

在編譯核心時如果啟用了對核心搶佔的支援, 則可以解決這些問題. 如果高優先順序程序有事情需要完成, 那麼在啟用了核心搶佔的情況下, 不僅使用者空間應用程式可以被中斷, 核心也可以被中斷,

linux核心搶佔是在Linux2.5.4版本釋出時加入的, 儘管使核心可搶佔需要的改動特別少, 但是該機制不像搶佔使用者空間程序那樣容易實現. 如果核心無法一次性完成某些操作(例如, 對資料結構的操作), 那麼可能出現靜態條件而使得系統不一致.

核心搶佔和使用者層程序被其他程序搶佔是兩個不同的概念, 核心搶佔主要是從實時系統中引入的, 在非實時系統中的確也能提高系統的響應速度, 但也不是在所有情況下都是最優的,因為搶佔也需要排程和同步開銷,在某些情況下甚至要關閉核心搶佔, 比如前面我們將主排程器的時候, linux核心在完成排程的過程中是關閉了核心搶佔的.

核心不能再任意點被中斷, 幸運的是, 大多數不能中斷的點已經被SMP實現標識出來了. 並且在實現核心搶佔時可以重用這些資訊. 如果核心可以被搶佔, 那麼單處理器系統也會像是一個SMP系統

4.3 核心搶佔的發生時機

要滿足什麼條件,kernel才可以搶佔一個任務的核心態呢?

  • 沒持有鎖。鎖是用於保護臨界區的,不能被搶佔。

  • Kernel code可重入(reentrant)。因為kernel是SMP-safe的,所以滿足可重入性。

核心搶佔發生的時機,一般發生在:

  1. 當從中斷處理程式正在執行,且返回核心空間之前。當一箇中斷處理例程退出,在返回到核心態時(kernel-space)。這是隱式的呼叫schedule()函式,當前任務沒有主動放棄CPU使用權,而是被剝奪了CPU使用權。

  2. 當核心程式碼再一次具有可搶佔性的時候,如解鎖(spin_unlock_bh)及使能軟中斷(local_bh_enable)等, 此時當kernel code從不可搶佔狀態變為可搶佔狀態時(preemptible again)。也就是preempt_count從正整數變為0時。這也是隱式的呼叫schedule()函式

  3. 如果核心中的任務顯式的呼叫schedule(), 任務主動放棄CPU使用權

  4. 如果核心中的任務阻塞(這同樣也會導致呼叫schedule()), 導致需要呼叫schedule()函式。任務主動放棄CPU使用權

核心搶佔,並不是在任何一個地方都可以發生,以下情況不能發生

  1. 核心正進行中斷處理。在Linux核心中程序不能搶佔中斷(中斷只能被其他中斷中止、搶佔,程序不能中止、搶佔中斷),在中斷例程中不允許進行程序排程。程序排程函式schedule()會對此作出判斷,如果是在中斷中呼叫,會打印出錯資訊。

  2. 核心正在進行中斷上下文的Bottom Half(中斷下半部,即軟中斷)處理。硬體中斷返回前會執行軟中斷,此時仍然處於中斷上下文中。如果此時正在執行其它軟中斷,則不再執行該軟中斷。

  3. 核心的程式碼段正持有spinlock自旋鎖、writelock/readlock讀寫鎖等鎖,處幹這些鎖的保護狀態中。核心中的這些鎖是為了在SMP系統中短時間內保證不同CPU上執行的程序併發執行的正確性。當持有這些鎖時,核心不應該被搶佔。

  4. 核心正在執行排程程式Scheduler。搶佔的原因就是為了進行新的排程,沒有理由將排程程式搶佔掉再執行排程程式。

  5. 核心正在對每個CPU“私有”的資料結構操作(Per-CPU date structures)。在SMP中,對於per-CPU資料結構未用spinlocks保護,因為這些資料結構隱含地被保護了(不同的CPU有不一樣的per-CPU資料,其他CPU上執行的程序不會用到另一個CPU的per-CPU資料)。但是如果允許搶佔,但一個程序被搶佔後重新排程,有可能排程到其他的CPU上去,這時定義的Per-CPU變數就會有問題,這時應禁搶佔。

5 核心搶佔的實現

5.1 核心如何跟蹤它能否被搶佔?

前面我們提到了, 系統中每個程序都有一個特定於體系結構的struct thread_info結構, 使用者層程式被排程的時候會檢查struct thread_info中的need_resched標識TLF_NEED_RESCHED標識來檢查自己是否需要被重新排程.

自然核心搶佔·也可以應用同樣的方法被實現, linux核心在thread_info結構中添加了一個自旋鎖標識preempt_count, 稱為搶佔計數器(preemption counter).

struct thread_info
{
    /*  ......  */
    int preempt_count;   /* 0 => preemptable, <0 => BUG */
    /*  ......  */
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
preempt_count值 描述
0
禁止核心搶佔, 其值標記了使用preempt_count的臨界區的數目
0 開啟核心搶佔
<0 鎖為負值, 核心出現錯誤

還有其他函式可用於開啟和關閉核心搶佔

函式 描述 定義
preempt_disable 通過preempt_count_inc來停用核心搶佔, 並且通過路障barrier同步來避免編譯器的優化
preempt_enable preempt_count_dec_and_test啟用核心搶佔, 然後通過__preempt_schedule檢測是夠有必要進行排程
preempt_check_resched 呼叫__preempt_schedule檢測是夠有必要進行排程
should_resched 檢查current的搶佔計數器是否為引數preempt_offset的值, 同時檢查 tif_need_resched是否為真

5.2 核心如何知道是否需要搶佔?

首先必須設定了TLF_NEED_RESCHED標識來通知核心有程序在等待得到CPU時間, 然後會在判斷搶佔計數器preempt_count是否為0, 這個工作往往通過preempt_check_resched或者其相關來實現

5.2.1 重新啟用核心搶佔時使用preempt_schedule檢查搶佔

在核心停用搶佔後重新啟用時, 檢測是否有程序打算搶佔當前執行的核心程式碼, 是一個比較好的時機, 如果是這樣, 應該儘快完成, 則無需等待下一次對排程器的例行呼叫.

搶佔機制中主要的函式是preempt_schedule, 設定了TIF_NEED_RESCHED標誌並不能保證可以搶佔核心, 核心可能處於臨界區, 不能被幹擾

//  http://lxr.free-electrons.com/source/kernel/sched/core.c?v=4.6#L3307

/*
 * this is the entry point to schedule() from in-kernel preemption
 * off of preempt_enable. Kernel preemptions off return from interrupt
 * occur there and call schedule directly.
 */
asmli