在多核心上編譯時,什麼可能導致 make 掛起?

在多核心上編譯時,什麼可能導致 make 掛起?

昨天我正在嘗試編譯來自來源的包。由於我是在 6 核怪物機器上編譯它,所以我決定繼續使用make -j 6.一開始編譯非常順利且非常快,但在某些時候make僅在一個核心上使用 100% CPU 時就會掛起。

我做了一些谷歌搜尋並發現在 ROOT 留言板上發文。由於我自己組裝了這台電腦,所以我擔心我沒有正確安裝散熱器,CPU 過熱或其他問題。不幸的是,我工作的地方沒有可以把它放進去的冰箱。

我安裝了該lm-sensors軟體包並make -j 6再次運行,這次監視 CPU 溫度。儘管溫度很高(接近 60°C),但從未超過高溫或臨界溫度。

我嘗試運行make -j 4,但在編譯過程中的某個時候再次make掛起,這次是在不同的位置。

最後我編譯了一下,運行了一下make,效果很好。我的問題是:為什麼它掛了?由於它停在兩個不同的位置,我猜這是由於某種競爭條件,但我認為make應該足夠聰明,讓一切都按正確的順序排列,因為它提供了選項-j

答案1

我沒有這個確切問題的答案,但我可以嘗試向您提供可能發生的情況的提示:Makefile 中缺少依賴項。

例子:

target: a.bytecode b.bytecode
    link a.bytecode b.bytecode -o target

a.bytecode: a.source
    compile a.source -o a.bytecode

b.bytecode: b.source
    compile b.source a.bytecode -o a.bytecode

如果你調用make target一切都會正確編譯。a.source首先(任意但確定性地)執行的編譯。然後b.source進行編譯。

但如果make -j2 target兩個compile命令將並行運行。你實際上會注意到你的 Makefile 的依賴關係被破壞了。第二次編譯假設a.bytecode已經編譯,但它沒有出現在依賴項中。所以很可能會發生錯誤。正確的依賴行應該b.bytecode是:

b.bytecode: b.source a.bytecode

回到你的問題,如果你不幸運,指令可能會因為缺少依賴項而掛在 100% CPU 迴圈中。這可能是這裡發生的情況,順序建構無法揭示缺少的依賴關係,但並行建構已經揭示了它。

答案2

我意識到這是一個非常老的問題,但它仍然出現在搜尋結果的頂部,所以這是我的解決方案:

GNU make 有一個 jobserver 機制來確保 make 及其遞歸子程序不會消耗超過指定數量的核心: http://make.mad-scientist.net/papers/jobserver-implementation/

它依賴所有進程共享的管道。每個想要分叉額外子程序的進程必須先消耗管道中的令牌,然後在完成後放棄它們。如果子進程不傳回它消耗的令牌,則頂層 make while 將永遠掛起,等待它們返回。

https://bugzilla.redhat.com/show_bug.cgi?id=654822

我在 Solaris 機器上使用 GNU make 建置 binutils 時遇到此錯誤,其中「sed」不是 GNU sed。透過修改 PATH 使 sed==gsed 優先於系統 sed 解決了該問題。不過,我不知道為什麼 sed 會消耗管道中的令牌。

答案3

make似乎造成了僵局。使用ps -ef,這些進程似乎是罪魁禍首:

根 695 615 1 22:18 ? 00:00:00 進行預先建置-j32
根 2127 695 20 22:18 ? 00:00:04 make -f Makefile.prenobuild

如果檢查每個進程正在做什麼,子進程正在寫入檔案描述符 4,而父進程正在等待所有子進程退出:

root@ltzj2-6hl3t-b98zz:/# strace -p 2127
strace:附加進程 2127
寫(4,“+”,1
root@ltzj2-6hl3t-b98zz:/# strace -p 695
strace:附加進程 695
{{等待4(-1, }}

文件描述符 4 恰好是一個管道:

root@ltzj2-6hl3t-b98zz:/# ls -la /proc/2127/fd/4
l-wx------ 1 root root 64 Sep 3 22:22 /proc/2127/fd/4 -> 'pipe:[1393418985]'

此管道僅位於父進程和子進程之間:

root@ltzj2-6hl3t-b98zz:/# lsof | grep 1393418985
使 695 根 3r FIFO 0,12 0t0 1393418985 管
使 695 根 4w FIFO 0,12 0t0 1393418985 管
使 2127 根 3r FIFO 0,12 0t0 1393418985 管
使 2127 根 4w FIFO 0,12 0t0 1393418985 管

因此,看起來 2127 試圖將輸出新增至管道回到 695,但 695 處於掛起狀態wait4(),因此它永遠不會清空該管道。

如果我使用 cat 從 shell 中清空管道,那麼建置將按預期恢復並完成...

root@ltzj2-6hl3t-b98zz:/# cat /proc/695/fd/3
+++++++++++++++++++++++++++++++++

建置解除阻塞並繼續運行...


我最初的理解是錯誤的,但經過更多調查後,我最終發現了這個 Linux 核心缺陷:

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=46c4c9d1beb7f5b4cec4dd90e7728720583ee348

關於如何掛起的確切解釋如下:https://lore.kernel.org/lkml/1628086770.5rn8p04n6j.none@localhost/

您可以透過將以下解決方法應用於 gnu make 原始碼來解決這個等待內核補丁的問題:

--- a/src/posixos.c 2020-01-02 23:11:27.000000000 -0800
+++ b/src/posixos.c 2021-09-18 09:12:02.786563319 -0700
@@ -179,8 +179,52 @@
 jobserver_release(int is_fatal)
 {
   整數 r;
- EINTRLOOP (r, write (job_fds[1], &token, 1));
- 如果 (r != 1)
+ 整數n;
+ 字元b[32];
+
+ /* 使用非阻塞寫入以避免多個 make 子程序造成的死鎖
+ * 同時釋放作業。 */
+ set_blocking(job_fds[1], 0);
+ memset(b,令牌,sizeof(b));
+ n = 1;
+ 同時 ( n > 0 )
+ {
+ r = 寫(job_fds[1], b, n);
+ /* 系統呼叫中斷,請重試 */
+ 如果 ( r == -1 )
+ {
+ if ( 錯誤編號 == EINTR )
+ 繼續;
+
+ /* 我們到達這裡是因為這個進程和另一個進程都試圖寫入管道
+ * 完全相同的時間,且管道僅包含 1 頁。我們輸了,另一個
+ * 進程獲勝(寫入管道)。我們只能先重置這個條件
+ * 從管道讀取。當然,這意味著我們需要額外回傳一個
+ * 令牌。 */
+ if ( errno == EWOULDBLOCK || errno == EAGAIN )
+ {
+ if ( jobserver_acquire(0) )
+ {
+ n++;
+ /* 可能接近不可能... */
+ 如果 (n > 32)
+ 中斷;
+ 繼續;
+ }
+ }
+ }
+ if ( r == 0 ) /* 寫入了 0 個位元組,但沒有錯誤,請重試 */
+ 繼續;
+ 如果 ( r > 0 )
+ {
+ n -= r;
+ 繼續;
+ }
+ 中斷; /* 所有其他錯誤,中斷。 */
+ }
+ set_blocking(job_fds[1], 1);
+
+ 如果 (n != 0)
     {
       如果(是致命的)
         pfatal_with_name (_("寫入作業伺服器"));

答案4

make您的系統可能沒問題,但這可能是並行運行建置時發生的競爭條件。

如果您的系統出現問題,它會在其他情況下掛起/崩潰,而不僅僅是在進行並行建置時。

相關內容