쉘 스크립트에서 사용자 프롬프트로 SIGINT 트랩을 어떻게 처리할 수 있습니까?

쉘 스크립트에서 사용자 프롬프트로 SIGINT 트랩을 어떻게 처리할 수 있습니까?

사용자가 실수로 ctrl-c를 눌렀을 경우 "종료하시겠습니까?(y/n)"라는 메시지가 표시되도록 SIGINT( )을 처리하려고 합니다 . CtrlCyes를 입력하면 스크립트를 종료합니다. 그렇지 않다면 인터럽트가 발생한 곳부터 계속하십시오. 기본적으로 ( )와 비슷하지만 조금 다른 방식으로 CtrlC작업 해야 합니다. 이를 달성하기 위해 다양한 방법을 시도했지만 예상한 결과를 얻지 못했습니다. 다음은 내가 시도한 몇 가지 시나리오입니다.SIGTSTPCtrlZ

사례: 1

스크립트:play.sh

#!/bin/sh
function stop()
{
while true; do 
    read -rep $'\nDo you wish to stop playing?(y/n)' yn
    case $yn in
        [Yy]* ) echo "Thanks for playing !!!"; exit 1;;
        [Nn]* ) break;;
        * ) echo "Please answer (y/n)";;
    esac
done
} 
trap 'stop' SIGINT 
echo "going to sleep"
for i in {1..100}
do
  echo "$i"
  sleep 3   
done
echo "end of sleep"

위 스크립트를 실행하면 예상한 결과를 얻습니다.

산출:

$ play.sh 
going to sleep
1
^C
Do you wish to stop playing?(y/n)y
Thanks for playing !!!

$ play.sh 
going to sleep
1
2
^C
Do you wish to stop playing?(y/n)n
3
4
^C
Do you wish to stop playing?(y/n)y
Thanks for playing !!! 
$  

케이스: 2

for루프를 새 스크립트로 이동하여 상위 프로세스와 하위 프로세스 가 loop.sh됩니다 .play.shloop.sh

스크립트:play.sh

#!/bin/sh
function stop()
{
while true; do 
    read -rep $'\nDo you wish to stop playing?(y/n)' yn
    case $yn in
        [Yy]* ) echo "Thanks for playing !!!"; exit 1;;
        [Nn]* ) break;;
        * ) echo "Please answer (y/n)";;
    esac
done
}
trap 'stop' SIGINT 
loop.sh

스크립트:loop.sh

#!/bin/sh
echo "going to sleep"
for i in {1..100}
do
  echo "$i"
  sleep 3   
done
echo "end of sleep"

이 경우 출력은 예상한 것과 다릅니다.

산출:

$ play.sh 
going to sleep
1
2
^C
Do you wish to stop playing?(y/n)y
Thanks for playing !!!

$ play.sh 
going to sleep
1
2
3
4
^C
Do you wish to stop playing?(y/n)n
$

프로세스가 신호를 받으면 SIGINT신호를 모든 하위 프로세스에 전파하므로 두 번째 사례가 실패한다는 것을 이해합니다. SIGINT하위 프로세스로 전파되는 것을 방지하여 loop.sh첫 번째 경우에 작동했던 방식과 동일하게 작업을 수행할 수 있는 방법이 있습니까 ?

메모: 이것은 실제 적용 사례일 뿐입니다. 제가 작업 중인 애플리케이션에는 play.sh및 에 여러 하위 스크립트가 있습니다 loop.sh. 을 수신할 때 애플리케이션이 SIGINT종료되지 않고 사용자에게 메시지를 표시해야 하는지 확인해야 합니다 .

답변1

좋은 예를 들어 작업 및 신호 관리에 대한 훌륭한 고전적인 질문입니다! 나는 신호 처리 메커니즘에 초점을 맞추기 위해 간단한 테스트 스크립트를 개발했습니다.

