Linux內核爲什麼會發生soft lockup?【轉】

轉自:https://blog.csdn.net/21cnbao/article/details/108250786

提到soft lockup,大家都不會陌生:

BUG: soft lockup - CPU#3 stuck for 23s! [kworker/3:0:32]

這個幾乎和panic,oops並列,也是非常難以排查甚至比panic更麻煩。至少panic之後你可以去分析一個靜態的屍體,然而soft lockup,那是一個動態的過程,甚至轉瞬即逝,自帶自愈功能。

那麼soft lockup是由於什麼原因導致的呢?

幾乎沒有這方面的文章,能找到的也只有個別的案例分析,所以我想趁着週末降至來寫一篇關於soft lockup的通用解釋。

首先澄清兩個關於soft lockup的誤區:

  • soft lockup並不僅僅是由死循環引起的。

  • soft lockup並不是說在一段代碼裏執行了23秒,22秒。

這裏簡單解釋一下上面的兩點。

事實上,死循環並不一定會導致soft lockup,比如Linux內核生命週期內的0號進程就是一個死循環,此外很多的內核線程都是死循環。

此外,更難指望一段代碼可以執行20多秒,要對現代計算機的速度有所概念。

soft lockup發生的真實場景是:

  1. soft lockup是針對單獨CPU而不是整個系統的。

  2. soft lockup指的是發生的CPU上在20秒(默認)中沒有發生調度切換。

第一點無須解釋,下面重點看第二點。

很顯然, 只要讓一個CPU在20秒左右的時間內都不發生進程切換,就會觸發soft lockup ,這個 “20秒內不切換” 就是soft lockup發生的根因!

好了,現在我們來看20秒不切換的場景。

  • 死循環的情況
    這是最簡單的場景,但細節往往不像看起來那麼簡單。比如你寫了一個死循環在內核中執行,它一定會導致soft lockup嗎?

我們來看一個內核死循環:

  1.  
    #include <linux/module.h>
  2.  
    #include <linux/kthread.h>
  3.  
     
  4.  
     
  5.  
    static int loop_func(void *arg)
  6.  
    {
  7.  
    int i = 0;
  8.  
    while(!kthread_should_stop()) {
  9.  
    i++;
  10.  
    }
  11.  
    return 0;
  12.  
    }
  13.  
     
  14.  
     
  15.  
    struct task_struct *kt;
  16.  
    static int __init init_loop(void)
  17.  
    {
  18.  
    kt = kthread_run(loop_func, NULL, "loop_thread");
  19.  
    if (IS_ERR(kt)) {
  20.  
    return -1;
  21.  
    }
  22.  
     
  23.  
     
  24.  
    return 0;
  25.  
    }
  26.  
     
  27.  
     
  28.  
    static void __exit exit_test(void)
  29.  
    {
  30.  
    kthread_stop(kt);
  31.  
    }
  32.  
     
  33.  
     
  34.  
    module_init(init_loop);
  35.  
    module_exit(exit_loop);
  36.  
    MODULE_LICENSE("GPL");
  37.  
     
  38.  
     

加載這個模塊,會soft lockup嗎?

我們知道,雖然loop thread是一個死循環,但是它看起來正如一個普通用戶態進程一樣,在執行i++循環的時候,其實是可以被其它task搶佔掉的,這是最基本的進程調度的常識。

但是如果你真的去加載這個模塊,你會發現在有些機器上,它確實會soft lockup,但有的機器上不會,這又是爲什麼?

這裏的關鍵在於 內核搶佔 。你看下自己系統內核的配置文件,如果下面的配置打開,意味着上述模塊的死循環不會造成soft lockup:

CONFIG_PREEMPT=y

如果這個配置沒有開,那麼便 刑不上內核 了,因爲它在內核態執行,所以沒有誰可以搶佔它,進而發生soft lockup。

我們對上述的死循環代碼是否會觸發soft lockup已經很明確了,下面我們看另一種情況。

如果死循環不在內核線程上下文,而是在軟中斷上下文,會怎樣?

很顯然,軟中斷不能被進程搶佔,所以一定會soft lockup。

當然,如果真的發生了死循環導致的soft lockup,那肯定是在一個循環代碼中執行超過20秒了,不說20秒,如果無人干涉,200000秒都是有的…

