为什么挂起 SSH 命令等待来自服务器上 'sshd' 中两端打开的管道的输出?
Why are hanging SSH commands waiting for output from a pipe with both ends open in 'sshd' on the server?
这是 Whosebug 上的,而不是 SuperUser/ServerFault,因为它与系统调用和 sshd 执行的 OS 交互有关,而不是我遇到的问题 using SSH(尽管也很感激 :p)。
上下文:
我通过 SSH 调用一系列复杂的脚本,例如ssh user@host -- /my/command
。远程命令进行了大量复杂的分叉和执行,最终导致远程主机上的后台守护进程 运行。偶尔(我正在慢慢发疯试图找出可靠的复制条件),ssh
命令永远不会 return 控制客户端 shell。在那些情况下,我可以进入目标主机并看到一个没有 children 的 sshd: user@notty
进程无限期挂起。
解决这个问题不是这个问题的目的。这个问题是关于 sshd
进程 在做什么 。
SSH实现为OpenSSH,version版本为5.3p1-112.el6_7.
问题:
如果我发现其中一个卡住 sshd
s 和 strace
它,我可以看到它在两个手柄上做 select,例如select(12, [3 6], [], NULL, NULL
或类似的。 lsof
告诉我这些句柄之一是连接回 SSH 客户端的 TCP 套接字。另一个是管道,管道的另一端只在同一个sshd
进程中打开。如果我使用 this SuperUser question 的答案按 ID 搜索该管道,唯一包含对该管道的引用的进程是同一进程。 lsof
证实了这一点:管道的读写端都在同一个进程中打开,例如(对于管道 788422703 和 sshd
PID 22744):
sshd 22744 user 6r FIFO 0,8 0t0 788422703 pipe
sshd 22744 user 7w FIFO 0,8 0t0 788422703 pipe
问题:
SSH 在等什么?如果管道没有连接到任何东西并且没有 child 进程,我无法想象它会发生什么事件。
"looped"pipe/what代表什么?我唯一的理论是,如果没有向 SSH 客户端提供 STDIN,目标主机 sshd
会打开一个虚拟 STDIN 管道,因此它的一些内部 child-management 代码可以更统一?但这似乎很脆弱。
SSH 是如何进入这种情况的?
我有什么Tried/Additional信息:
- 最初,我认为这是守护进程的句柄泄漏。可以通过发出一个后台命令来创建一个等待的 child-less
sshd
进程,例如ssh user@host -- 'sleep 60 &'
; sshd
将等待流关闭到守护进程;不只是它的直接退出child。由于有问题的脚本最终会导致(沿着进程树向下)启动一个守护进程,最初看起来可能是守护进程持有一个句柄。然而,这似乎并不能成立——以 sleep 60 &
命令为例,sshd
进程与守护进程通信,select 在 four 打开管道,不只是两个,并且至少有两个管道从 sshd
连接到守护进程,而不是循环。除非有一个我不知道的管道 tracking/pointing 的方法(并且可能是 - 例如,我不知道 dup
ed 文件句柄如何进入 close()
信号量等待或管道),我认为 pipe-to-self 情况不代表 waiting-on-daemon 情况。
sshd
定期接收 TCP socket/ssh 连接本身的通信,这会在 select
秒内唤醒它进行短暂的通信(在此期间 strace
显示它阻塞了 SIGCHLD),然后它返回等待相同的 FD。
- 我可能受到 this race condition (SIGCHLD getting delivered before the kernel makes data available in the pipe). However, that seems unlikely, both given the rate at which this condition manifests, and the fact that the processes being run on the target host are Perl scripts, and the Perl runtime closes and flushes open file descriptors on shutdown 的影响。
您似乎在描述通知管道。 OpenSSH sshd 主循环调用 select()
等待它有事可做。被轮询的文件描述符包括与客户端的 TCP 连接和用于服务活动通道的任何描述符。
sshd 希望能够在收到 SIGCHLD 信号时中断 select() 调用。为此,sshd 为 SIGCHLD 安装一个信号处理程序并创建一个管道。当接收到 SIGCHLD 信号时,信号处理程序将一个字节写入管道。管道的读取端包含在 select() 轮询的文件描述符列表中。写入管道的行为将导致 select() 调用 return 并指示通知管道可读。
所有代码都在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;
}
设置和执行 select 调用的代码在另一个名为 wait_until_can_do_something()
的函数中。它相当长,所以我不会在这里包含它。 OpenSSH 是开源的,this page 描述了如何下载源代码。
这是 Whosebug 上的,而不是 SuperUser/ServerFault,因为它与系统调用和 sshd 执行的 OS 交互有关,而不是我遇到的问题 using SSH(尽管也很感激 :p)。
上下文:
我通过 SSH 调用一系列复杂的脚本,例如ssh user@host -- /my/command
。远程命令进行了大量复杂的分叉和执行,最终导致远程主机上的后台守护进程 运行。偶尔(我正在慢慢发疯试图找出可靠的复制条件),ssh
命令永远不会 return 控制客户端 shell。在那些情况下,我可以进入目标主机并看到一个没有 children 的 sshd: user@notty
进程无限期挂起。
解决这个问题不是这个问题的目的。这个问题是关于 sshd
进程 在做什么 。
SSH实现为OpenSSH,version版本为5.3p1-112.el6_7.
问题:
如果我发现其中一个卡住 sshd
s 和 strace
它,我可以看到它在两个手柄上做 select,例如select(12, [3 6], [], NULL, NULL
或类似的。 lsof
告诉我这些句柄之一是连接回 SSH 客户端的 TCP 套接字。另一个是管道,管道的另一端只在同一个sshd
进程中打开。如果我使用 this SuperUser question 的答案按 ID 搜索该管道,唯一包含对该管道的引用的进程是同一进程。 lsof
证实了这一点:管道的读写端都在同一个进程中打开,例如(对于管道 788422703 和 sshd
PID 22744):
sshd 22744 user 6r FIFO 0,8 0t0 788422703 pipe
sshd 22744 user 7w FIFO 0,8 0t0 788422703 pipe
问题:
SSH 在等什么?如果管道没有连接到任何东西并且没有 child 进程,我无法想象它会发生什么事件。
"looped"pipe/what代表什么?我唯一的理论是,如果没有向 SSH 客户端提供 STDIN,目标主机 sshd
会打开一个虚拟 STDIN 管道,因此它的一些内部 child-management 代码可以更统一?但这似乎很脆弱。
SSH 是如何进入这种情况的?
我有什么Tried/Additional信息:
- 最初,我认为这是守护进程的句柄泄漏。可以通过发出一个后台命令来创建一个等待的 child-less
sshd
进程,例如ssh user@host -- 'sleep 60 &'
;sshd
将等待流关闭到守护进程;不只是它的直接退出child。由于有问题的脚本最终会导致(沿着进程树向下)启动一个守护进程,最初看起来可能是守护进程持有一个句柄。然而,这似乎并不能成立——以sleep 60 &
命令为例,sshd
进程与守护进程通信,select 在 four 打开管道,不只是两个,并且至少有两个管道从sshd
连接到守护进程,而不是循环。除非有一个我不知道的管道 tracking/pointing 的方法(并且可能是 - 例如,我不知道dup
ed 文件句柄如何进入close()
信号量等待或管道),我认为 pipe-to-self 情况不代表 waiting-on-daemon 情况。 sshd
定期接收 TCP socket/ssh 连接本身的通信,这会在select
秒内唤醒它进行短暂的通信(在此期间strace
显示它阻塞了 SIGCHLD),然后它返回等待相同的 FD。- 我可能受到 this race condition (SIGCHLD getting delivered before the kernel makes data available in the pipe). However, that seems unlikely, both given the rate at which this condition manifests, and the fact that the processes being run on the target host are Perl scripts, and the Perl runtime closes and flushes open file descriptors on shutdown 的影响。
您似乎在描述通知管道。 OpenSSH sshd 主循环调用 select()
等待它有事可做。被轮询的文件描述符包括与客户端的 TCP 连接和用于服务活动通道的任何描述符。
sshd 希望能够在收到 SIGCHLD 信号时中断 select() 调用。为此,sshd 为 SIGCHLD 安装一个信号处理程序并创建一个管道。当接收到 SIGCHLD 信号时,信号处理程序将一个字节写入管道。管道的读取端包含在 select() 轮询的文件描述符列表中。写入管道的行为将导致 select() 调用 return 并指示通知管道可读。
所有代码都在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;
}
设置和执行 select 调用的代码在另一个名为 wait_until_can_do_something()
的函数中。它相当长,所以我不会在这里包含它。 OpenSSH 是开源的,this page 描述了如何下载源代码。