Estranho: terminar bash mata o pai?

4

Peço desculpas se a resposta for realmente simples. Registrado em um servidor Linux, eu estava praticando diferentes controles de trabalho e cheguei ao comando suspender . Sendo curioso, fiz a primeira coisa que alguém faria: digite "suspender" e ver o que acontece.

user@server:~$ suspend
-bash: suspend: cannot suspend a login shell

Então eu criei um subshell e tentei suspendê-lo:

user@server:~$ bash
user@server:~$ suspend

[1]+ Stopped   bash
user@server:~$ 

Tudo bem. Ou então eu pensei! Estando satisfeito com o funcionamento do comando de suspensão, decidi terminar com essa sub-rotina:

user@server:~$ kill %1

[1]+ Stopped   bash
user@server:~$ user@server:~$

Estranho, pensei. Ignorando o fato de que eu não consegui terminar a sub-shell, recebi dois avisos nessa linha. Então eu apertei enter para obter um prompt mais ordenado, e:

user@server:~$ user@server:~$ logout
user@server:~$ Connection to server closed.
user@client:~$

Isso é uma surpresa. Ele também funciona com um terminal local, não requer estar conectado a um servidor remoto. Um terminal local retornará ao prompt de login. Um terminal em uma sessão da área de trabalho será fechado.

Então, como tentar matar um subnível em segundo plano resulta na morte do pai?

    
por ajhlinuxuser 19.07.2017 / 12:57

2 respostas

3

Eu posso reproduzi-lo em um Ubuntu 16 assim:

  • Crie uma nova janela do Gnome Terminal.

  • Executar uma criança bash ; então suspend

  • 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 the SIGTTIN signal and so its TTY read receives an EIO, and it bails. A bash which has suspended itself with suspend 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:

Epílogo da criança saindo:

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.

    
por 20.07.2017 / 00:41
1

Parece um bug no bash. Ele replica no meu Ubuntu GNU bash, version 4.3.48(1)-release (x86_64-pc-linux-gnu) .

  • Não é necessário suspend , como ocorre também depois de kill -STOP bash_pid .
  • Isso não ocorre se você tiver kill -9 %1 em vez de kill %1 .
  • Isso não ocorre se você tiver kill pid em vez de kill %1 .
  • Não ocorre se o subprocesso for algo diferente de bash (tente dash ou sleep 999 ). Nesse caso, no entanto, o comportamento bash ainda é inesperado para mim - bash não deveria SIGCONT sleep 999 neste caso, mas aparentemente faz.
  • Ele não ocorre em outros shells (incluindo dash executando um subprocesso dash ) e eles matam de maneira mais esperada. Nosso subprocesso parado e morto permanece parado (o ps uw mostra consistentemente o subprocesso no estado T ). Depois de acordar o subprocesso com o SIGCONT, ele processa o SIGTERM e morre sem afetar seu pai.
por 19.07.2017 / 22:42

Tags