쉘 스크립트에서 사용자 프롬프트를 사용하여 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)를 시작한 후 호출하고 wait, INT 신호를 받은 후 killPID와 동일한 PGID를 사용하여 프로세스 그룹을 호출하세요.

  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의 일부 구현에서 "잘못된 옵션" 또는 "잘못된 신호 사양"이라고 알려줍니다.

아래의 간단한 코드가 모든 것을 설명합니다. 신호 처리기를 설정하고 3개의 하위 프로세스를 생성한 다음 신호 처리기의 프로세스 그룹 명령이 스스로 종료되기를 trap기다리는 무한 대기 루프에 들어갑니다 .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-백슬래시)를 입력할 때마다 인터럽트 신호 또는 종료 신호가 전경 프로세스 그룹의 모든 프로세스로 전송됩니다." 여 리차드 스티븐스. "UNIX® 환경의 고급 프로그래밍." 애디슨-웨슬리. 1993. 246쪽.

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

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

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

답변3

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

source ./loop.sh

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

답변4

문제는 입니다 sleep.

때를CTRL+C당신이 한 명을 죽였습니다 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]$

관련 정보