Eu posso reproduzi-lo em um Ubuntu 16 assim:
-
Crie uma nova janela do Gnome Terminal.
-
Executar uma criança
bash
; entãosuspend
-
kill %1
A janela morre. UPDATE : se usarmos kill -KILL
, isso não será reproduzido!
TL; DR:
My current hypothesis (not entirely conclusive) from the below analysis is that when the child bash receives the
SIGTERM
, it seizes the terminal by forcing itself into the foreground process group. The parent Bash is likely blocking theSIGTTIN
signal and so its TTYread
receives anEIO
, and it bails. A bash which has suspended itself withsuspend
should not assert itself into the foreground when it resumes executing due to a fatal signal.
Para obter mais informações, anexei strace -f -p <pid>
ao shell pai para ver as chamadas do sistema.
Parece que ele está sendo encerrado porque, por algum motivo, ele recebe um -1 de retorno de um read
da entrada padrão, com errno
sendo EIO
: em outras palavras, erro de E / S em entrada padrão.
Aqui está o final do strace
log: PID 18860
é o pai, 18910
é o filho:
18910 exit_group(0) = ?
18910 +++ exited with 0 +++
O TTY% do pairead
é interrompido de forma reinicializável pelo SIGCHLD
:
18860 <... read resumed> 0x7ffe891c6717, 1) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
18860 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=18910, si_uid=1001, si_status=0, si_utime=0, si_stime=1} ---
O sinal do pai manipula chamadas wait4
para coletar filho:
18860 wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], WNOHANG|WSTOPPED|WCONTINUED, NULL) = 18910
18860 wait4(-1, 0x7ffe891c6010, WNOHANG|WSTOPPED|WCONTINUED, NULL) = -1 ECHILD (No child processes)
O pai executa o retorno ao kernel a partir do manipulador de sinal:
18860 rt_sigreturn({mask=[]}) = 0
E agora vem o kicker estranho, o que diabos? O read
foi reiniciado com erro de E / S:
18860 read(0, 0x7ffe891c6717, 1) = -1 EIO (Input/output error)
E o pai começa a sair:
18860 ioctl(0, TCGETS, {B38400 opost isig icanon echo ...}) = 0
18860 ioctl(0, SNDCTL_TMR_STOP or TCSETSW, {B38400 opost isig -icanon -echo ...}) = 0
18860 ioctl(0, TCGETS, {B38400 opost isig -icanon -echo ...}) = 0
[ ... ]
18860 write(2, "exit\n", 5) = 5
18860 rt_sigaction(SIGINT, {0x460390, [], SA_RESTORER, 0x7f598a157860}, {0x460390, [], SA_RESTORER, 0x7f598a157860}, 8) = 0
18860 stat("/local/home/kaz/.bash_history", {st_mode=S_IFREG|0600, st_size=57362, ...}) = 0
18860 open("/local/home/kaz/.bash_history", O_WRONLY|O_APPEND) = 3
18860 write(3, "echo $$\nbash\nkill %1\n", 21) = 21
18860 close(3) = 0
[ ... ]
etc.
Realmente parece que a terminação é uma resposta ao erro de E / S, o que é quase certamente inesperado.
Portanto, a pergunta é: qual foi o término da criança para causar esse erro de E / S subsequente? Se a criança não tiver oportunidade de fazer nada ( kill -KILL %1
), ela não reproduz, sugerindo que a criança bash
execute algumas etapas que colocam o TTY em um estado no qual ele gera -1/EIO
.
Parece que o kernel pode estar implicado nisso como uma possível causa raiz.
Além disso, tentei isso mais algumas vezes. Às vezes, as chamadas ioctl(0, ...)
que o pai emite ao sair também falham com -1/EIO
; às vezes não.
No kernel, tty_read
pode parar com EIO
por alguns motivos. O próximo passo seria adicionar um pouco de depuração printk
para ver exatamente qual. Aqui é de 4.12.2, cortesia de free-electrons.com:
static ssize_t tty_read(struct file *file, char __user *buf, size_t count,
loff_t *ppos)
{
int i;
struct inode *inode = file_inode(file);
struct tty_struct *tty = file_tty(file);
struct tty_ldisc *ld;
if (tty_paranoia_check(tty, inode, "tty_read"))
return -EIO;
if (!tty || tty_io_error(tty))
return -EIO;
/* We want to wait for the line discipline to sort out in this
situation */
ld = tty_ldisc_ref_wait(tty);
if (!ld)
return hung_up_tty_read(file, buf, count, ppos);
if (ld->ops->read)
i = ld->ops->read(tty, file, buf, count);
else
i = -EIO;
tty_ldisc_deref(ld);
if (i > 0)
tty_update_time(&inode->i_atime);
return i;
}
Não é quase certo que a disciplina de linha não tenha uma função read
(a última EIO
). Uma verificação de paranoia com falha ou tty
sendo nulo ou o tty_io_error
sendo verdadeiro.
Não é a verificação de paranóia, porque quando isso acontece, ele registra uma mensagem de aviso. Eu não vejo um no meu log do kernel. A verificação deve ser ativada em tempo de compilação e verifica se o ponteiro tty
está nulo. tty
sendo nulo por algum motivo não pode ser descartado.
tty_io_error
testa um sinalizador na estrutura TTY:
static inline bool tty_io_error(struct tty_struct *tty)
{
return test_bit(TTY_IO_ERROR, &tty->flags);
}
Se isso for definido de alguma forma, teremos um retorno EIO
persistente de read
tentativas e provavelmente outras syscalls. Isso é, no entanto, algo que é indicado por drivers TTY de nível inferior, como código serial.
Portanto, talvez a operação de disciplina de linha ld->ops->read(tty, file, buf, count);
esteja retornando -EIO
. O TTY deve estar na disciplina de linha POSIX em todos os momentos aqui numerados N_TTY
. Eu vejo que o nome do arquivo não mudou em vinte anos; ainda está em n_tty.c
. Queremos n_tty_read
Isso tem apenas uma situação EIO
:
if (test_bit(TTY_OTHER_CLOSED, &tty->flags)) {
retval = -EIO;
break;
}
Esse sinalizador está relacionado à interação TTY / PTY. O PTY aqui deve ser um dispositivo controlado pelo terminal gnome; não há razão para que isso se encerre nessa situação.
Ah, mas veja o que acontece na entrada em n_tty_read
:
c = job_control(tty, file);
if (c < 0)
return c;
Aqui é onde eu suspeito strongmente que a "arma fumegante" possa estar. Este código tem EIO
retornos e tem a ver com o controle do trabalho. Isso acaba na seguinte função, com o argumento sig
sendo SIGTTIN
.
int __tty_check_change(struct tty_struct *tty, int sig)
{
unsigned long flags;
struct pid *pgrp, *tty_pgrp;
int ret = 0;
if (current->signal->tty != tty)
return 0;
rcu_read_lock();
pgrp = task_pgrp(current);
spin_lock_irqsave(&tty->ctrl_lock, flags);
tty_pgrp = tty->pgrp;
spin_unlock_irqrestore(&tty->ctrl_lock, flags);
if (tty_pgrp && pgrp != tty->pgrp) {
if (is_ignored(sig)) {
if (sig == SIGTTIN)
ret = -EIO;
} else if (is_current_pgrp_orphaned())
ret = -EIO;
else {
kill_pgrp(pgrp, sig, 1);
set_thread_flag(TIF_SIGPENDING);
ret = -ERESTARTSYS;
}
}
rcu_read_unlock();
if (!tty_pgrp)
tty_warn(tty, "sig=%d, tty->pgrp == NULL!\n", sig);
return ret;
}
Aqui, existem duas condições para EIO
. Uma é que a tarefa de chamada que está tentando ler do TTY não está no grupo de processos em primeiro plano e está ignorando o sinal SIGTTIN
.
Isso está de acordo com o POSIX (edição 7 de 2016) que diz:
Any attempts by a process in a background process group to read from its controlling terminal cause its process group to be sent a SIGTTIN signal unless one of the following special cases applies: if the reading process is ignoring the SIGTTIN signal or the reading thread is blocking the SIGTTIN signal, or if the process group of the reading process is orphaned, the read() shall return -1, with errno set to [EIO] and no signal shall be sent. The default action of the SIGTTIN signal shall be to stop the process to which it is sent. [11.1.3 The Controlling Terminal]
O problema é que não esperamos que o shell pai fique órfão.
Pode ser simplesmente que o filho de saída force-se em primeiro plano à medida que sai, deixando o pai inesperadamente em segundo plano?
De fato, o que estou vendo em um dos meus strace
logs é que o pai está saindo antes do filho um, e o filho está fazendo tcsetpgrp
para fazer o primeiro plano. Ie em alguns casos, o pai nem recebe o sinal SIGCHLD
; ele recebe o erro de E / S da interferência e das fitas de TTY da criança finalizadora. Então a criança termina sua terminação.