Bash:无限休眠(无限阻塞)

241

我使用startx来启动X,在其中运行.xinitrc。在我的.xinitrc中,使用/usr/bin/mywm启动了窗口管理器。现在,如果我杀死我的WM(例如为了测试其他WM),X也会终止,因为.xinitrc脚本已经到达了EOF。

所以我在.xinitrc的末尾添加了这个:

while true; do sleep 10000; done

这样,如果我结束我的WM,X就不会终止。现在我的问题是:如何进行无限睡眠而不是循环睡眠?是否有一种命令可以冻结脚本?


4
有趣... "sleep infinity" 可以让 X 保持唤醒状态 :-) - jehon
我喜欢其中的讽刺意味。 - undefined
12个回答

514

sleep infinity恰如其名,而且不需要滥用cat。


30
好的,遗憾的是我的BusyBox不理解。 - not-a-user
20
BSD(或至少是OS X)也不理解 sleep infinity,尽管对于Linux来说这是一个很酷的东西值得了解。然而,while true; do sleep 86400; done 应该是一个足够好的替代方案。 - Ivan X
36
关于这个问题,我做了一些研究并在另一个答案中进行了记录。总结一下:在C语言中,“infinity”被转换为“double”类型。然后将该“double”类型的值截断为允许的最大值 timespec,这意味着非常大的秒数(与计算机结构有关),但理论上是有限的。 - jp48
31
我搜索了“虐猫”,不确定会找到什么。那么,“此脚本的执行过程中没有伤害到任何猫咪”如何? - ahoffer
3
值得一提的是,现在的macOS Big Sur可以理解“sleep infinity”的命令,但Mojave不行。(我跳过了Catalina) - Glenn
显示剩余5条评论

107

tail 不会阻塞

一如既往:对于任何问题,总有一个简短易懂、易于跟随但完全错误的答案。在这里,tail -f /dev/null 就属于这个范畴 ;)

如果你使用 strace tail -f /dev/null 查看它,你会发现这个解决方案远非阻塞!在 Linux 下,它甚至比问题中的 sleep 解决方案更糟糕,因为它使用了宝贵的资源,如 inotify 系统。同时,其他写入 /dev/null 的进程也会让 tail 循环。(在我的 Ubuntu64 16.10 上,在一个已经繁忙的系统上,这会增加数十个系统调用每秒。)

问题是关于阻塞命令的

不幸的是,这样的命令不存在...

意思是:我不知道如何直接通过 shell 实现这个。

所有东西(甚至是sleep infinity)都可以被某些信号中断。因此,如果您想确保它不会异常返回,它必须像您已经为sleep所做的那样在循环中运行。请注意,在Linux上,/bin/sleep 显然被限制在24天(查看strace sleep infinity),因此您能做的最好的可能是:

while :; do sleep 2073600; done

(请注意,我相信sleep在超过24天的时间内会循环,但这意味着:它不是阻塞的,而是非常缓慢地循环。那么为什么不将这个循环移到外部呢?)

...但你可以用一个未命名的fifo接近实现。

你可以创建一个真正阻塞的东西,只要没有信号发送到进程中,它就会一直阻塞。以下示例使用bash 4、2个PID和1个fifo

bash -c 'coproc { exec >&-; read; }; eval exec "${COPROC[0]}<&-"; wait'

如果你愿意,你可以使用strace来检查这个是否真的被阻塞了:

strace -ff bash -c '..see above..'

如何构建

read块如果没有输入数据(请参见其他答案)。 但是,tty(又名stdin)通常不是一个好的来源,因为当用户注销时它会关闭。 它还可能从tty中窃取一些输入。 不好。

为了使read阻塞,我们需要等待像fifo这样的东西,它永远不会返回任何内容。 在bash 4中,有一条命令可以为我们提供完全这样的fifocoproc。 如果我们还等待阻塞的read(即我们的coproc),我们就完成了。 不幸的是,这需要保持两个PID和一个fifo打开。

使用命名fifo的变体

如果您不介意使用命名fifo,则可以按以下方式执行此操作:

