通过前台终端访问在后台运行命令

通过前台终端访问在后台运行命令

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

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

run() {
    "$@"
}

但因为我需要在它运行时与其交互,所以我使用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
}

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

"$@"命令可以是任何内容。我的函数可以与run lsandrun 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

为什么使用协进程?

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

run() { "$@" & wait; }

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

为什么要避免 ptys?

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

为什么不使用期望?

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

答案1

我添加了代码,以便:

  • 它适用于你的三个例子
  • 交互发生在等待之前。
  • 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
    }
    

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

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

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

  • 答案2

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

    这是因为交互式 shell 在不同的进程组中生成它,但(尚未)将 tty 移交给该组,即将其“置于后台”。这是正常的作业控制行为,移交 tty 的正常方法是使用fg.当然,shell 会转到后台并因此被挂起。

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

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

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

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

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

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

    但是,如果您在子 shell 仍然拥有 tty 的情况下不使用父 shell 中的 tty,那么可能不会造成任何损害。我所说的“不要使用”是指不要echo与ttyprintf进行read交互,当然也不要在子进程仍在运行时运行其他可能访问 tty 的程序。

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

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

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

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

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

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

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


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

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

    基本上,每个会话都是进程的集合,并通过与“会话领导者”的 pid 相对应的 id 进行标识。因此,“会话领导者”是属于该会话的进程之一,并且正是第一个启动该特定会话的进程。

    全部特定会话的进程(领导者和非领导者)可以访问与其所属会话关联的 tty。但第一个区别是:仅任何给定时刻的进程都可以是所谓的“前台进程”,而所有其他的在此期间是“后台进程”。 “前台”进程可以自由访问 tty。相反,如果“后台”进程敢于访问其 tty,它们将被内核中断。这并不是说根本不允许后台进程,而是内核向它们发出“现在轮不到他们说话”的信号。

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

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

    “前景”的意思是“存在”合法地那一刻正在使用 tty”

    “背景”的意思只是“当时没有使用 tty”

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

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

    合法的访问 tty。

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

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

    但是:对于交互式 shell,它是贝壳因此,在将 tty 移交给其在后台的子级之一后,选择挂起自身(在 wait(2) 或 select(2) 或任何它认为此时最合适的阻塞系统调用中) 。

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

    与有何cmd &不同cmd

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

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

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

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

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


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

    这是相关的,因为到目前为止我所说的“前台”概念实际上并不局限于“单个进程”,而是扩展到“单个进程组”。

    那就是:碰巧平常常见“前台”的情况是只有一个进程可以合法访问 tty,但内核实际上允许更高级的情况,其中整个进程进程组(仍然属于同一会话)对 tty 具有合法访问权限。

    事实上,为了移交 tty“前景”而调用的函数被命名为并非错误tcsetpgrp,而不是类似(例如)tcsetpid

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

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

    正如进程分组的实际示例一样,我可以选择在上面的解决方案中使用“重新获得前台进程组”方法,而不是“移交前台组”方法。

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

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

    最后,为什么run()从 Bash 的命令提示符而不是从脚本(或作为一个脚本) ?

    因为run()从命令提示符调用是由 Bash 自己的进程(*)执行的,而当从脚本调用时,它是由不同的进程(组)执行的,交互式 Bash 已经愉快地将 tty 移交给了该进程。

    因此,从脚本来看,Bash 为避免与 tty 竞争而实施的最后“防御”很容易通过保存和恢复 stdin/stdout/stderr 的文件描述符这一众所周知的简单技巧来规避。

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

    华泰

    答案3

    我不完全理解这个问题,但这是您正在寻找的一些内容。

    前景和背景是shell概念。

    • 前台作业可以访问 tty。可以通过按 ctrl-z 暂停它。
    • 挂起的作业可以移动到前台 ( fg) 或后台 ( bg)。
    • 作业可以在后台启动«command»&
    • 可以将后台作业带到前台fg %jobid
    • 作业是一个进程,加上 shell 提供的其他元数据帮助。作业只能从启动它的 shell 中作为作业进行访问。从其他角度来看,这只是一个过程。

    答案4

    我认为它失败了,因为 Vim 检测到它是一个后台进程并且没有终端访问权限,因此它不会弹出编辑窗口,而是自行挂起。我想修复它,以便 Vim 表现正常。

    它实际上与前景或背景无关。

    vim 所做的就是调用isatty()函数,这会说它没有连接到终端。解决这个问题的唯一方法是让 vim 连接到终端。有两种方法可以做到这一点:

    • 确保不要使用任何标准输出的重定向。如果您有重定向,即使您最终重定向到终端,isatty()也会指向管道而不是终端,并且 vim 会自行后台运行。
    • 使用伪 tty。是的,我知道你说过你不想那样;但如果需要重定向,那么避免使用伪 ttys 是不可能的

    相关内容