Почему зависшие команды SSH ожидают вывода из канала с обоими открытыми концами в «sshd» на сервере?

Это происходит в StackOverflow, а не в SuperUser/ServerFault, поскольку это связано с системными вызовами и взаимодействием с ОС, выполняемыми sshd, а не с проблемой, с которой я использую SSH (хотя помощь в этом приветствуется, тоже :р).

Контекст:

Я запускаю сложную серию сценариев через SSH, например. ssh user@host -- /my/command. Удаленная команда выполняет много сложных разветвлений и выполнения и в конечном итоге приводит к запуску фонового процесса демона на удаленном хосте. Иногда (я медленно схожу с ума, пытаясь выяснить надежные условия воспроизведения), команда ssh никогда не возвращает управление клиентской оболочке. В таких ситуациях я могу зайти на целевой хост и увидеть процесс sshd: user@notty без дочерних процессов, висящих на неопределенный срок.

Устранение этой проблемы не является тем, о чем этот вопрос. Этот вопрос касается того, что этот sshd процесс делает.

Реализация SSH — OpenSSH, а версия — 5.3p1-112.el6_7.

Проблема:

Если я найду один из этих застрявших sshd и strace, я увижу, что он делает выбор на двух дескрипторах, например. select(12, [3 6], [], NULL, NULL или аналогичный. lsof сообщает мне, что один из этих дескрипторов — это TCP-сокет, соединяющийся с клиентом SSH. Другой — канал, другой конец которого открыт только в том же sshd процессе. Если я ищу этот канал по идентификатору, используя ответ на этот вопрос суперпользователя, единственный процесс, который содержит ссылки на этот канал, — это тот же процесс. 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 & в качестве примера, sshd процессы, взаимодействующие с демонами, удерживают и выбирают четыре открытых канала, а не только два, а по крайней мере два из каналы подключены от sshd к процессу демона, а не зациклены. Если не существует метода отслеживания/указания на канал, о котором я не знаю (а он, вероятно, есть — например, я понятия не имею, как duped файловые дескрипторы играют в close() семафорное ожидание или конвейер), я не думаю, что канал Ситуация с самим собой представляет случай ожидания на демоне.
  • sshd периодически получает связь через TCP-сокет/ssh-соединение, что пробуждает его из selects на короткий период связи (во время которого strace показывает, что он блокирует SIGCHLD), а затем возвращается к ожиданию на тех же FD.
  • Возможно, на меня влияет это состояние гонки (SIGCHLD доставляется до того, как ядро ​​сделает данные доступными в конвейере). Однако это кажется маловероятным, учитывая скорость, с которой проявляется это состояние, и тот факт, что процессы, выполняемые на целевом хосте, являются сценариями Perl, а Среда выполнения Perl закрывается и сбрасывает дескрипторы открытых файлов при завершении работы.

person Zac B    schedule 12.06.2016    source источник
comment
Вы пробовали перезапускать sshd с включенной отладкой и смотреть логи?   -  person xxfelixxx    schedule 12.06.2016
comment
Ключевое слово в вашем вопросе — OpenSSH. Вместо того, чтобы гадать, что делает черный ящик внутри, прочитайте код себя. Это довольно хорошо написано.   -  person msw    schedule 12.06.2016
comment
Спасибо за это подробное описание - оно кратко описывает проблему, с которой я столкнулся. В моем случае команда ssh использовала packer для запуска виртуальной машины VirtualBox. Packer был настроен так, чтобы оставить виртуальную машину работающей, если сборка не удалась. После прочтения ссылки на состояние гонки, которую вы разместили, я покопался, и кажется, что VirtualBox наследует один из каналов STDOUT / STDERR упаковщика и держит его открытым после выхода упаковщика. Это приводит к зависанию sshd в ожидании закрытия канала. Уничтожение VirtualBox приводит к правильному закрытию соединения ssh.   -  person Matt    schedule 26.02.2018


Ответы (1)


Кажется, вы описываете канал уведомлений. Основной цикл OpenSSH sshd вызывает select(), чтобы дождаться, пока ему не нужно будет что-то делать. Опрашиваемые файловые дескрипторы включают TCP-соединение с клиентом и любые дескрипторы, используемые для обслуживания активных каналов.

sshd хочет иметь возможность прерывать вызов select() при получении сигнала SIGCHLD. Для этого 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 является открытым исходным кодом, и на этой странице описано, как загрузить исходный код.

person Kenster    schedule 12.06.2016