Pull to refresh

Bash: запускаем демон с дочерними процессами

Reading time 5 min
Views 42K
Доброго всем настроения!
Прочитал я вот эту статью, и решил немного сам взять в руки шашки, и попробовать сделать что-нибудь приятное для себя и для других.
Мой скрипт не делает никаких полезных вещей, но думаю для более менее начинающих писателей на bash он чему-нибудь научит, да и если будут комментарии, то и я научусь от тех людей которые укажут на мои ошибки.

Вводная

Скрипт будет запускаться в фоне демоном. Сразу думаю надо договориться что сам процесс который будет висеть в памяти постоянно я буду называть «Родителем». Родитель будет в определенном каталоге искать определенный файл, и если он существует, то файл будет удален и запущен процесс, который я буду называть «Потомок», целью которого будет просто спать какое-то время, и после чего завершиться. Но Родитель не должен будет запускать более одного Потомка в единицу времени. В принципе, если Вы прочитали вышеуказанную статью, то смысл я думаю понятен.

Начнем-с

Итак, определим наши переменные
# Имя файла который будем искать в каталоге
FILE_NAME="run_Lola_run"

# Каталог в котором будем искать файл
WATCH_DIR="/home/mcleod/test"

# Имя файла по которому будем определять запущен ли уже Родитель
LOCK_FILE="${WATCH_DIR}/monitor_file.lock"

# Файл где будем хранить номер работающего процесса Родителя
PID_FILE="${WATCH_DIR}/monitor_file.pid"

# Имя файла по которому будем определять запущен ли Потомок
JOB_LOCK_FILE="${WATCH_DIR}/job_monitor_file.lock"

# В этот файл будем писать ход выполнения скрипта
LOG="${WATCH_DIR}/monitor_file_work.log"

# В этот файл будут попадать ошибки при работе скрипта
ERR_LOG="${WATCH_DIR}/monitor_file_error.log"

# Определяем максимельное время работы Потомка в секундах
RANGE=100


Далее для удобного управления запуском и остановом Родителя напишем небольшое условие
case $1 in
        "start")
                start
                ;;
        "stop")
                stop
                ;;
        *)
                usage
                ;;
esac
exit


Каркас готов, все переменные тоже определены, теперь надо бы описать используемые функции
  • start — функция запуска, которая переводит скрипт в режим демона
  • stop — функция остановки демона
  • usage — функция вывода на экран помощи
  • _log — функция записи в лог файл
  • run_job — функция запуска Потомка


Теперь опишу функции по мере их усложнения. Самая простая их них это функция usage, выглядит она так
# Выводим помощь
usage()
{
        echo "$0 (start|stop)"
}

Далее по слжности идет функция _log
# Функция логирования
_log()
{
        process=$1
        shift
        echo "${process}[$$]: $*"
}

Тепеь функция остановки демона
# Функция остановки демона
stop()
{
        # Если существует pid файл, то убиваем процесс с номером из pid файла
        if [ -e ${PID_FILE} ]
        then
                _pid=$(cat ${PID_FILE})
                kill $_pid
                rt=$?
                if [ "$rt" == "0" ]
                then
                        echo "Daemon stop"
                else
                        echo "Error stop daemon"
                fi
        else
                echo "Daemon is't running"
        fi
}

Здесь я не использую функцию логирования, т.к. здесь сообщения должны выводиться на консоль.
Теперь пожалуй приведу самую сложную функцию start. В ней находится вся логика работы и сам момент демонизации скрипта.
# Функция запуска демона
start()
{
	# Если существует файл с pid процесса не запускаем еще одну копию демона
	if [ -e $PID_FILE ]
	then
		_pid=$(cat ${PID_FILE})
		if [ -e /proc/${_pid} ]
		then
			echo "Daemon already running with pid = $_pid"
			exit 0
		fi
	fi

	# Создаем файлы логов
	touch ${LOG}
	touch ${ERR_LOG}

	# переходим в корень, что бы не блокировать фс
	cd /

	# Перенаправляем стандартный вывод, вывод ошибок и стандартный ввод
	exec > $LOG
	exec 2> $ERR_LOG
	exec < /dev/null

	# Запускаем подготовленную копию процесса, вообщем форкаемся. Здесь происходит вся работа скрипта
	(
		# Не забываем удалять файл с номером процесса и файл очереди при выходе 
		trap  "{ rm -f ${PID_FILE}; exit 255; }" TERM INT EXIT 
		# Основной цикл работы скрипта
		while [ 1 ]
		do
			# Просматриваем каталог на наличие файла
			if ls -1 ${WATCH_DIR} | grep "^${FILE_NAME}$" 2>&1 >/dev/null 
			then
				_log "parent" "File found"
				rm -f ${WATCH_DIR}/${FILE_NAME}
				_log "parent" "File deleted"
				# Вычисляем сколько будет спать Потомок в секундах и запоминаем в массиве
				number=$RANDOM
				let "number %= $RANGE"
				_log "parent" "Genereated number $number"
				JOBS[${#JOBS[@]}]=$number
			fi
                        
			# Если размер массива больше 0, то запускаем Потомка, и удаляем первый элемент из массива
			if [ "${#JOBS[@]}" -gt "0" ]
			then
				if [ ! -e ${JOB_LOCK_FILE} ]
				then
					run_job
					_log "parent" "Running job with pid $!"
					unset JOBS[0]
					JOBS=("${JOBS[@]}")
					_log "parent" "Jobs in queue [${#JOBS[@]}]"
				fi
			fi

			# Дадим процессору отдохнуть
			sleep 1
		done
		exit 0
	)&

	# Пишем pid потомка в файл, и заканчиваем работу
	echo $! > ${PID_FILE}
}

Ну и сама функция запуска Потомка
# Функция запуска Потомка. Потомок берет первый элемент из массива JOBS, и работает
run_job()
{
	# Здесь происходит порождение Потомка. Здесь уже ничего не подготавливаем и не переопределяем стандартный ввод/вывод, т.к. это уже сделано в Родителе
	(
		# Не забываем удалять после окончания работы файл
		trap "{ rm -f ${JOB_LOCK_FILE}; exit 255; }" TERM INT EXIT
		# Дополнительная проверка что бы убедиться что Потомок один
		if [ ! -e ${JOB_LOCK_FILE} ]
		then
			# Пишем номер pid процесса в файл, на всякий случай
			echo "$$" > ${JOB_LOCK_FILE}
			_log "child" "Job with pid $$"
			# Запоминаем первый элемент массива
			seconds=${JOBS[0]}
			# Очищаем массив, хоть память сейчас и дешевая, но зачем ее занимать зря
			unset JOBS
			_log "child" "Sleep seconds $seconds"
			sleep ${seconds}
		else
			_log "child" "Lock file is exists"
		fi
		# Выходим
		exit 0
	)&
}

Ну и теперь если все вышеприведенное объеденить в файл и дать ему права на выполнение, думаю у Вас это не составит труда. Кстати перед запуском советую изменить значение переменной WATCH_DIR на путь к каталогу, в котором скрипт будет искать файл с именем run_Lola_run.
Вот мой вывод лог файла
parent[32338]: File found
parent[32338]: File deleted
parent[32338]: Genereated number 96
parent[32338]: Running job with pid 32385
parent[32338]: Jobs in queue [0]
child[32338]: Job with pid 32338
child[32338]: Sleep seconds 96
parent[32338]: File found
parent[32338]: File deleted
parent[32338]: Genereated number 46
parent[32338]: File found
parent[32338]: File deleted
parent[32338]: Genereated number 1
parent[32338]: Running job with pid 32694
parent[32338]: Jobs in queue [1]
child[32338]: Job with pid 32338
child[32338]: Sleep seconds 46
parent[32338]: Running job with pid 371
parent[32338]: Jobs in queue [0]
child[32338]: Job with pid 32338
child[32338]: Sleep seconds 1

Надеюсь кому-нибудь данный пост поможет в освоении bash.

ЗЫ: Внимательный читатель наверняка увидел что в логе везде один и тот-же номер процесса. Т.к. в bash нет чистого форк, здесь происходит его, можно сказать, эмулирование через запуск необходимого кода в ()&, что запускает его в отдельном процессе, но при этом сохраняет переменные неизменными. А мы знаем что номер текущего процесса в bash хранится в переменной $$. Поэтому в лог файле и отображается один и тот же номер процесса. Именно поэтому функция start заканчивается строкой echo $! > ${PID_FILE}.
Tags:
Hubs:
+20
Comments 15
Comments Comments 15

Articles