프런트엔드 터미널 액세스를 통해 백그라운드에서 명령 실행

프런트엔드 터미널 액세스를 통해 백그라운드에서 명령 실행

임의의 명령을 실행하고 하위 프로세스와 상호 작용(구체적인 세부 정보는 생략됨)한 다음 종료될 때까지 기다릴 수 있는 함수를 만들려고 합니다. 성공하면 입력이 run <command>마치 알몸인 것처럼 동작합니다 <command>.

하위 프로세스와 상호작용하지 않았다면 간단히 다음과 같이 작성했을 것입니다.

run() {
    "$@"
}

하지만 실행되는 동안 상호 작용해야 하므로 coproc및 를 사용하여 더 복잡한 설정을 사용합니다 wait.

run() {
    exec {in}<&0 {out}>&1 {err}>&2
    { coproc "$@" 0<&$in 1>&$out 2>&$err; } 2>/dev/null
    exec {in}<&- {out}>&- {err}>&-

    # while child running:
    #     status/signal/exchange data with child process

    wait
}

(이것은 단순화한 것입니다. 모든 coproc리디렉션이 실제로 유용한 작업을 수행하지는 않지만 "$@" &실제 프로그램에서는 리디렉션이 필요합니다.)

명령은 "$@"무엇이든 될 수 있습니다. 내 기능은 run lsrun make등에서 작동하지만 run vim. Vim이 백그라운드 프로세스이고 터미널 액세스 권한이 없음을 감지하여 편집 창을 표시하는 대신 자체적으로 정지하기 때문에 실패한 것 같습니다. Vim이 제대로 작동하도록 수정하고 싶습니다.

coproc "$@"상위 셸이 "백그라운드"가 되는 동안 "전경"에서 실행되도록 하려면 어떻게 해야 합니까 ?"어린이와 상호작용" 부분은 터미널에서 읽거나 쓰지 않으므로 포그라운드에서 실행할 필요가 없습니다. tty의 제어권을 코루틴에 넘겨주게 되어 기쁩니다.

run()내가 하고 있는 일에서는 부모 프로세스와 "$@"자식 프로세스에 있는 것이 중요합니다. 나는 이 역할을 바꿀 수 없습니다. 하지만 나는할 수 있는전경과 배경을 바꿉니다. (그냥 어떻게 해야할지 모르겠어요.)

저는 Vim 특정 솔루션을 찾고 있는 것이 아닙니다. 나는 pseudo-tty를 피하는 것을 선호합니다. 내 이상적인 솔루션은 stdin과 stdout이 tty, 파이프에 연결되거나 파일에서 리디렉션될 때 동일하게 잘 작동합니다.

run echo foo                               # should print "foo"
echo foo | run sed 's/foo/bar/' | cat      # should print "bar"
run vim                                    # should open vim normally

왜 코프로세스를 사용하나요?

나는 coproc 없이도 이 문제를 작성할 수 있습니다.

run() { "$@" & wait; }

나는 방금 같은 행동을 사용했습니다 &. 하지만 내 사용 사례에서는 FIFO coproc 설정을 사용하고 있으며 cmd &.coproc cmd

왜 ptys를 피해야 합니까?

run()자동화된 환경에서 사용할 수 있습니다. 파이프나 리디렉션에 사용된 경우 에뮬레이트할 터미널이 없으며 pty를 설정하면 오류가 발생합니다.

기대치를 사용하지 않는 이유는 무엇입니까?

나는 vim을 자동화하거나 vim에 입력을 보내거나 그와 비슷한 것을 보내고 싶지 않습니다.

답변1

