Создание демон-процесса на Java

    Доброго всем времени суток.

    Недавно, по долгу службы, появилась необходимость написать background process для IBM AIX 5.2 на Java с контролирующим shell скриптом.

    Раз работа проделана, документация поднята, код написан, почему бы не поделиться с общественностью? По сему, переходим к делу.



    1 Демонизация



    Для того чтобы демонизировать приложение, необходимо отключить его от терминала, в котором происходит его непосредственный запуск. Для этого необходимо выполнить следующие шаги:
    1. Отключить stdin от терминала
    2. Запустить процесс в background'e путем указания амперсанда «&» в конце строки запуска
    3. Закрыть stdin, stdout непосредственно в приложении (в нашем случае, для Java, это будет System.in.close(); System.out.close();)

    Таким образом, минимальная строка запуска должна выглядеть следующим образом:

    java daemon_app <&- &


    где "<&-" отключит stdin;
    "&" как и говорилось, позволит перевести работу приложения из foreground в background режим.

    Код daemon_app должен выглядеть приблизительно следующим образом:

    public class daemon_app
    {
        public static int main(String[] args)
        {
            try
            {
                daemonize();
            }
            catch (Throwable e)
            {
                System.err.println("Startup failed. " + e.getMessage());
                return 1;
            }
    
            doProcessing();
    
            return 0;
        }
    
        static private void daemonize() throws Exception
        {
            System.in.close();
            System.out.close();
        }
    
        static private void doProcessing()
        {
            //Do some processing
        }
    }
    

    Метод daemonize отключает приложение от stdin и stdout. Единым активным остается поток stderr, который мы можем использовать для логирования ошибок на стадии инициализации.

    Метод doProcessing предназначен для реализации основной логики.

    Далее мы можем использовать какой-либо фреймворк для логирования, например log4j.

    Следующим усовершенствованием будет изменение строки запуска для перехвата данных отправленных нашим процессом в stderr. Для этого, модифицируем строку запуска следующим образом:

    java daemon_app <&- 2>/var/log/daemon_app_error.log &

    где «2>/var/log/daemon_app_error.log» перенаправит вывод с stderr в файл /var/log/daemon_app_error.log.

    Если читатель не в курсе, то потоки ввода-вывода в unix shell имеют следующие идентификаторы:

    stdin – 0
    stdout – 1
    stderr – 2

    2 Организация обработки сигналов прерывания



    В методе doProcessing мы можем организовать бесконечный цикл с предусловием, по которому будет осуществляться окончание работы процесса. Таким условием может быть SIGTERM отправленный из операционной системы, например, посредством kill -15 <pid>.

    Код 15 (SIGTERM) одинаков как для AIX, HP-UX так и для обычной Linux-based системы.
    Список сигналов и их кодов можно получить с помощью команды kill -l.

    Есть несколько способов обработки сигналов в Java:
    • Использование sun.misc.Signal и sun.misc.SignalHandler с последующим созданием собственного класса-обработчика, который имплементирует sun.misc.SignalHandler. Более детальную информацию по данному методу вы можете найти тут (http://www.ibm.com/developerworks/ibm/library/i-signalhandling/)
    • Использование метода Runtime.getRuntime().addShutdownHook. (сигнатура метода: public void addShutdownHook(Thread hook))

    Первый метод не очень хорош из за использования классов из пакета sun.misc. Почему?

    Вот, что Sun пишет (http://java.sun.com/products/jdk/faq/faq-sun-packages.html) про package sun.*:
    The sun.* packages are not part of the supported, public interface.
    A Java program that directly calls into sun.* packages is not guaranteed to work on all Java-compatible platforms. In fact, such a program is not guaranteed to work even in future versions on the same platform…

    2-й метод предусматривает передачу в него в качестве аргумента класс наследованный от Thread. Это значит, что при получении SIGTERM будет создан новый thread, который будет совершать определенные программистом действия для завершения работы процесса в целом.

    Обратимся к документации (http://java.sun.com/docs/books/jvms/second_edition/html/Concepts.doc.html#19152), какие же предусловия необходимы, для завершения работы Java программы?

    The Java virtual machine terminates all its activity and exits when one of two things happens:
    • All the threads that are not daemon threads (§2.19) terminate.
    • Some thread invokes the exit method of class Runtime or class System, and the exit operation is permitted by the security manager.

    Поэтому:
    1. Если у нас есть запущенные не демон-нити, необходимо обеспечить их корректное завершение и присоединить их к main, используя join. Демон-thread'ы, т.е. те, для которых было выполнено setDaemon(true), останавливать не обязательно.
    2. Остановить работу thread main, что подразумевает выход из main метода запускаемого класса.

    Можно попросту сделать System.exit(), если Вам не нужно проводить специфические операции по завершению работы программы, закрывая используемые ресурсы и активные соединения.

    Модифицированный код с использованием Runtime.getRuntime().addShutdownHook для создания обработчика сигналов прерывания приведен ниже:

    public class daemon_app
    {
        static private boolean shutdownFlag = false;
    
        public static int main(String[] args)
        {
            try
            {
                daemonize();
            }
            catch (Throwable e)
            {
                System.err.println("Startup failed. " + e.getMessage());
                return 1;
            }
    
            registerShutdownHook();
    
            doProcessing();
    
            return 0;
        }
    
        static private void doProcessing()
        {
            while (false == shutdownFlag)
            {
                //Do some processing
            }
        }
    
        static public void setShutdownFlag() {shutdownFlag = true;}
    
        private static void registerShutdownHook()
        {
            Runtime.getRuntime().addShutdownHook(
                new Thread() {
                    public void run() {
                            daemon_app.setShutdownFlag();
                    }
                }
            );
        }
    
        static private void daemonize() throws Exception
        {
            System.in.close();
            System.out.close();
        }
    }

    Итак, мы имеем метод registerShutdownHook, который вызывается из main и регистрирует обработчик на сигнал прерывания.

    При получении сигнала прерывания, вызывается статический метод setShutdownFlag, который меняет значение статического булевого свойства shutdownFlag на true, по значению которого организован цикл в методе doProcessing с предусловием.

    3 Контроллирующий скрипт



    Итак, процесс-демон написан. Теперь необходимо создать скрипт управляющий его запуском, остановкой и мониторингом состояния.

    Описывать весь процесс создания shell control скрипта я не буду, приведу только несколько полезных процедур.

    Пример проверки переменных окружения необходымых для запуска/работы процесса

    Процедура без аргументов. Перебираем необходимые переменные окружения циклом for. Если переменной нет, выводим предупреждение. Если хотя бы одна переменная не установлена, прекращаем выполнение с кодом ошибки 1.

    check_env()
    {
            exit_flag=0
    
    	for env_var in JAVA_HOME ORACLE_HOME TUXEDO_HOME ;
    	do
    		eval "env_value=\$$env_var"
    		if [ -z "$env_value" ]
    		then
    			echo "ERROR: environment variable '$env_var' is not set"
    			exit_flag=1
    		fi
    	done
    	
    	if [ $exit_flag -eq 1 ]
    	then
    		echo "Exiting. No process started"
    		exit 1
    	fi
    }
    

    Проверка элементов classpath

    Аргументом этой процедуры является строка с classpath, в котором элементы отделены двоеточием.
    Заменяем двоеточие пробельным символом — в результате получаем возможность проверить каждый элемент.

    check_classpath()
    {
    	#Checking files in classpath are exists and readable
    	for resource in `echo $1 | sed -e "s/:/ /g"`;
    	do
    		if [ ${#resource} -gt 0 ] && [ ! -r $resource ] # if file not exists or not readable
    		then
    			echo "WARNING: Resource '$resource' included in CLASSPATH does not exist or not readable"
    		fi
    	done
    }

    Пример процедуры запуска процесса

    Данная процедура предусматривает наличие установленных переменных — PID_DIR, PROCESS_NAME, CPATH

    где

    PID_DIR – директория для хранения pid файлов
    PROCESS_NAME — имя процесса
    CPATH — строка с classpath

    Идентификатор процесса запущенного в background'е, можно определить с помощью shell-переменной «$!», который впоследствии записывается в pid файл.

    После чего происходит проверка на протяжении 5 секунд, или процесс «не упал» в процессе запуска. Каждую секунду происходит проверка с помощью ps -p статус процесса.

    launch_daemon()
    {
    	JVM_OPTS=""
    
            # Set max memory to 1G
            JVM_OPTS="$JVM_OPTS -Xmx1G"
    
    	echo "Starting process \c"
    
    	# Run process in background with closed input stream, to detach it from terminal
    	$JAVA_HOME/bin/java $JVM_OPTS -cp $CPATH daemon_app <&- 2>/var/log/$PROCESS_NAME.pid &
    
    	#Write pid to pid file
    	echo $! > $PID_DIR/$PROCESS_NAME.pid
    
    	if [ ${#!} -eq 0 ]
    	then
    		echo "... [ failed ]"
    	else # Checking for 5 seconds if process is alive
    		timer=0
    
    		while [ $timer -lt 6 ]
    		do
    			echo ".\c"
    			sleep 1
    			timer=$timer+1
    		done
    
    		if [ `ps -p $! | wc -l` -gt 1 ]
    		then
    			echo " [ started ]"
    			exit 0
    		else
    			echo " [ failed ]"
    			exit 1
    		fi
    	fi
    }

    Пример процедуры остановки процесса

    Приведенная реализация принимает 2 аргумента на вход:
    • id процесса
    • абсолютный путь к pid файлу

    Процедура посылает процессу SIGTERM и после 5 секунд ожидания, если процесс не остановлен, посылает ему SIGKILL, после чего удаляет pid файл.

    stop_daemon()
    {
    	echo "Stopping process \c"
    
    	timer=0
    
    	if [ `ps -p $1 | wc -l` -eq 1 ]
    	then
    		echo " not running"
    	else
    
    		kill -TERM $1
    
    		while [ `ps -p $1 | wc -l` -gt 1 ]
    		do
    			if [ $timer -gt 5 ]
    			then
    				kill -KILL $1
    				timer=0
    			else
    				echo ".\c"
    				sleep 1
    			fi
    			timer=$timer+1
    		done
    
    		echo " stopped "
    	fi
    
    	rm $2
    }


    Пример процедуры проверки статуса процесса


    Принимает 1 аргумент — абсолютный путь к pid файлу

    Возвращает значение отличное от 0, если процесс запущен и работает, 0 — если процесс неактивен.

    check_running()
    {
    	if [ -e $1 ]
    	then
    		fpid=`cat $1`
    		if [ ${#fpid} -gt 0 ]
    		then
    			lines=`ps -p $fpid | wc -l`
    			echo $(($lines-1))
    		else
    			echo 0
    		fi
    	else
    		echo 0
    	fi
    }


    Вот в принципе и все, что хотел рассказать.

    Интересно услышать, кто что думает на эту тему.
    Поделиться публикацией

    Похожие публикации

    Комментарии 26

      0
      Забыли про кат.
        0
        Спасибо большое! Статья очень полезная.
        p.s. Мне как раз на работе, в недалёком будущем, нужно будет подобную штуку реализовать! :)
        • НЛО прилетело и опубликовало эту надпись здесь
            0
            Можно и проще сделать.
            Например так:

            #include <stdlib.h>
            #include <stdio.h>

            int main (int argc, char** argv)
            {
            char* java_home = getenv(«JAVA_HOME»);

            if (java_home == NULL) {
            printf(«JAVA_HOME is not set! Exiting.»);
            exit(1);
            } else {
            strcat(java_home,"/bin/java");
            execv(java_home,argv);
            }
            }

            execv перезапишет образ процесса на новый, а передавая в качестве argv[0] для нового процесса имя текущего, получим следующее

            # ps ax | grep my_daemon
            27061 pts/3 Sl+ 0:00 ./my_daemon test

            Фактически т.е. вместо java увидим имя запускаемого executable.
            • НЛО прилетело и опубликовало эту надпись здесь
                0
                Обертка на shell, в данном случае все равно понадобится.

                А отлов сигналов будет происходить уже средствами Java приложения, т.к. после выполнения execv никаких посредников между оберткой и JVM не будет.
                Фактически — «27061 pts/3 Sl+ 0:00 ./my_daemon test» это тоже, что и «27061 pts/3 Sl+ 0:00 ./java test» только с перезаписанным argv[0].
              0
              есть готовый пакет — Apache Daemon. Только похоже забросили его делать давно
              +1
              Интересно, а зачем карму-то минуснули? :)
              • НЛО прилетело и опубликовало эту надпись здесь
                  0
                  плюсанул +)
                  0
                  Весьма похожую штуку писали для мониторинга WLS кластеров, пока не узнали про WLST :)
                  Только вы забыли упомянуть, что скрипт у вас sh, а в AIX ЕМНИП ksh по дефолту (поправьте если ошибся).
                    +1
                    Вы правы. Дефолтный там ksh, а то, что я привел — для sh/psh.
                    0
                    для этих целей отлично подходит Java Service Wrapper
                    wrapper.tanukisoftware.org

                    Features list:

                    Ability to run as a Windows Service
                    Ability to run as a UNIX daemon
                    Codeless Integration
                    Capture and logging of all console output from JVM
                    Flexible classpath configuration, including wildcards
                    Custom working directory
                    JVM Monitoring and restart functionality
                    Single instance enforcement
                    Ability to control process priority
                      0
                      Правда в свободной версии отсутствует server license, а покупать полную версию как-то не по нашему :).
                        0
                        для внутреннего использования (т.е. не для продажи) вполне хватает Community License:

                        While we encourage corporate and government users to make use of either a Server or Development License Agreement, the Community License Agreement is acceptable as long as the application is for internal use or will be always be distributed along with its full source.
                      +1
                      с запуском можно былобы и несколько проще
                      nohup java -jar daemon.jar &

                      и логи в файлике на всяк случай. хотя конечно для логов надо пользовать логгеры…
                        0
                        По всей видимости, поле shutdownFlag в классе daemon_app надо сделать transient.
                          0
                          Если уж ставить, то лучше volatile.
                            0
                            Да, конечно volatile. Опечатался так.
                          0
                          commons.apache.org/daemon/jsvc.html
                          Умеет всё что нужно. Плюс запускать сервис от нужного пользователя.
                            –1
                            извините, но начиная со строки запуска все это какой-то изврат… Почему не реализовать эту задачу на C или еще проще на perl?
                              0
                              Позвольте уточнить, дабы не делать поспешных умозаключений, что именно вы имеете ввиду под извратом?
                                0
                                Во-первых, ваша строка запуска
                                java daemon_app <&- 2>/var/log/daemon_app_error.log &

                                напоминает какое-то магическое заклинание: «Стань же демоном!» а ведь удобнее без всяких скриптов ./my_daemon и полетели, как почти всегда и бывает в мире unix.
                                Во-вторых, я не увидел в вашем способе установки обработчика привязки конкретно к сигналу SIGTERM. А как мне установить обработчик на SIGHUP и перечитать конфигурационный файл?
                                В-третьих, вы делаете процесс демоном с помощью механизма управления заданиями, который, между прочим, поддерживают не все оболочки, но хуже всего то, что ваш процесс по-прежнему находится в одной сессии с процессом командной оболочки и связан с управляющим терминалом со всеми вытекающими.
                              0
                              Мон пардон, ни разу не специалист в этом, но разве в функции check_classpath в строчке if [ ${#resource} -gt 0 ] && [! -r $resource ] не должен быть оператор «или» вместо «и»?
                                0
                                [ ${#resource} -gt 0 ] проверяет длину элемента в classpath
                                [! -r $resource ] — проверяет или элемент доступен для чтения

                                условие должно быть тут именно «и».

                                Ведь элементы с нулевой длинной нас не интересуют. Нам нужно проверить те элементы, которые указаны в classpath и не существуют или для чтения которых у нас не достаточно прав.

                                Если хотя бы одно из условий не выполнится, будет отражено предупреждение.
                                  0
                                  Да, понял когда уже коммент отправил :) Мне сначала подумалось, что ${#resource} -gt 0 проверяет файловый дескриптор, а не строку. Прошу прощения, как уже говорил, совсем не специалист в шелл-скриптах :)

                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                              Самое читаемое