歡迎光臨
每天分享高質量文章

宋寶華: 是誰關閉了Linux搶佔,而搶佔又關閉了誰?

是誰殺了我,而我又殺了誰。
——《武林外傳》



前言

本人的目的在於解惑,把讀者從糊裡糊塗的狀態解救出來。本文對比分析:

  • preempt_disable() 

  • local_irq_disable()/local_irq_save(flags)

  • spin_lock()

  • spin_lock_irq()/spin_lock_irqsave(lock, flags)

哪些關閉了搶佔?另外,再說清楚,搶佔又關閉了誰。


首先,把這幾個API的關係圖再勾勒一下。

我們理解,spin_lock()會呼叫preempt_disable() 導致本核的搶佔排程被關閉(preempt_disable函式實際增加preempt_count來達到此效果),其次我們理解spin_lock_irq()是local_irq_disable()+preempt_disable()的合體。

下麵一幅圖描述了幾個函式之間的包含關係如下:




local_irq_disable()/local_irq_save()的disable和save版的唯一區別是,要不要儲存CPU對中斷的遮蔽狀態。

spin_lock_irq()/spin_lock_irqsave(lock, flags)的唯一區別是,要不要儲存CPU對中斷的遮蔽狀態。


是誰殺了搶佔?

Kernel的程式碼明確顯示,執行搶佔排程的時候,會同時檢測“non-zero preempt_count or interrupts are disabled”:

我們可以進一步展開preemptible():

對於ARM處理器而言,判斷irqs_disabled(),其實就是判斷CPSR中的IRQMASK_I_BIT是否被設定。

所以,我們得出一個結論,前言這一節裡面,列出的所有函式,都能關閉本核的搶佔排程。因為,無論是preempt_count計數狀態,還是中斷被關閉,都會導致kernel認為無法搶佔!


通殺邏輯如下:




殺手的差異在哪裡?

既然都關閉了搶佔,那麼區別在哪裡呢?

我們看兩段程式碼,假設下麵的程式碼都發生在NICE為0的普通行程:

preempt_disable()

xxx(1)

preempt_enable()

local_irq_disable()

xxx(2)

local_irq_enable()

首先,xxx(1)和xxx(2)裡面,都是不可以搶佔的。一個搞定了preempt_count,一個搞定了中斷。

但是假設xxx(1)內喚醒了一個高優先順序的RT任務,那麼在preempt_enable()的時刻,直接就是一個搶佔點,這個時候,發生schedule,高優先順序RT任務進來跑;假設xxx(2)內喚醒了一個高優先順序的RT任務,那麼在local_irq_enable()的時刻,不是一個搶佔點,高優先順序RT的任務必須等待下一個搶佔點。下一個搶佔點,可能是時鐘tick處理傳回、中斷傳回、軟中斷結束、yield()等等多種情況。


在preempt_enable()中,會執行一次preempt_schedule():


而local_irq_enable()只是單純的開啟CPU對中斷的響應,對於ARM而言,它就是:


再來看大boss,spin_lock_irq是同時呼叫了preempt_disable和local_irq_disable:

而對應的spin_unlock_irq()則同時呼叫了local_irq_enable()和preempt_enable():

大家想一想,為何preempt_enable()比local_irq_enable()後發生呢?如果程式碼順序是這樣的:

preempt_enable()

local_irq_enable()

第一句preempt_enable()想執行搶佔排程的話,即便呼叫了preempt_schedule(),但是由於IRQ還是關門的,preempt_schedule()函式會立即傳回(詳見《是誰殺了搶佔?》一節),所以無法搶佔;後一句local_irq_enable()不會執行搶佔排程。所以,如果這麼乾的話,

spin_lock_irq()

xxx(3)

spin_unlock_irq()

如果xxx(3)喚醒了高優先順序的RT,在spin_unlock_irq()的時刻,將無法直接搶佔!

還好,真正的順序是:

local_irq_enable()

preempt_enable()

所以,在spin_unlock_irq()的時刻,RT行程就換入執行了。


看小一點的boss,spin_lock():

spin_lock()

xxx(4)

spin_unlock()

如果xxx(4)喚醒了RT行程,在spin_unlock()的時刻,會立即搶入。因為spin_unlock()會呼叫preempt_enable()。


而搶佔又殺了誰?

理論上,關搶佔,並沒有徹底的關閉排程器,因為行程還是可以主動地sleep:

上述程式碼,在spin_lock的區間裡面,呼叫了msleep(),這個時候,不屬於搶佔,Linux還是會pick下一個task來跑。

不過這樣的程式碼,一般在後期蘊藏著巨大的風險,導致後期的莫名崩潰。所以呢,實際的工程裡面,我們是嚴格地禁止的。

建議大家開啟Kernel裡面的config裡面的DEBUG_ATOMIC_SLEEP,一旦出現這種情況,讓kernel去彙報錯誤。

這種情況下,kernel檢測到有人在atomic背景關係裡面執行可能睡眠的行為,會直接報執行的棧回溯。

贊(0)

分享創造快樂