在跟随模式/滚动超过缓冲区末尾后解除对“less”的阻止

在跟随模式/滚动超过缓冲区末尾后解除对“less”的阻止

我使用less来捕获实时程序的输出。使用F可激活跟随模式,显示实时输出。但是,进入跟随模式后(或者更一般地说,滚动浏览缓冲输出,例如使用G),我无法less再进行控制。

Ctrl-c在这里可以工作,但不幸的是它会杀死其他程序。

less滚动过缓冲区后,有没有办法重新获得控制权?

答案1

简单情况

在我的 Debian 9 中,如果我运行less读取常规文件,例如:

less /etc/fstab

然后通过F(即Shift+ F)然后我将看到

Waiting for data... (interrupt to abort)

这表明中断在设计上是“撤消”的正确方法FCtrl+确实C做到了这一点。


你的情况

我正在使用它less来捕获实时程序的输出。

我猜是这样的:

program1 | less
# or
program1 | program2 | … | less

现在问题出现了(Ctrl+C杀死了另一个程序),因为发生了以下情况:

  1. shell 将管道的每个部分(即less和每个programN)放置在一个进程组中,其 PGID(进程组 ID)等于第一个命令(即 )的 PI​​D(进程 ID)program1
  2. shell 通知终端此 PGID 现在是前台进程组。
  3. 当您点击Ctrl+时C,终端会发送SIGINT到前台进程组。
  4. 有效less 每次programN收到SIGINT

这个答案这篇博文一个相关的有趣案例是这个问题

一些非常基本的 shell(例如posh)可能会运行一切在 shell 本身的进程组中。这不会影响问题(less 每次programN接收SIGINT),但可能会使某些解决方案无效。您的 shell 很可能“足够复杂”。在我的 Debian 中,甚至sh来自也busybox足够复杂。


解决方案

您想要lessless仅能收到SIGINT。可能性:

  1. SIGINT仅发送至less

     kill -s SIGINT $pid_of_less
    

其中$pid_of_less表示正确less进程的 PID(您需要以某种方式找到它)。如果您确定less只有正确进程才less允许您发送信号,或者您不介意向其他less进程发送信号,那么这是最简单的方法:

    killall -s SIGINT less

此解决方案应该始终有效。它不需要您事先修改管道。

您可以在单独的 shell(单独的控制台)中运行kill(或)或者(如果您的 shell 允许这样做)使用+killall暂停管道,运行您需要的任何内容,最后使用 将管道带回前台。CtrlZfg

  1. 使一切都less不受免疫SIGINT。几种可能性:

     sh -c 'trap "" SIGINT; program1' | less
     sh -c 'trap "" SIGINT; exec program1' | less
     sh -c 'trap "" SIGINT; program1 | program2 | …' | less
    
     ( trap "" SIGINT; program1 ) | less
     ( trap "" SIGINT; exec program1 ) | less
     ( trap "" SIGINT; program1 | program2 | … ) | less
    

