trap

trap [-lp] [[arg] signal_spec ...]

스크립트를 작성할때 임시파일이나 named pipe 같은 리소스를 생성해 사용할 수 있고 실행 중에는 임시 결과물이 발생할 수도 있습니다. 스크립트가 정상적으로 종료되면 뒤처리 작업을 거치게 되므로 문제가 없겠지만 실행 도중에 사용자가 Ctrl-c 로 종료를 시도한다던지, 아니면 터미널 프로그램을 종료시킨다면 문제가 될 수 있습니다.

trap 명령은 시스템에서 비동기적으로 발생하는 신호를 잡아서 필요한 작업을 수행할 수 있게 해줍니다.

  • 사용자 signal handler 를 등록하여 실행
trap 'myhandler' INT

myhandler() { ... ;}
  • 기존에 설정했던 사용자 handler 를 default handler 로 reset
trap INT
trap - INT
  • signal 을 ignore 하여 handler 가 실행되지 못하게 함
trap '' INT
  • 아무 설정도 하지 않으면...
default handler 가 실행됨

trap 에 사용할수 없는 신호들

다음 3 개의 신호는 trap 명령을 이용해 ignore 하거나 signal handler 를 등록해 사용할 수 없습니다. 다시 말해 무조건 default handler 가 실행됩니다. 그러므로 프로세스를 종료시킬 때 HUP, INT, QUIT, TERM 같은 종료신호로 종료가 안될 경우 KILL(9) 신호를 사용하면 바로 종료하게 됩니다.

보통 프로그램 내에서 종료신호에 signal handler 를 등록해 종료전에 필요한 뒤처리 작업을 하므로 프로세스를 종료시킬 때 처음부터 KILL(9) 신호를 사용하는 것은 좋은 방법이 아닙니다.

  • SIGKILL
  • SIGSTOP
  • SIGCONT

Pseudo signals

스크립트를 작성할 때 trap 명령에서 사용할 수 있는 신호들을 signal table 에서 찾아보면 Termination Signals, Job Control Signals 카테고리에 있는 신호들 정도입니다. 테이블을 보면 알 수 있듯이 스크립트가 종료될 때 실행되는 handler 를 등록하려면 HUP, INT, QUIT, TERM 신호를 모두 등록해야 합니다. 그리고 정상 종료될 경우에도 실행이 돼야 하므로 다음과 같이 작성해야 합니다.

#!/bin/bash

trap 'myhandler' HUP INT QUIT TERM  

myhandler() { ... ;}
...
...
...
myhandler  # 정상종료시 처리를 위해

이와 같은 불편을 없애기 위해 shell 에서는 EXIT pseudo-signal 을 제공합니다. trap 에 EXIT 신호를 사용하면 어떤 종료 상황에서도 마지막에 handler 가 실행됩니다. 다시말해 정상종료, Ctrl-c 에의한 종료, 터미널 프로그램의 종료, 시스템 shutdown 에의한 종료 상관없이 스크립트가 exit 될때 실행됩니다. 그러므로 위의 예를 EXIT 신호를 이용해 다시 작성하면 다음과 같이 할 수 있습니다.

#!/bin/bash

trap 'myhandler' EXIT

myhandler() { ... ;}
...
...

한가지 참고할 사항은 EXIT 신호는 스크립트가 종료될때 사용되는 신호입니다. 그러므로 다음과 같이 EXIT 신호를 subshell 에서 사용한다고 하더라도 Ctrl-c 종료시 마지막 echo end... 는 실행되지 않습니다. 또한 EXIT 신호는 pseudo signal 로 ignore 할 수 없습니다.

#!/bin/bash

echo start...

( trap 'echo trap subshell...' EXIT; cat; echo 111 )

echo end...

########## 실행결과 ###########

$ ./test.sh 
start...
^Ctrap subshell...