이를 달성하려면 백그라운드에서 하위 항목(loop.sh)을 시작한 후 waitINT 신호를 수신하면 killPGID가 PID와 동일한 프로세스 그룹을 호출하십시오.

  1. 문제의 스크립트의 경우 play.sh다음을 수행하여 수행할 수 있습니다.

  2. 함수 에서 다음으로 stop()대체하십시오 .exit 1

    kill -TERM -$$  # note the dash, negative PID, kills the process group
    
  3. loop.sh백그라운드 프로세스로 시작 (여기에서 여러 백그라운드 프로세스를 시작하고 에서 관리할 수 있음 play.sh)

    loop.sh &
    
  4. wait모든 하위 항목을 기다리려면 스크립트 끝에 추가하세요 .

    wait
    

$$스크립트가 프로세스를 시작하면 해당 하위는 상위 셸에 있는 상위 프로세스의 PID와 동일한 PGID를 가진 프로세스 그룹의 구성원이 됩니다 .

예를 들어, 스크립트는 백그라운드에서 trap.sh세 개의 프로세스를 시작했고 현재 프로세스를 진행 중입니다. 프로세스 그룹 ID 열(PGID)이 상위 프로세스의 PID와 동일하다는 점에 유의하세요.sleepwait

  PID  PGID STAT COMMAND
17121 17121 T    sh trap.sh
17122 17121 T    sleep 600
17123 17121 T    sleep 600
17124 17121 T    sleep 600

killUnix와 Linux에서는 음수 값으로 호출하여 해당 프로세스 그룹의 모든 프로세스에 신호를 보낼 수 있습니다 PGID. 음수를 지정하면 kill-PGID로 사용됩니다. 스크립트의 PID( $$)가 PGID와 동일하므로 다음을 kill사용하여 셸에서 프로세스 그룹을 만들 수 있습니다.

kill -TERM -$$    # note the dash before $$

신호 번호나 이름을 제공해야 합니다. 그렇지 않으면 kill의 일부 구현에서 "잘못된 옵션" 또는 "잘못된 신호 사양"이라고 알려줄 것입니다.

아래의 간단한 코드는 이 모든 것을 보여줍니다. 신호 핸들러를 설정하고 trap3개의 자식을 생성한 다음 끝없는 대기 루프에 들어가 kill신호 핸들러의 프로세스 그룹 명령에 의해 스스로 종료되기를 기다립니다.

$ cat trap.sh
#!/bin/sh

signal_handler() {
        echo
        read -p 'Interrupt: ignore? (y/n) [Y] >' answer
        case $answer in
                [nN]) 
                        kill -TERM -$$  # negative PID, kill process group
                        ;;
        esac
}

trap signal_handler INT 

for i in 1 2 3
do
    sleep 600 &
done

wait  # don't exit until process group is killed or all children die

샘플 실행은 다음과 같습니다.

$ ps -o pid,pgid,stat,args
  PID  PGID STAT COMMAND
 8073  8073 Ss   /bin/bash
17111 17111 R+   ps -o pid,pgid,stat,args
$ 

확인, 추가 프로세스가 실행되지 않습니다. 테스트 스크립트를 시작하고, 중단하고( ^C), 중단을 무시하도록 선택한 다음 일시 중지합니다( ^Z).

$ sh trap.sh 
^C
Interrupt: ignore? (y/n) [Y] >y
^Z
[1]+  Stopped                 sh trap.sh
$

실행 중인 프로세스를 확인하고 프로세스 그룹 번호( PGID)를 기록해 두십시오.

$ ps -o pid,pgid,stat,args
  PID  PGID STAT COMMAND
 8073  8073 Ss   /bin/bash
17121 17121 T    sh trap.sh
17122 17121 T    sleep 600
17123 17121 T    sleep 600
17124 17121 T    sleep 600
17143 17143 R+   ps -o pid,pgid,stat,args
$

테스트 스크립트를 포그라운드( fg)로 가져오고 다시 인터럽트( ^C)합니다. 이번에는 다음을 선택합니다.~ 아니다무시하다:

$ fg
sh trap.sh
^C
Interrupt: ignore? (y/n) [Y] >n
Terminated
$

실행 중인 프로세스를 확인하세요. 더 이상 잠들지 마세요.

$ ps -o pid,pgid,stat,args
  PID  PGID STAT COMMAND
 8073  8073 Ss   /bin/bash
17159 17159 R+   ps -o pid,pgid,stat,args
$ 

셸에 대한 참고 사항:

내 시스템에서 실행되도록 코드를 수정해야 했습니다. #!/bin/sh스크립트의 첫 번째 줄이 있지만 스크립트는 /bin/sh에서 사용할 수 없는 확장명(bash 또는 zsh의)을 사용합니다.

답변2

"프로세스가 SIGINT 신호를 받으면 해당 신호를 모든 하위 프로세스에 전파한다는 것을 이해합니다."

어디서 이런 잘못된 생각을 얻었나요?

$ perl -E '$SIG{INT}=sub { say "ouch $$" }; if (fork()) { say "parent $$"; sleep 3; kill 2, $$ } else { say "child $$"; sleep 99 }'
parent 25831
child 25832
ouch 25831
$

주장된 대로 전파가 있었다면 하위 프로세스에서 "아야"를 예상했을 수도 있습니다.

실제로:

"터미널의 인터럽트 키(종종 DELETE 또는 Control-C) 또는 종료 키(종종 Control-백슬래시)를 입력할 때마다 인터럽트 신호 또는 종료 신호가 전경 프로세스 그룹의 모든 프로세스로 전송됩니다." -- W .리차드 스티븐스. "UNIX® 환경의 고급 프로그래밍". 애디슨-웨슬리. 1993. p.246.

이는 다음을 통해 관찰할 수 있습니다.

$ perl -E '$SIG{INT}=sub { say "ouch $$" }; fork(); sleep 99'
^Couch 25971
ouch 25972
$

SIGINT포그라운드 프로세스 그룹의 모든 프로세스 는 에서 a를 먹기 때문에 Control+C해당 신호를 적절하게 처리하거나 무시하거나 하위 프로세스가 새로운 포그라운드 프로세스 그룹이 되도록 모든 프로세스를 설계해야 합니다. 예를 들어 쉘)은 더 이상 포그라운드 프로세스 그룹에 속하지 않기 때문에 신호를 볼 수 없습니다.

답변3

소스 에서 play.sh루프 파일은 다음과 같습니다.

source ./loop.sh

트랩으로 인해 한 번 종료된 서브쉘은 트랩 종료 시 다시 돌아갈 수 없습니다.

답변4

문제는 sleep.

때를CTRL+C당신은 a를 죽인다 sleep- 당신이 아니라sh (귀하의 구문이 #!/bin/sh약간 의심스럽더라도). 그러나 전체 프로세스 그룹은 신호를 받습니다. 왜냐하면 loop.sh서브셸인 핸들러를 설치하지 않았기 때문에 신호는 즉시 종료됩니다.

에 트랩이 필요합니다 loop.sh. 명시적으로 지정하지 않는 한 시작되는 모든 서브쉘에 대해 트랩이 지워집니다.trap ''SIG부모로부터 무시당함.

-m또한 자녀를 추적할 수 있도록 부모의 작업 제어 모니터링이 필요합니다 . wait예를 들어, 작업 제어에서만 작동합니다. -m감시자 모드는 쉘이 터미널과 상호 작용하는 방식입니다.

sh  -cm '  . /dev/fd/3' 3<<""                     # `-m`onitor is important
     sh -c ' kill -INT "$$"'&                     # get the sig#
     wait %%;INT=$?                               # keep it in $INT
     trap  " stty $(stty -g;stty -icanon)" EXIT   # lose canonical input
     loop()( trap    exit INT                     # this is a child
             while   [ "$#" -le 100 ]             # same as yours
             do      echo "$#"                    # writing the iterator
                     set "" "$@"                  # iterating
                     sleep 3                      # sleeping
             done
     )
     int(){  case    $1      in                   # handler fn
             ($INT)  printf  "\33[2K\rDo you want to stop playing? "
                     case    $(dd bs=1 count=1; echo >&3)  in
                     ([Nn])  return
             esac;   esac;   exit "$1"
     }       3>&2    >&2     2>/dev/null
     while   loop ||
             int "$?"
     do :;   done

0
1
2
3
4
Do you want to stop playing? n
0
1
2
3
Do you want to stop playing? N
0
1
Do you want to stop playing? y
[mikeserv@desktop tmp]$

관련 정보