http://ir.bagesoft.com/640

1. 데몬(Daemon)이란 

Daemon 이란 백그라운드로 실행되면서, 사용자의 인터페이스(tty)가 없는 프로그램을 말한다. 우리가 흔히 사용하는 리눅스 서비스들은 대부분 데몬으로 동작하며, -d로 끝나는 프로그램(예: sshd, syslogd)이 모두 해당된다. 리눅스 명령인 nohup으로 백그라운드 구동은 가능하지만, kill 명령으로 종료해야 한다.따라서 비정상 종료시의 처리를 하려면, OS로부터 signal을 받아야 하므로 데몬으로 구동시켜야 한다.( 참고 : http://ir.bagesoft.com/640 )

데몬이란 주기적인 서비스 요청을 처리하기 위해서 커널상에 백그라운드 모드로 실행되는 프로세스로, 메모리 관리 방법에 따라 단독 데몬과 xinetd로 분리된다. ( 지금부터 참고 : http://lyb1495.tistory.com/11 )

> 단독데몬
 항상 백그라운드 모드로 실행되고 메모리를 상대적으로 많이 소비한다. 그러나 서비스(응답속도)가 빠르다. httpd와 같은 웹서비스 데몬이 대표적.
> xinetd(슈퍼데몬)
 요청이 있을때마다 xinetd가 서비스를 싱행시켜주므로 메모리 소비가 적다. 그러나 단독데몬에 비해 상대적으로 서비스 속도가 느리다.

 2. 간단한 자바 데몬 만들기
 nohup을 이용해서 java를 실행시킨다.

 터미널이 종료될 때(쉘이 종료될 때) 프로세스가 죽는 이유는 해당 프로세스가 쉘의 자식 프로세스 이기 때문이다. 따라서, 부모 프로세스가 죽을대 던지는 SIGHUP을 자식 프로세스가 받게 된다.

 nohup은 부모 프로세스가 죽을때 자식 프로세스에게 SIGHUP을 던지지 않는 프로세스를 말한다.
$ nohup java 클래스명 &

  
 사용하기 편한 장점은 있으나, 문제는 중지를 시킬수 없다. 즉, 해당 프로세스 ID를 찾아내 kill하는 수 밖에 없다. 물론 파일 체크 기법, 소켓을 이용한 제어 방법등을 사용할 수 있지만 스스로 구현해야 하는 번거로움이 있다.

3. apache commons daemon 이용하기

 Java는 UNIX의 시그널 처리를 할수 없기때문에, 중지 신호(TERM signal)를 처리하기 위해서는 따로 구현을 해야한다. 이런 번거로움을 해결하기 위해 자카르타의 하위 프로젝트중의 commons daemon을 이용한다. commons daemon은 중지 신호를 받으면 미리 지정된 메소드를 실행한다.

 ** 다운로드: http://commons.apache.org/daemon/

 UNIX용 Jsvc와 윈도우용 Procrun 있다.
 여기서는 Jsvc를 이용해보도록 하겠다.


Apache Commons Daemon에서 제공하는 Jsvc를 이용하여, 간단한 데몬 프로그램을 작성해 보자.
(우리가 잘 알고 있는 Tomcat도 Jsvc를 이용하여, 데몬으로 프로세스를 실행한다고 한다.)  

근데 여기서 잠깐! 

 (참고 : http://heartdev.wordpress.com/2013/06/22/apache-commons-daemon-%EC%82%AC%EC%9A%A9-%EB%B0%A9%EB%B2%95-%EC%A0%95%EB%A6%AC/ )


JSVC를 깔기 위해서는 다음과 같은 4가지 것들이 필요하다. 

  • GNU AutoConf (at least version 2.53)
  • An ANSI-C compliant compiler (GCC is good)
  • GNU Make
  • A Java Platform 2 compliant SDK

위의 4가지를 깔기 위해서는 어떤 명령어가 필요할까? 

먼저 JDK가 설치되어 있지 않다면 JDK를 설치한다. 

gcc 및 make가 설치되어 있지 않다면, 아래 명령을 이용해 한방에 설치하자. 

$ yum install build-essential   ( 원래는 우분투 에서 : $ sudo apt-get install build-essential )

centos의 경우 yum을 사용하므로 이게 맞을꺼다. (지금 기억이 가물;;) 여튼 근데 정작 문제는 이 명령어가 안 먹는다는거! 그래서 gcc랑 make가 설치가 안되는데 음..... 모르겠다. 일단 이거 없이도 성공은 했었었다. 

$ yum install autoconfig   ( 원래는 우분투에서  $ sudo apt-get install autoconf ) 

명령어로 역시 centos에서 Autoconf설치를 끝낸다. 

mkdir /root/commons-daemon
cd /root/commons-daemon

wget http://archive.apache.org/dist/commons/daemon/binaries/commons-daemon-1.0.5.jar
wget http://archive.apache.org/dist/commons/daemon/source/commons-daemon-1.0.5-src.tar.gz 
(위 소스는 경로가 변경되어서 내가 수정하였다.) 

tar zxvf commons-daemon-1.0.5-src.tar.gz
cd commons-daemon-1.0.5-src/src/native/unix/
support/buildconf.sh     --> 사실 이부분이 잘 안되는데 이유를 모르겠고 그냥 일단 넘어갔다. 이 폴더까지 가서 ./buildconf.sh 라고 쳐도 된다. (음... 어떤데는 sh buildconf.sh 라고 씌여있는걸 보기도 했는데 같은 방법인지 확실하지 않다....ㅠ)

./configure --with-java=/usr/local/jdk1.7.0_51/    ( 여기서 usr/local/jdk1.7.0_51이 내 jdk가 있는곳 )
make
mv  jsvc  /root/commons-daemon

위와 같이 실행하면, 작업할 경로(/root/commons-daemon)에 jar과 jsvc파일이 생성되어 있을 것이다. 

아래부터는 또 가져왔다. 

 2. 실행할 JAVA 클래스 작성
jsvc에 의해서 실행되는 JAVA 클래스는 Daemon 인터페이스를 구현해야 한다. 또한 스레드로 실행되어야 하므로, Runnable도 구현해야 한다. 만약 스레드로 구동하지 않을 경우, 종료처리가 제대로 되지 않으며, "Service exit with a return value of 143"라는 오류를 표시할 것이다.

테스트를 위해, 1초마다 숫자를 하나씩 증가시키면서 찍는 프로그램을 JAVA로 아래와 같이 만들었다.
TestDaemon.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package com.bagesoft.test.daemon;
 
import org.apache.commons.daemon.Daemon;
import org.apache.commons.daemon.DaemonContext;
import org.apache.commons.daemon.DaemonInitException;
 
public class TestDaemon implements Daemon, Runnable {
    private String status = "";
    private int no = 0;
    private Thread thread = null;
 
    @Override
    public void init(DaemonContext context) throws DaemonInitException,
            Exception {
        System.out.println("init...");
        String[] args = context.getArguments();
        if (args != null) {
            for (String arg : args) {
                System.out.println(arg);
            }
        }
        status = "INITED";
        this.thread = new Thread(this);
        System.out.println("init OK.");
        System.out.println();
    }
 
    @Override
    public void start() {
        System.out.println("status: " + status);
        System.out.println("start...");
        status = "STARTED";
        this.thread.start();
        System.out.println("start OK.");
        System.out.println();
    }
 
    @Override
    public void stop() throws Exception {
        System.out.println("status: " + status);
        System.out.println("stop...");
        status = "STOPED";
        this.thread.join(10);
        System.out.println("stop OK.");
        System.out.println();
    }
 
    @Override
    public void destroy() {
        System.out.println("status: " + status);
        System.out.println("destroy...");
        status = "DESTROIED";
        System.out.println("destroy OK.");
        System.out.println();
    }
 
    @Override
    public void run() {
        while (true) {
            System.out.println(no);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (no > 1000) {
                break;
            }
            no++;
        }
    }
}

위의 클래스를 jar로 묶어서 BageSoft.jar 라는 파일을 만들고 작업경로(/root/commons-daemon/)에 저장했다.


3. JAVA를 실행할 쉘스크립트 작성
쉘스크립트를 작성할 때, 위에서 다운 받아서 압축 풀었던 위치(commons-daemon-1.0.5-src/src/samples)에서 ServiceDaemon.sh를 참고하면 된다.
아래에서 $DAEMON_HOME은 작업 경로를 의미한다.
나중에 디버깅의 편의성 때문에 에러 출력을 표준출력과 같은 파일에 저장하도록 했다.
>> 주- 저 같은 경우에는 패키지 이름을 test로 했기 때문에 14번째 줄의 MAIN_CLASS = test.TestDaemon으로 바꾸었다. 
>> 1번째 줄의 JAVA_home 도 /usr/local/jdk1.7 로 바꾸어 주었다. 

TestDaemon.sh
#!/bin/sh --> 이 부분은 잘 모르겠지만, bin이 명령어들? 모음 같은 실행파일이고 sh도 명렁어임을 기억
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
JAVA_HOME=/opt/jdk1.6.0_24
JSVC=/root/commons-daemon/jsvc
USER=root
 
DAEMON_HOME=/root/commons-daemon
PID_FILE=$DAEMON_HOME/daemon.pid
OUT_FILE=$DAEMON_HOME/daemon.out
#ERR_FILE=$DAEMON_HOME/daemon.err
 
CLASSPATH=\
$DAEMON_HOME/commons-daemon-1.0.5.jar:\
$DAEMON_HOME/BageSoft.jar
 
MAIN_CLASS=com.bagesoft.test.daemon.TestDaemon
case "$1" in
  start)
    #
    # Start Daemon
    #
    rm -f $OUT_FILE
    $JSVC \
    -user $USER \
    -java-home $JAVA_HOME \
    -pidfile $PID_FILE \
    -outfile $OUT_FILE \
    -errfile $OUT_FILE \
    -cp $CLASSPATH \
    $MAIN_CLASS
    #
    # To get a verbose JVM
    #-verbose \
    # To get a debug of jsvc.
    #-debug \
    exit $?
    ;;
 
  stop)
    #
    # Stop Daemon
    #
    $JSVC \
    -stop \
    -nodetach \
    -java-home $JAVA_HOME \
    -pidfile $PID_FILE \
    -outfile $OUT_FILE \
    -errfile $OUT_FILE \
    -cp $CLASSPATH \
    $MAIN_CLASS
    exit $?
    ;;
 
  *)
    echo "[Usage] TestDaemon.sh start | stop"
    exit 1;;
esac
4. 실행/종료
실행 
/root/commons-daemon/TestDaemon.sh start

로그를 남기도록 몇초 기다리고.... 
종료
/root/commons-daemon/TestDaemon.sh stop

제대로 처리 되는지는 daemon.out의 내용을 확인하면 된다.
init(), start(), stop(), destroy()가 모두 수행되면 정상이다.

아래는 start 후 10초 후에 stop 했을 때의 daemon.out의 내용이다.
init... init OK. status: INITED start... start OK. 0 1 2 3 4 5 6 7 8 9
status: STARTED stop... stop OK. status: STOPED destroy... destroy OK.

진심으로 웹에 정보를 올려주신 분들께 감사하다.

도움이 되셨나요?^^



  1. APO 2014.07.06 03:14

    이 예제문 만들려고 하는데 궁금증이 있습니다 혹시 도움을 주실 수 있으시나요?

  2. 사용자 보이머 2014.07.07 10:38 신고

    글세요. 저도 잘 몰라서 도움을 드릴수 있을지 모르겠지만. 메일 주세요 ㅎㅎ

  3. 2015.05.30 17:51

    비밀댓글입니다

+ Recent posts