Bash 에서 제공하는 pseudo-signals 에는 EXIT 외에 함수나 source 한 파일에서 return 할때 사용할수 있는 RETURN 신호, 명령이 0 이 아닌 값으로 종료했을때 사용할 수 있는 ERR 신호, 스크립트 디버깅에 사용할 수 있는 DEBUG 신호를 제공합니다. 이들 신호 설정은 subshell 까지 적용될 수 있으며( functrace, errtrace 옵션 설정시 ), child process 에는 적용되지 않습니다.

Signal Description
EXIT shell 이 exit 할때 발생. ( subshell 에도 적용 가능 )
ERR 명령이 0 이 아닌 값을 리턴할 경우 발생.
DEBUG 명령 실행전에 매번 발생.
RETURN 함수에서 리턴할때, source 한 파일에서 리턴할때 발생.

RETURN

#!/bin/bash

# functrace 옵션 설정은 모든 함수에 적용됨
set -o functrace
trap 'echo return trap' RETURN

f1() {
# functrace 옵션 설정을 안하고 f1 함수에만 적용하려면
#    trap 'echo f1 return trap' RETURN

    echo 111
    echo 222
}

echo start---
f1
f1
echo end---

############# output #############

start---
111
222
return trap
111
222
return trap
end---

ERR, DEBUG

Debugging 메뉴 참조

EXIT 신호와 다른 신호를 함께 등록하면?

EXIT 신호와 INT 신호를 함께 등록했다면 Ctrl-c 에 의한 종료 시 INT handler 가 먼저 실행되고 마지막에 스크립트가 종료될 때 EXIT handler 가 실행됩니다.

trap handler 설정시 quotes 의 사용

trap handler 를 설정할때 double quotes 을 사용하면 설정 당시에 변수값이 확장되어 정의가 되므로 실행 시에 원하는 값이 표시되지 않을 수 있습니다.

------------- trap1.sh -------------
#!/bin/bash 

AA=100
trap "echo $AA" EXIT   # double quotes 사용
AA=200

------------- trap2.sh -------------
#!/bin/bash 

AA=100
trap 'echo $AA' EXIT   # single quotes 사용
AA=200

-------------- 실행 ----------------

$ ./trap1.sh
100

$ ./trap2.sh
200

trap handler 는 현재 shell 에서 실행됩니다.

신호가 전달되어 signal handler 가 실행될 때는 따로 프로세스가 생성되거나 하지 않고 현재 실행 중이던 프로세스가 정지된 상태에서 그대로 signal handler 코드를 실행하고 다시 돌아온다고 생각하면 됩니다. 따라서 signal handler 를 작성할 때는 reentrant 하게 작성해야 합니다. 이말은 signal handler 를 실행하고 돌아왔을 때 다음에 이어지는 코드 실행에서 문제가 생기면 안된다는 것입니다.

예를 들어 index 라는 변수값을 100 으로 설정한 상태에서 비동기적으로 신호가 발생하여 signal handler 가 실행되었는데 여기서도 index 값을 200 으로 설정한 후 돌아오면 index 값이 100 이 아니라 200 이 돼있겠죠. 이것은 index 값을 100 으로 생각하고 있을 현재 프로세스에게 문제가 됩니다. 신호라는게 언제 어디서 발생해서 signal handler 가 실행될지 알 수 없는 것이므로 signal handler 를 작성할 때는 주의해야 합니다. 이것은 C 프로그래밍에서도 마찬가지로 그래서 C 에서는 signal handler 에 Async-Signal-Safe 한 함수만 사용합니다. ( 어떻게 작성해야 Async-Signal-Safe 한 함수가 될지 한번 생각해보세요? )

#!/bin/bash

trap 'f1' INT

f1() { 
    echo "\$$ : $$, \$BASHPID : $BASHPID"
    AA=200 
}

AA=100

echo "\$$ : $$, \$BASHPID : $BASHPID"
echo "before interrupt : $AA"

cat

echo "after interrupt : $AA"

-------------- 실행결과 ---------------

