服务管理程序踩坑总结

闲言碎语

现在是23:58,因为我写的服务管理程序存在种种问题,今天是第二天加班到十一点半了,今天看样子是解决了问题,原因竟然是让人啼笑皆非的低级问题,在此记录一下

程序简介

为了提升系统性能,便于管理由我们自己启动的服务而写的一个总的管理程序。逻辑为:读取配置文件,killall 配置的服务(防止管理程序重启后多开服务),启动配置中的服务,如果配置文件中的服务被杀死,接收到信号后重启,具体原理如下图:

服务管理

此程序的第一版就如上图,后开考虑到管理程序自己可能出意外,被脚本重启,在读取配置文件后先killall一次配置文件中的程序,然后再去启动,killall的动作和启动调用的是同一个函数,函数如下:

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
int do_system(int *svc_pid, const char *exec_file, list<string> arg_list)
{
pid_t pid;
int status;
sigset_t chldmask, savemask;
list<string>::iterator list_iterator;

if (exec_file == NULL)
{
return (-1);
}

/* now block SIGCHLD */
sigemptyset(&chldmask);
sigaddset(&chldmask, SIGCHLD);
if (sigprocmask(SIG_BLOCK, &chldmask, &savemask))
{
return (-2);
}

pid = fork();
if (pid < 0)
{
/* Error. */
return (-3);
}
else if (pid == 0)
{
/* Child. */
int n = 0;
char **argv;
int argc = 0;
char **p;
n = arg_list.size();
p = argv = (char **)malloc((n + 2) * sizeof(char *));
p[argc] = (char *)exec_file;
if (n > 0)
{
for (list_iterator = arg_list.begin(); list_iterator != arg_list.end(); list_iterator++)
{
p[++argc] = (char*)(*list_iterator).c_str();
}
}

p[++argc] = NULL;

n = execvp(exec_file, argv);
/* on error */
free(argv);
exit(n);
}
else
{
/* Parent. */
if (svc_pid)
{
*svc_pid = pid;
}

sigprocmask(SIG_SETMASK, &savemask, NULL);
return (0);
}

执行killall时:do_system(NULL, “killall”, service_list)

启动程序时:do_system(NULL, service_name, arg_list)

第一个坑

在挂测时发现低概率会有僵尸进程产生,而且总是第一个进程变成僵尸进程,看log发现在killall执行完的信号来之前,就开始启动进程了,因为killall中服务的顺序是启动的逆序,所以很可能启动第一个进程时,正好也在kill他,由于进程在初始化,不能完全杀死,没有信号的返回,导致了僵尸进程的产生,所以修改了函数,可以选择是否等待结果返回:

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
73
74
75
76
77
78
79
80
81
82
int do_system(unsigned char wait, int *svc_pid, const char *exec_file, list<string> arg_list)
{
pid_t pid;
int status;
sigset_t chldmask, savemask;
list<string>::iterator list_iterator;

if (exec_file == NULL)
{
return (-1);
}

/* now block SIGCHLD */
sigemptyset(&chldmask);
sigaddset(&chldmask, SIGCHLD);
if (sigprocmask(SIG_BLOCK, &chldmask, &savemask))
{
return (-2);
}

pid = fork();
if (pid < 0)
{
/* Error. */
sigprocmask(SIG_SETMASK, &savemask, NULL);
return (-3);
}
else if (pid == 0)
{
/* Child. */
int n = 0;
char **argv;
int argc = 0;
char **p;
n = arg_list.size();
p = argv = (char **)malloc((n + 1) * sizeof(char *));
p[argc] = (char *)exec_file;
if (n > 0)
{
for (list_iterator = arg_list.begin(); list_iterator != arg_list.end(); list_iterator++)
{
p[++argc] = (char*)(*list_iterator).c_str();
ALOGI("param:%s",p[argc]);
}
}

p[++argc] = NULL;

n = execvp(exec_file, argv);
/* on error */
free(argv);
exit(n);
}
else
{
/* Parent. */
if (svc_pid)
{
*svc_pid = pid;
}

if (wait)
{
while (waitpid(pid, &status, 0) < 0)
{
if (errno != EINTR)
{
sigprocmask(SIG_SETMASK, &savemask, NULL);
return (-1);
}
}
}
else
{
sigprocmask(SIG_SETMASK, &savemask, NULL);
return (0);
}
}

sigprocmask(SIG_SETMASK, &savemask, NULL);
return WEXITSTATUS(status);
}

当第一个参数传1时,就会一直wait直到执行完成,这样就可以在执行一些shell命令时传1,在启动服务时传0

第二个坑

整个程序的结构分为三层,最上面是service,负责和client通讯,保证可以通过client端打开/关闭/刷新配置等操作;最下面是进程的实例类,存放进程的信息包括name,pid,enable,status等;中间层维护一个进程信息的列表,主要逻辑在这一层完成,所以这一层使用了单例

在终于爬出第一个坑之后,发现程序还是有概率启动进程失败,这次是一个都没启动,使用strace查看后发现是死锁了,为了找到哪里死锁也是耗费了一晚上的时间,最后恍然大悟是单例造成的死锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*单例的实现*/


function *function:get_instance()
{
if(!m_function)
{
std::lock_guard<std::mutex> lk(m_mutex);
if(!m_function)
{
m_function = std::shared_ptr<Singleton>(new m_function);
}
}

return m_function;

}

我本意是启动一个线程负责开机的一些操作,包括处理信号,所以有了下面的写法:

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

void signal_function(int sig)
{
switch (sig)
{
case SIGCHLD:
/* Serivce Dead? Check It. */
function::get_instance()->check_srv();
break;

default:
break;
}
}

function::function()
{
/*读取配置文件*/
read_conf();
/*开一个线程并执行run函数*/
start();
}

void function::run()
{
signal(SIGCHLD, signal_function);

killall_srv();
sleep(1);
startall_srv();
}

在最上层调用get_instance()时,会上锁然后去创建function类,这时注册好信号处理函数后,恰好来了信号,此时单例还没创建完成,锁也还没释放,就又去get_instance()导致又上了一次锁,此问题的解决方法共有三种:

信号处理函数不使用单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function *g_func = NULL;
void signal_function(int sig)
{
switch (sig)
{
case SIGCHLD:
/* Serivce Dead? Check It. */
g_gunc->check_srv();
break;

default:
break;
}
}

function::function()
{
g_gunc = this;
/*读取配置文件*/
read_conf();
/*开一个线程并执行run函数*/
start();
}

不用线程

不开线程,run()函数只是个普通函数,上层初始化完成后,主动调用run() :

1
2
3
4
5
6
7
8
9
10
11
12
13

service::service()
{
m_function = function::get_instance();
m_function->run();
}

function::function()
{
/*读取配置文件*/
read_conf();
}

在main()中注册信号处理函数

1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
signal(SIGCHLD, signal_function);
service m_service();

while(1)
{
sleep(1000 * 1000);
}

return 0;
}

总结

  1. 写代码要认真,要提前规划好,不要想到哪写到哪
  2. 使用锁要小心,仔细分析代码走向,防止死锁,或者没有锁住
  3. 不能停止学习,尤其是编程语言的基础数据结构和特性

其实解决问题并不难,难的是发现问题在哪,两个晚上的时间基本都花在了找原因上了,所以要学习掌握一些基本的调试工具,如gdb, strace, valgrind等,接下来我会抽空学习并总结一下这三个工具的使用方法


服务管理程序踩坑总结
https://carl-5535.github.io/2021/10/20/工作总结/服务管理程序踩坑总结/
作者
Carl Chen
发布于
2021年10月20日
许可协议