【实战指南】守护进程服务实现

本文涉及的产品
Serverless 应用引擎免费试用套餐包,4320000 CU,有效期3个月
注册配置 MSE Nacos/ZooKeeper,182元/月
云原生网关 MSE Higress,422元/月
简介: 本文介绍了在Linux系统中实现守护进程异常重启的几种方案。通过理解僵死进程和信号处理机制,提出了基于SIGCHLD信号监听、轮询proc文件系统及waitpid接口的三种方法,并给出了C++实现代码。最终选择轮询方式以提升稳定性,确保服务进程在崩溃后能自动重启,保障系统可靠性。


开篇

  在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至父进程。因此可按如下流程:

  1. 父进程先创建一个子进程,在子进程中通过execl拉起需要的bin。此时父进程缓存bin文件对应路径和对应的pid。
  2. 父进程注册信号SIGCHLD监听,在处理函数中,通过wait()/waitpid()获取异常子进程的pid。
  3. 通过pid匹配异常进程对应的bin文件路径,再重新拉起此进程。

方案二

  进程在启动时,都会在/proc下创建一个对应的目录/proc/[pid]/。可通过监测此路径实现,流程如下:

  1. 同方案一。
  2. 获取到子进程的pid后,父进程一直检测/proc/[pid]是否存在,不存在时,重新拉起进程。

方案三

  由于waitpid()可以获取所有僵死子进程的pid,因此通过轮询此接口可实现,流程如下:

  1. 同方案一
  2. 通过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系统部署用户进程时,加入此方案,能够避免进程异常导致的系统宕机等其他严重问题。
相关文章
|
8月前
|
监控 搜索推荐 开发工具
2025年1月9日更新Windows操作系统个人使用-禁用掉一下一些不必要的服务-关闭占用资源的进程-禁用服务提升系统运行速度-让电脑不再卡顿-优雅草央千澈-长期更新
2025年1月9日更新Windows操作系统个人使用-禁用掉一下一些不必要的服务-关闭占用资源的进程-禁用服务提升系统运行速度-让电脑不再卡顿-优雅草央千澈-长期更新
629 2
2025年1月9日更新Windows操作系统个人使用-禁用掉一下一些不必要的服务-关闭占用资源的进程-禁用服务提升系统运行速度-让电脑不再卡顿-优雅草央千澈-长期更新
|
9月前
|
运维 监控 Linux
Linux操作系统的守护进程与服务管理深度剖析####
本文作为一篇技术性文章,旨在深入探讨Linux操作系统中守护进程与服务管理的机制、工具及实践策略。不同于传统的摘要概述,本文将以“守护进程的生命周期”为核心线索,串联起Linux服务管理的各个方面,从守护进程的定义与特性出发,逐步深入到Systemd的工作原理、服务单元文件编写、服务状态管理以及故障排查技巧,为读者呈现一幅Linux服务管理的全景图。 ####
|
11月前
|
数据挖掘 程序员 调度
探索Python的并发编程:线程与进程的实战应用
【10月更文挑战第4天】 本文深入探讨了Python中实现并发编程的两种主要方式——线程和进程,通过对比分析它们的特点、适用场景以及在实际编程中的应用,为读者提供清晰的指导。同时,文章还介绍了一些高级并发模型如协程,并给出了性能优化的建议。
127 3
|
存储 Linux Docker
CentOS 7.6安装Docker实战案例及存储引擎和服务进程简介
关于如何在CentOS 7.6上安装Docker、介绍Docker存储引擎以及服务进程关系的实战案例。
580 3
CentOS 7.6安装Docker实战案例及存储引擎和服务进程简介
|
11月前
|
Java 关系型数据库 MySQL
java控制Windows进程,服务管理器项目
本文介绍了如何使用Java的`Runtime`和`Process`类来控制Windows进程,包括执行命令、读取进程输出和错误流以及等待进程完成,并提供了一个简单的服务管理器项目示例。
141 1
|
人工智能 PyTorch 算法框架/工具
Xinference实战指南:全面解析LLM大模型部署流程,携手Dify打造高效AI应用实践案例,加速AI项目落地进程
【8月更文挑战第6天】Xinference实战指南:全面解析LLM大模型部署流程,携手Dify打造高效AI应用实践案例,加速AI项目落地进程
Xinference实战指南:全面解析LLM大模型部署流程,携手Dify打造高效AI应用实践案例,加速AI项目落地进程
|
消息中间件 Kafka 数据安全/隐私保护
Python IPC实战指南:构建高效稳定的进程间通信桥梁
【9月更文挑战第11天】在软件开发中,随着应用复杂度的提升,进程间通信(IPC)成为构建高效系统的关键。本文通过一个分布式日志处理系统的案例,介绍如何使用Python和套接字实现可靠的IPC。案例涉及定义通信协议、实现日志发送与接收,并提供示例代码。通过本教程,你将学会构建高效的IPC桥梁,并了解如何根据需求选择合适的IPC机制,确保系统的稳定性和安全性。
148 5
|
Linux 调度
Linux源码阅读笔记05-进程优先级与调度策略-实战分析
Linux源码阅读笔记05-进程优先级与调度策略-实战分析
|
12月前
|
调度 Python
python3多进程实战(python3经典编程案例)
该文章提供了Python3中使用多进程的实战案例,展示了如何通过Python的标准库`multiprocessing`来创建和管理进程,以实现并发任务的执行。
321 0
|
数据处理 调度 Python
Python并发编程实战指南:深入理解线程(threading)与进程(multiprocessing)的奥秘,打造高效并发应用!
【7月更文挑战第8天】Python并发编程探索:使用`threading`模块创建线程处理任务,虽受限于GIL,适合I/O密集型工作。而`multiprocessing`模块通过进程实现多核利用,适用于CPU密集型任务。通过实例展示了线程和进程的创建与同步,强调了根据任务类型选择合适并发模型的重要性。
136 5