mkfifo "$HOME/.pause.fifo" 2>/dev/null; read <"$HOME/.pause.fifo"

在读取时不使用循环有点草率,但您可以随意重复使用此fifo并使用touch "$HOME/.pause.fifo"使read终止(如果有多个读取正在等待,则所有读取都会同时终止)。

或者使用Linux的pause()系统调用

对于无限阻塞,有一个名为pause()的Linux系统调用可以实现我们想要的功能:永久等待(直到收到信号)。但是目前还没有用户空间程序可以使用它。

C

创建这样的程序很容易。以下是一个片段,用于创建一个非常小的Linux程序,名为pause,它可以无限期地暂停(需要像gcc这样的C编译器,并使用diet等工具来生成小型二进制文件):
printf '#include <unistd.h>\nint main(){for(;;)pause();}' > pause.c;
diet -Os cc pause.c -o pause;
strip -s pause;
ls -al pause

Python

如果您不想自己编译某些东西,但已经安装了Python,则可以在Linux下使用以下命令:

python -c 'while 1: import ctypes; ctypes.CDLL(None).pause()'

注意:使用exec python -c ...替换当前shell,这将释放一个PID。此解决方案还可以通过一些IO重定向来改进,以释放未使用的FD。这取决于您。

工作原理: ctypes.CDLL(None)加载“主程序”(包括C库)并在循环内运行其中的pause()函数。虽然不如C版本高效,但仍能正常工作。

我的建议:

继续使用循环休眠。它易于理解,非常便携,并且大部分时间都会阻塞。


1
@Andrew 通常情况下,您不需要 trap(它会修改shell对信号的行为)或后台运行(允许shell拦截终端发送的信号,如 Strg+C)。 因此,只需使用 sleep infinity 就足够了(如果它是最后一个语句,则行为类似于 exec sleep infinity。要查看差异,请使用 strace -ffDI4 bash -c 'YOURCODEHERE')。循环睡眠更好,因为在某些情况下,sleep 可能会返回。例如,您不希望 X11 在 killall sleep 上突然关闭,只是因为.xstartup 结束于 sleep infinity 而不是循环睡眠。 - Tino
3
可能有些晦涩,但s6-pause是一个用户命令,用于运行pause(),可以选择忽略不同的信号。 - Patrick
1
@Tino /bin/sleep并不像你所说的那样被限制在24天。如果您能更新一下就太好了。目前在Linux上,这段代码是活跃的。它将单个的nanosleep()系统调用限制为24天,但会在循环中调用它们。因此,sleep infinity不应该在24天后退出。正无穷大的double会被转换为struct timespec。在GDB中查看rpl_nanosleep,在Ubuntu 16.04上,infinity会被转换为{ tv_sec = 9223372036854775807, tv_nsec = 999999999 } - nh2
2
暂停(pause())选项在Perl中非常容易实现: perl -MPOSIX -e 'pause()' - tgoodhart
4
在下一个 coreutils 版本中,sleep infinity 将会真正地休眠无限长的时间而不会循环执行:https://lists.gnu.org/archive/html/bug-gnulib/2020-02/msg00081.html - Vladimir Panteleev
显示剩余6条评论

75
也许这看起来很丑陋,但为什么不直接运行cat并让它永远等待输入呢?

8
如果没有一个悬挂的管子可以读取,这种方法就行不通。请给予建议。 - Matt Joiner
2
@Matt,也许可以创建一个管道并使用cat命令? mkfifo pipe && cat pipe - Michał Trybus
1
像@twalberg所说的那样,但是你还可以立即将其重新赋值为3并取消链接,如此处所示:https://superuser.com/a/633185/762481 - jp48

57
自 GNU coreutils 版本 9 起,在 Linux 系统上,sleep infinity 可以正常工作。之前(以及其他系统中),实现是实际上睡眠允许的最长时间,这是有限的。

