簡體   English   中英

為什么在服務器上的'sshd'中掛起SSH命令等待管道兩端打開的輸出?

[英]Why are hanging SSH commands waiting for output from a pipe with both ends open in 'sshd' on the server?

這是在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。

問題:

如果我發現那些被卡住一個sshd S和strace它,我可以看到它在做一個選擇上的兩個手柄,如select(12, [3 6], [], NULL, NULL或相似。 lsof告訴我其中的一個handle是連接回SSH客戶端的TCP套接字。另一個是管道, 另一端只在同一個sshd進程中打開 。如果我使用這個SuperUser問題的答案通過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等待? 如果管道沒有連接任何東西並且沒有子進程,我無法想象它可能會發生什么事件。

什么是“循環”管道/它代表什么? 我唯一的理論是,如果STDIN沒有提供給SSH客戶端,目標主機sshd打開一個虛擬STDIN管道,因此它的一些內部子管理代碼可以更加統一嗎? 但這看起來很脆弱。

SSH是如何進入這種情況的?

我嘗試過的/附加信息:

  • 最初,我認為這是對守護進程的句柄泄漏。 可以通過發出一個背景本身的命令來創建一個等待的,無子項的sshd進程,例如ssh user@host -- 'sleep 60 &' ; sshd將等待流被關閉到守護進程; 不只是直接孩子的退出。 由於有問題的腳本最終會在啟動的守護進程中導致(在進程樹中向下),因此守護進程最初可能會保留在句柄上。 然而,這似乎並不成立 - 使用sleep 60 & command作為示例,與守護進程通信的sshd進程保持並選擇四個打開的管道,而不僅僅是兩個,並且至少有兩個管道從sshd連接到守護進程,而不是循環。 除非有一種跟蹤/指向我不知道的管道的方法(並且可能存在 - 例如,我不知道dup文件句柄如何發揮close()信號量等待或管道),我不知道認為管道到自身的情況代表了等待守護進程的情況。
  • sshd定期接收TCP套接字/ ssh連接本身的通信,這會在短暫的通信期間將其從select喚醒(在此期間strace顯示它阻止SIGCHLD),然后它返回等待相同的FD。
  • 我可能會受到這種競爭條件的影響(在內核使數據在管道中可用之前,SIGCHLD會被提供)。 但是,這似乎不太可能,既考慮了這種情況的顯示速度,又考慮了在目標主機上運行的進程是Perl腳本,並且Perl運行時關閉並在關閉時刷新打開的文件描述符

您似乎在描述通知管道。 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;
}

設置和執行select調用的代碼在另一個名為wait_until_can_do_something()函數中。 它相當長,所以我不會在這里包含它。 OpenSSH是開源的, 本頁面描述了如何下載源代碼。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM