闲言碎语
现在是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); }
sigemptyset(&chldmask); sigaddset(&chldmask, SIGCHLD); if (sigprocmask(SIG_BLOCK, &chldmask, &savemask)) { return (-2); }
pid = fork(); if (pid < 0) { return (-3); } else if (pid == 0) { 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); free(argv); exit(n); } else { 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); }
sigemptyset(&chldmask); sigaddset(&chldmask, SIGCHLD); if (sigprocmask(SIG_BLOCK, &chldmask, &savemask)) { return (-2); }
pid = fork(); if (pid < 0) { sigprocmask(SIG_SETMASK, &savemask, NULL); return (-3); } else if (pid == 0) { 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); free(argv); exit(n); } else { 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: function::get_instance()->check_srv(); break;
default: break; } }
function::function() { read_conf(); 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: g_gunc->check_srv(); break;
default: break; } }
function::function() { g_gunc = this; read_conf(); 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; }
|
总结
- 写代码要认真,要提前规划好,不要想到哪写到哪
- 使用锁要小心,仔细分析代码走向,防止死锁,或者没有锁住
- 不能停止学习,尤其是编程语言的基础数据结构和特性
其实解决问题并不难,难的是发现问题在哪,两个晚上的时间基本都花在了找原因上了,所以要学习掌握一些基本的调试工具,如gdb, strace, valgrind等,接下来我会抽空学习并总结一下这三个工具的使用方法