TL;DR

kill -9 로 프로세스를 종료하면 시스템에 악영향을 줄 수 있습니다.

대신 kill -INTkill -TERM 을 사용하세요.


들어가며

시스템 작업하던 직원이 java 관련 프로세스 종료를 kill -9 로 하는 것을 보고 왜 이렇게 하는지 물어보니 "프로세스 종료시 이렇게 하는 게 아닌가요?" 라는 답변이 돌아왔습니다.


검색을 해 보니 많은 블로그들이 프로세스 종료시 kill -9  로 처리하라고 설명하고 있어서 kill 명령의 의미 및 안전하게 프로세스 종료 하는 법을 정리해 봅니다.


signal 과 event

kill 은 프로세스에 시그널에 보내는 명령어로 signal 을 받은 프로세스의 기본 동작이 종료이기때문에 이렇게 명명되었습니다.

하지만 이제는 signal 종류가 많아지고 기본 동작이 종료가 아니므로 kill 보다는 send_signal 이 더 적합한 이름이며 이런 맥락을 모르면 명령어의 기능에 대해서 오해하게 됩니다.


signal 은 software interrupt 의 일종으로 어떤 이벤트가 발생했음을 프로세스에게 알려주는 매커니즘입니다.

사용자가 인터럽트 키(Ctrl + C)를 눌렀거나 프로세스가 잘못된 메모리에 접근했을 경우가 이벤트 발생 예이며 프로세스는 해당 이벤트에 맞는 signal 을 전송받게 됩니다.


시그널은 숫자 1부터 시작하는데 숫자는 기억하기 어려우므로 의미있는 이름으로도 정의되어 있습니다.


리눅스의 목록은 /usr/include/signal.h 에 정의되어 있으며 다음 명령으로도 전체 시그널 목록을 확인할 수 있습니다. 

$ kill -l

1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
BASH

시그널의 숫자와 의미는 유닉스의 표준중 하나인 POSIX(Portable Operating System Interface) 에 정의되어 있습니다.


kill 명령어 뒤에 시그널의 숫자 또는 SIG 를 뺀 이름(예: SIGINT 일 경우 INT)을 주고 프로세스 id 를 주면 해당 프로세스에 시그널을 보낼 수 있습니다.


즉 아래 2개의 명령은 동일한 역할을 수행합니다.

$ kill -INT 123
BASH
$ kill -2 123
BASH

kill 명령어 뒤에 시그널 이름이나 숫자를 제외하고 실행하면 TERM(15번) signal 이 전송됩니다.


signal handler

signal 발생은 비동기 작업이며 개발자는 프로그램을 만들 때 특정 시그널을 수신했을 때 실행되기를 바라는 함수(시그널 핸들러)을 등록할 수 있습니다.


시그널 핸들러를 특별히 지정하지 않았을 경우 커널에 기본 정의된 액션을 실행하며 기본 정의 액션은 시그널의 종류에 따라 다르며 기본 정의된 액션은 다음 표처럼 총 5가지입니다.


action 명기본 동작
TERM시그널을 수신한 프로세스 종료
IGN시그널 무시
CORE시그널을 수신한 프로세스를 종료하면서 코어 덤프(core dump) 파일 생성
STOP시그널을 수신한 프로세스 정지
CONT중지된 프로세스 재시작


프로세스에 시그널을 보내는 명령어가 kill 인 이유는 크게 다음 2가지때문입니다.

  1. 시그널 종류를 명시하지 않고 kill 명령어를 호출할 경우 전송되는 기본 시그널은 TERM signal
  2. signal handler 를 작성하지 않았을 경우 대부분의 기본 액션은 해당 프로세스 종료


$stringEscapeUtils.escapeHtml($body)
CPP


왜 kill -9 를 쓰면 안 되는가?


위에서 얘기한대로 개발자는 signal 종류 별로 handler 를 지정할 수 있는데 개념있는 개발자라면 프로세스 종료의 의미로 사용하는 signal (INT, HUP, TERM 등)을 받으면 사용했던 리소스(파일, 소켓, DB 연결등)를  닫고 저장하는 cleanup 함수를 작성하고 이를 시그널 핸들러로 등록합니다.


