Соединения SSH, работающие в фоновом режиме, не выводятся, если несколько соединений были запущены одной и той же оболочкой

По-видимому, если одна и та же оболочка запускает несколько ssh-соединений на один и тот же сервер, они не возвращаются после выполнения команды, которую они выдают, но будут вешать ( Stopped (tty input) ) навсегда. Проиллюстрировать:

 #!/bin/bash ssh localhost sleep 2 echo "$$ DONE!" 

Если я запускаю сценарий выше одного раза в фоновом режиме, он никогда не выходит:

 $ for i in {1..3}; do foo.sh & done [1] 28695 [2] 28696 [3] 28697 $ ## Hit enter [1] Stopped foo.sh [2]- Stopped foo.sh [3]+ Stopped foo.sh $ ## Hit enter again $ jobs -l [1] 28695 Stopped (tty input) foo.sh [2]- 28696 Stopped (tty input) foo.sh [3]+ 28697 Stopped (tty input) foo.sh 

Детали

  • Я нашел это, потому что я использовал ssh'ing в скрипте Perl для запуска команды. Такое же поведение возникает при использовании вызова system() Perl для запуска ssh .
  • Такая же проблема возникает при использовании модулей Perl вместо system() . Я попробовал Net::SSH::Perl , Net:SSH2 и Net::OpenSSH .
  • Если я запускаю несколько команд ssh из разных оболочек (открывают несколько терминалов), они работают так, как ожидалось.
  • Ничего явно полезного в информации об отладке ssh:

     OpenSSH_7.5p1, OpenSSL 1.1.0f 25 May 2017 debug1: Reading configuration data /home/terdon/.ssh/config debug1: Reading configuration data /etc/ssh/ssh_config debug2: resolving "localhost" port 22 debug2: ssh_connect_direct: needpriv 0 debug1: Connecting to localhost [::1] port 22. debug1: Connection established. debug1: identity file /home/terdon/.ssh/id_rsa type 1 debug1: key_load_public: No such file or directory debug1: identity file /home/terdon/.ssh/id_rsa-cert type -1 debug1: key_load_public: No such file or directory debug1: identity file /home/terdon/.ssh/id_dsa type -1 debug1: key_load_public: No such file or directory debug1: identity file /home/terdon/.ssh/id_dsa-cert type -1 debug1: key_load_public: No such file or directory debug1: identity file /home/terdon/.ssh/id_ecdsa type -1 debug1: key_load_public: No such file or directory debug1: identity file /home/terdon/.ssh/id_ecdsa-cert type -1 debug1: key_load_public: No such file or directory debug1: identity file /home/terdon/.ssh/id_ed25519 type -1 debug1: key_load_public: No such file or directory debug1: identity file /home/terdon/.ssh/id_ed25519-cert type -1 debug1: Enabling compatibility mode for protocol 2.0 debug1: Local version string SSH-2.0-OpenSSH_7.5 debug1: Remote protocol version 2.0, remote software version OpenSSH_7.5 debug1: match: OpenSSH_7.5 pat OpenSSH* compat 0x04000000 debug2: fd 3 setting O_NONBLOCK debug1: Authenticating to localhost:22 as 'terdon' debug3: hostkeys_foreach: reading file "/home/terdon/.ssh/known_hosts" debug3: record_hostkey: found key type ECDSA in file /home/terdon/.ssh/known_hosts:47 debug3: load_hostkeys: loaded 1 keys from localhost debug3: order_hostkeyalgs: prefer hostkeyalgs: ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521 debug3: send packet: type 20 debug1: SSH2_MSG_KEXINIT sent debug3: receive packet: type 20 debug1: SSH2_MSG_KEXINIT received debug2: local client KEXINIT proposal debug2: KEX algorithms: curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha256,diffie-hellman-group14-sha1,ext-info-c debug2: host key algorithms: ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-ed25519-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa debug2: ciphers ctos: chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com,aes128-cbc,aes192-cbc,aes256-cbc debug2: ciphers stoc: chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com,aes128-cbc,aes192-cbc,aes256-cbc debug2: MACs ctos: umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1 debug2: MACs stoc: umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1 debug2: compression ctos: none,zlib@openssh.com,zlib debug2: compression stoc: none,zlib@openssh.com,zlib debug2: languages ctos: debug2: languages stoc: debug2: first_kex_follows 0 debug2: reserved 0 debug2: peer server KEXINIT proposal debug2: KEX algorithms: curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256,diffie-hellman-group14-sha1 debug2: host key algorithms: ssh-rsa,rsa-sha2-512,rsa-sha2-256,ecdsa-sha2-nistp256,ssh-ed25519 debug2: ciphers ctos: chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com debug2: ciphers stoc: chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com debug2: MACs ctos: umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1 debug2: MACs stoc: umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1 debug2: compression ctos: none,zlib@openssh.com debug2: compression stoc: none,zlib@openssh.com debug2: languages ctos: debug2: languages stoc: debug2: first_kex_follows 0 debug2: reserved 0 debug1: kex: algorithm: curve25519-sha256 debug1: kex: host key algorithm: ecdsa-sha2-nistp256 debug1: kex: server->client cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: none debug1: kex: client->server cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: none debug3: send packet: type 30 debug1: expecting SSH2_MSG_KEX_ECDH_REPLY debug3: receive packet: type 31 debug1: Server host key: ecdsa-sha2-nistp256 SHA256:uxhkh+gGPiCJQPaP024WXHth382h3BTs7QdGMokB9VM debug3: hostkeys_foreach: reading file "/home/terdon/.ssh/known_hosts" debug3: record_hostkey: found key type ECDSA in file /home/terdon/.ssh/known_hosts:47 debug3: load_hostkeys: loaded 1 keys from localhost debug1: Host 'localhost' is known and matches the ECDSA host key. debug1: Found key in /home/terdon/.ssh/known_hosts:47 debug3: send packet: type 21 debug2: set_newkeys: mode 1 debug1: rekey after 134217728 blocks debug1: SSH2_MSG_NEWKEYS sent debug1: expecting SSH2_MSG_NEWKEYS debug3: receive packet: type 21 debug1: SSH2_MSG_NEWKEYS received debug2: set_newkeys: mode 0 debug1: rekey after 134217728 blocks debug2: key: /home/terdon/.ssh/id_rsa (0x555a5e4b5060) debug2: key: /home/terdon/.ssh/id_dsa ((nil)) debug2: key: /home/terdon/.ssh/id_ecdsa ((nil)) debug2: key: /home/terdon/.ssh/id_ed25519 ((nil)) debug3: send packet: type 5 debug3: receive packet: type 7 debug1: SSH2_MSG_EXT_INFO received debug1: kex_input_ext_info: server-sig-algs=<ssh-ed25519,ssh-rsa,rsa-sha2-256,rsa-sha2-512,ssh-dss,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521> debug3: receive packet: type 6 debug2: service_accept: ssh-userauth debug1: SSH2_MSG_SERVICE_ACCEPT received debug3: send packet: type 50 debug3: receive packet: type 51 debug1: Authentications that can continue: publickey,password debug3: start over, passed a different list publickey,password debug3: preferred publickey,keyboard-interactive,password debug3: authmethod_lookup publickey debug3: remaining preferred: keyboard-interactive,password debug3: authmethod_is_enabled publickey debug1: Next authentication method: publickey debug1: Offering RSA public key: /home/terdon/.ssh/id_rsa debug3: send_pubkey_test debug3: send packet: type 50 debug2: we sent a publickey packet, wait for reply debug3: receive packet: type 60 debug1: Server accepts key: pkalg rsa-sha2-512 blen 279 debug2: input_userauth_pk_ok: fp SHA256:OGvtyUIFJw426w/FK/RvIhsykeP8kIEAtAeZwYBIzok debug3: sign_and_send_pubkey: RSA SHA256:OGvtyUIFJw426w/FK/RvIhsykeP8kIEAtAeZwYBIzok debug3: send packet: type 50 debug3: receive packet: type 52 debug1: Authentication succeeded (publickey). Authenticated to localhost ([::1]:22). debug2: fd 6 setting O_NONBLOCK debug1: channel 0: new [client-session] debug3: ssh_session2_open: channel_new: 0 debug2: channel 0: send open debug3: send packet: type 90 debug1: Requesting no-more-sessions@openssh.com debug3: send packet: type 80 debug1: Entering interactive session. debug1: pledge: network debug3: receive packet: type 80 debug1: client_input_global_request: rtype hostkeys-00@openssh.com want_reply 0 debug3: receive packet: type 91 debug2: callback start debug2: fd 3 setting TCP_NODELAY debug3: ssh_packet_set_tos: set IPV6_TCLASS 0x08 debug2: client_session2_setup: id 0 debug1: Sending command: sleep 2 debug2: channel 0: request exec confirm 1 debug3: send packet: type 98 debug2: callback done debug2: channel 0: open confirm rwindow 0 rmax 32768 debug2: channel 0: rcvd adjust 2097152 debug3: receive packet: type 99 debug2: channel_input_status_confirm: type 99 id 0 debug2: exec request accepted on channel 0 
  • Это не зависит от моей настройки ~/.ssh/config . Переименование файла ничего не меняет.

  • Это происходит на нескольких машинах. Я пробовал 4 или 5 разных машин, на которых были обновлены дистрибутивы Ubuntu и Arch.
  • Команда ( sleep в примере с манекеном, но нечто более сложное в реальной жизни) успешно завершается и делает то, что он должен делать. Это не зависит от команды, которую вы запускаете, это проблема ssh.
  • Это худшее из них: это непротиворечиво . Время от времени один из экземпляров будет выходить и возвращать управление родительскому скрипту. Но не всегда, и нет никакой картины, которую я смог различить.
  • Переименование ~/.bashrc не имеет значения. Кроме того, я запустил это на машинах, на которых запущен Ubuntu (стандартная dash входа в систему) и Arch (по умолчанию bash умолчанию, называемой sh ).
  • Интересно, что проблема возникает, если я ударил любой ключ (например, Enter , но любой из них, похоже, работает) после запуска цикла, но до выхода первого скрипта. Если я выйду из терминала один, они закончат, как ожидалось.