注意事项和怪癖:

  • 示例sh -c …需要额外的引号。如果原始行中有引号,则可能会不方便。如果主 shell 中有需要扩展的内容,则单引号不能包含它 (外部引号很重要)可能会变得复杂。

  • sh -c …需要与 兼容的语法的示例sh。如果您的原始行使用了sh无法理解的语法或工具(如内置函数),那么您应该考虑其他可能性。

  • 带有子 shell 的示例 ( ( … )) 使用您使用的任何 shell。它很可能会理解trap "" SIGINT并按预期工作,但通常可能不会。

  • 每个都programN可以单独注册一个处理程序SIGINT。注意less,我们甚至可以从忽略的 shell 启动它SIGINT

         ( trap "" SIGINT; program1 | less )
         # similarly with other examples
    

    并且它仍然能够对Ctrl+做出反应C。但如果另一个程序再次使自己易受攻击,那么它的目的就失败了。

  • programN当您退出时,Any可能不会终止less。这可能是因为

    • 它还不知道它写入的管道已经关闭,因为自从管道关闭以来它还没有尝试写入任何东西,所以SIGPIPE还没有生成(这是正常的,比较我的这个答案(英文):
    • 或者根据设计它不会退出SIGPIPE
    • 或者它是一个循环,运行在 上退出的命令SIGPIPE,但循环并不关心并一遍又一遍地运行它们。

    在这种情况下,最简单的恢复方法是按Ctrl+ C,但显然如果你programN对它免疫,SIGINT那么这对它不起作用。你需要用SIGTERM或其他信号杀死它。请注意,只杀死子 shell 或附加shshell 不会杀死其子 shell。向 (子) shell 的整个进程组发出信号是一种好方法,如下所示:

         kill -- -$pgid
    

    其中$pgid表示正确的进程组ID(并且在指示目标组而不仅仅是一个进程-之前)。如果子shell首先在管道中,则将是(子)shell的PID。kill$pgid

    如果在交互式 shell 中启用了作业控制,则无需知道任何 ID。例如,在 Bash 中按Ctrl+Z然后按kill %+。这应该会终止整个作业。

  1. 将所有内容放置less在前台进程组之外。

  2. setsid

        setsid -w program1 | less
        # or
        setsid -w sh -c 'program1 | program2 | …' | less
    

    和以前一样,any在退出programN时可能不会终止less。和以前一样,如果发生这种情况,您将需要使用信号将其终止。不同之处在于这次SIGINT可以是信号。另一方面,技巧kill %-将不起作用。

  3. 使用进程替换(如果您的 shell 支持)。我将详细说明 Bash。

        # in Bash
        less -f <(program1)
        # or
        less -f <(program1 | program2 | …)
    

    不幸的是,当您退出时,任何进程programN都可能无法终止less。但请注意, 中的任何进程都<( )在原始 shell 的进程组中启动,因此,只要 shell 将其自己的进程组再次注册为前台组,您就可以使用+将其发送SIGINT给它。在脚本中,这可能是一个问题,但在交互式 shell 中,在退出后收到提示时按+就足够了。是的,在这种情况下, +将发送给一些似乎在后台工作的进程(但它们属于当前前台进程组,因此正式来说它们在前台)。CtrlCCtrlClessCtrlCSIGINT

    您可以自动执行此操作:将向其自己的进程组发出信号,因此只需在 shell 的进程组中kill 0运行即可,这非常容易。就像这样:kill

        # in Bash
        less -f <(program1); kill -s SIGINT 0
        # or
        less -f <(program1 | program2 | …); kill -s SIGINT 0
    

    上述解决方案非常优雅。请记住,kill此处仅用于指示否则会保留的程序。如果您的程序自行退出,则您可能kill根本无法运行。另一方面,如果有其他shell 进程组中的程序(例如你所做的exec 3< <(whatever)kill 0也会向它们发出信号。

    关于 Zsh 的注意事项:我的测试表明,在 Zsh 中你可以使用less -f <( … )并阻止Ctrl+C发送SIGINT给程序,就像在 Bash 中一样;但 Zsh 会在完后还有进程组(而不是 shell 的进程组),因此用的技巧kill 0将不起作用。

    还请注意这将不是工作:

        # not a solution
        less < <(program1)
        # or
        less < <(program1 | program2 | …)
    

    因为这一次里面的所有内容都<( )将正式从其进程组降级less并共享,该进程组将是前台进程组,它将SIGINTCtrl+时接收C。之所以会从其正式降级,less是因为less最初是作为子 shell 启动的,它为自己设置重定向,然后才用其替换自身less(这是 shell 运行程序的正常方式)。因此,在进程树中,它将显示为less已生成 shell,该 shell 已生成program1等(比较这个问题在哪里sshd 据称生成sleep)。

    虽然在这种情况下Ctrl+C可以到达 each programN,但是当您不使用+programN退出时 any 可能仍不会终止。那么 with 的技巧将不起作用。完全不是解决方案。lessCtrlCkill 0

  4. 使用单独的调用。

    less以某种方式单独调用,使其成为前台进程组中的唯一命令,并且其他任何进程都不会SIGINTCtrl+时接收C。您需要一个命名管道或常规文件来将数据从 传递program1 …less

    1. 创建文件:

          mkfifo myfile    # of the type named fifo
          # or
          : > myfile       # of the type regular file
      
    2. 运行program1 …并让其写入文件

    • 在单独的 shell 或单独的终端中(不是您要运行的终端less):

              program1 … >> myfile
      
    • 或者在你要运行的终端的后台less(注意,对于运行一切在 shell 本身的进程组中 - 还记得吗,真的是基本的 shell:

              program1 … >> myfile &
      

      我使用了>>,而不是>。对于 fifo 来说,这无关紧要。对于常规文件,它允许您稍后截断文件而不会出现问题(比较这个答案)。

    1. 跑步less

          less -f myfile
          # or
          less < myfile
      

      -f如果是常规文件则不需要)。

      现在此终端中的Ctrl+C只会影响less

    2. 如果你退出less,其余部分仍在运行,并且你希望它停止,那么

    • 如果它在单独的终端中运行,Ctrl+C在那里;
    • 如果它在同一个终端的后台运行: - fg,则Ctrl+ C, - 或kill -s SIGINT %+,如果支持(如果需要,指定不同的信号), - 或kill -s SIGINT -- -"$!"(如果需要,指定不同的信号)。
    1. 删除文件:

          rm myfile
      

笔记

  • program1在后台或内部运行<( )会将其标准输入与终端分离。如果这会产生问题,less那么当程序尝试同时从终端读取数据时,您就会遇到问题。您报告没有问题(或者已经处理并program1从终端分离),因此显然程序不会从终端读取数据。因此在后台或内部运行<( )不会破坏任何东西。
  • 在我的测试中less,在成功“撤消”F而不终止任何操作(任何解决方案都适用)后,最终会停止读取数据。这会阻塞管道的其余部分(通常;写入programN可能设计为不等待,它可能会退出或其他)。这是意料之中的,我提到这一点是为了经验不足的用户。要解除阻塞,我需要F再次通过。但是如果我使用常规文件作为和之间的接口programNless那么程序就不会因为less(文件系统可能会变满)而阻塞。

相关内容