문제는 유닉스의 표준상 handler 를 등록할 수 없는 2개의 시그널이 있는데 바로 SIGKILL(9) 과  SIGSTOP(19) 이며 kill -9 명령어는 KILL signal 을 보내겠다는 의미입니다.


kill -9 로 signal 을 보내면 개발자가 구현한 종료 함수가 호출되지 않고 즉시 프로세스가 종료되어 버리므로 데이타가 유실되거나 리소스가 제대로 안 닫히는 큰 문제가 발생할 수 있습니다.


개인적으로 추천하는 방법은 먼저 kill -TERM PIDkill -INT PID 같이 종료를 의미하는 signal 을 여러 번 전송해 주는 것이며 제대로 된 프로그램은 보통 cleanup 코드를 수행하고 종료하게 됩니다.


Java JRE 도 TERM 시그널이면 깨끗하게 종료되는데 가끔 덩치가 큰 자바 프로그램(제가 블로그로 사용하는 Confluence 같은)등은 종료 시간이 오래 걸리는 경우가 있습니다.


이런 경우 약간의 시간을 두고  다시 INT 나 TERM 시그널을 다시 전송해 주면 방법을 사용하면 됩니다.


여러개의 프로세스를 종료시킬 경우 이름으로 시그널을 전송할 수 있는 killall 명령어를 사용하거나 다음과 같은 awk script 를 이용해서 종료할 수 있습니다.


$ ps -eaf httpd|grep -v grep|awk '{print "kill -TERM "$2}' | sh -x
BASH


모든 child process 까지 종료시키는 스크립트가 필요하다면 StackOverflow의 Best way to kill all child processes 쓰레드를 참조하세요.

위 쓰레드에서 발췌한 killtree.sh

#!/bin/bash

killtree() {
    local _pid=$1
    local _sig=${2:-TERM}
    kill -stop ${_pid} # needed to stop quickly forking parent from producing child between child killing and parent killing
    for _child in $(ps -o pid --no-headers --ppid ${_pid}); do
        killtree ${_child} ${_sig}
    done
    kill -${_sig} ${_pid}
}

if [ $# -eq 0 -o $# -gt 2 ]; then
    echo "Usage: $(basename $0) <pid> [signal]"
    exit 1
fi

killtree $@
BASH


사용자 정의 시그널

kill -l 로 보면 SIGUSR1 과 SIGUSR2 가 있는데 이 시그널의 용도는 사용자 정의용입니다.

즉 개발자가 저 어떤 용도로 사용할지 결정하면 되며 이에 따라 프로그램마다 용도가 각각 다릅니다.


nginx 의 경우 USR1 시그널을 받으면 로그 파일을 다시 open 하는 용도로 사용하고 있습니다.


사용자가 폭주하는 웹 서버일 경우 쌓이는 로그 파일 용량이 상당하므로 log rotate 기능을 사용하여 주기적으로 기존 로그 파일을 날자명등을 붙여서 rename 하고 새로운 로그 파일을 생성합니다.


리눅스의 특성상 이렇게 새로 로그 파일이 생성되도 nginx 는 rename 된 기존 로그 파일을 열고 있습니다.

웹 서버를 종료하고 재구동하면 해결되지만 사용자가 많을 경우 재구동 시간이 오래 걸리며 기존에 연결한 클라이언트들에 문제가 생길수 있습니다.


이런 경우 systemctl 이나 service 명령어로 프로세스를 재구동하지 말고 USR1 시그널을 전송하면 nginx 는 로그 파일을 다시 오픈하므로 log rotate 기능이 정상작동됩니다. (http://nginx.org/en/docs/control.html 참고하세요)

$ killall -USR1 nginx
CODE


아파치 httpd 는 prefork 모델을 사용할 경우 부모 프로세스가 USR1 을 받으면 설정 파일을 다시 읽고 로그 파일도 다시 오픈합니다. 그리고  모든 자식 프로세스에게 현재 요청이 끝나면 종료하도록 지시하고 자식 프로세스가 종료되면 새로운 설정과 로그 파일을 기반으로 자식 프로세스를 생성합니다.


그러므로 아파치 웹 서버도 log rotate 나 설정 파일 변경시 재구동을 하지 말고 USR1 를 전송해 주면 됩니다.

같이 보기

참고

  • Unix signal handling example in C, SIGINT, SIGALRM, SIGHUP...