Шаг 22 - Проверка на повторный запуск программы

При разработке сервисов или программ, которые работают в фоновом режиме (так называемые демоны) часто требуется проконтролировать была ли запущена программа до этого. К примеру, если Вы разрабатываете сервер принимающий соединения по tcp протоколу, то попытка занять тот же сокет завершится неудачей, в заголовочном файле /usr/include/asm-generic/errno.h этот код ошибки:


#define EADDRINUSE      98     /* Address already in use */

Конечно в случае tcp-сервиса упасть с таким кодом это нормально. Но часто может быть, что демон является обработчиком какой-нибудь очереди заданий и обрабатывает файлы или директории. Запускать несколько экземпляров программы в таком случае может быть противопоказано, так как могут возникнуть конфликты в работе или вообще не предсказуемые результаты.

Нужно отдельно уметь проанализировать эту ситуацию. В попытке упростить этот процесс многие применяют возможности shell окружения, запуская команду скриптами типа:


#!/bin/sh

if pgrep prog_name > /dev/null; then
    echo "Error: Already running"
else
    ./prog_name
fi

Но можно ли полагаться на такую защиту? Случайный запуск "вручную" не через этот "запускающий" скрипт приведет к неприятным последствиям. В поисках какого-то готового решения я решил поискать, и нашел stackoverflow.com: Determine programmatically if a program is running. Все предложенные там варианты рабочие, но не до конца.

Конечно же анализировать придется все через /proc файловую систему, которая обеспечивает доступ к данным от ядра системы Linux, но существует несколько мест, откуда мы может взять имя процесса:

Пусть не сочтут за плагиат, но эти два ответа я тут приведу, чтобы не потерялось, первая программа анализирует cmdline:


#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <dirent.h>
#include <sys/types.h>

pid_t proc_find(const char* name) 
{
    DIR* dir;
    struct dirent* ent;
    char* endptr;
    char buf[512];

    if (!(dir = opendir("/proc"))) {
        perror("can't open /proc");
        return -1;
    }

    while((ent = readdir(dir)) != NULL) {
        /* if endptr is not a null character, the directory is not
         * entirely numeric, so ignore it */
        long lpid = strtol(ent->d_name, &endptr, 10);
        if (*endptr != '\0') {
            continue;
        }

        /* try to open the cmdline file */
        snprintf(buf, sizeof(buf), "/proc/%ld/cmdline", lpid);
        FILE* fp = fopen(buf, "r");

        if (fp) {
            if (fgets(buf, sizeof(buf), fp) != NULL) {
                /* check the first token in the file, the program name */
                char* first = strtok(buf, " ");
                if (!strcmp(first, name)) {
                    fclose(fp);
                    closedir(dir);
                    return (pid_t)lpid;
                }
            }
            fclose(fp);
        }

    }

    closedir(dir);
    return -1;
}


int main(int argc, char* argv[]) 
{
    if (argc == 1) {
        fprintf("usage: %s name1 name2 ...\n", argv[0]);
        return 1;
    }

    int i;
    for(int i = 1; i < argc; ++i) {
        pid_t pid = proc_find(argv[i]);
        if (pid == -1) {
            printf("%s: not found\n", argv[i]);
        } else {
            printf("%s: %d\n", argv[i], pid);
        }
    }

    return 0;
}

И второй вариант, по мнению автора более правильный, который анализирует stat:


#include <sys/types.h>
#include <dirent.h>
#include <unistd.h>

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

pid_t proc_find(const char* name) 
{
    DIR* dir;
    struct dirent* ent;
    char buf[512];

    long  pid;
    char pname[100] = {0,};
    char state;
    FILE *fp=NULL; 

    if (!(dir = opendir("/proc"))) {
        perror("can't open /proc");
        return -1;
    }

    while((ent = readdir(dir)) != NULL) {
        long lpid = atol(ent->d_name);
        if(lpid < 0)
            continue;
        snprintf(buf, sizeof(buf), "/proc/%ld/stat", lpid);
        fp = fopen(buf, "r");

        if (fp) {
            if ( (fscanf(fp, "%ld (%[^)]) %c", &pid, pname, &state)) != 3 ){
                printf("fscanf failed \n");
                fclose(fp);
                closedir(dir);
                return -1; 
            }
            if (!strcmp(pname, name)) {
                fclose(fp);
                closedir(dir);
                return (pid_t)lpid;
            }
            fclose(fp);
        }
    }

    closedir(dir);
    return -1;
}

int main(int argc, char* argv[]) 
{
    int i;
    if (argc == 1) {
        printf("usage: %s name1 name2 ...\n", argv[0]);
        return 1;
    }

    for( i = 1; i < argc; ++i) {
        pid_t pid = proc_find(argv[i]);
        if (pid == -1) {
            printf("%s: not found\n", argv[i]);
        } else {
            printf("%s: %d\n", argv[i], pid);
        }
    }

    return 0;
}

Объясняю почему все эти популярные ответы не будут работать должным образом.

Давайте разработаем небольшого демона ничего не делающего daemon.c:


#include <unistd.h>

int main() {
	daemon(1,1);
	while (1) sleep(5);
	return 0;
}

Скомпилируем эту программу командой:

$ gcc daemon.c -o nothing_doing_daemon_programm

Получаем экземпляр программы. Я полагаю, что если я положу эту программу в разные папки, то это разные прораммы. Например у вас есть несколько версий проекта в разных каталогах /home/user/daemon-ver0.1 и /home/user/daemon-ver0.2, в обеих этих папках содержатся разные версии программ и могут работать по разному, но компилируются в одно имя nothing_doing_daemon_programm. Сделаем приготовления:

$ mkdir /home/user/daemon-ver0.1
$ mkdir /home/user/daemon-ver0.2
$ cp ./nothing_doing_daemon_programm /home/user/daemon-ver0.1
$ cp ./nothing_doing_daemon_programm /home/user/daemon-ver0.2

Теперь запустим эти два демона следующим образом:

$ cd /home/user/daemon-ver0.1 && ./nothing_doing_daemon_programm 
$ cd /home/user/daemon-ver0.2 && ./nothing_doing_daemon_programm

Теперь смотрим, что нам покажет программа ps:

$ ps aux | grep nothing_doing
user       47642  0.0  0.0   2356    80 ?        Ss   15:09   0:00 ./nothing_doing_daemon_programm
user       47644  0.0  0.0   2356    76 ?        Ss   15:09   0:00 ./nothing_doing_daemon_programm
user       47647  0.0  0.0   9064   736 pts/2    S+   15:10   0:00 grep --color=auto nothing_doing

Т.е. имеем два процесса, запущенных из разных директорий, но с одним названием. Для первого 47642 имеем:

$ cat /proc/47642/cmdline
./nothing_doing_daemon_programm

$ cat /proc/47642/stat
47642 (nothing_doing_d) S 1506 47642 47642 0 -1 1077936192 8 0 0 0 0 0 0 0 20 0 1 0 
3298232 2412544 20 18446744073709551615 94775205928960 94775205933589 140732498649552 
0 0 0 0 0 0 1 0 0 17 2 0 0 0 0 0 94775205944752 94775205945360 94775235919872 
140732498653907 140732498653939 140732498653939 140732498657240 0

$ cat /proc/47642/comm
nothing_doing_d

А также для второго процесса 47644 имеем:

$ cat /proc/47644/cmdline
./nothing_doing_daemon_programm

$ cat /proc/47644/stat
47644 (nothing_doing_d) S 1506 47644 47644 0 -1 1077936192 8 0 0 0 0 0 0 0 20 0 1 0 
3298232 2412544 19 18446744073709551615 94610351038464 94610351043093 140737311360992 
0 0 0 0 0 0 1 0 0 17 2 0 0 0 0 0 94610351054256 94610351054864 94610382802944 
140737311367910 140737311367942 140737311367942 140737311371224 0

$ cat /proc/47644/comm
nothing_doing_d

Вы видите проблемы с этими файлами ? Мы никаким образом не можем отличить откуда были запущены эти программы, более того, в файлах stat и comm от названия процесса у нас остались рожки да ножки, всего лишь 16 байт (вместе с нулевым).