現在我們來看另一種複雜的情況,即timer的情況。在討論timer時,我假設系統的內核搶佔是開啓的,這樣更容易分類討論,否則,如果關閉了內核搶佔,那麼事情會變得更加嚴重。

  • timer的情況

我們先看下面的timer回調函數:

  1.  
    static void timer_func(unsigned long data)
  2.  
    {
  3.  
    mdelay(1);
  4.  
    mod_timer(&timer, jiffies + 200);
  5.  
    }

僅僅執行1ms的函數,它會導致超過20秒不調度切換的soft lockup嗎?

初看,應該不會,但是如果我們詳細看了Linux內核timer的執行原理,就會明白:

  • pending在一個CPU上的所有過期timer是順序遍歷執行的。

  • 一輪timer的順序遍歷執行是持有自旋鎖的。

這意味着在執行一輪過期timer的過程中,watchdog實時線程將無法被調度從而餵狗,這意味着:

  • 同一CPU上的過期timer積累到一定量,其回調函數的延時之和大於20秒,將會soft lockup。

我們需要進一步瞭解一下Linux timer的工作機制。

可以把timer的執行過程抽象成下面的邏輯:

  1.  
    run_timers()
  2.  
    {
  3.  
    while (now > base.early_jiffies) {
  4.  
    for_each_timer(timer, base.list) {
  5.  
    detach_timer(timer)
  6.  
    forward_early_jiffies(base)
  7.  
    call_timer_fn(timer)
  8.  
    }
  9.  
    }
  10.  
    }

很簡單的流程,內核把當前過期的timer執行到結束。run_timers可以在軟中斷上下文中執行,也可以在softirqd內核線程上下文中執行,爲了營造soft lockup,我們假設它是在時鐘中斷退出時的軟中斷上下文中執行的(記住之前還有個假設,即系統是開啓內核搶佔的!),此時,run_timers不能被watchdog搶佔。

如果一個timer中耗時1ms,那麼一個循環需要20000個timer遍歷執行,才能湊齊20秒的不能被搶佔的時間,進而引發soft lockup。我的天,20000個timer,不可思議!

其實根本就不需要20000個timer,200個足矣!

問題就出現在call_timer_fn,它實際上是調用該timer回調函數的封裝!我們知道,timer回調函數中執行了mod_timer的操作,它的邏輯如下:

  1.  
    mod_timer(timer, expires)
  2.  
    {
  3.  
    list_add_timer(timer, expires, base.list)
  4.  
    }

它事實上是把timer又插回了list,如果我們把這個list看作是一條時間線的話,它事實上只是往後移了expires這麼遠的距離:

假設所有timer的expire都是固定的常量,如果:

  • 我們的timer的足夠多,多到按照其expires重新requeue時恰好能填補中間的那段空隙。

  • 我們的timer回調函數耗時恰好和timer的expires流逝速率相一致。

那麼,兩個甚至多個batch就合併成了一個batch,這意味着一輪timer的執行將不會結束!

我們來試一下:

#include <linux/module.h>
#include <linux/slab.h>
#include <asm-generic/delay.h>
#include <linux/kernel.h>
#include <linux/kallsyms.h>
#include <linux/delay.h>

 
static int stop = 1;
 
 
// timer的數量
static int size = 1;
module_param(size, int, 0644);
MODULE_PARM_DESC(size, "size");
 
 
// timer的expires
static int interval = 200;
module_param(interval, int, 0644);
MODULE_PARM_DESC(interval, "");
 
 
// 回調函數耗時
static int dt = 100;
module_param(dt, int, 0644);
MODULE_PARM_DESC(dt, "");

struct wrapper {
  struct timer_list timer;
  spinlock_t lock;
};

struct wrapper *wr;