Что происходит? Это ошибка в ssh? Есть ли опция, которую мне нужно установить? Как запустить несколько экземпляров скрипта, который запускает команду над ssh из той же оболочки?

Передние процессы и контроль доступа к терминалу

Чтобы понять, что происходит, вам нужно немного узнать об использовании терминалов. Что происходит, когда две программы пытаются читать с одного и того же терминала одновременно? Каждый входной байт идет случайным образом в одну из программ. (Не случайно, так как в ядре используется RNG для решения, просто случайный, как в непредсказуемом на практике.) То же самое происходит, когда две программы читаются из канала или любой другой тип файла, который представляет собой поток байтов, перемещаемых из одного места к другому (сокет, символьное устройство, …), а не к байтовому массиву, где любой байт может быть прочитан несколько раз (обычный файл, блок-устройство). Например, запустите оболочку в терминале, определите имя терминала и запустите cat .

 $ tty /dev/pts/18 $ cat 

Затем с другого терминала запустите cat /dev/pts/18 . Теперь введите терминал и посмотрите, как строки иногда переходят к одному из процессов cat а иногда и к другому. Линии отправляются в целом, когда терминал находится в режиме приготовления. Если вы поместите терминал в необработанный режим, каждый байт будет отправлен независимо.

Это грязно. Разумеется, должен существовать механизм, позволяющий решить, что одна программа получает терминал, а другие – нет. Ну, есть! Он запускается в типичных случаях, но не в описанном выше сценарии. Этот сценарий необычен, потому что cat /dev/pts/18 не запускался с /dev/pts/18 . Необычно доступ к терминалу из программы, которая не запускалась внутри этого терминала. В обычном случае вы запускаете оболочку в терминале, и вы запускаете программы из этой оболочки. Тогда правило заключается в том, что программа на переднем плане получает терминал, а программы в фоновом режиме – нет. Это называется контролем доступа к терминалу . Как это работает:

  • Каждый процесс имеет управляющий терминал (или его нет, обычно потому, что у него нет открытого дескриптора файла, который является терминалом).
  • Когда процесс пытается получить доступ к своему управляющему терминалу, если процесс не находится на переднем плане, тогда он блокирует его. (Условия применяются. Доступ к другим терминалам не регулируется.)
  • Оболочка решает, кто является процессом переднего плана. (На самом деле группа процессов переднего плана.) Она вызывает tcsetpgrp чтобы ядро tcsetpgrp кто должен быть на переднем плане.