$$ : 15528, $BASHPID : 15528
before interrupt : 100
^C$$ : 15528, $BASHPID : 15528   # handler 실행을 위해 ctrl-c 종료
after interrupt : 200

sh 에서의 trap 설정

sh 에서는 bash 와 같은 pseudo signals 을 제공하지 않습니다. EXIT 신호를 제공하기는 하지만 bash 와 같은 기능이 아니고 exit 명령을 이용해 종료하거나 정상 종료 시에 만 호출됩니다. 그러므로 사용자가 Ctrl-c 로 종료를 시도할 경우 호출되지 않습니다

# 직접 exit 명령을 사용하거나, 정상 종료 시에만 호출된다.
#!/bin/sh
trap 'echo trap handler...' EXIT 
...
...

다음과 같이 설정한다면 Ctrl-c 로 종료를 시도할 경우는 호출되지만 exit, 정상 종료 시에는 호출되지 않습니다. 여기서 한가지 주목해야 될 점은 bash 의 경우는 Ctrl-c 에 의해 trap handler 가 실행된 후에 바로 스크립트 전체가 종료되지만 sh 의 경우는 이어서 나머지 명령이 실행되는 것을 볼 수 있습니다.

#!/bin/sh
trap 'echo trap handler...' HUP INT QUIT TERM
echo start...
cat 
echo end...

$ ./test.sh 
start...
^Ctrap handler...      # '^C' 는 cat 명령에서 Ctrl-c 입력
end...                 # bash 처럼 바로 종료하지 않고 나머지 명령이 실행된다.
-----------------------------------------------------------------------------

# trap handler 에 exit 을 추가하면 나머지 명령이 실행되는 것을 방지할 수 있다.
#!/bin/sh
trap 'echo trap handler...; exit' HUP INT QUIT TERM     # exit 추가
echo start...
cat 
echo end...

$ ./test.sh 
start...
^Ctrap handler...       # end... 가 제거됨

만약에 아래와 같이 EXIT 과 INT 신호를 함께 설정한다면 정상 종료 시에는 한 번만 호출되지만 Ctrl-c 로 종료를 시도할 경우에는 INT 신호에 의해 한번, EXIT 신호에 의해 한번, 두 번 호출되게 됩니다.

$ cat test.sh
#!/bin/sh
trap 'echo trap handler...; exit' EXIT HUP INT QUIT TERM
echo start...
cat 
echo end...

$ ./test.sh 
start...
^Ctrap handler...      # INT 신호에 한번
trap handler...        # EXIT 신호에 한번

따라서 bash 에서 EXIT 신호를 설정하는 것과 같이 정상 종료 시, Ctrl-c 에의한 종료 시 모두 실행되고 trap handler 는 한 번만 실행되게 하려면 다음과 같이 설정합니다. 또한 이 방법은 종료 상태 값도 bash 에서와 같이 정상 종료 시와 Ctrl-c 에의한 종료 시 구분되어 설정됩니다.

#!/bin/sh
trap 'exit' HUP INT QUIT TERM
trap 'echo trap handler...' EXIT
. . .
. . .

----------------------------------
#!/bin/sh
trap 'exit' HUP INT QUIT TERM
trap 'echo trap handler...' EXIT
echo start...
cat 
echo end...

$ ./test.sh             # 한 번만 trap handler 가 실행된다.
start...
^Ctrap handler...

$ echo $?               # 종료 상태 값도 구분되어 설정된다.
130

Ctrl-c

Ctrl-c 키의 SIGINT 신호를 이용한 trap 은 다음 세 가지로 활용할 수 있습니다.

  • 사용자가 Ctrl-c 로 종료하지 못하게 ignore 하기

  • child process 만 종료시키기

  • 전체 process group 을 종료시키기

