使用前台终端访问在后台运行命令

Joh*_*man 7 shell job-control tty background-process coprocesses

我正在尝试创建一个可以运行任意命令的函数,与子进程交互(省略细节),然后等待它退出。如果成功,打字run <command>看起来就像一个裸的<command>.

如果我不与子进程交互,我会简单地写:

run() {
    "$@"
}
Run Code Online (Sandbox Code Playgroud)

但是因为我需要在它运行时与之交互,所以我使用coproc和 进行了更复杂的设置wait

run() {
    exec {in}<&0 {out}>&1 {err}>&2
    { coproc "$@" 0<&$in 1>&$out 2>&$err; } 2>/dev/null
    exec {in}<&- {out}>&- {err}>&-

    # while child running:
    #     status/signal/exchange data with child process

    wait
}
Run Code Online (Sandbox Code Playgroud)

(这是一个简化。虽然coproc重定向和所有重定向在这里并没有真正做任何不能做的有用的"$@" &事情,但我在我的实际程序中需要它们。)

"$@"命令可以是任何东西。我拥有的功能可以使用run ls等等run make,但是当我这样做时它失败了run vim。我认为它失败了,因为 Vim 检测到它是一个后台进程并且没有终端访问权限,所以它不会弹出一个编辑窗口,而是自己挂起。我想修复它,以便 Vim 正常运行。

如何coproc "$@"在“前台”中运行而父 shell 成为“后台”?“与孩子交互”部分既不读取也不写入终端,所以我不需要它在前台运行。我很高兴将 tty 的控制权交给协进程。

对于我正在做的事情来说,重要的run()是在父进程中并"$@"在它的子进程中。我不能互换这些角色。但是我可以交换前景和背景。(我只是不知道该怎么做。)

请注意,我不是在寻找特定于 Vim 的解决方案。我宁愿避免使用伪 tty。当 stdin 和 stdout 连接到 tty、管道或从文件重定向时,我的理想解决方案同样有效:

run echo foo                               # should print "foo"
echo foo | run sed 's/foo/bar/' | cat      # should print "bar"
run vim                                    # should open vim normally
Run Code Online (Sandbox Code Playgroud)

为什么要使用协进程?

我可以在没有 coproc 的情况下写这个问题,只需

run() { "$@" & wait; }
Run Code Online (Sandbox Code Playgroud)

我只用&. 但在我的用例中,我使用的是 FIFO coproc 设置,我认为最好不要过度简化问题,以防cmd &和之间存在差异coproc cmd

为什么要避免 ptys?

run()可以在自动化上下文中使用。如果它用于管道或重定向,则不会有任何终端可以模拟;设置 pty 将是一个错误。

为什么不使用期望?

我不是想自动化 vim,向它发送任何输入或类似的东西。

Col*_*rse 5