Это работает в типичных случаях. Запустите программу в оболочке, и эта программа станет процессом переднего плана. Запустите программу в фоновом режиме (с помощью & ), и программа не будет находиться на переднем плане. Когда оболочка отображает приглашение, оболочка ставит себя на передний план. Когда вы возобновляете приостановленное задание с помощью fg , работа становится на переднем плане. С bg это не так.

Если фоновый процесс пытается прочитать с терминала, ядро ​​отправляет ему сигнал SIGTTIN. Действие по умолчанию для сигнала – приостановить процесс (например, SIGSTOP). Родитель процесса может узнать об этом, вызвав waitpid с флагом WSTOPPED ; когда дочерний процесс получает сигнал, который приостанавливает его, возвращается waitpid вызов родительского элемента и позволяет родителям узнать, что такое сигнал. Это то, как оболочка знает, чтобы напечатать «Stopped (tty input)». То, что это говорит вам, заключается в том, что эта работа приостановлена ​​из-за SIGTTIN.

Поскольку процесс приостанавливается, с ним ничего не произойдет, пока он не возобновится или не будет убит (с сигналом о том, что процесс не поймает, потому что, если процесс установил обработчик сигнала, он не будет работать с момента приостановки процесса). Вы можете возобновить процесс, отправив ему SIGCONT, но ничего не достигнет, если процесс будет считываться с терминала, он сразу получит другой SIGTTIN. Если вы возобновите процесс с помощью fg , он перейдет на передний план, и поэтому чтение будет выполнено успешно.

