複数のコアでコンパイルするときに make がハングする原因は何でしょうか?

複数のコアでコンパイルするときに make がハングする原因は何でしょうか?

昨日私はソースからパッケージ化します。6 コアのモンスターマシンでコンパイルしていたので、を使用して複数のコアを使用してビルドすることにしましたmake -j 6。最初はコンパイルがスムーズに進み、非常に高速でしたが、ある時点でmake1 つのコアで 100% CPU を使用してハングしました。

グーグルで調べてみたらこれROOT メッセージ ボードに投稿してください。このコンピューターは自分で組み立てたので、ヒートシンクを適切に取り付けておらず、CPU が過熱しているのではないかと心配していました。残念ながら、職場にそれを入れられる冷蔵庫がありません。;-)

パッケージをインストールしlm-sensorsて再度実行しmake -j 6、今度は CPU 温度を監視しました。温度は高くなりましたが (60 ℃ 近く)、高温または臨界温度を超えることはありませんでした。

make -j 4実行してみましたがmake、コンパイル中にまたハングしてしまいました。今度は別の場所でハングしました。

結局、実行しながらコンパイルしたところmake、問題なく動作しました。私の疑問は、なぜハングしたのかということです。2 つの異なる場所で停止したため、何らかの競合状態が原因であると推測しますが、オプション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 の依存関係が壊れていることに気付くでしょう。2 番目のコンパイルでは がa.bytecodeすでにコンパイルされていると想定していますが、依存関係には表示されません。そのため、エラーが発生する可能性があります。 の正しい依存関係行はb.bytecode次のようになります。

b.bytecode: b.source a.bytecode

問題に戻ると、運が悪ければ、依存関係が欠落しているためにコマンドが 100% CPU ループでハングする可能性があります。おそらくここで起こっているのはそれです。欠落している依存関係は順次ビルドでは明らかにできませんでしたが、並列ビルドでは明らかになりました。

答え2

これは本当に古い質問だとわかっていますが、まだ検索結果の一番上に表示されるので、ここに私の解決策を示します。

GNU make には、make とその再帰的な子プロセスが指定された数を超えるコアを消費しないようにするためのジョブサーバー メカニズムがあります。 http://make.mad-scientist.net/papers/jobserver-implementation/

これは、すべてのプロセスで共有されるパイプに依存します。追加の子をフォークする各プロセスは、最初にパイプからトークンを消費し、完了したらトークンを放棄する必要があります。子プロセスが消費したトークンを返さない場合、トップレベルの make while はトークンが返されるのを永久に待機します。

バグ報告

Solaris ボックスで GNU make を使用して binutils をビルドしているときにこのエラーが発生しました。ここでの「sed」は GNU sed ではありません。PATH をいじって sed==gsed がシステム sed よりも優先されるようにすると、問題は解決しました。ただし、sed がパイプからトークンを消費していた理由はわかりません。

答え3

makeデッドロックが発生しているようです。 を使用するとps -ef、これらのプロセスが原因のようです:

ルート 695 615 1 22:18 ? 00:00:00 PREBUILD -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 が接続されました
{{wait4(-1, }}

ファイル記述子 4 はパイプです。

root@ltzj2-6hl3t-b98zz:/# ls -la /proc/2127/fd/4
l-wx------ 1 ルート ルート 64 9月 3日 22:22 /proc/2127/fd/4 -> 'パイプ:[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 を使用してシェルからパイプを空にすると、ビルドが再開され、期待どおりに完了します...

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 ) の場合
+ {
+ (エラー番号 == EINTR)の場合
+ 続行します。
+
+ /* このプロセスと他のプロセスの両方がパイプに書き込みを試みているため、ここに表示されます。
+ * 同時に、パイプには1ページしか含まれていません。私たちは負けました、他の
+ * プロセスが勝利しました(パイプに書き込みました)。この状態をリセットするには、まず
+ * パイプから読み取る。もちろん、それは余分なものを返す必要があることを意味します
+ * トークン。 */
+ if ( errno == EWOULDBLOCK || errno == EAGAIN )
+ {
+ if ( ジョブサーバ_acquire(0) )
+ {
+ n++;
+ /* おそらく不可能に近い... */
+ (n > 32)の場合
+ ブレーク;
+ 続行します。
+ }
+ }
+ }
+ if ( r == 0 ) /* 0 バイトを書き込みましたが、エラーではありません。もう一度試してください */
+ 続行します。
+ ( r > 0 ) の場合
+ {
+ n -= r;
+ 続行します。
+ }
+ break; /* その他のエラーの場合は break します。 */
+ }
+ set_blocking (job_fds[1], 1);
+
+ (n != 0)の場合
     {
       (致命的) の場合
         pfatal_with_name (_("ジョブサーバーへの書き込み"));

答え4

システムは正常かもしれませんが、makeビルドを並行して実行しているときに競合状態が発生する可能性があります。

システムに何らかの問題がある場合、並列ビルドを実行するときだけでなく、他のシナリオでもハング/クラッシュが発生します。

関連情報