Contents

在Linux中将终端附加到分离进程

1. 简介

使用 Linux 通常意味着使用终端。与具有可点击窗口的图形用户界面 (GUI) 环境不同,在命令行界面 (CLI) 中切换和控制后台进程很少是简单的。

在本教程中,我们处理将终端附加到当前 shell 之外的进程的方法。首先,我们简要探讨如何在 GUI 环境中处理切换工作。接下来,我们将研究使用 CLI 实现类似机制的方法。之后,我们在面对后台作业时检查异常。我们继续介绍附加到进程、分离和识别分离进程的基础知识。接下来,我们深入研究一种方法来确认进程附加在哪里。最后,我们遵循使用调试器和专用工具重新附加进程所需的所有步骤。

我们使用 GNU Bash 5.1.4 在 Debian 11 (Bullseye) 上测试了本教程中的代码。它应该在大多数符合 POSIX 的环境中工作。

2. GUI进程切换

在GNOME 、KDE 和Xfce 等桌面环境中,控件可作为图形元素在窗口和栏上使用。例如,GNOME 的任务栏(Gnome 面板 的一部分)允许通过简单的单击来切换窗口和进程。

此外,可能还有用于常见任务的键盘快捷键,例如最小化、最大化和更改窗口和控件的焦点。

当然,如果我们没有可用的桌面环境,上述任何一项都是不可能的。此外,进程可以是无头的或完全没有 GUI 和窗口。

即使在这些情况下,我们至少应该始终能够使用终端。让我们探索一下我们可以在那里做些什么。

3. CLI GUI 仿真

事实上,Linux 强加命令行方式来处理输入是很常见的。结果,我们恢复到终端 和shell 来满足我们的需要。

例如,有screen 和/tmux 命令。它们在 CLI 中启用模拟窗口环境。 部署后,这两个命令都在终端中执行,但会公开一个基本界面,允许用户不仅运行命令,还可以:

  • 通过拆分主窗口来创建新的终端窗口
  • 重新排列窗口
  • 在窗口之间切换
  • 向不同的终端发送命令

实际上,在 没有后者的情况下,**我们在桌面环境中获得了相当于多个排列良好的终端窗口。**在这样的设置中,进程切换很方便。

例如,在screen中,我们可以使用CTRL-a c创建一个新窗口并使用CTRL-a n切换到它。同样,tmux提供了具有类似功能的CTRL-b c和CTRL-b o快捷方式。

但是,即使在这些条件下,我们仍然可能有无法直接控制的流程。

4. 后台作业

Linux 支持作业 。简而言之,作业使用户能够运行后台进程,而无需让他们接管当前终端。

这可以非常方便地允许在等待长时间任务完成时在同一个 shell 中工作。在后台运行作业的最简单方法是附加一个 & 符号:

$ sleep 10 &
[1] 666
$

请注意,我们得到了后台作业的 PID: 666。接下来,我们可以检查作业的状态以及启动的命令行:

$ jobs
[1]+  Running                 sleep 10 &
$

我们怎样才能回到工作岗位?通过使用fg将其置于前台:

$ fg
sleep 10
[...]
$

值得注意的是,切换作业和在作业之间切换的机制类似于将我们的终端附加到不同的子任务。

5. 连接终端

将终端附加到进程意味着什么?基本上,我们重定向当前的标准流 :

  • 标准输入(0)
  • 标准输出(1)
  • 标准错误(2)

附加意味着确保所有三个都正确分配到给定进程的终端。因此,任何过程输入或输出都来自我们控制的终端。

事实上,正如我们在上面看到的,当我们从给定的 Bash 会话启动进程时,通常默认情况下会发生这种情况。现在,让我们探索如何分离。

6. 分离的进程

分离的进程不属于任何终端或shell。有几种常用的方法可以隔离这样的进程。

6.1. disown

我们已经介绍了disown 内置:

$ sleep 3600 &
[1] 666
$ jobs
[1]+  Running                 sleep 3600 &
$ disown 666
$ jobs
$

此时,我们无法通过当前 shell直接访问后台任务(PID 666 )。 虽然disown本身是一个内置的,但它不是标准化的。这意味着它的功能可能不同或完全不存在,具体取决于外壳。让我们探索一下 POSIX 方法。

6.2. nohup

分离进程的另一种方法是外部但符合 POSIX 的nohup 工具:

$ nohup sleep 3600 &
[1] 666
nohup: ignoring input and appending output to 'nohup.out'
$ jobs
[1]+  Running                 nohup sleep 3600 &

请注意,jobs 命令仍然在后台向我们显示该命令。 此外,我们再次获得了 PID,但也注意到了我们进程的流。由于nohup是标准的,我们将使用它来满足我们的需要,而不是disown。

7. 识别分离的进程

我们可以使用ps 和我们从后台收到的PID 找到分离的进程:

$ ps 666
  PID TTY      STAT   TIME COMMAND
  666 pts/0    S      0:00 sleep 3600

注意TTY列中的pts/0 。这意味着伪终端 0 负责我们后台进程的输入/输出 (IO)。如果我们回到链接到pts/0的**bash会话,我们可以确认该作业不存在:

$ tty
/dev/pts/0
$ jobs
$

使用tty ,我们确保打开了正确的终端。然后我们看到该进程无法通过jobs访问。

我们将原始进程的这种状态称为“分离”:

  • 不属于任何终端的工作列表
  • 它原来的终端仍然开放

让我们退出,开始*sleep *的终端:

$ tty
/dev/pts/0
$ exit

接下来,我们移动到另一个 Bash 会话并通过 PID 检查进程:

$ tty
/dev/pts/1
$ ps 666
  666 ?        S      0:00 sleep 3600

现在,我们在TTY列中看到一个问号 ( ? ) 。它表明该进程缺少附加的 TTY 。事实上,我们必须使用ps的x标志来在输出中包含此类进程。

我们称这种状态为“完全分离”。重要的是,在这种状态下,任何等待输入的进程都将被终止,因为*stdin *已经关闭。

8. /proc目录文件描述符

尽管 POSIX 没有为/proc伪文件系统 提供标准,但大多数主要的 Linux 发行版都支持它。

简而言之,它提供了一个树状文件结构,其中包含有关进程的信息。当然,其中一部分是打开的文件描述符,我们可以通过ls 获得:

$ ls -l /proc/666/fd/
total 0
lrwx------ 1 root root 64 Mar 03 06:56 0 -> /dev/pts/0
lrwx------ 1 root root 64 Mar 03 06:56 1 -> /dev/pts/0
lrwx------ 1 root root 64 Mar 03 06:56 2 -> /dev/pts/0

值得注意的是,这些都与我们的终端相关联。但是,将数据管道传输到/dev/pts/0*除了填满我们原来的终端外,并没有做太多的事情*。正因为如此,我们不能以这种方式与我们分离的进程进行交互。

这些描述符如何变化?让我们终止原始进程的shell并找出:

$ tty
/dev/pts/0
$ exit

我们看到所有描述符都被删除了:

$ tty
/dev/pts/1
$
total 0
lrwx------ 1 root root 64 Mar 03 06:56 0 -> '/dev/pts/0 (deleted)'
lrwx------ 1 root root 64 Mar 03 06:56 1 -> '/dev/pts/0 (deleted)'
lrwx------ 1 root root 64 Mar 03 06:56 2 -> '/dev/pts/0 (deleted)'

事实上,如果我们要检查以nohup启动的进程的相同描述符,结果会有所不同:

$ nohup sleep 3600 &
[1] 666
nohup: ignoring input and appending output to 'nohup.out'
$ ls -l /proc/666/fd/
total 0
lrwx------ 1 root root 64 Mar 03 16:56 0 -> /dev/null
lrwx------ 1 root root 64 Mar 03 16:56 1 -> /nohup.out
lrwx------ 1 root root 64 Mar 03 16:56 2 -> /nohup.out

在这里,根据通知,输入被忽略(/dev/null ),而所有输出都进入文件。此外,有许多方法可以检查专门以nohup 启动的程序,以及在另一个 Bash 会话中跟踪它们的输出。

一旦我们知道哪个进程被分离,让我们探索重新附加它的方法。

9. 通过gdb附加

之前,我们讨论了运行进程输出重定向 。在那里,我们使用gdb (GNU 项目调试器)来实现我们的目标。

同样,我们可以使用 GNU 调试器来重定向进程的输入和输出:

  1. 创建命名管道
  2. 运行一个需要输入的进程
  3. 在gdb中打开进程
  4. 将输入重定向到管道
  5. 将所有输出重定向到文件
  6. 通过管道和文件与进程交互

让我们看看上面的代码。