想知道为什么这个没有被记录在任何地方,我花时间阅读了GNU coreutils的来源,发现它大致执行以下操作:

  1. 使用C标准库中的strtod函数将第一个参数转换为双精度浮点数,以便将“无穷大”转换为双精度值。假设采用IEEE 754双精度表示法,则64位的正无穷大seconds变量中存储。
  2. 调用gnulib中的xnanosleep(seconds)函数,它进而调用gnulib中的dtotimespec(seconds)函数,将其从double类型转换为struct timespec类型。
  3. struct timespec只是一对数字:整数部分(以秒为单位)和小数部分(以纳秒为单位)。天真地将正无穷大转换为整数会导致未定义行为(请参见C标准文档中的§6.3.1.4节),因此它截断为TYPE_MAXIMUM(time_t)
  4. TYPE_MAXIMUM(time_t)的实际值没有在标准中设置(甚至不包括sizeof(time_t)),因此为了举例说明,让我们从最近的Linux内核中选择x86-64。
这是Linux内核中的TIME_T_MAX,在time.h中定义为:
(time_t)((1UL << ((sizeof(time_t) << 3) - 1)) - 1)

请注意,time_t__kernel_time_ttime_tlong;使用LP64数据模型,因此sizeof(long)为8(64位)。

这导致:TIME_T_MAX = 9223372036854775807

也就是说:sleep infinite导致的实际休眠时间为9223372036854775807秒(10^11年)。对于32位Linux系统(sizeof(long)为4(32位)):2147483647秒(68年;另见year 2038 problem)。


编辑:显然,调用的nanoseconds函数不是直接的系统调用,而是一个依赖于操作系统的包装器(也在gnulib中定义)。

结果多了一步:对于一些HAVE_BUG_BIG_NANOSLEEPtrue的系统,睡眠时间被截断为24天,然后在循环中调用。这对于一些(或全部?)Linux发行版是适用的。请注意,如果configure-time测试成功,则可能不使用此包装器(源代码)。

特别地,这将是24 * 24 * 60 * 60 = 2073600秒(加上999999999纳秒);但是这会在循环中调用以尊重指定的总睡眠时间。因此,先前的结论仍然有效。


总的来说,得到的睡眠时间不是无限的,但对于所有实际目的而言足够高,即使得到的实际时间流逝不具有可移植性;这取决于操作系统和架构。
回答最初的问题,这显然已经足够好了,但如果出于某些原因(非常受资源限制的系统),您确实想要避免无用的额外倒计时器,我认为最正确的替代方法是使用其他答案中描述的cat方法。 编辑:最近的GNU coreutils版本将尝试使用pause系统调用(如果可用)而不是循环。当在Linux(可能还有BSD)中针对这些更新版本时,先前的参数就不再有效。

可移植性

这是一个重要且有效的关注点:

  • sleep infinity 是 GNU coreutils 的扩展,不在 POSIX 中考虑。GNU 的实现也支持时间持续时间的“花式”语法,例如 sleep 1h 5.2s,而 POSIX 只允许正整数(例如,sleep 0.5 允许)。
  • 一些兼容的实现:GNU coreutils、FreeBSD(至少从版本 8.2 开始),Busybox(需要使用选项 FANCY_SLEEPFLOAT_DURATION 进行编译)。
  • strtod 行为与 C 和 POSIX 兼容(即在符合 C99 的实现中,strtod("infinity", 0) 总是有效的,参见 §7.20.1.3)。


10
在下一个核心工具包中,sleep infinity现在将会真正地永久休眠而无需循环:https://lists.gnu.org/archive/html/bug-gnulib/2020-02/msg00081.html - Vladimir Panteleev
@VladimirPanteleev,两年多过去了,“下一个coreutils”已经发布了吗?如果是这样,它的版本是什么?(我尝试查看链接以获取版本号,但没有找到。)谢谢! - Pedro A
2
@PedroA 它应该在 coreutils 9 中。 - Vladimir Panteleev
我刚刚检查了一下,在FreeBSD 13.1-RELEASE上的sleep不支持“infinity”。 - Mateusz Piotrowski

13

sleep infinity 看起来最优雅,但有时由于某些原因它不起作用。在这种情况下,您可以尝试其他阻塞命令,例如 cat, read, tail -f /dev/null, grep a 等。


