Почему запуск фоновой задачи поверх ssh не выполняется, если псевдо-tty выделен?

Недавно я столкнулся с немного странным поведением при запуске команд над ssh. Мне было бы интересно услышать любые объяснения поведения ниже.

Запуск ssh localhost 'touch foobar &' создает файл с именем foobar, как и ожидалось:

[[email protected] ~]$ ssh localhost 'touch foobar &'
[[email protected] ~]$ ls foobar
foobar

Однако выполнение одной и той же команды, но с параметром -t для принудительного выделения псевдо-tty не создает foobar:

[[email protected] ~]$ ssh -t localhost 'touch foobar &'
Connection to localhost closed.
[[email protected] ~]$ echo $?
0
[[email protected] ~]$ ls foobar
ls: cannot access foobar: No such file or directory

Моя нынешняя теория состоит в том, что, поскольку процесс касания подкрепляется информацией, псевдо-tty выделяется и нераспределяется до того, как процесс имеет шанс запустить. Конечно, добавление одного второго сна позволяет приступить к работе, как ожидалось:

[[email protected] ~]$ ssh -t localhost 'touch foobar & sleep 1'
Connection to localhost closed.
[[email protected] ~]$ ls foobar
foobar

Если у кого-то есть окончательное объяснение, мне было бы очень интересно его услышать. Спасибо.

Ответ 1

О, это хороший.

Это связано с тем, как работают группы процессов, как bash ведет себя при вызове в качестве неинтерактивной оболочки с -c и эффектом & в командах ввода.

Ответ предполагает, что вы знакомы с тем, как работает управление заданиями в UNIX; если вы этого не сделаете, вот представление высокого уровня: каждый процесс принадлежит к группе процессов (процессы в той же группе часто помещаются там как часть конвейера команд, например cat file | sort | grep 'word' помещает процессы, выполняемые cat(1), sort(1) и grep(1) в той же группе процессов). bash - это процесс, подобный любому другому, и он также относится к группе процессов. Группы процессов являются частью сеанса (сеанс состоит из одной или нескольких групп процессов). В сеансе имеется не более одной группы процессов, называемой группой процессов переднего плана и, возможно, многими группами фонового процесса. Группа процессов переднего плана управляет терминалом (если имеется терминал управления, подключенный к сеансу); руководитель сеанса (bash) перемещает процессы от фона на передний план и от переднего плана до фона с помощью tcsetpgrp(3). Сигнал, отправленный в группу процессов, доставляется каждому процессу в этой группе.

Если концепция групп процессов и управления заданиями для вас совершенно новая, я думаю, вам нужно будет прочитать об этом, чтобы полностью понять этот ответ. Большой ресурс для изучения этого - глава 9 расширенного программирования в среде UNIX (3-е издание).

Сказав это, посмотрим, что здесь происходит. Мы должны собрать каждый кусочек головоломки.

В обоих случаях удаленная сторона ssh вызывает bash(1) с помощью -c. Флаг -c вызывает bash(1) для запуска в качестве неинтерактивной оболочки. С manpage:

Интерактивная оболочка запускается без аргументов без опций и без опции -c, стандартный ввод и ошибка которой оба подключен к терминалам (как определено isatty (3)), или один запущен с опцией -i. PS1 установлен, а $- включает i, если bashинтерактивный, разрешая оболочку script или загрузочный файл для проверки этого состояние.

Кроме того, важно знать, что контроль заданий отключен, когда bash запущен в неинтерактивном режиме. Это означает, что bash не будет создавать отдельную группу процессов для запуска команды, так как управление заданиями отключено, нет необходимости переместить эту команду между передним и задним фонами, чтобы она могла оставаться в одной и той же группе процессов как bash. Это произойдет, если вы принудительно назначили PTY на ssh с помощью -t.

Однако использование & имеет побочный эффект, заставляя оболочку не ждать завершения команды (даже если управление заданиями отключено). С manpage:

Если команда завершается управляющим оператором &, оболочка выполняет команду в фоновом режиме в подоболочке. Оболочка не дожидаться завершения команды, а статус возврата - 0. Команды, разделенные символом a; выполняются последовательно; оболочка ждет для каждой команды для завершения по очереди. Возвращаемым статусом является выход статус последней выполненной команды.

Итак, в обоих случаях bash не будет ждать выполнения команды, а touch(1) будет выполняться в той же группе процессов, что и bash(1).

Теперь рассмотрим, что произойдет, когда лидер сеанса завершит работу. Цитирование из setpgid(2) manpage:

Если сеанс имеет управляющий терминал и флаг CLOCAL для этого терминал не установлен, и происходит зависание терминала, затем сеанс лидер отправляется SIGHUP. Если лидер сеанса выходит, то SIGHUP сигнал будет также отправляться каждому процессу в процессе переднего плана группа управляющего терминала.

(Акцент мой)

Если вы не используете -t

Если вы не используете -t, на удаленной стороне не назначается PTY, поэтому bash не является лидером сеанса, и на самом деле новый сеанс не создается. Поскольку sshd работает как демон, процесс bash, который имеет forked + exec() 'd, не будет иметь управляющий терминал. Таким образом, несмотря на то, что оболочка завершается очень быстро (вероятно, до touch(1)), в группу процессов не отправляется SIGHUP, потому что bash не был лидером сеанса (и нет управляющего терминала). Итак, все работает.

Когда вы используете -t

-t принудительно выделяет PTY, что означает, что удаленная сторона ssh вызовет setsid(2), выделит псевдотерминал + для нового процесса с помощью forkpty(3), подключит вход и выход главного устройства PTY к конечным точкам сокета которые приводят к вашей машине, и, наконец, выполните bash(1). forkpty(3) открывает ведомую сторону PTY в разветвленном процессе, который станет bash; поскольку там нет управляющего терминала для текущего сеанса, а терминальное устройство открыто, устройство PTY становится управляющим терминалом для сеанса, а bash становится лидером сеанса.

Затем происходит одно и то же: touch(1) выполняется в той же группе процессов и т.д., yadda yadda. Дело в том, что на этот раз есть руководитель сеанса и контрольный терминал. Итак, поскольку bash не беспокоится о ожидании из-за &, когда он выходит, SIGHUP доставляется в группу процессов, а touch(1) умирает преждевременно.

О nohup

nohup(1) здесь не работает, потому что все еще есть состояние гонки. Если bash(1) завершается до того, как nohup(1) имеет возможность настроить необходимую обработку сигнала и перенаправление файлов, он не будет иметь никакого эффекта (что, вероятно, происходит)

Возможное исправление

Сильное повторное включение управления заданиями исправляет это. В bash вы делаете это с помощью set -m. Это работает:

ssh -t localhost 'set -m ; touch foobar &'

Или принудительно bash дождаться завершения touch(1):

ssh -t localhost 'touch foobar & wait `pgrep touch`'