Теперь вы понимаете, что происходит, когда вы запускаете cat в фоновом режиме:

 $ cat & $ [1] + Stopped (tty input) cat $ 

Случай SSH

Теперь давайте сделаем то же самое с SSH.

 $ ssh localhost sleep 999999 & $ $ $ [1] + Stopped (tty input) ssh localhost sleep 999999 $ 

Нажатие Enter иногда переходит к оболочке (которая находится на переднем плане), а иногда и к процессу SSH (после чего он останавливается SIGTTIN). Зачем? Если ssh читал с терминала, он должен немедленно получить SIGTTIN, и если бы не тогда, то почему он получил SIGTTIN?

Что происходит, так это то, что SSH-процесс вызывает системный вызов select чтобы знать, когда вход доступен для любого из интересующих его файлов (или если выходной файл готов для получения большего количества данных). Источники входного сигнала включают в себя, по меньшей мере, терминал и сетевой разъем. В отличие от read , select не запрещается для фоновых процессов, и ssh не получает SIGTTIN, когда он вызывает select . Цель select – выяснить, доступны ли данные, не нарушая ничего. В идеале select не изменит состояние системы вообще, но на самом деле это не совсем так. Когда select сообщает SSH-процессу, что вход доступен в дескрипторе файла терминала, ядро ​​должно зафиксировать отправку ввода, если процесс вызывает read после этого. (Если это не так, и процесс называется read , то в этот момент не может быть никакого ввода, поэтому возвращаемое значение из select было бы ложью.) Поэтому, если ядро ​​решает направлять некоторый ввод в процесс SSH , он решает к моменту возврата системного вызова select . Затем SSH вызывает read , и в этот момент ядро ​​видит, что фоновый процесс пытался считывать с терминала и приостанавливать его с помощью SIGTTIN.

Обратите внимание, что вам не нужно запускать несколько подключений к одному и тому же серверу. Одного достаточно. Несколько соединений просто увеличивает вероятность возникновения проблемы.

Решение: не читайте с терминала

Если вам нужен сеанс SSH для чтения с терминала, запустите его на переднем плане.

Если вам не нужен сеанс SSH для чтения с терминала, убедитесь, что его вход не поступает с терминала. Есть два способа сделать это:

  • Вы можете перенаправить ввод:

     ssh … </dev/null 
  • Вы можете поручить SSH не пересылать терминальное соединение с -n или -f . ( -n эквивалентно </dev/null ; -f позволяет самому SSH считывать с терминала, например, читать пароль, но сама команда не будет открывать терминал.)

     ssh -n … 

Обратите внимание, что отключение терминала и SSH должно произойти на клиенте. Процесс sleep выполняющийся на сервере, никогда не будет считываться с терминала, но SSH не имеет возможности это знать. Если клиент получает ввод на стандартном вводе, он должен перенаправить его на сервер, который сделает данные доступными в буфере, если приложение когда-либо решит его прочитать (и если приложение вызывает select , будет сообщено, что данные доступен).

Вы можете найти помощь на странице руководства:

  -n Redirects stdin from /dev/null (actually, prevents reading from stdin). This must be used when ssh is run in the background. A common trick is to use this to run X11 programs on a remote machine. For example, ssh -n shadows.cs.hut.fi emacs & will start an emacs on shadows.cs.hut.fi, and the X11 connection will be automatically forwarded over an encrypted channel. The ssh program will be put in the background. (This does not work if ssh needs to ask for a password or passphrase; see also the -f option.) 

Если это все еще не помогает, я попробую -T (отключить распределение псевдо-tty), только по прихоти.

По-видимому, если одна и та же оболочка запускает несколько ssh-соединений на один и тот же сервер, они не возвращаются после выполнения команды, которую они выдают, но будут вешать (Stopped (tty input)) навсегда.

Это обычное поведение одновременного доступа к TTY. Весь процесс уже задан, и когда он пытается записать вывод, ему не разрешается обращаться к TTY и получать сигнал ( SIGTTOU ), который не попадает в процесс bash и поэтому выполняется действие по умолчанию ( Stop ).

Опция -n как описано в другом ответе или перенаправлении IO на некоторые файлы, поможет вам. Не уверен, есть ли что-то более подробное описание, но если да, уточните.