在多核上编译时,什么可能导致 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 从外壳中清空管道,那么构建将按预期恢复并完成......

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您的系统可能没问题,但这可能是并行运行构建时发生的竞争条件。

如果您的系统出现问题,它会在其他情况下挂起/崩溃,而不仅仅是在进行并行构建时。

相关内容