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 trap 'myhandler' EXIT
. . .
. . .
trap '' EXIT
- 아무 설정도 하지 않으면...
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 # 정상종료시 처리를 위해
이와 같은 불편을 없애기 위해 bash 에서는 EXIT pseudo-signal 을 제공합니다. trap 에 EXIT 신호를 사용하면 어떤 종료 상황에서도 마지막에 handler 가 실행됩니다. 다시말해 정상종료, Ctrl-c 에의한 종료, 터미널 프로그램의 종료, 시스템 shutdown 에의한 종료 상관없이 스크립트가 exit 될 때 실행됩니다. 그러므로 위의 예를 EXIT pseudo 신호를 이용해 다시 작성하면 다음과 같이 할 수 있습니다.
#!/bin/bash
trap 'myhandler' EXIT
myhandler() { ... ;}
...
...
한 가지 참고할 사항은 EXIT 신호는 스크립트가 종료될 때 사용되는 신호입니다.
그러므로 다음과 같이 EXIT 신호를 subshell 에서 사용한다고 하더라도
Ctrl-c 종료시 마지막 echo end...
는 실행되지 않습니다.
또한 EXIT 신호는 pseudo signal 로 ignore 할 수 없습니다.
$ cat ./test.sh
#!/bin/bash
echo start...
( trap 'echo trap subshell...' EXIT; cat; echo 111 )
echo end...
$ ./test.sh
start...
^Ctrap subshell...
--------------------------------------------------
$ cat ./test.sh
#!/bin/bash
trap 'echo trap handler...' EXIT # trap 추가
echo start...
( trap 'echo trap subshell...' EXIT; cat; echo 111 )
echo end...
$ ./test.sh
start...
^Ctrap subshell...
trap handler... # EXIT handler 만 실행된다.
Bash 에서 제공하는 pseudo-signals 에는 EXIT
외에 함수나 source 한 파일에서 return 할 때
사용할 수 있는 RETURN
신호, 명령이 0
이 아닌 값으로 종료했을 때 사용할 수 있는
ERR
신호, 스크립트 디버깅에 사용할 수 있는 DEBUG
신호를 제공합니다.
이들 신호 설정은 subshell 까지 적용될 수 있으며( functrace
, errtrace
옵션 설정 시 ),
child process 에는 적용되지 않습니다.
Signal | Description |
---|---|
EXIT | shell 이 종료할 때 발생. ( subshell 에도 적용 가능 ) |
ERR | 명령이 0 이 아닌 종료 상태 값을 반환할 경우 발생. |
DEBUG | 명령 실행전에 매번 발생. |
RETURN | 함수에서 리턴할 때, source 한 파일에서 리턴할 때 발생. |
ERR, DEBUG, RETURN
Debugging 메뉴를 참조 하세요
EXIT 신호와 다른 신호를 함께 등록하면?
EXIT 신호와 INT 신호를 함께 등록했다면 Ctrl-c 에 의한 종료 시 INT 신호에 의해 한번 실행되고 마지막에 스크립트가 종료될 때 EXIT 신호에 의해 한번 실행됩니다.
$ cat test.sh
#!/bin/bash
trap 'echo trap handler ...' INT EXIT
echo start...
cat
echo end...
-------------------------------------
$ ./test.sh
start...
^Ctrap handler ... # INT 신호에 한번
end...
trap handler ... # EXIT 때 한번
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
다음과 같이 double quotes 을 이용해 작성하면 trap 이 정의될 때 first $AA
는
first 100
이 되고 second : $var
는 second : $( echo $AA )
로 정의됩니다.
나중에 스크립트가 exit 되어 handler 가 실행될 때는 $AA
값이 200 이 되므로
100, 200 모두 출력됩니다.
#!/bin/bash
AA=100
var='$( echo $AA )'
trap "echo first : $AA, second : $var" EXIT
AA=200
######### 실행 결과 #########
$ ./test.sh
first : 100, second : 200
trap handler 는 현재 shell 에서 실행됩니다.
신호가 전달되어 signal handler 가 실행될 때는 따로 프로세스가 생성되거나 하지 않고 현재 실행 중이던 프로세스가 정지된 상태에서 그대로 signal handler 코드를 실행하고 다시 돌아온다고 생각하면 됩니다. 예를 들어 index 라는 변수값을 100 으로 설정한 상태에서 비동기적으로 신호가 발생하여 signal handler 가 실행되었는데 여기서도 index 값을 200 으로 설정한 후 돌아오면 index 값이 100 이 아니라 200 이 돼있겠죠. 이것은 index 값을 100 으로 생각하고 실행 중이던 현재 프로세스에게 문제가 될 수 있습니다. 신호라는게 언제 어디서 발생해서 signal handler 가 실행될지 알 수 없는 것이므로 signal handler 를 작성할 때는 주의해야 합니다.
#!/bin/bash
trap 'f1' INT
f1() {
echo "trap \$$ : $$, \$BASHPID : $BASHPID"
AA=200
}
AA=100
echo "\$$ : $$, \$BASHPID : $BASHPID"
echo "before interrupt : $AA"
cat
echo "after interrupt : $AA"
-------------- 실행결과 ---------------
$$ : 15528, $BASHPID : 15528
before interrupt : 100
^Ctrap $$ : 15528, $BASHPID : 15528 # handler 실행을 위해 ctrl-c 종료
after interrupt : 200
sh 에서의 trap 설정
sh
에서는 bash 와 같은 pseudo signals 을 제공하지 않습니다.
EXIT 신호를 제공하기는 하지만 bash 와 같은 기능이 아니고
스크립트가 정상 종료될 때, exit 명령을 이용한 종료, set -e
에의해 종료될 때만 호출됩니다.
그러므로 사용자가 Ctrl-c 로 종료를 시도할 경우 호출되지 않습니다
# 직접 exit 명령을 사용하거나, 정상 종료 시에만 호출된다.
#!/bin/sh
trap 'echo trap handler...' EXIT
...
...
다음과 같이 설정한다면 Ctrl-c 로 종료를 시도할 경우는 호출되지만 exit, 정상 종료 시에는 호출되지 않습니다.
#!/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...
-----------------------------------------------------------------------------
# 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' HUP INT QUIT TERM EXIT
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
/bin/sh
로 작성된 스크립트를 보다 보면 EXIT trap 을 사용할때(exit 1); exit 1;
와 같은 방식으로 종료하는 경우가 있는데 이것은 다음과 같은 이유 때문이라고 합니다.
( in some shells, such as Solaris /bin/sh, an exit trap ignores the exit command's argument. In these shells, a trap cannot determine whether it was invoked by plain exit or by exit 1. The workaround is to make sure that $? has the exit status before the exit command is executed, so that it will definitely have that value when the EXIT trap is executed. )
Ctrl-c ( INT 신호 )
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 0.0.0.1
echo B.sh ------ end
---------------------
$ ./A.sh
A.sh --- start
B.sh ------ start
PING 0.0.0.1 (0.0.0.1) 56(84) bytes of data.
^C
--- 0.0.0.1 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 0.0.0.1
echo B.sh ------ end
---------------------
$ ./A.sh
A.sh --- start
B.sh ------ start
PING 0.0.0.1 (0.0.0.1) 56(84) bytes of data.
^C
--- 0.0.0.1 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 999ms
trap INT in B.sh # ping 명령과 함께 B.sh, A.sh 이 모두 같이 종료 되었다.
다음은 B.sh 스크립트 파일 대신에 subshell 을 이용한 예입니다.
subshell 이므로 $$
대신에 $BASHPID
가 사용된 걸 볼 수 있습니다.
------- A.sh -------
#!/bin/bash
echo A.sh --- start
(
trap 'trap - INT; kill -INT $BASHPID' INT
ping 0.0.0.1
)
echo A.sh --- end
--------------------
$ ./A.sh # Ctrl-c 키를 입력했을 때 스크립트 전체가 종료됩니다.
A.sh --- start
PING 0.0.0.1 (0.0.0.1) 56(84) bytes of data.
^C
--- 0.0.0.1 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 0.0.0.1
echo A.sh --- end
--------------------
CHLD 신호
shell 에서 CHLD 신호를 trap 하는것은 별 의미가 없습니다. 왜냐하면 shell 은 기본적으로 명령을 다루므로 매 명령 실행시마다 CHLD 신호 trap 이 발생하기 때문입니다.
$ ( trap 'echo 1111111111111' CHLD; date; date; date )
Sat Jan 26 19:36:28 KST 2019
1111111111111
Sat Jan 26 19:36:28 KST 2019
1111111111111
Sat Jan 26 19:36:28 KST 2019
1111111111111
# subshell 에서 실행될 때는 trap 이 안된다. 다른 프로세스에서 실행되는 것이므로
$ ( trap 'echo 1111111111111' CHLD; ( date; date; date ) )
Sat Jan 26 19:34:50 KST 2019
Sat Jan 26 19:34:50 KST 2019
Sat Jan 26 19:34:50 KST 2019
1111111111111
만약에 command1 명령이 종료됐을 때 CHLD 신호를 trap 해서 뒤처리 작업으로 command2 명령을 실행하려고 한다면 shell 에서는 그냥 command1, command2 명령을 차례로 실행하면 됩니다.
$ ( trap 'command2' CHLD; command1 )
$ ( command1; command2; )
trap handler 내에서의 종료 상태 값
Ctrl-c 에 의해 프로세스가 종료될 때 EXIT handler 내에서는 종료 상태 값을 구할 수가 없습니다.
아래 EXIT 신호의 경우를 보면 cat 명령에서 실행이 중단됬을 때 Ctrl-c 로 종료할 경우
종료 상태 값이 제대로 표시되지 않는 것을 볼 수 있습니다.
또한 EXIT handler 는 스크립트가 종료될 때 실행되는 것이므로
뒤이은 echo 111
명령은 실행되지 않는 것을 볼 수 있습니다.
$ ( trap 'echo exit : $?' EXIT; cat; echo 111 ) # EXIT 신호
^Cexit : 0
$ echo $? # $? 변수값이 설정된다.
130
이번에 INT 신호를 trap 한 경우에는 cat 명령의 종료 상태 값이 정상적으로 표시되고
trap handler 실행 후에 echo 111
명령이 실행되는 것을 볼 수 있습니다.
$ ( trap 'echo exit : $?' INT; cat; echo 111 ) # INT 신호
^Cexit : 130
111
$ echo $? # 마지막으로 실행된 echo 111 명령이 정상 종료하였으므로 $? 값은 0 이된다.
0 # 만약에 echo 111 명령을 삭제하면 $? 값은 130 이된다.
따라서 다음과 같이 하면 특정 명령의 종료 상태 값을 trap handler 에서 변경할 수 있습니다.
$ ( trap 'exit 0' INT; cat )
^C
$ echo exit : $?
exit : 0
Quiz
shell 스크립트 파일의 경우 새로 프로세스가 생성돼서 실행이 되므로 EXIT
trap 을
사용할 수가 있는데요. shell 함수에서 EXIT
trap 을 사용하려면 어떻게 할까요?
다음과 같이 함수를 정의할때 { }
대신에 ( )
subshell 을 이용해 정의하면
함수에서도 EXIT
trap 을 사용할 수 있습니다.
하지만 이방법은 스크립트 파일 에서처럼 현재 shell 의 변수값을 변경하거나,
환경설정을 변경할 수 없습니다.
foo() ( foo() (
echo start .... echo start ....
trap handler EXIT trap handler EXIT
handler() { handler() {
echo xxxxxxxxxxx echo xxxxxxxxxxx
} }
date date
echo end ... exit 3 # exit
) echo end ...
)
--------------------------
--------------------------
$ foo
start .... $ foo
Thu Oct 7 19:36:59 KST 2021 start ....
end ... Fri Oct 8 00:46:04 KST 2021
xxxxxxxxxxx xxxxxxxxxxx
bash 의 경우는 함수가 return 될때 RETURN
trap 을 사용할 수 있습니다.
이 방법은 함수를 정의할때 기존과 같이 { }
를 사용하므로
현재 shell 의 변수값이나, 환경설정을 변경할 수 있습니다.
RETURN trap 에대한 좀더 자세한 설명은 debugging 메뉴를 참조하세요
bar() { bar() {
echo start .... echo start ....
trap handler RETURN trap handler RETURN
handler() { handler() {
echo xxxxxxxxxxx echo xxxxxxxxxxx
} }
date date
echo end ... return 3 # return
} echo end ...
}
----------------------------
----------------------------
$ bar
start .... $ bar
Thu Oct 7 20:14:50 KST 2021 start ....
end ... Thu Oct 7 20:09:53 KST 2021
xxxxxxxxxxx xxxxxxxxxxx
2.
Shell 에서 EXIT trap 같은 기능을 perl 언어에서 사용하려면 어떻게 할까요?
perl 은 스크립트 실행이 정상 종료하거나 또는 exit
, die
에 의한 종료시
자동으로 END
블록이 실행됩니다. 따라서 다음과 같이
INT
, QUIT
, TERM
, HUP
시그널 핸들러에서 exit
함수를 호출해 주면 됩니다.
#!/usr/bin/perl
END { # END 블록 실행중에는 기본적으로
print "cleaning...\n"; # DEFAULT 시그널 핸들러가 실행됩니다.
}
BEGIN {
$SIG{INT} = $SIG{QUIT} = $SIG{TERM} = $SIG{HUP} = sub {
print "\nreceived signal $_[0].\n";
exit ${{HUP=>1, INT=>2, QUIT=>3, TERM=>15}}{$_[0]}; # exit 함수 호출.
}
}
print "program start...\n";
print "enter message: "; <>;
# open fh, "< not_exist_file.txt" or die $!;
print "program end...\n"
--------------------------------------------------
$ ./test.pl
program start...
enter message: hello # 정상 종료시
program end...
cleaning...
$ ./test.pl
program start...
enter message: ^C # Ctrl-c 에의한 종료시
received signal INT.
cleaning... # 모두 cleaning... 이 출력된다.
$ echo $?
2 # INT
참고로 die
가 발생했을 때 바로 종료하지 않고 뒤처리를 하려면 다음과 같이
eval
과 $@
변수를 활용하면 됩니다. 일종의 try-catch 를 만들 수 있습니다.
#!/usr/bin/perl
END {
print "cleaning...\n";
}
BEGIN {
$SIG{INT} = $SIG{QUIT} = $SIG{TERM} = $SIG{HUP} = sub {
print "\nreceived signal $_[0].\n";
exit ${{HUP=>1, INT=>2, QUIT=>3, TERM=>15}}{$_[0]};
}
}
print "program start...\n";
eval { # (try 블록에 해당)
open my $fh, "<", "not_exist_file.txt" or die "OPEN FILE\n"; # \n 필요
# 다음 두 라인은 die 가 발생하면 실행되지 않는다.
print "11111\n";
print "22222\n";
};
if ($@) { # eval 블록에서 오류가 발생했을 경우 (catch 에 해당)
if ($@ == "OPEN FILE") { # die 에서 설정한 메시지
print "open file error!\n";
} else {
print "$@"; # 예) division by zero
}
}
print "program end...\n";
-----------------------------------------------------
$ ./test.pl
program start...
open file error!
program end...
cleaning...