提到soft lockup,大家都不會陌生:
BUG:softlockup-CPU#3stuckfor23s![kworker/332]
這個幾乎和panic,oops并列,也是非常難以排查甚至比panic更麻煩。至少panic之后你可以去分析一個靜態(tài)的尸體,然而soft lockup,那是一個動態(tài)的過程,甚至轉(zhuǎn)瞬即逝,自帶自愈功能。
那么soft lockup是由于什么原因?qū)е碌哪兀?/p>
幾乎沒有這方面的文章,能找到的也只有個別的案例分析,所以我想趁著周末降至來寫一篇關(guān)于soft lockup的通用解釋。
首先澄清兩個關(guān)于soft lockup的誤區(qū):
soft lockup并不僅僅是由死循環(huán)引起的。
soft lockup并不是說在一段代碼里執(zhí)行了23秒,22秒。
這里簡單解釋一下上面的兩點。
事實上,死循環(huán)并不一定會導(dǎo)致soft lockup,比如Linux內(nèi)核生命周期內(nèi)的0號進(jìn)程就是一個死循環(huán),此外很多的內(nèi)核線程都是死循環(huán)。
此外,更難指望一段代碼可以執(zhí)行20多秒,要對現(xiàn)代計算機(jī)的速度有所概念。
soft lockup發(fā)生的真實場景是:
soft lockup是針對單獨CPU而不是整個系統(tǒng)的。
soft lockup指的是發(fā)生的CPU上在20秒(默認(rèn))中沒有發(fā)生調(diào)度切換。
第一點無須解釋,下面重點看第二點。
很顯然,只要讓一個CPU在20秒左右的時間內(nèi)都不發(fā)生進(jìn)程切換,就會觸發(fā)soft lockup,這個“20秒內(nèi)不切換”就是soft lockup發(fā)生的根因!
好了,現(xiàn)在我們來看20秒不切換的場景。
死循環(huán)的情況
這是最簡單的場景,但細(xì)節(jié)往往不像看起來那么簡單。比如你寫了一個死循環(huán)在內(nèi)核中執(zhí)行,它一定會導(dǎo)致soft lockup嗎?
我們來看一個內(nèi)核死循環(huán):
#include
加載這個模塊,會soft lockup嗎?
我們知道,雖然loop thread是一個死循環(huán),但是它看起來正如一個普通用戶態(tài)進(jìn)程一樣,在執(zhí)行i++循環(huán)的時候,其實是可以被其它task搶占掉的,這是最基本的進(jìn)程調(diào)度的常識。
但是如果你真的去加載這個模塊,你會發(fā)現(xiàn)在有些機(jī)器上,它確實會soft lockup,但有的機(jī)器上不會,這又是為什么?
這里的關(guān)鍵在于內(nèi)核搶占。你看下自己系統(tǒng)內(nèi)核的配置文件,如果下面的配置打開,意味著上述模塊的死循環(huán)不會造成soft lockup:
CONFIG_PREEMPT=y
如果這個配置沒有開,那么便刑不上內(nèi)核了,因為它在內(nèi)核態(tài)執(zhí)行,所以沒有誰可以搶占它,進(jìn)而發(fā)生soft lockup。
我們對上述的死循環(huán)代碼是否會觸發(fā)soft lockup已經(jīng)很明確了,下面我們看另一種情況。
如果死循環(huán)不在內(nèi)核線程上下文,而是在軟中斷上下文,會怎樣?
很顯然,軟中斷不能被進(jìn)程搶占,所以一定會soft lockup。
當(dāng)然,如果真的發(fā)生了死循環(huán)導(dǎo)致的soft lockup,那肯定是在一個循環(huán)代碼中執(zhí)行超過20秒了,不說20秒,如果無人干涉,200000秒都是有的…
現(xiàn)在我們來看另一種復(fù)雜的情況,即timer的情況。在討論timer時,我假設(shè)系統(tǒng)的內(nèi)核搶占是開啟的,這樣更容易分類討論,否則,如果關(guān)閉了內(nèi)核搶占,那么事情會變得更加嚴(yán)重。
timer的情況
我們先看下面的timer回調(diào)函數(shù):
static void timer_func(unsigned long data){ mdelay(1); mod_timer(&timer, jiffies + 200);}
僅僅執(zhí)行1ms的函數(shù),它會導(dǎo)致超過20秒不調(diào)度切換的soft lockup嗎?
初看,應(yīng)該不會,但是如果我們詳細(xì)看了Linux內(nèi)核timer的執(zhí)行原理,就會明白:
pending在一個CPU上的所有過期timer是順序遍歷執(zhí)行的。
一輪timer的順序遍歷執(zhí)行是持有自旋鎖的。
這意味著在執(zhí)行一輪過期timer的過程中,watchdog實時線程將無法被調(diào)度從而喂狗,這意味著:
同一CPU上的過期timer積累到一定量,其回調(diào)函數(shù)的延時之和大于20秒,將會soft lockup。
我們需要進(jìn)一步了解一下Linux timer的工作機(jī)制。
可以把timer的執(zhí)行過程抽象成下面的邏輯:
run_timers(){ while (now > base.early_jiffies) { for_each_timer(timer, base.list) { detach_timer(timer) forward_early_jiffies(base) call_timer_fn(timer) } }}
很簡單的流程,內(nèi)核把當(dāng)前過期的timer執(zhí)行到結(jié)束。run_timers可以在軟中斷上下文中執(zhí)行,也可以在softirqd內(nèi)核線程上下文中執(zhí)行,為了營造soft lockup,我們假設(shè)它是在時鐘中斷退出時的軟中斷上下文中執(zhí)行的(記住之前還有個假設(shè),即系統(tǒng)是開啟內(nèi)核搶占的?。藭r,run_timers不能被watchdog搶占。
如果一個timer中耗時1ms,那么一個循環(huán)需要20000個timer遍歷執(zhí)行,才能湊齊20秒的不能被搶占的時間,進(jìn)而引發(fā)soft lockup。我的天,20000個timer,不可思議!
其實根本就不需要20000個timer,200個足矣!
問題就出現(xiàn)在call_timer_fn,它實際上是調(diào)用該timer回調(diào)函數(shù)的封裝!我們知道,timer回調(diào)函數(shù)中執(zhí)行了mod_timer的操作,它的邏輯如下:
mod_timer(timer, expires){ list_add_timer(timer, expires, base.list)}
它事實上是把timer又插回了list,如果我們把這個list看作是一條時間線的話,它事實上只是往后移了expires這么遠(yuǎn)的距離:
假設(shè)所有timer的expire都是固定的常量,如果:
我們的timer的足夠多,多到按照其expires重新requeue時恰好能填補(bǔ)中間的那段空隙。
我們的timer回調(diào)函數(shù)耗時恰好和timer的expires流逝速率相一致。
那么,兩個甚至多個batch就合并成了一個batch,這意味著一輪timer的執(zhí)行將不會結(jié)束!
我們來試一下:
#include
我的測試虛擬機(jī)HZ為1000,這意味1ms將會產(chǎn)生一次時鐘中斷,我們以每個timer函數(shù)持鎖執(zhí)行1ms,一共400個timer來加載模塊,看下結(jié)果:
單核跑滿,這意味著timer已經(jīng)拼接成龍,20秒后,我們將看到soft lockup:
事實上,每個timer回調(diào)函數(shù)delay 800us,一共200個timer即可觸發(fā)soft lockup!使用這個代碼,你基本可以確定你要測試的機(jī)器的timer執(zhí)行時間的安全閾值。
這就是timer導(dǎo)致的soft lockup的動力學(xué)。
關(guān)于HZ1000
1ms間隔的時鐘中斷對于服務(wù)器而言是悲哀的,1ms的時間無法容納太多的timer,也不允許每個timer中有哪怕稍微的合理耗時,1ms一次中斷很容易觸發(fā)run_timers在軟中斷上下文中被執(zhí)行,但很遺憾,這就是事實。
拋開timer不談,HZ1000更多的意義在于快速響應(yīng)事件而不是增加系統(tǒng)吞吐,這對服務(wù)器的單機(jī)性能是有傷害的!
說了這么多,現(xiàn)在讓我們考慮一下現(xiàn)實。
除了不要在內(nèi)核中寫死循環(huán)之外,我們也不應(yīng)該讓timer回調(diào)函數(shù)執(zhí)行過久,特別是系統(tǒng)中timer特別多,且expires特別短的情況下。
回到現(xiàn)實中,我們來看一個實例。
假設(shè)你使用的內(nèi)核版本還不支持TCP的lockless listener,那么我們特別要注意一個函數(shù),即inet_csk_reqsk_queue_prune:
這是一個在TCP的per listener的timer中執(zhí)行的函數(shù)。
這個函數(shù)的實現(xiàn)采用兩層循環(huán),循環(huán)耗時取決于:
外層循環(huán):該listener的backlog大小,受程序配置控制。
內(nèi)層循環(huán):該listener的半連接隊列的大小,受系統(tǒng)快照控制。
如果系統(tǒng)中的listener特別多,在收到SYN掃描攻擊時,特別容易陷入soft lockup的深淵!幸運(yùn)的是,這個問題已經(jīng)在TCP lockless listener的版本中修了,它的效果如下:
將per listener的半連接隊列timer換成了per request timer,減少了回調(diào)函數(shù)處理耗時。
per request timer增加了timer的數(shù)量,會不會抵消縮短回調(diào)耗時帶來的收益,需要攻擊來驗證。
我們看一個相關(guān)issue和patch:
https://patchwork.ozlabs.org/patch/452426/
好了,再次回到核心主題。
觸發(fā)soft lockup的當(dāng)然不止死循環(huán)和timer,我只是用這兩個來說明soft lockup的動力學(xué),即超過2倍的kernel.watchdog_thresh時間不能進(jìn)行進(jìn)程調(diào)度,就會觸發(fā)soft lockup告警。至于說stuck for 23s!那只是表象,并不是如其字面表達(dá)的那樣,23秒的時間在執(zhí)行一段代碼。
此外,頻繁的spinlock,rwlock也會導(dǎo)致soft lockup,我這有一個關(guān)于IPv6路由查詢機(jī)制的實例,詳情參見:
https://blog.csdn.net/dog250/article/details/91046131
總之,所有的情況將不勝枚舉,也不可能通過一篇文章來展示,所以說,遇到此類問題,還是要有一個明確的排查思路或者說范式,才能快速定位問題的根因并且解決之。
當(dāng)然了,經(jīng)理并不關(guān)注這些爛八七糟的東西。
-
cpu
+關(guān)注
關(guān)注
68文章
11080瀏覽量
217159 -
Linux
+關(guān)注
關(guān)注
87文章
11511瀏覽量
213880
原文標(biāo)題:Linux內(nèi)核為什么會發(fā)生soft lockup?
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
網(wǎng)絡(luò)光纖出問題一般是什么原因導(dǎo)致的呢

評論