我添加了代码,以便:

  • 它适用于你的三个例子
  • 交互发生在等待之前。
  • interact() {
        pid=$1
        ! ps -p $pid && return
        ls -ld /proc/$pid/fd/*
        sleep 5; kill -1 $pid   # TEST SIGNAL TO PARENT
    }
    
    run() {
        exec {in}<&0 {out}>&1 {err}>&2
        { coproc "$@" 0<&$in 1>&$out 2>&$err; } 2>/dev/null
        exec {in}<&- {out}>&- {err}>&-
        { interact $! <&- >/tmp/whatever.log 2>&1& } 2>/dev/null
        fg %1 >/dev/null 2>&1
        wait 2>/dev/null
    }
    
    Run Code Online (Sandbox Code Playgroud)

    将为fg %1所有命令运行(%1根据并发作业的需要进行更改),并且在正常情况下会发生以下两种情况之一:

  • 如果命令立即退出,interact()则将立即返回,因为没有任何事情可做,并且将fg不执行任何操作。
  • 如果命令没有立即退出,interact()则可以进行交互(例如,5 秒后向协进程发送 HUP),并且将fg使用最初运行它的相同 stdin/out/err 将协进程置于前台(您可以检查此和ls -l /proc/<pid>/df)。

    最后三个命令中到 /dev/null 的重定向是装饰性的。它们看起来与您单独run <command>运行时完全相同。command


    LL3*_*LL3 2

    在您的示例代码中,一旦 Vim 尝试从 tty 读取数据或可能为其设置某些属性,它就会通过 SIGTTIN 信号被内核挂起。

    \n\n

    这是因为交互式 shell 在不同的进程组中生成它,而没有将 tty 移交给该组,即把它放在 \xe2\x80\x9cin 背景\xe2\x80\x9d 中。这是正常的作业控制行为,移交 tty 的正常方法是使用fg. 当然,它\xe2\x80\x99s shell会进入后台并因此被挂起。

    \n\n

    当 shell 是交互式的时,所有这些都是有意为之的,否则就好像您在使用 Vim 编辑文件时被允许在提示符处继续键入命令一样。

    \n\n

    您可以通过将整个run函数变成脚本来轻松解决这个问题。这样,它将由交互式 shell 同步执行,而不会与 tty 竞争。如果您这样做,您自己的示例代码已经完成您所要求的所有操作,包括run\xc2\xa0(然后是脚本)和 coproc 之间的并发交互。

    \n\n

    如果不能将其放在脚本中,那么您可能会看到除 Bash 之外的 shell 是否允许对将交互式 tty 传递给子进程进行更精细的控制。我个人并不是更高级 shell 的专家。

    \n\n

    如果您确实必须使用 Bash 并且确实必须通过交互式 shell 运行的函数来拥有此功能,那么我担心唯一的出路是使用允许您访问的语言创建一个帮助程序tcsetpgrp(3) 和 sigprocmask(2)。

    \n\n

    目的是在子进程(你的 coproc)中执行父进程(交互式 shell)中未完成的操作,以便强行获取 tty。

    \n\n

    但请记住,这显然被认为是不好的做法。

    \n\n

    但是,如果您在子 shell 仍然拥有 tty 的情况下不\xe2\x80\x99t 使用父 shell 中的 tty,那么可能不会造成任何损害。通过 \xe2\x80\x9cdon\xe2\x80\x99t use\xe2\x80\x9d 我的意思是 don\xe2\x80\x99t echodon\xe2\x80\x99t printfdon\xe2\x80\x99tread到/从 tty,并且当然,不要在子进程仍在运行时运行其他可能访问 tty 的程序。

    \n\n

    Python 中的帮助程序可能如下所示:

    \n\n
    #!/usr/bin/python3\n\nimport os\nimport sys\nimport signal\n\ndef main():\n    in_fd = sys.stdin.fileno()\n    if os.isatty(in_fd):\n        oldset = signal.pthread_sigmask(signal.SIG_BLOCK, {signal.SIGTTIN, signal.SIGTTOU})\n        os.tcsetpgrp(in_fd, os.getpid())\n        signal.pthread_sigmask(signal.SIG_SETMASK, oldset)\n    if len(sys.argv) > 1:\n        # Note: here I used execvp for ease of testing. In production\n        #\xc2\xa0you might prefer to use execv passing it the command to run\n        # with full path produced by the shell\'s completion\n        # facility\n        os.execvp(sys.argv[1], sys.argv[1:])\n\nif __name__ == \'__main__\':\n    main()\n
    Run Code Online (Sandbox Code Playgroud)\n\n

    它在 C 中的等价物只会更长一点。

    \n\n

    这个帮助程序需要由 coproc 使用 exec 来运行,如下所示:

    \n\n
    run() {\n    exec {in}<&0 {out}>&1 {err}>&2\n    { coproc exec grab-tty.py "$@" {side_channel_in}<&0 {side_channel_out}>&1 0<&${in}- 1>&${out}- 2>&${err}- ; } 2>/dev/null\n    exec {in}<&- {out}>&- {err}>&-\n\n    # while child running:\n    #     status/signal/exchange data with child process\n\n    wait\n}\n
    Run Code Online (Sandbox Code Playgroud)\n\n

    对于所有示例案例,此设置在 Ubuntu 14.04 上适用于 Bash 4.3 和 Python 3.4,通过我的主交互式 shell 获取函数并run从命令提示符运行。

    \n\n

    如果您需要从 coproc 运行脚本,则可能需要使用 来运行它bash -i,否则 Bash 可能会在 stdin/stdout/stderr 上以管道或 /dev/null 启动,而不是继承 Python 脚本抓取的 tty。另外,无论您在 coproc 内(或其下方)运行什么,最好不要调用额外的run()s。(实际上不确定,haven\xe2\x80\x99t 测试了这种情况,但我认为它至少需要仔细的封装)。

    \n\n
    \n\n

    为了回答您的具体(子)问题,我需要介绍一些理论。

    \n\n

    每个 tty 有一个且只有一个,即所谓的 \xe2\x80\x9csession\xe2\x80\x9d。(不过,并非每个会话都有 tty,例如典型守护进程的情况,但我认为这与此处无关)。

    \n\n

    基本上,每个会话都是进程的集合,并通过与 \xe2\x80\x9csessionleader\xe2\x80\x9d\xe2\x80\x99s pid 对应的 id 进行标识。因此,\xe2\x80\x9csessionleader\xe2\x80\x9d 是属于该会话的进程之一,并且正是第一个启动该特定会话的进程。

    \n\n

    特定会话的所有进程(领导者和非领导者)都可以访问与其所属会话关联的 tty。但第一个区别是:在任何给定时刻只有一个进程可以是所谓的 \xe2\x80\x9c 前台进程 \xe2\x80\x9d,而该时间内的所有其他进程都是 \xe2\x80\x9c 后台进程进程\xe2\x80\x9d。\xe2\x80\x9cforeground\xe2\x80\x9d 进程可以自由访问 tty。相反,如果 \xe2\x80\x9cbackground\xe2\x80\x9d 进程敢于访问其 tty,它们将被内核中断。它\xe2\x80\x99s 不像根本不允许后台进程,它\xe2\x80\x99s 而不是它们被内核发出信号,表明\xe2\x80\x9cit\xe2\x80\x99s 不允许轮到他们发言\xe2\x80\x9d。

    \n\n

    那么,回答您的具体问题:

    \n\n
    \n

    “前景”和“背景”到底是什么意思?

    \n
    \n\n

    \xe2\x80\x9cforeground\xe2\x80\x9d 表示 \xe2\x80\x9c当时正在合法使用 tty\xe2\x80\x9d

    \n\n

    \xe2\x80\x9cbackground\xe2\x80\x9d 表示 \xe2\x80\x9c 此时未使用 tty\xe2\x80\x9d

    \n\n

    或者,换句话说,再次引用您的问题:

    \n\n
    \n

    我想知道前台进程和后台进程的区别

    \n
    \n\n

    合法访问 tty。

    \n\n
    \n

    当父进程继续运行时,是否可以将后台进程带到前台?

    \n
    \n\n

    一般来说:后台进程(父进程或非父进程)确实会继续运行,只是如果它们尝试访问其 tty,它们就会(默认情况下)停止。(注意:它们可以忽略或以其他方式处理这些特定信号(SIGTTIN 和 SIGTTOU),但通常情况并非如此,因此默认配置是挂起进程

    \n\n

    但是:在交互式 shell 的情况下,\xe2\x80\x99 是选择挂起自身的shell (在 wait(2) 或 select(2) 或它认为的任何阻塞系统调用中\xe2\x80\ x99 是当时最合适的一个)在它将 tty 交给其在后台的子级之一之后。

    \n\n

    由此,您的具体问题的准确答案是:当使用 shell 应用程序时,取决于您使用的 shell 是否为您提供了一种方法(内置命令或其他命令),以便在发出命令后不停止自身命令fg。AFAIK Bash 不允许你这样的选择。我不了解其他 shell 应用程序。

    \n\n
    \n

    与有何cmd &不同cmd

    \n
    \n\n

    在 a 上cmd,Bash 生成一个属于它自己的会话的新进程,将 tty 交给它,然后让自己处于等待状态。

    \n\n

    在 a 上cmd &,Bash 生成一个属于其自己会话的新进程。

    \n\n
    \n

    如何将前台控制权交给子进程

    \n
    \n\n

    一般来说:您需要使用 tcsetpgrp(3)。实际上,这可以由父母或孩子来完成,但建议的做法是由父母来完成。

    \n\n

    在 Bash 的特定情况下:您发出命令fg,通过这样做,Bash 使用 tcsetpgrp(3) 支持该子进程,然后将其自身置于等待状态。

    \n\n
    \n\n

    从这里,您可能会发现感兴趣的进一步见解是,实际上,在相当新的 UNIX 系统上,会话进程之间还有一个额外的层次结构:所谓的 \xe2\x80\x9c进程组\xe2 \x80\x9d。

    \n\n

    这是相关的,因为到目前为止我所说的关于 \xe2\x80\x9cforeground\xe2\x80\x9d 概念的 \xe2\x80\x99ve 实际上并不限于 \xe2\x80\x9cone 单进程\xe2\x80\ x9d,它\xe2\x80\x99s而不是扩展到\xe2\x80\x9cone单个进程组\xe2\x80\x9d。

    \n\n

    也就是说: \xe2\x80\x9cforeground\xe2\x80\x9d 的常见情况是只有一个进程可以合法访问 tty,但内核实际上允许一种更高级的情况,其中整个进程进程组(仍然属于同一会话)可以合法访问 tty。

    \n\n

    事实上,为了移交 tty \xe2\x80\x9cforegroundness\xe2\x80\x9d 而调用的函数被命名为tcsetpgrp,而不是类似(例如)tcsetpid ,这并不是错误的。

    \n\n

    然而,实际上,Bash 显然并没有故意利用这种更高级的可能性。

    \n\n

    不过,您可能想利用它。这完全取决于您的具体应用。

    \n\n

    正如进程分组的实际示例一样,我可以选择在上面的解决方案中使用 \xe2\x80\x9cregain 前台进程组\xe2\x80\x9d 方法,代替前台组上的 \xe2\x80\x9chand \xe2\x80\x9d 方法。

    \n\n

    也就是说,我可以让 Python 脚本使用 os.setpgid() 函数(它包装了 setpgid(2) 系统调用),以便将进程重新分配给当前的前台进程组(可能是 shell 进程本身,但是不一定如此),从而重新获得 Bash 没有交出的前台状态。

    \n\n

    然而,这将是达到最终目标的一种相当间接的方式,并且还可能产生不良的副作用,因为进程组的其他几种用途与 tty 控制无关,最终可能会涉及到您的 coproc。例如,UNIX 信号通常可以传递到整个进程组,而不是单个进程。

    \n\n

    最后,为什么run()从 Bash\xe2\x80\x99s 命令提示符调用您自己的示例函数与从脚本(或作为脚本)调用您自己的示例函数有如此不同?

    \n\n

    因为run()从命令提示符调用是由 Bash\xe2\x80\x99s 自己的进程 (*) 执行的,而从脚本调用时,它\xe2\x80\x99s 由交互式 Bash 所拥有的不同进程(组)执行已经很高兴地把tty交给了。

    \n\n

    因此,从脚本中,Bash 为避免与 tty 竞争而放置的最后一个 \xe2\x80\x9cdefense\xe2\x80\x9d 很容易通过保存和恢复 stdin/stdout/stderr\xe2 的简单众所周知的技巧来规避\x80\x99s 文件描述符。

    \n\n

    (*) 或者它可能会产生一个属于其自己的同一进程组的新进程。实际上,我从未研究过交互式 Bash 使用什么确切方法来运行函数,但它在 tty 方面并没有产生任何区别。

    \n\n

    华泰

    \n