기본적으로 Ctrl-c 키를 입력했을때 발생하는 SIGINT 신호는 foreground process group 에 전달되므로 같은 process group 에 속한 명령들은 모두 종료하게 됩니다. 다음은 A.sh 에서 B.sh 을 실행하고 B.sh 에서는 sleep 외부명령을 실행하고 있는 예입니다. sleep 명령에 의해 실행이 중단됐을때 Ctrl-c 키를 입력하면 A.sh, B.sh 모두 종료하는 것을 볼 수 있습니다.

------- A.sh -------
#!/bin/bash
echo A.sh --- start
./B.sh
echo A.sh --- end

-------- B.sh --------
#!/bin/bash
echo B.sh ------ start
sleep 100
echo B.sh ------ end
----------------------

$ ./A.sh     
A.sh --- start
B.sh ------ start
^C                # Ctrl-c 키를 입력했을때 A.sh, B.sh 모두 종료된다.

이번에는 child process 만 종료시키는 예입니다. 먼저 B.sh 에 trap 설정을 하는데 마지막 명령으로 exit 을 사용하였습니다. 프롬프트에서 A.sh 을 실행시킨후 Ctrl-c 를 입력한 결과는 A.sh 의 child process 인 B.sh 만 종료가 되고 이후에 A.sh 스크립트의 나머지 부분이 실행되어 A.sh --- end 메시지를 볼 수 있습니다.

------- B.sh -------
#!/bin/bash

trap 'echo trap INT in B.sh; exit' INT

echo B.sh ------ start
sleep 100
echo B.sh ------ end
---------------------

$ ./A.sh
A.sh --- start
B.sh ------ start
^Ctrap INT in B.sh
A.sh --- end        # B.sh 만 종료되고 A.sh 의 나머지 부분이 실행된다.


########## #!/bin/sh #########

sh 을 사용하는 경우는 A.sh 에서 default INT handler 가 실행되지 않게 
`trap ':' INT` 설정을 해주시면 됩니다.

------- A.sh -------
#!/bin/sh

trap ':' INT

echo A.sh --- start
./B.sh
echo A.sh --- end

-------- B.sh --------
#!/bin/sh
echo B.sh ------ start
sleep 100
echo B.sh ------ end

이와 같은 예는 ping 명령을 통해서도 확인할 수 있습니다. 아래의 경우 B.sh 에서 ping 명령을 실행시킨 후 Ctrl-c 로 종료하였으나 ping 명령 자체만 종료가 되고 B.sh, A.sh 의 나머지 명령들은 모두 실행되는 것을 볼 수 있습니다. 다시 말해 ping 명령 내에서의 trap 설정은 위의 예와 같이 trap 'command ... ; exit' INT 형식으로 되어있다고 할 수 있습니다.

------- B.sh -------
#!/bin/bash
echo B.sh ------ start
ping 1.1.1.0
echo B.sh ------ end
---------------------

$ ./A.sh
A.sh --- start
B.sh ------ start
PING 1.1.1.0 (1.1.1.0) 56(84) bytes of data.
^C
--- 1.1.1.0 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 999ms

B.sh ------ end    # ping 명령만 종료되고 B.sh, A.sh 나머지 명령이 모두 실행된다.
A.sh --- end