9.1. 管道和流程创建

让我们首先创建一个管道并开始我们的流程:

$ mkfifo /stdin_pipe
$ nohup perl -e '$|++; sleep 60; print STDOUT "Output."; print STDERR "Error."; $s = ; open($f, ">", "/perl_input") or die $!; print $f $s;' & exit
[1] 666

该过程本身,一个perl 单线(-e),执行以下操作:

  1. 每次输出后强制刷新缓冲区
  2. 睡一分钟
  3. 打印“输出”。到标准输出
  4. 打印“错误”。到标准错误
  5. 将一行标准输入分配给*$s*
  6. 打开(或创建)/perl_input进行写入
  7. 将$s打印到*/perl_input*

上面的 sleep 是针对我们在重定向stdin之前关闭 Bash 会话的情况。在这些情况下,进程会立即终止,因为它将通过等待关闭句柄上的输入开始。

9.2. gdb修改

接下来,我们通过进程的 PID启动gdb:

$ gdb -p 666
[...]
(gdb) call close(0)
$1 = 0
(gdb) call close(1)
$2 = 0
(gdb) call close(2)
$3 = 0
(gdb) call open("/stdin_pipe", 0x180)
$4 = 0
(gdb) call open("/stdout_regular", 0x441, 0x1FF)
$5 = 0
(gdb) call open("/stderr_regular", 0x441, 0x1FF)
$6 = 0
(gdb) quit
A debugging session is active.
        Inferior 1 [process 666] will be detached.
Quit anyway? (y or n) y
Detaching from program: /usr/bin/perl, process 666
[Inferior 1 (process 666) detached]

首先,我们使用call close来移除与标准流的所有关联。接下来,我们按顺序打开它们:

  1. stdin (0) 作为管道
  2. stdout (1) 作为常规文件*/stdout_regular*
  3. stderr (2) 作为常规文件*/stderr_regular*

让我们继续与我们的流程进行交互。

9.3. 重新附加的进程交互

重要的是,当**在gdb中打开*/stdin_pipe时,我们必须从另一个终端向管道发送一些输入,**以便我们可以在gdb*中继续:

$ echo 'Input.' > /stdin_pipe

我们在这里写入/stdin_pipe的内容将是发送到我们的perl*片段*并因此写入*/perl_input*的实际输入。

请注意,我们用于标准流的open() 参数很重要。特别是,可以在文档中看到标志 的值。

退出gdb后,我们可以通过ps 666确认该过程何时完成。最后,我们可以确保所有文件都收到正确的数据:

$ cat /perl_input
Input.
$ cat /stdout_regular
Output.
$ cat /stderr_regular
Output.

虽然不是标准方法,但gdb允许我们处理所有文件描述符,包括标准文件描述符,因此我们实际上可以将它们附加到任何东西。

理论上,这包括终端,但实际上,我们必须为此创建另一个工具。它的工作是捕获终端的流作为自己的流,然后将它们分配给我们的进程,就像我们在上面使用 GNU 调试器所做的那样。

事实上,已经有这样的实用程序了。

10. 使用reptyr附加

方便的是,reptyr 是一种将终端流附加到给定进程的工具。在我们的例子中,它大大缩短了上一节中的过程:

  1. 运行一个需要输入的进程。
  2. 记下进程的PID。
  3. 将 PID 传递给新终端中的reptyr。
  4. 与流程互动。

让我们像以前一样开始这个过程:

$ nohup perl -e '$|++; sleep 60; print STDOUT "Output."; print STDERR "Error."; $s = ; open($f, ">", "/perl_input") or die $!; print $f $s;' & exit
[1] 666

接下来,**我们启动另一个终端并为其 PID运行reptyr **:

$ reptyr 666
[-] Timed out waiting for child stop.

该通知通常可以安全地忽略,因为它取决于进程对SIGSTOP信号 的反应。它应该强制转换到停止状态 (T) ,但并非总是如此。 在此之后,我们可以发送我们的输入行:

$ reptyr 666
[-] Timed out waiting for child stop.
Input.

最后,睡眠完成后,我们收到所有预期的输出:

$ reptyr 666
[-] Timed out waiting for child stop.
Input.
Output.Error.

检查*/perl_input*文件显示一切都符合预期:

$ cat /perl_input
Input.

reptyr是如何做到这一点的?**通过利用strace 的基础ptrace,**我们已经将其用于以下进程输出 。