이상한 점: 백그라운드 bash를 종료하면 상위 프로세스가 종료되나요?

이상한 점: 백그라운드 bash를 종료하면 상위 프로세스가 종료되나요?

답변이 정말 간단했다면 사과드립니다. Linux 서버에 로그인하여 다양한 작업 제어 내장 기능을 연습하다가 다음 단계에 도달했습니다.정지시키다주문하다. 호기심에서 나는 누구나 할 수 있는 첫 번째 일을 했습니다. "pause"를 입력하고 무슨 일이 일어났는지 확인하는 것이었습니다.

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

그래서 서브셸을 만들고 일시 중지하려고 했습니다.

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

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

이것은 좋다. 아니면 나는 그렇게 생각했다! hang 명령이 작동하는 것에 만족했기 때문에 서브쉘을 종료하기로 결정했습니다.

user@server:~$ kill %1

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

이상하다고 생각했습니다. 실제로 서브셸을 종료하지 못했다는 사실을 무시하고 해당 줄에 두 개의 프롬프트가 표시되었습니다. 그래서 더 간결한 프롬프트를 표시하기 위해 Enter 키를 쳤습니다.

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

이것은 놀라운 일이다. 로컬 터미널에서도 작동하므로 원격 서버에 연결할 필요가 없습니다. 로컬 터미널이 로그인 프롬프트로 돌아갑니다. 데스크톱 세션의 터미널이 닫힙니다.

그렇다면 배경 하위 쉘을 죽이려고 하면 어떻게 상위 쉘이 죽게 될까요?

답변1

Ubuntu 16에서 다음과 같이 재현할 수 있습니다.

  • 새 Gnome 터미널 창을 만듭니다.

  • bash그럼 아이를 낳아 라suspend

  • kill %1

창문이 죽었습니다. 고쳐 쓰다: 이것을 사용하면 kill -KILL다시는 이런 일이 발생하지 않습니다!

TL;박사:

아래 분석을 바탕으로 내 현재 가설(완전히 결론적이지는 않음)은 하위 bash가 이를 수신할 때 SIGTERM포그라운드 프로세스 그룹에 강제로 들어가 터미널을 점유한다는 것입니다. 상위 Bash는 SIGTTIN신호를 차단하여 TTY가 read신호를 수신한 EIO다음 종료되도록 할 수 있습니다.bash가 suspend치명적인 신호로 인해 실행을 재개하면 포그라운드로 일시 중지되어서는 안 됩니다.

더 많은 정보를 얻기 위해 strace -f -p <pid>상위 셸에 연결하여 시스템 호출을 확인했습니다.

어떤 이유로 stdin에서 -1 반환 , read즉 stdin의 I/O 오류를 수신 하기 때문에 종료되는 것처럼 보입니다 .errnoEIO

이것은 로그의 꼬리입니다 strace. PID 18860는 부모이고 18910자식입니다.

아동 탈퇴에 대한 결론:

18910 exit_group(0)                     = ?
18910 +++ exited with 0 +++

상위 TTY는 다음을 read통해 다시 시작 가능한 방식으로 중단됩니다 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} ---

부모의 신호 처리기가 호출되어 wait4자식을 수집합니다.

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)

상위 실행은 신호 처리기에서 커널로 반환됩니다.

18860 rt_sigreturn({mask=[]})           = 0

이제 이상한 점이 나타났습니다. 그게 무엇인가요? 복구 후 readI/O 오류 :

18860 read(0, 0x7ffe891c6717, 1)        = -1 EIO (Input/output error)

부모가 종료되기 시작합니다.

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.

종료는 I/O 오류에 대한 응답처럼 보이지만 이는 거의 예상치 못한 일입니다.

그렇다면 문제는 후속 I/O 오류를 일으킨 하위 프로세스의 종료가 무엇이었는가 하는 것입니다. 하위 프로세스가 아무것도 수행할 기회를 얻지 못하면( kill -KILL %1) 다시 나타나지 않습니다. 이는 하위 프로세스가 bashTTY를 생성된 상태로 만들기 위해 몇 가지 단계를 수행했음을 나타냅니다 -1/EIO.

커널이 가능한 근본 원인으로 이것과 관련이 있을 수 있는 것처럼 보입니다.

또한 몇 번 더 시도했습니다. ioctl(0, ...)종료 시 부모가 수행한 호출도 표시와 함께 실패하는 경우 -1/EIO도 있습니다.

커널에서는 tty_read여러 가지 이유로 중단이 제공될 수 있습니다. EIO다음 단계는 printk디버깅을 추가하여 그것이 어느 것인지 확인하는 것입니다. 이것은 free-electrons.com에서 제공하는 4.12.2의 내용입니다.

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;
}

이는 라인 규칙이 작동하지 않기 때문이 아닐 가능성이 거의 확실합니다 read(마지막 규칙 EIO). 편집증 검사가 실패했거나, tty비어 있거나, tty_io_error사실입니다.