위의 ping 명령과 같은 경우에 사용자가 Ctrl-c 키를 입력하였을때 ping 명령 뿐만아니라 B.sh, A.sh 모두 같이 종료해야될 경우가 있습니다. 사실 위와 같은 결과가 나오는 이유는 bash 의 경우 사용자 handler 를 실행한 후에는 default handler 를 실행하지 않기 때문인데요
( shebang 라인을 #!/bin/sh 로 바꾸어 실행해보면 B.sh, A.sh 모두 바로 종료합니다 )

그러므로 기존의 trap 설정에서 exit 을 빼고, trap 설정을 reset 한다음( trap - INT ) 다시 자기 자신에게 INT 신호를 보내면 B.sh, A.sh 에서 default INT handler 가 실행되어 바로 종료하게 됩니다.

------- B.sh -------
#!/bin/bash

trap 'echo trap INT in B.sh; trap - INT; kill -INT $$' INT

echo B.sh ------ start
ping 1.1.1.0
echo B.sh ------ end
---------------------

$ ./A.sh         
A.sh --- start
B.sh ------ start
PING 1.1.1.0 (1.1.1.0) 56(84) bytes of data.
^C
--- 1.1.1.0 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 999ms

trap INT in B.sh  # ping 명령과 함께 A.sh, B.sh 이 모두 같이 종료 되었다.

다음은 B.sh 스크립트 파일 대신에 subshell 을 이용한 예입니다.
subshell 이므로 $$ 대신에 $BASHPID 가 사용된 걸 볼 수 있습니다.

------- A.sh -------
#!/bin/bash
echo A.sh --- start
(
trap 'trap - INT; kill -INT $BASHPID' INT
ping 1.1.1.0
)
echo A.sh --- end
--------------------

$ ./A.sh      # Ctrl-c 키를 입력했을때 스크립트 전체가 종료됩니다.
A.sh --- start
PING 1.1.1.0 (1.1.1.0) 56(84) bytes of data.
^C
--- 1.1.1.0 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 999ms


##########  #!/bin/sh  ##########

sh 에서는 기본적으로 모두 default INT handler 가 실행되므로
별다른 trap 설정을 해줄 필요가 없습니다.

------- A.sh -------
#!/bin/sh
echo A.sh --- start
ping 1.1
echo A.sh --- end
--------------------

trap handler 내에서의 종료 상태 값

Ctrl-c 에 의해 프로세스가 종료될 때 EXIT handler 내에서는 종료 상태 값을 구할 수가 없습니다.

아래 EXIT 신호의 경우를 보면 cat 명령에서 실행이 중단됬을때 Ctrl-c 로 종료할 경우 종료 상태 값이 제대로 표시되지 않는 것을 볼 수 있습니다. 또한 EXIT handler 는 default INT handler 에의해 스크립트가 종료될때 실행되므로 뒤이은 echo 111 명령은 실행되지 않는 것을 볼 수 있습니다.

$ ( trap 'echo exit : $?' EXIT; cat; echo 111 ); echo 222   # EXIT 신호
^Cexit : 0

$ echo $?
130

이번에 INT 신호를 trap 한 경우에는 cat 명령의 종료 상태 값이 정상적으로 표시되고 trap handler 실행 후에 echo 111 명령이 실행되는 것을 볼 수 있습니다.

$ ( trap 'echo exit : $?' INT; cat; echo 111 )    # INT 신호
^Cexit : 130
111

다음과 같이 하면 특정 명령의 종료 상태 값을 trap handler 에서 변경할 수 있습니다.

$ ( trap 'exit 0' INT; cat ); echo exit : $?
^Cexit : 0

PGID 를 변경하여 child process 실행

스크립트를 a.sh -> b.sh -> c.sh -> d.sh 순서로 실행하여 현재 sleep 상태에 있다면 Ctrl-c 를 누를경우 tty driver 에 의해 INT 신호가 foreground process group 에 전달되어 4개의 프로세스는 모두 종료하게 됩니다. 이때 만약에 b.sh 에서 c.sh 을 실행할때 pgid 를 변경할수 있다면 c.sh 과 d.sh 까지만 종료하고 a.sh, b.sh 은 나머지 명령이 실행되게 할 수 있습니다. ( c.sh, d.sh 만 foreground process group 이 되므로 )

shell 에서는 setsid 외부 명령을 사용하여 sid, pgid 를 변경해 명령을 실행할수 있지만 setpgid 같은 명령은 없습니다. 하지만 간접적으로 set -o monitor 옵션 설정을 통해서 이후에 실행되는 명령이 다른 pgid 를 갖게 할 수 있습니다.

---------- b.sh ---------
#!/bin/bash
...
...
set -o monitor
./c.sh            # c.sh 은 다른 pgid 로 실행된다
...