static void timer_func(unsigned long data)
{
  int i = data;
  struct wrapper *w = &wr[i];
 
 
  spin_lock_bh(&(w->lock));
  if (stop == 0) {
    udelay(dt); // 以忙等模擬耗時
  }
  spin_unlock_bh(&(w->lock));

 
  w->timer.data = i;
  if (stop == 0) {
    mod_timer(&(w->timer), jiffies + interval);
  }
}
 
 
static int __init maint_init(void)
{
  int i;
 
 
  wr = (struct wrapper *)kzalloc(size*sizeof(struct wrapper), GFP_KERNEL);
 
 
  for (i = 0; i < size; i++) {
    struct wrapper *w = &wr[i];
    spin_lock_init(&(w->lock));
    init_timer(&(w->timer));
    w->timer.expires = jiffies + 20;
    w->timer.function = timer_func;
    w->timer.data = i;
    add_timer(&(w->timer));
  }
  stop = 0;
 
 
  return 0;
}
 
 
static void __exit maint_exit(void)
{
  int i;
 
 
  stop = 1;
  udelay(100);
  for (i = 0; i < size; i++) {
    struct wrapper *w = &wr[i];
    del_timer_sync(&(w->timer));
  }
  kfree(wr);
 
 
}
 
 
module_init(maint_init);
module_exit(maint_exit);
MODULE_LICENSE("GPL");

 

 

我的測試虛擬機HZ爲1000,這意味1ms將會產生一次時鐘中斷,我們以每個timer函數持鎖執行1ms,一共400個timer來加載模塊,看下結果:

單核跑滿,這意味着timer已經拼接成龍,20秒後,我們將看到soft lockup:

事實上,每個timer回調函數delay 800us,一共200個timer即可觸發soft lockup!使用這個代碼,你基本可以確定你要測試的機器的timer執行時間的安全閾值。

這就是timer導致的soft lockup的動力學。

關於HZ1000
1ms間隔的時鐘中斷對於服務器而言是悲哀的,1ms的時間無法容納太多的timer,也不允許每個timer中有哪怕稍微的合理耗時,1ms一次中斷很容易觸發run_timers在軟中斷上下文中被執行,但很遺憾,這就是事實。

拋開timer不談,HZ1000更多的意義在於快速響應事件而不是增加系統吞吐,這對服務器的單機性能是有傷害的!


說了這麼多,現在讓我們考慮一下現實。

除了不要在內核中寫死循環之外,我們也不應該讓timer回調函數執行過久,特別是系統中timer特別多,且expires特別短的情況下。

回到現實中,我們來看一個實例。

假設你使用的內核版本還不支持TCP的lockless listener,那麼我們特別要注意一個函數,即 inet_csk_reqsk_queue_prune :

  • 這是一個在TCP的per listener的timer中執行的函數。

  • 這個函數的實現採用兩層循環,循環耗時取決於:

  1. 外層循環:該listener的backlog大小,受程序配置控制。

  2. 內層循環:該listener的半連接隊列的大小,受系統快照控制。

如果系統中的listener特別多,在收到SYN掃描攻擊時,特別容易陷入soft lockup的深淵!幸運的是,這個問題已經在TCP lockless listener的版本中修了,它的效果如下:

  • 將per listener的半連接隊列timer換成了per request timer,減少了回調函數處理耗時。

  • per request timer增加了timer的數量,會不會抵消縮短回調耗時帶來的收益,需要攻擊來驗證。

我們看一個相關issue和patch:
https://patchwork.ozlabs.org/patch/452426/


好了,再次回到核心主題。

觸發soft lockup的當然不止死循環和timer,我只是用這兩個來說明soft lockup的動力學,即 超過2倍的kernel.watchdog_thresh時間不能進行進程調度,就會觸發soft lockup告警。 至於說 stuck for 23s! 那只是表象,並不是如其字面表達的那樣,23秒的時間在執行一段代碼。

此外,頻繁的spinlock,rwlock也會導致soft lockup,我這有一個關於IPv6路由查詢機制的實例,詳情參見:
https://blog.csdn.net/dog250/article/details/91046131

總之,所有的情況將不勝枚舉,也不可能通過一篇文章來展示,所以說,遇到此類問題,還是要有一個明確的排查思路或者說範式,才能快速定位問題的根因並且解決之。

當然了,經理並不關注這些爛八七糟的東西。


浙江溫州皮鞋溼,下雨進水不會胖。

版權聲明:本文爲博主原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。本文鏈接:https://blog.csdn.net/dog250/article/details/104997385

(END)

Linux閱碼場原創精華文章彙總

更多精彩,盡在"Linux閱碼場",掃描下方二維碼關注

分享、在看與點贊,至少我要擁有一個吧

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章