3
“tail -f /dev/null” 在 SaaS 平台上对我也是一个可行的解决方案。 - schmunk
3
tail -f /dev/null 的优点之一是不会占用标准输入。出于这个原因,我使用了它。 - Sudo Bash
4
考虑此选项的人应该阅读 这篇回答 来了解该选项的影响。 - Shadow

12
让我解释一下为什么`sleep infinity`可以工作,即使它没有被记录在文档中。根据jp48的回答,这也很有用。
最重要的是:通过指定`inf`或`infinity`(不区分大小写),您可以睡眠最长时间,即实现允许的较小值(即`HUGE_VAL`和`TYPE_MAXIMUM(time_t)`的较小值)。
现在我们来深入了解细节。`sleep`命令的源代码可以从coreutils/src/sleep.c中阅读。本质上,该函数执行以下操作:
double s; //seconds
xstrtod (argv[i], &p, &s, cl_strtod); //`p` is not essential (just used for error check).
xnanosleep (s);

理解 xstrtod (argv[i], &p, &s, cl_strtod)

xstrtod()

根据gnulib/lib/xstrtod.c的描述,调用xstrtod()将字符串argv[i]转换为浮点数,并使用转换函数cl_strtod()将其存储到*s中。

cl_strtod()

coreutils/lib/cl-strtod.c可以看出,cl_strtod()使用strtod()将字符串转换为浮点数。

strtod()

根据man 3 strtodstrtod()将字符串转换为double类型的值。手册中说:

(字符串的)预期格式是...或(iii)无穷大,或...

无穷大被定义为

无穷大是“INF”或“INFINITY”,不区分大小写。

尽管文档说明了

如果正确的值会导致溢出,则返回正或负的HUGE_VALHUGE_VALFHUGE_VALL

但是不清楚无穷大是如何处理的。因此,让我们查看源代码gnulib/lib/strtod.c。我们想要阅读的内容是

else if (c_tolower (*s) == 'i'
         && c_tolower (s[1]) == 'n'
         && c_tolower (s[2]) == 'f')
  {
    s += 3;
    if (c_tolower (*s) == 'i'
        && c_tolower (s[1]) == 'n'
        && c_tolower (s[2]) == 'i'
        && c_tolower (s[3]) == 't'
        && c_tolower (s[4]) == 'y')
      s += 5;
    num = HUGE_VAL;
    errno = saved_errno;
  }

因此,INFINFINITY(大小写不敏感)被认为是HUGE_VAL

HUGE_VAL家族

让我们使用C标准N1570。宏HUGE_VALHUGE_VALFHUGE_VALL在§7.12-3中定义。


    HUGE_VAL
展开为正的双精度常量表达式,不一定可表示为浮点数。宏
    HUGE_VALF
    HUGE_VALL
分别是HUGE_VAL的float和long double模拟。

HUGE_VALHUGE_VALFHUGE_VALL可以是支持无穷大的实现中的正无穷大。

在§7.12.1-5中:

如果浮点结果溢出并且默认舍入生效,则函数根据返回类型返回宏HUGE_VALHUGE_VALFHUGE_VALL的值。

理解xnanosleep(s)

现在,我们理解了xstrtod()的所有实质。从上面的解释可以清楚地看出,我们最先看到的xnanosleep(s)实际上意味着xnanosleep(HUGE_VALL)xnanosleep() 根据源代码gnulib/lib/xnanosleep.cxnanosleep(s)本质上做到这一点:
struct timespec ts_sleep = dtotimespec (s);
nanosleep (&ts_sleep, NULL);

dtotimespec()

这个函数将一个类型为double的参数转换为一个类型为struct timespec的对象。由于这个函数非常简单,让我引用源代码gnulib/lib/dtotimespec.c。所有的注释都是我添加的。