다음과 같이 코드를 추가했습니다.

  • 세 가지 예에서 작동합니다.
  • 기다리기 전에 상호 작용이 발생합니다.
  • interact() {
        pid=$1
        ! ps -p $pid && return
        ls -ld /proc/$pid/fd/*
        sleep 5; kill -1 $pid   # TEST SIGNAL TO PARENT
    }
    
    run() {
        exec {in}<&0 {out}>&1 {err}>&2
        { coproc "$@" 0<&$in 1>&$out 2>&$err; } 2>/dev/null
        exec {in}<&- {out}>&- {err}>&-
        { interact $! <&- >/tmp/whatever.log 2>&1& } 2>/dev/null
        fg %1 >/dev/null 2>&1
        wait 2>/dev/null
    }
    

    fg %1모든 명령에 대해 실행 되며 ( %1동시 작업에 필요한 대로 변경) 일반적인 상황에서는 다음 두 가지 중 하나가 발생합니다.

  • 명령이 즉시 종료되면 interact()수행할 작업이 없고 fg아무 작업도 수행되지 않으므로 즉시 반환됩니다.
  • 명령이 즉시 종료되지 않으면 interact()상호 작용할 수 있으며(예: 5초 후에 HUP를 보조 프로세스에 전송) fg보조 프로세스는 원래 실행되었던 것과 동일한 stdin/out/err을 사용하여 포그라운드로 가져옵니다(확인할 수 있음). 이 금액 ls -l /proc/<pid>/df).

    마지막 세 명령에서 /dev/null에 대한 리디렉션은 외관상입니다. run <command>개별적으로 실행할 때와 완전히 똑같아 보입니다 .command

  • 답변2

    예제 코드에서 Vim이 tty에서 데이터를 읽거나 일부 속성을 설정하려고 하면 SIGTTIN 신호를 통해 커널에 의해 일시 중단됩니다.

    이는 대화형 쉘이 이를 다른 프로세스 그룹에 생성하지만 (아직) 해당 그룹에 tty를 넘겨주지 않기 때문입니다. 즉, "백그라운드에 배치"합니다. 이는 정상적인 작업 제어 동작이며 tty를 넘겨주는 일반적인 방법은 를 사용하는 것입니다 fg. 물론 셸은 백그라운드로 이동하므로 일시 중지됩니다.

    이 모든 것은 쉘이 대화형일 때 의도된 것입니다. 그렇지 않으면 Vim으로 파일을 편집하는 동안 프롬프트에 명령을 계속 입력할 수 있는 것과 같습니다.

    run전체 함수를 스크립트로 전환하면 이 문제를 쉽게 해결할 수 있습니다. 이렇게 하면 tty와 경쟁하지 않고 대화형 쉘에 의해 동기적으로 실행됩니다. 이렇게 하면 자신의 샘플 코드가 이미 run (당시 스크립트)와 coproc 간의 동시 상호 작용을 포함하여 요청한 모든 작업을 수행합니다 .

    스크립트에 넣을 수 없다면 Bash 이외의 쉘이 대화형 tty를 하위 프로세스에 전달하는 것을 더 세밀하게 제어할 수 있는지 확인할 수 있습니다. 저는 개인적으로 고급 쉘의 전문가는 아닙니다.

    정말로 Bash를 사용해야 하고 대화형 셸에서 실행되는 함수를 통해 이 기능을 가져야 한다면 유감스럽게도 유일한 방법은 다음을 허용하는 언어로 도우미 프로그램과 sigprocmask(2)를 만드는 것입니다. tcsetpgrp(3)에 액세스합니다.

    목적은 tty를 강제로 획득하기 위해 하위 프로세스(coproc)의 상위 프로세스(대화형 쉘)에서 완료되지 않은 작업을 수행하는 것입니다.

    그러나 이것은 분명히 나쁜 습관으로 간주된다는 점을 명심하십시오.

    그러나 하위 쉘이 여전히 tty를 소유하고 있는 동안 상위 쉘에서 tty를 사용하지 않으면 아마도 아무런 해를 끼치지 않을 것입니다. "사용하지 않음"이란 echotty와 printf상호 작용 하지 않는다는 의미 read이며, 하위 프로세스가 아직 실행 중인 동안 tty에 액세스할 수 있는 다른 프로그램을 실행하지 않는다는 의미입니다.

    Python의 도우미 프로그램은 다음과 같습니다.

    #!/usr/bin/python3
    
    import os
    import sys
    import signal
    
    def main():
        in_fd = sys.stdin.fileno()
        if os.isatty(in_fd):
            oldset = signal.pthread_sigmask(signal.SIG_BLOCK, {signal.SIGTTIN, signal.SIGTTOU})
            os.tcsetpgrp(in_fd, os.getpid())
            signal.pthread_sigmask(signal.SIG_SETMASK, oldset)
        if len(sys.argv) > 1:
            # Note: here I used execvp for ease of testing. In production
            # you might prefer to use execv passing it the command to run
            # with full path produced by the shell's completion
            # facility
            os.execvp(sys.argv[1], sys.argv[1:])
    
    if __name__ == '__main__':
        main()
    

    C에서의 상응하는 내용은 조금 더 깁니다.

    이 도우미는 다음과 같이 exec를 사용하여 coproc에서 실행해야 합니다.

    run() {
        exec {in}<&0 {out}>&1 {err}>&2
        { coproc exec grab-tty.py "$@" {side_channel_in}<&0 {side_channel_out}>&1 0<&${in}- 1>&${out}- 2>&${err}- ; } 2>/dev/null
        exec {in}<&- {out}>&- {err}>&-
    
        # while child running:
        #     status/signal/exchange data with child process
    
        wait
    }
    

    모든 예제 사례에서 이 설정은 Ubuntu 14.04, Bash 4.3 및 Python 3.4에서 작동하며 기본 대화형 셸은 함수를 가져와 run명령 프롬프트에서 실행합니다.

    coproc에서 스크립트를 실행해야 하는 경우 를 사용하여 실행해야 할 수 있습니다 bash -i. 그렇지 않으면 Bash는 Python 스크립트에서 가져온 tty를 상속하는 대신 stdin/stdout/stderr 또는 /dev/null의 파이프로 시작할 수 있습니다. 또한 coproc 내부(또는 그 아래)에서 무엇을 실행하든 extra run()s를 호출하지 않는 것이 가장 좋습니다. (실제로는 확실하지 않으며 이 경우를 테스트하지 않았지만 적어도 신중한 캡슐화가 필요할 것이라고 생각합니다.)


    귀하의 특정 (하위) 질문에 대답하려면 몇 가지 이론을 소개해야 합니다.

    각 tty에는 소위 "세션"이 하나만 있습니다. (일반적인 데몬의 경우처럼 모든 세션에 tty가 있는 것은 아니지만 여기서는 관련이 없다고 생각합니다).

    기본적으로 각 세션은 "세션 리더"의 pid에 해당하는 ID로 식별되는 프로세스 모음입니다. 따라서 "세션 리더"는 해당 세션에 속하는 프로세스 중 하나이며 정확히 해당 특정 세션을 시작하는 첫 번째 프로세스입니다.

    모두특정 세션(리더 및 비리더)의 프로세스는 자신이 속한 세션과 연결된 tty에 액세스할 수 있습니다. 그러나 첫 번째 차이점은 다음과 같습니다.하나특정 순간의 프로세스는 "포그라운드 프로세스"라고 할 수 있습니다.그 외 모든 것들이 시간 동안은 "백그라운드 프로세스"입니다. "포그라운드" 프로세스는 tty에 무료로 액세스할 수 있습니다. 대신, "백그라운드" 프로세스가 자신의 tty에 액세스하려고 하면 커널에 의해 중단됩니다. 이는 백그라운드 프로세스가 전혀 허용되지 않는다는 의미가 아니라 커널이 "지금은 그들이 말할 차례가 아닙니다"라는 신호를 보낸다는 의미입니다.

    따라서 특정 질문에 대답하려면 다음을 수행하십시오.

    "전경"과 "배경"은 정확히 무엇을 의미하나요?

    "Prospect"는 "존재"를 의미합니다.법적으로그 순간에는 tty가 사용되고 있습니다."

    "배경"은 "당시 tty가 사용되지 않았습니다"를 의미합니다.

    즉, 귀하의 질문을 다시 인용하자면 다음과 같습니다.

    포그라운드 프로세스와 백그라운드 프로세스의 차이점을 알고 싶습니다.

    합법적인tty에 액세스하십시오.

    상위 프로세스가 계속 실행되는 동안 백그라운드 프로세스를 포그라운드로 가져올 수 있습니까?

    일반적으로: 백그라운드 프로세스(상위 또는 비상위)하다계속 실행하세요. tty에 액세스하려고 하면 (기본적으로) 중지됩니다. (참고: 이러한 특정 신호(SIGTTIN 및 SIGTTOU)를 무시하거나 처리할 수 있지만 일반적으로 그렇지 않으므로 기본 구성은 프로세스를 일시 중지하는 것입니다.)

    그러나 대화형 셸의 경우 다음과 같습니다.껍데기따라서 백그라운드에서 자식 중 하나에 tty를 전달한 후 자체를 일시 중단하기로 선택합니다(wait(2) 또는 select(2) 또는 당시 가장 적절하다고 판단되는 차단 시스템 호출에서).

    이를 통해 귀하의 특정 질문에 대한 정확한 답변은 다음과 같습니다.쉘 애플리케이션을 사용하는 경우이는 사용 중인 셸이 명령을 실행한 후 자체를 중지하지 않는 방법(내장 명령 또는 기타)을 제공하는지 여부에 따라 다릅니다 fg. AFAIK Bash는 그러한 선택을 허용하지 않습니다. 다른 쉘 응용 프로그램에 대해서는 모르겠습니다.

    와 어떻게 cmd &다른 가요 cmd?

    에서 cmdBash는 자체 세션의 새 프로세스를 생성하고 tty를 건네주고 대기 상태에 들어갑니다.

    에서 cmd &Bash는 자체 세션인 새 프로세스를 생성합니다.

    자식 프로세스에 전경 제어권을 부여하는 방법

    일반적으로 tcsetpgrp(3)을 사용해야 합니다. 실제로 이는 부모나 자녀가 수행할 수 있지만 권장되는 접근 방식은 부모가 수행하는 것입니다.

    Bash의 특정 경우: 명령을 fg실행하면 Bash는 tcsetpgrp(3)를 사용하여 하위 프로세스를 백업한 다음 대기 상태에 들어갑니다.


    여기에서 흥미로운 사실을 발견할 수 있다면 실제로 상당히 새로운 UNIX 시스템에는 세션 프로세스 간에 추가 계층 구조가 있다는 것입니다.프로세스 그룹".

    이는 제가 지금까지 "포그라운드" 개념에 대해 말한 내용이 실제로 "단일 프로세스"에 국한되지 않고 "단일 프로세스 그룹"으로 확장되기 때문에 관련이 있습니다.

    즉, 우연히흔한"포그라운드" 경우는 단 하나의 프로세스만 tty에 합법적으로 액세스할 수 있지만 커널은 실제로 전체 프로세스가 tty에 액세스할 수 있는 고급 사례를 허용합니다.프로세스 그룹(여전히 동일한 세션에 속해 있음)은 tty에 대한 합법적인 액세스 권한을 갖습니다.

    tty "포그라운드"를 넘겨주기 위해 호출된 함수가 에러가 아니라는 사실을 명명tcsetpgrp, 다음과 같은 것보다는 (예를 들어)tcsetpid.

    그러나 실제로 Bash는 이러한 더 높은 가능성을 의도적으로 활용하지 않는 것 같습니다.

    그래도 그것을 활용하고 싶을 수도 있습니다. 그것은 모두 특정 애플리케이션에 따라 다릅니다.

    프로세스 그룹화의 실제 예와 마찬가지로 위 솔루션의 "프런트 그룹 넘겨주기" 접근 방식 대신 "전경 프로세스 그룹 다시 확보" 접근 방식을 사용하도록 선택할 수도 있었습니다.

    즉, Python 스크립트에서 os.setpgid() 함수(setpgid(2) 시스템 호출을 래핑함)를 사용하여 프로세스를 현재 전경 프로세스 그룹(셸 프로세스 자체일 수도 있지만 반드시 그럴 필요는 없음)에 다시 할당하도록 할 수 있습니다. 이로써 Bash가 항복하지 않은 전경 상태를 되찾았습니다.

    그러나 이는 최종 목표를 달성하기 위한 다소 간접적인 방법이며 바람직하지 않은 부작용이 있을 수도 있습니다. 왜냐하면 프로세스 그룹의 여러 다른 사용은 tty 제어와 아무 관련이 없고 결국 coproc과 관련될 수 있기 때문입니다. 예를 들어, UNIX 신호는 일반적으로 단일 프로세스가 아닌 전체 프로세스 그룹에 전달될 수 있습니다.

    마지막으로 run()스크립트(또는 스크립트) 대신 Bash의 명령 프롬프트에서 이 작업을 수행하는 이유는 무엇입니까?로서스크립트)?

    run()명령 프롬프트에서의 호출은 Bash의 자체 프로세스(*)에 의해 수행되는 반면, 스크립트에서 호출될 때는 다른 프로세스(그룹)에 의해 수행되기 때문에 대화형 Bash는 기꺼이 tty를 해당 프로세스에 넘겨주었습니다 .

    따라서 스크립트로 판단하면 tty와의 경쟁을 피하기 위해 Bash가 구현한 마지막 "방어"는 stdin/stdout/stderr에 대한 파일 설명자를 저장하고 복원하는 잘 알려진 간단한 트릭에 의해 쉽게 우회됩니다.

    (*) 또는 다음에 속하는 새로운 프로세스가 생성될 수도 있습니다.그 자체와 동일프로세스 그룹. 나는 실제로 대화형 Bash가 기능을 실행하기 위해 어떤 정확한 방법을 사용하는지 조사한 적이 없지만 tty 측면에서는 아무런 차이가 없습니다.

    화타이

    답변3

    질문을 완전히 이해하지 못했지만, 여기에 귀하가 찾고 있는 내용이 있습니다.

    전경과 배경은 쉘 개념입니다.

    • 포그라운드 작업은 tty에 액세스할 수 있습니다. ctrl-z를 눌러 일시중지할 수 있습니다.
    • 대기 중인 작업은 전경( fg) 또는 배경( bg)으로 이동할 수 있습니다.
    • 작업은 백그라운드에서 시작될 수 있습니다 «command»&.
    • 백그라운드 작업을 포그라운드로 가져올 수 있음fg %jobid
    • 작업은 프로세스이자 셸에서 제공하는 기타 메타데이터 도움말입니다. 작업은 해당 작업을 시작한 셸에서만 작업으로 액세스할 수 있습니다. 다른 관점에서 보면 그것은 단지 하나의 과정일 뿐입니다.

    답변4

    Vim이 백그라운드 프로세스이고 터미널 액세스 권한이 없음을 감지하여 편집 창을 표시하는 대신 자체적으로 정지하기 때문에 실패한 것 같습니다. Vim이 제대로 작동하도록 수정하고 싶습니다.

    실제로 전경이나 배경과는 아무런 관련이 없습니다.

    vim이 하는 일은 호출뿐이다isatty()이 기능은 터미널에 연결되어 있지 않다는 것을 의미합니다. 이 문제를 해결하는 유일한 방법은 vim을 터미널에 연결하는 것입니다. 이를 수행하는 방법에는 두 가지가 있습니다.

    • 사용하지 않도록 하세요어느표준 출력의 리디렉션. 리디렉션이 있는 경우 결국 터미널로 리디렉션되더라도 isatty()터미널 대신 파이프를 가리키며 vim은 자체적으로 백그라운드에서 실행됩니다.
    • 의사 tty를 사용하십시오. 예, 당신이 그것을 원하지 않는다고 말한 것을 압니다. 하지만 리디렉션이 필요한 경우 의사 tty를 사용하지 마십시오. 예불가능한.

    관련 정보