이는 발생 시 경고 메시지를 기록하므로 편집증 검사가 아닙니다. 커널 로그에 이 정보가 표시되지 않습니다. 이 검사는 컴파일 타임에 활성화되어야 하며 tty포인터가 null인지 여부를 검사합니다. tty어떤 이유에서인지 비어 있는 것도 배제할 수 없습니다.

tty_io_errorTTY 구조의 플래그를 테스트합니다.

static inline bool tty_io_error(struct tty_struct *tty)
{
    return test_bit(TTY_IO_ERROR, &tty->flags);
}

이것이 어떻게든 설정되면 try 및 다른 시스템 호출에서 지속적인 반환을 받게 EIO됩니다 . read그러나 이는 직렬 코드와 같은 하위 수준 TTY 드라이버에 의해 결정됩니다.

따라서 ld->ops->read(tty, file, buf, count);일선 징계 조치가 다시 시작될 수도 있습니다 -EIO. TTY는 항상 POSIX 회선 규칙에 따라 번호가 매겨져야 합니다 N_TTY. 나는 그 파일 이름이 20년 동안 변하지 않았다는 것을 발견했습니다 n_tty.c. 우리는 원한다n_tty_read

단 하나의 경우가 있습니다 EIO:

            if (test_bit(TTY_OTHER_CLOSED, &tty->flags)) {
                retval = -EIO;
                break;
            }

그러나 이 플래그는 TTY/PTY 상호 작용과 관련이 있습니다. 여기서 PTY는 gnome 터미널에 의해 제어되는 장치여야 합니다. 이 경우 닫을 이유가 없습니다.

아, 그런데 들어가면 무슨 일이 일어나는지 보세요n_tty_read:

c = job_control(tty, file);
if (c < 0)
    return c;

이것이 내가 "스모킹 건"을 강하게 의심하는 부분입니다. 이 코드에는 EIO리턴이 있으며 작업 제어와 관련이 있습니다. 이는 sig인수를 사용하여 다음 함수로 끝납니다 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;
}

여기에는 두 가지 조건이 있습니다 EIO. 하나는 TTY에서 읽으려는 호출 작업이 포그라운드 프로세스 그룹에 있지 않고 신호 SIGTTIN가 무시된다는 것입니다.

이는 POSIX(2016년 7호)를 완벽하게 준수합니다.

백그라운드 프로세스 그룹의 프로세스가 제어 터미널에서 데이터를 읽으려고 시도하면 다음과 같은 특별한 경우가 적용되지 않는 한 SIGTTIN 신호가 프로세스 그룹으로 전송됩니다. 읽기 프로세스가 SIGTTIN 신호 또는 읽기 스레드를 무시하는 경우 SIGTTIN 신호를 차단하고 있거나 읽기 프로세스의 프로세스 그룹이 고아인 경우 read()는 -1을 반환하고 errno를 [EIO]로 설정하며 신호가 전송되지 않습니다. SIGTTIN 신호의 기본 동작은 신호가 전송되는 프로세스를 중지하는 것입니다.[11.1.3 제어단자]

문제는 우리가 상위 쉘이 고아가 되는 것을 원하지 않는다는 것입니다.

종료하는 하위 bash가 종료 시 강제로 전경으로 들어가고 부모 프로세스가 실수로 백그라운드에 남겨지는 것이 가능합니까?

실제로 strace로그 중 하나에서 본 것은 상위 bash가 종료되고 있다는 것이었습니다.앞으로아이도 그중 하나인데, 아이가 하는 일은 tcsetpgrp자신을 유망주로 만드는 일이다.즉, 어떤 경우에는 부모가 신호를 받지도 못하고 SIGCHLDTTY 간섭으로 인해 I/O 오류가 발생하여 자식 프로세스에서 빠져 나옵니다. 그런 다음 하위 프로세스 종료가 완료됩니다.

답변2

Bash의 버그처럼 보입니다. 내 우분투에서 복제됩니다 GNU bash, version 4.3.48(1)-release (x86_64-pc-linux-gnu).

  • suspend나중에도 그렇게 될 것이기 때문에 꼭 그럴 필요는 없습니다 kill -STOP bash_pid.
  • kill -9 %1대신에 이런 일은 일어나지 않을 것입니다 kill %1.
  • kill pid대신에 이런 일은 일어나지 않을 것입니다 kill %1.
  • 하위 프로세스가 아닌 경우 bash(try dash또는 ) sleep 999이런 일이 발생하지 않습니다 . 그러나 이 경우 bash의 동작은 여전히 ​​예상치 못한 것입니다. bash는 이 경우 SIGCONT하면 안 되지만 sleep 999분명히 그렇습니다.
  • dash다른 셸(하위 프로세스 실행 포함)에서는 발생하지 않으며 dash보다 예상된 방식으로 종료됩니다. 우리가 중지하고 종료한 하위 프로세스는 중지된 상태로 유지됩니다( ps uw항상 하위 프로세스가 상태로 표시됨 T). SIGCONT를 사용하여 하위 프로세스가 활성화된 후 SIGTERM을 처리하고 상위 프로세스에 영향을 주지 않고 종료됩니다.

관련 정보