struct timespec
dtotimespec (double sec)
{
  if (! (TYPE_MINIMUM (time_t) < sec)) //underflow case
    return make_timespec (TYPE_MINIMUM (time_t), 0);
  else if (! (sec < 1.0 + TYPE_MAXIMUM (time_t))) //overflow case
    return make_timespec (TYPE_MAXIMUM (time_t), TIMESPEC_HZ - 1);
  else //normal case (looks complex but does nothing technical)
    {
      time_t s = sec;
      double frac = TIMESPEC_HZ * (sec - s);
      long ns = frac;
      ns += ns < frac;
      s += ns / TIMESPEC_HZ;
      ns %= TIMESPEC_HZ;

      if (ns < 0)
        {
          s--;
          ns += TIMESPEC_HZ;
        }

      return make_timespec (s, ns);
    }
}

由于time_t被定义为一个整数类型(参见§7.27.1-3),因此我们自然会假设类型time_t的最大值小于HUGE_VAL(类型为double),这意味着我们进入了溢出情况。(实际上,这种假设是不需要的,因为在所有情况下,该过程本质上都是相同的。)

make_timespec()

我们需要解决的最后一个问题是make_timespec()。非常幸运的是,它非常简单,只需引用源代码gnulib/lib/timespec.h即可。

_GL_TIMESPEC_INLINE struct timespec
make_timespec (time_t s, long int ns)
{
  struct timespec r;
  r.tv_sec = s;
  r.tv_nsec = ns;
  return r;
}

8

那么向自身发送SIGSTOP信号怎么样呢?

这会使进程暂停,直到接收到SIGCONT信号。而在您的情况下:永远不会接收到。

kill -STOP "$$";
# grace time for signal delivery
sleep 60;

8
信号是异步的。因此以下情况可能发生:a) shell调用kill b) kill告诉内核shell应该接收信号STOP c) kill终止并返回shell d) shell继续运行(也许由于脚本结束而终止)e) 内核最终找到时间将信号STOP传递给shell。 - not-a-user
1
@temple 很有见地,我没有考虑到信号的异步性质。谢谢! - michuelnik

3

最近我有这样的需求。我想到了以下函数,可以让bash永远睡眠而不调用任何外部程序:

snore()
{
    local IFS
    [[ -n "${_snore_fd:-}" ]] || { exec {_snore_fd}<> <(:); } 2>/dev/null ||
    {
        # workaround for MacOS and similar systems
        local fifo
        fifo=$(mktemp -u)
        mkfifo -m 700 "$fifo"
        exec {_snore_fd}<>"$fifo"
        rm "$fifo"
    }
    read ${1:+-t "$1"} -u $_snore_fd || :
}

注意:我之前发布了一个版本,每次都会打开和关闭文件描述符,但是我发现在某些系统上,每秒钟这样做数百次最终会锁定。因此,新的解决方案在调用函数之间保留文件描述符。无论如何,Bash将在退出时清理它。

它可以像/bin/sleep一样被调用,并且会按照要求的时间休眠。如果没有参数调用,它将永远挂起。

snore 0.1  # sleeps for 0.1 seconds
snore 10   # sleeps for 10 seconds
snore      # sleeps forever

这里有一篇关于如何在Bash中无需子进程睡眠以及如何永久睡眠的博客文章,详细介绍了此技术。


3

这种方法不会消耗任何资源来保持进程的活跃。

while :; do :; done & kill -STOP $! && wait

分解

  • while :; do :; done & 在后台创建一个虚假进程
  • kill -STOP $! 停止后台进程
  • wait 等待后台进程,这将一直阻塞,因为后台进程已经被停止了

注释

  • 仅在脚本文件中有效。

在 MacOS 上会导致忙等待(100% CPU)。 - Miao1007
@Miao1007 这只发生在 zsh 中,而在 sh 中则不会发生。我不知道为什么 zsh 会出现这种情况。 - qoomon
@Miao1007 我明白了,这是因为在zsh中wait会恢复给定的进程。 - qoomon
2
@Miao1007,我通过从wait命令中删除$!来修复了我的命令。现在它在zsh中也可以工作了。 - qoomon

0

不要杀死窗口管理器,尝试使用--replace-replace(如果可用)来运行新的窗口管理器。


1
如果我使用 --replace ,我总是会收到警告,如“另一个窗口管理器已经在运行”。但那对我来说没有太多意义。 - watain

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接