$ pgrep nothing_doing
47642
47644

$ pgrep nothing_doing_daemon_programm
пусто

Из-за того, что process grep - pgrep анализирует названия программ по comm, то он не может найти программы во втором случае, а это очень плохо. Программа запущена и работает, но мы не можем об этом узнать...

К счастью остался последний вариант, это символьная ссылка exe:

$ ls -l /proc/47642 | grep exe
lrwxrwxrwx  1 user user 0 jul  9 15:25 exe -> /home/user/daemon-ver0.1/nothing_doing_daemon_programm

$ ls -l /proc/47644 | grep exe
lrwxrwxrwx  1 user user 0 jul  9 15:25 exe -> /home/user/daemon-ver0.2/nothing_doing_daemon_programm

Почувствовали разницу ?! Это то, что нам надо... И путь полный есть, откуда прозводился запуск и название программы есть. Даже если название совпадает, но запущено из разных мест, то это разные программы, думаю не стоит это отрицать.

Вот и будем это анализировать, программа uniq.c:


#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <dirent.h>

int is_already_running() {
	char proc_path[100];

	char proc_own_exe[1024];
	ssize_t own_exe_len;

	char proc_cmp_exe[1024];
	ssize_t cmp_exe_len;

	DIR *proc_dir;
	struct dirent *proc_dir_entry;

	pid_t mypid = getpid();

	sprintf(proc_path,"/proc/%d/exe", mypid);
	own_exe_len = readlink(proc_path, proc_own_exe, sizeof(proc_own_exe) - 1);
	if (own_exe_len == -1) return -1;
	proc_own_exe[own_exe_len] = '\0';

	int ret = 0;

	proc_dir = opendir("/proc");
	if (!proc_dir) return -1;

	while ( (proc_dir_entry = readdir(proc_dir)) != NULL) {
		char *end;
		pid_t cmp_pid;
		cmp_pid = strtol(proc_dir_entry->d_name, &end, 10);
		if (*end != '\0') continue;
		if (cmp_pid == mypid) continue;

		sprintf(proc_path,"/proc/%d/exe", cmp_pid);
		cmp_exe_len = readlink(proc_path, proc_cmp_exe, sizeof(proc_cmp_exe) - 1);
		if (cmp_exe_len == -1) continue;
		proc_cmp_exe[cmp_exe_len] = '\0';

		if (own_exe_len != cmp_exe_len) continue;
		if (strcmp(proc_own_exe, proc_cmp_exe) == 0) { ret = 1; break; }
	}
	closedir(proc_dir);

	return ret;
}

int main() {
	pid_t temppid = getpid();

	printf("sizeof(pid_t) = %ld, pid = %d \n", sizeof(pid_t), temppid);

	if (is_already_running()) {
		printf("already running\n");
		return 1;
	} else {
		printf("not running yet\n");
		daemon(0,0);
		while (1) sleep(5);
	}
	return 0;
}

Помоему код даже меньше получился и делает более правильные вещи. Мы сравниваем не только название процесса, которое больше не имеет ограничение по длине названия в 15 символов, но еще и путь где программа располагается. Конечно же надо подумать над размерами буферов или даже делать malloc/free, но это уже тонкости, думаю в реальной жизни 1024 байт должно хватить для любого случая.

$ gcc uniq.c -o uniqproc

$ ./uniqproc
sizeof(pid_t) = 4, pid = 48031 
not running yet

$ ./uniqproc
sizeof(pid_t) = 4, pid = 48035 
already running

$ ./uniqproc
sizeof(pid_t) = 4, pid = 48039 
already running

$ ./uniqproc
sizeof(pid_t) = 4, pid = 48040 
already running

Все. Можете пользоваться.

Еще одним из методов проверки повторного запуска программы это создание PID файла, в который вы после запуска записываете свой идентификатор процесса. Повторно запускаемый процесс должен прочитать из этого файла этот идентификатор и удостовериться, что данный pid запущен. Можно вернуться к этой теме позже.


Предыдущий Шаг | Оглавление
Автор Кузин Андрей - 09.07.2021