开篇
在Linux平台,自研服务进程通常以守护进程
的形式在后台常驻运行。但偶尔也会遇到服务进程异常crash,导致产品基本功能异常,影响恶劣。
解决这种问题,通常两种应对措施:
① 定位crash原因,上传补救措施。
② 后台重新拉起异常进程,避免影响基本功能。
对于措施①,系统部署coredump文件,通过gdb解析coredump文件就能很快定位到原因,本篇主要记录下措施②实现流程。
基础概念
守护进程
守护进程
(daemon)是一类在后台运行的特殊进程,用于执行特定的系统任务。很多守护进程在系统引导的时候启动,并且一直运行直到系统关闭。另一些只在需要的时候才启动,完成任务后就自动结束。
守护进程
的特点是不占用终端,后台运行。在终端只需要在启动进程时加&
,即可启动一个守护进程:
$ ./testBin &
僵死进程(zombie)
long long ago,子进程终止后,会在系统中完全消失,父进程无从查询子进程的信息。因此,UNIX设计者们作出一个这样的约定:如果一个子进程在父进程之前结束,内核应该把子进程设置为一个特殊的状态。处于这种状态的进程叫做僵死(zombie)进程。
僵死进程只保留一些最小的概要信息,直至父进程获取这些信息,才会完全消失,否则一直保持僵死状态。父进程可通过wait()/waitpid()
获取这些状态。
如果父进程先退出,子进程被init接管,子进程退出后init会回收其占用的相关资源。通过终端查看僵死进程
(后缀带有<defunct>):
$ ps -ef | grep defunct dx 10144 10135 0 18:08 pts/2 00:00:00 [test] <defunct>
设计思路
通过对僵死进程
概念的理解:子进程先于父进程结束时,会在系统产生一个僵死进程
,直至父进程对其回收。则可以通过这点,实现进程异常crash的重启。
方案一
在《Linux系统编程》中,有讲道:当子进程终止时,会发送SIGCHLD
至父进程。因此可按如下流程:
- 父进程先创建一个子进程,在子进程中通过
execl
拉起需要的bin。此时父进程缓存bin文件对应路径和对应的pid。 - 父进程注册信号
SIGCHLD
监听,在处理函数中,通过wait()/waitpid()
获取异常子进程的pid。 - 通过pid匹配异常进程对应的bin文件路径,再重新拉起此进程。
方案二
进程在启动时,都会在/proc
下创建一个对应的目录/proc/[pid]/
。可通过监测此路径实现,流程如下:
- 同方案一。
- 获取到子进程的pid后,父进程一直检测
/proc/[pid]
是否存在,不存在时,重新拉起进程。
方案三
由于waitpid()
可以获取所有僵死子进程的pid,因此通过轮询此接口可实现,流程如下:
- 同方案一
- 通过
waitpid()
可获取所有僵死子进程的,匹配对应bin路径,重新拉起。
其中,方案一
是触发式监测,属于其中最优雅的方法。但是在实测过程中发现,子进程异常终止时,父进程存在小概率收到不到信号SIGCHLD
,网上的说法是SIGCHLD
不可靠。从而导致监测子进程状态失败,因此将终端触发改为轮询,衍生了方案三
。
方案二
也是可行的,网上很多也是这么做的。既然有waitpid
接口,个人更倾向于方案三
。
源码实现
代码同时实现了方案一
和方案三
,用CONFIG_SUPPORT_SIGCHLD
控制。为1时,为方案一
实现;为0时,为方案三
实现(实测方案一,SIGCHLD偶尔接收不到)。
#define CONFIG_SUPPORT_SIGCHLD 0 // SIGCHLD不可靠。 1: 信号中断 0: 轮询 #define LOG(fmt, args...) printf(fmt, ##args) #define LOGD(fmt, args...) printf("[%d] %-20s %-4d D: " fmt, getpid(), __FUNCTION__, __LINE__, ##args) #define LOGE(fmt, args...) printf("[%d] %-20s %-4d E: " fmt, getpid(), __FUNCTION__,__LINE__, ##args) typedef struct exeInfo { char path[20]; int times; } SExeInfo; typedef std::map<int, shared_ptr<SExeInfo>> TMapPid2Path; const char PROC_PATH[] = "/proc"; TMapPid2Path pidMap; static std::mutex sigchildMtx; static std::condition_variable sigchildCond; static bool existExeByProc(int pid) { struct stat fileStat; char pidPath[20] = {0}; snprintf(pidPath, sizeof(pidPath), "%s/%d", PROC_PATH, pid); int ret = lstat(pidPath, &fileStat); if (ret) { //LOGD("%s lstat failed. errno = %d (%s)\n", pidPath, errno, strerror(errno)); return false; } // /proc/pid/ 为目录则当前进程正常 if (S_ISDIR(fileStat.st_mode)) { return true; } return false; } static void dumpPidMapInfo(const TMapPid2Path &aMap) { LOGD("PID PATH TIME+\n"); LOGD("----------------------------------\n"); for (auto it = aMap.begin(); it != aMap.end(); ++it) { LOGD("%-6d %-20s %-2d \n", it->first, it->second->path, it->second->times); } LOGD("----------------------------------\n"); } static void startExe(const char *pExePath) { static int pid = -1; pid = fork(); if (pid == -1) { LOGE("fork failed. errno = %d (%s).\n", errno, strerror(errno)); } else if (pid == 0) { // 子进程 static int startCount = 0; LOGD("Pull up %s (%d).\n", pExePath, ++startCount); execl(pExePath, pExePath); } else { // 父进程 LOGD("Child fork pid: %d.\n", pid); auto it = pidMap.begin(); for (; it != pidMap.end(); ++it) { if (!strncmp(it->second->path, pExePath, strlen(pExePath))) { LOG("find %s %s.\n", it->second->path, pExePath); break; } } if (it == pidMap.end()) { shared_ptr<SExeInfo> spInfo = make_shared<SExeInfo>(); strncpy(spInfo->path, pExePath, sizeof(spInfo->path)); spInfo->times = 1; pidMap.insert(pair<int, shared_ptr<SExeInfo>>(pid, spInfo)); } else { it->second->times++; pidMap.insert(pair<int, shared_ptr<SExeInfo>>(pid, it->second)); pidMap.erase(it); } } } #if CONFIG_SUPPORT_SIGCHLD static bool sigchildRcv = false; static void handler(int sig, siginfo_t *si, void *unused) { LOGD("Receive sig: %d.\n", sig); sigchildRcv = true; sigchildCond.notify_one(); } #endif // Eg. ./exe /tmp/test_bin int main(int argc, char *argv[]) { if (argc < 2) { LOGE("usage: %s [path].\n", argv[0]); return 0; } #if CONFIG_SUPPORT_SIGCHLD struct sigaction sa; sigemptyset(&sa.sa_mask); sa.sa_sigaction = handler; if (sigaction(SIGCHLD, &sa, NULL) == -1) { LOGE("sigaction failed! errno = %d (%s) \n", errno, strerror(errno)); } #endif for (int i = 1; i < argc; i++) { startExe(argv[i]); } thread th1([&]() { while(1) { #if !(CONFIG_SUPPORT_SIGCHLD) int pid = 0, status = 0; while( (pid = waitpid(-1, &status, WNOHANG)) > 0) { if (!existExeByProc(pid) && pidMap.count(pid)) { startExe(pidMap[pid]->path); } } #else std::unique_lock<std::mutex> lk(sigchildMtx); sigchildRcv = false; sigchildCond.wait(lk, []{ int pid = 0, status = 0; while( (pid = waitpid(-1, &status, WNOHANG)) > 0) { if (!existExeByProc(pid) && pidMap.count(pid)) { startExe(pidMap[pid]->path); } } LOGD("Receive SIGCHLD.\n"); return sigchildRcv; }); lk.unlock(); #endif dumpPidMapInfo(pidMap); sleep(1); } }); th1.join(); return 0; }
测试
实验bin
: /tmp/test为2s crash的bin文件,tmp/lambda正常运行bin。预期
: test
进程2s挂掉,会被自动拉起;lambda
进程正常运行,不受影响。
$ ./exe /tmp/test /tmp/lambda [12737] startExe 99 D: Child fork pid: 12738. [12737] startExe 99 D: Child fork pid: 12739. [12737] dumpPidMapInfo 78 D: PID PATH TIME+ [12737] dumpPidMapInfo 79 D: ---------------------------------- [12737] dumpPidMapInfo 81 D: 12738 /tmp/test 1 [12737] dumpPidMapInfo 81 D: 12739 /tmp/lambda 1 [12737] dumpPidMapInfo 83 D: ---------------------------------- [12738] startExe 96 D: Pull up /tmp/test (1). [12739] startExe 96 D: Pull up /tmp/lambda (1). [12737] dumpPidMapInfo 78 D: PID PATH TIME+ [12737] dumpPidMapInfo 79 D: ---------------------------------- [12737] dumpPidMapInfo 81 D: 12738 /tmp/test 1 [12737] dumpPidMapInfo 81 D: 12739 /tmp/lambda 1 [12737] dumpPidMapInfo 83 D: ---------------------------------- [12737] dumpPidMapInfo 78 D: PID PATH TIME+ [12737] dumpPidMapInfo 79 D: ---------------------------------- [12737] dumpPidMapInfo 81 D: 12738 /tmp/test 1 [12737] dumpPidMapInfo 81 D: 12739 /tmp/lambda 1 [12737] dumpPidMapInfo 83 D: ---------------------------------- [12737] startExe 99 D: Child fork pid: 12742. find /tmp/test /tmp/test. [12737] dumpPidMapInfo 78 D: PID PATH TIME+ [12737] dumpPidMapInfo 79 D: ---------------------------------- [12737] dumpPidMapInfo 81 D: 12739 /tmp/lambda 1 [12737] dumpPidMapInfo 81 D: 12742 /tmp/test 2 [12737] dumpPidMapInfo 83 D: ---------------------------------- [12742] startExe 96 D: Pull up /tmp/test (1). [12737] dumpPidMapInfo 78 D: PID PATH TIME+ [12737] dumpPidMapInfo 79 D: ---------------------------------- [12737] dumpPidMapInfo 81 D: 12739 /tmp/lambda 1 [12737] dumpPidMapInfo 81 D: 12742 /tmp/test 2 [12737] dumpPidMapInfo 83 D: ---------------------------------- [12737] dumpPidMapInfo 78 D: PID PATH TIME+ [12737] dumpPidMapInfo 79 D: ---------------------------------- [12737] dumpPidMapInfo 81 D: 12739 /tmp/lambda 1 [12737] dumpPidMapInfo 81 D: 12742 /tmp/test 2 [12737] dumpPidMapInfo 83 D: ---------------------------------- [12737] startExe 99 D: Child fork pid: 12744. find /tmp/test /tmp/test. [12737] dumpPidMapInfo 78 D: PID PATH TIME+ [12737] dumpPidMapInfo 79 D: ---------------------------------- [12737] dumpPidMapInfo 81 D: 12739 /tmp/lambda 1 [12737] dumpPidMapInfo 81 D: 12744 /tmp/test 3 [12737] dumpPidMapInfo 83 D: ---------------------------------- [12744] startExe 96 D: Pull up /tmp/test (1).
总结
- 在开发阶段,应优先查后台进程异常终止的原因。通常由系统配置生成coredump文件,配合gdb可以快速定位到crash代码行号。
- 至于
方案一
偶尔收不到SIGCHLD
,缩短处理函数的响应时间,排除信号处理函数不可重入因素,还是存在问题。网上查到的原因此信号不可靠,具体原因尚不清晰。 - 经过此方案,在Linux系统部署用户进程时,加入此方案,能够避免进程异常导致的系统宕机等其他严重问题。