为什么挂起的 SSH 命令会等待服务器上“sshd”中两端都打开的管道的输出?

Zac*_*c B 5 linux ssh fork pipe sshd

这是 StackOverflow 上的问题,而不是 SuperUser/ServerFault 上的问题,因为它与 sshd 执行的系统调用和操作系统交互有关,而不是我使用SSH时遇到的问题(尽管对此的帮助也值得赞赏:p)。

语境:

我通过 SSH 调用一系列复杂的脚本,例如ssh user@host -- /my/command. 远程命令执行大量复杂的分叉和执行,最终导致在远程主机上运行后台守护进程。有时(我正在慢慢地疯狂地试图找出可靠的复制条件),该ssh命令永远不会将控制权返回给客户端 shell。在这些情况下,我可以进入目标主机并看到sshd: user@notty没有子进程无限期挂起的进程。

解决这个问题不是这个问题的目的。这个问题是关于该sshd进程正在什么的。

SSH实现是OpenSSH,版本版本是5.3p1-112.el6_7。

问题:

如果我找到其中一个卡住的sshds 和strace它,我可以看到它正在对两个句柄进行选择,例如select(12, [3 6], [], NULL, NULL或类似的。lsof告诉我这些句柄之一是连接回 SSH 客户端的 TCP 套接字。另一个是管道,其另一端仅在同一sshd进程中打开。如果我使用此超级用户问题的答案按 ID 搜索该管道,则包含对该管道的引用的唯一进程是同一进程。lsof证实了这一点:管道的读写端在同一进程中打开,例如(对于管道 788422703 和sshdPID 22744):

sshd    22744 user    6r  FIFO                0,8      0t0 788422703 pipe
sshd    22744 user    7w  FIFO                0,8      0t0 788422703 pipe 
Run Code Online (Sandbox Code Playgroud)

问题:

SSH 还在等什么?如果管道没有连接到任何东西并且没有子进程,我无法想象它会发生什么事件。

那个“环形”管道是什么/它代表什么?我唯一的理论是,也许如果 STDIN 没有提供给 SSH 客户端,目标主机sshd会打开一个虚拟 STDIN 管道,这样它的一些内部子管理代码就可以更加统一?但这似乎很脆弱。

SSH 是如何陷入这种情况的呢?

我尝试过的/附加信息:

  • 最初,我认为这是守护进程的句柄泄漏。可以sshd通过发出一个后台命令来创建一个等待的、无子进程的进程,例如ssh user@host -- 'sleep 60 &'sshd将等待流对守护进程关闭;不仅仅是其直接子级的退出。由于有问题的脚本最终导致(沿着进程树向下)启动一个守护进程,因此最初看起来该守护进程可能持有一个句柄。然而,这似乎并不能成立——以该sleep 60 &命令为例,与守护进程通信的进程在四个sshd打开的管道上进行保留和选择,而不仅仅是两个,并且至少有两个管道连接到守护进程,不循环。除非有一种跟踪/指向我不知道的管道的方法(而且很可能是这样——例如,我不知道ed 文件句柄如何参与信号量等待或管道),我不认为管道 - to-self 情况代表等待守护进程的情况。sshddupclose()
  • sshd定期接收 TCP 套接字/ssh 连接本身的通信,这会将其从selects 中唤醒并进行短暂的通信(在此期间strace显示它阻塞 SIGCHLD),然后它返回到等待相同的 FD。
  • 我可能受到这种竞争条件的影响(SIGCHLD 在内核使数据在管道中可用之前被传递)。然而,考虑到这种情况出现的速度,以及目标主机上运行的进程是 Perl 脚本,并且Perl 运行时在 shutdown 时关闭并刷新打开的文件描述符这一事实,这似乎不太可能。

Ken*_*ter 5

您似乎正在描述通知管道。OpenSSH sshd 主循环调用select()等待,直到有事情要做。被轮询的文件描述符包括到客户端的 TCP 连接以及用于服务活动通道的任何描述符。

sshd 希望能够在收到 SIGCHLD 信号时中断 select() 调用。为此,sshd 为 SIGCHLD 安装一个信号处理程序并创建一个管道。当接收到 SIGCHLD 信号时,信号处理程序将一个字节写入管道。管道的读取端包含在 select() 轮询的文件描述符列表中。写入管道的行为将导致 select() 调用返回,并指示通知管道可读。

所有代码都在serverloop.c

/*
 * we write to this pipe if a SIGCHLD is caught in order to avoid
 * the race between select() and child_terminated
 */
static int notify_pipe[2];
static void
notify_setup(void)
{
        if (pipe(notify_pipe) < 0) {
                error("pipe(notify_pipe) failed %s", strerror(errno));
        } else if ((fcntl(notify_pipe[0], F_SETFD, 1) == -1) ||
            (fcntl(notify_pipe[1], F_SETFD, 1) == -1)) {
                error("fcntl(notify_pipe, F_SETFD) failed %s", strerror(errno));
                close(notify_pipe[0]);
                close(notify_pipe[1]);
        } else {
                set_nonblock(notify_pipe[0]);
                set_nonblock(notify_pipe[1]);
                return;
        }
        notify_pipe[0] = -1;    /* read end */
        notify_pipe[1] = -1;    /* write end */
}
static void
notify_parent(void)
{
        if (notify_pipe[1] != -1)
                write(notify_pipe[1], "", 1);
}
[...]

/*ARGSUSED*/
static void
sigchld_handler(int sig)
{
        int save_errno = errno;
        child_terminated = 1;
#ifndef _UNICOS
        mysignal(SIGCHLD, sigchld_handler);
#endif
        notify_parent();
        errno = save_errno;
}
Run Code Online (Sandbox Code Playgroud)

设置和执行 select 调用的代码位于另一个名为 的函数中wait_until_can_do_something()。它相当长,所以我不会在这里包含它。OpenSSH 是开源的,本页介绍如何下载源代码。