4. 守护进程的PID文件管理

任务说明

本案例实现一个带PID文件管理的守护进程,核心目标是:进程启动时创建PID文件并写入自身PID,若PID文件已存在且对应进程存活,则拒绝启动第二个实例;进程退出时自动清理PID文件。通过该机制确保同一时间仅有一个守护进程实例在运行。

实现方案

守护进程采用经典的双fork + setsid方式脱离终端。PID文件管理包含三个关键操作:检查(判断PID文件是否存在且进程存活)、创建(写入当前进程PID)、清理(退出时删除文件)。检查存活状态使用kill(pid, 0)系统调用,若返回0或errno为EPERM表示进程存在。

完整代码


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/types.h>

#define PID_FILE "/tmp/mydaemon.pid"
#define MAX_PID_LEN 16

/* 检查PID文件对应的进程是否存活 */
static int is_process_alive(pid_t pid) {
    if (pid <= 0) return 0;
    if (kill(pid, 0) == 0) return 1;
    return (errno == EPERM) ? 1 : 0;
}

/* 检查并创建PID文件,成功返回0,失败返回-1 */
static int check_and_create_pidfile(void) {
    int fd;
    char buf[MAX_PID_LEN];
    pid_t existing_pid;
    ssize_t n;

    fd = open(PID_FILE, O_RDWR | O_CREAT, 0644);
    if (fd < 0) {
        perror("open pidfile");
        return -1;
    }

    /* 尝试获取文件锁(非阻塞) */
    if (flock(fd, LOCK_EX | LOCK_NB) < 0) {
        if (errno == EWOULDBLOCK) {
            fprintf(stderr, "另一个守护进程实例已在运行\n");
            close(fd);
            return -1;
        }
        perror("flock");
        close(fd);
        return -1;
    }

    /* 读取已有PID并检查进程是否存活 */
    n = read(fd, buf, sizeof(buf) - 1);
    if (n > 0) {
        buf[n] = '\0';
        existing_pid = (pid_t)atoi(buf);
        if (is_process_alive(existing_pid)) {
            fprintf(stderr, "检测到存活进程 PID=%d,拒绝启动\n", existing_pid);
            close(fd);
            return -1;
        }
    }

    /* 截断文件并写入当前PID */
    if (ftruncate(fd, 0) < 0) {
        perror("ftruncate");
        close(fd);
        return -1;
    }
    lseek(fd, 0, SEEK_SET);
    snprintf(buf, sizeof(buf), "%d\n", getpid());
    if (write(fd, buf, strlen(buf)) != (ssize_t)strlen(buf)) {
        perror("write pid");
        close(fd);
        return -1;
    }
    fsync(fd);
    /* 注意:fd保持打开,持有文件锁直到进程退出 */
    return 0;
}

/* 清理PID文件 */
static void cleanup_pidfile(void) {
    unlink(PID_FILE);
}

/* 信号处理:退出时清理 */
static void handle_signal(int sig) {
    if (sig == SIGTERM || sig == SIGINT) {
        cleanup_pidfile();
        exit(0);
    }
}

/* 守护化 */
static void daemonize(void) {
    pid_t pid;

    pid = fork();
    if (pid < 0) exit(1);
    if (pid > 0) exit(0);   /* 父进程退出 */

    setsid();                /* 新会话 */

    pid = fork();
    if (pid < 0) exit(1);
    if (pid > 0) exit(0);   /* 会话首进程退出 */

    chdir("/");
    umask(0);
    /* 重定向标准输入/输出/错误到 /dev/null */
    int fd = open("/dev/null", O_RDWR);
    if (fd >= 0) {
        dup2(fd, STDIN_FILENO);
        dup2(fd, STDOUT_FILENO);
        dup2(fd, STDERR_FILENO);
        if (fd > 2) close(fd);
    }
}

int main(void) {
    /* 1. 守护化 */
    daemonize();

    /* 2. 检查并创建PID文件 */
    if (check_and_create_pidfile() < 0) {
        exit(1);
    }

    /* 3. 注册信号处理 */
    signal(SIGTERM, handle_signal);
    signal(SIGINT, handle_signal);

    /* 4. 主循环:模拟工作 */
    while (1) {
        sleep(10);
    }

    return 0;
}

编译与运行


# 编译
gcc -o mydaemon mydaemon.c

# 启动第一个实例
./mydaemon

# 验证PID文件内容
cat /tmp/mydaemon.pid

# 尝试启动第二个实例(应被拒绝)
./mydaemon
# 预期输出:另一个守护进程实例已在运行

# 停止守护进程(使用PID文件中的PID)
kill $(cat /tmp/mydaemon.pid)

# 验证PID文件已被清理
ls /tmp/mydaemon.pid
# 预期输出:ls: 无法访问 '/tmp/mydaemon.pid': 没有那个文件或目录

验证方法与判定标准

按以下步骤验证,全部满足即判定通过:

  • 单实例验证:启动第一个实例后,立即再次执行./mydaemon,终端应输出拒绝信息且进程退出码非0。
  • PID文件内容验证:cat /tmp/mydaemon.pid输出的PID与ps aux | grep mydaemon查到的进程PID一致。
  • 异常残留处理:手动执行kill -9 $(cat /tmp/mydaemon.pid)强制杀死进程(PID文件残留),再次启动./mydaemon应能正常启动并覆盖PID文件——因为残留PID对应的进程已不存在,kill(pid,0)返回-1且errno为ESRCH。
  • 退出清理验证:用kill $(cat /tmp/mydaemon.pid)发送SIGTERM后,PID文件应被自动删除。
PID文件管理流程与状态判定 启动实例 daemonize() 双fork+setsid 打开PID文件 open(O_RDWR|O_CREAT) flock 加锁 LOCK_EX|LOCK_NB 加锁失败 拒绝启动:另一实例持有锁 退出码 1 加锁成功 读取已有PID并检查存活 read() → kill(pid, 0) 判断 进程存活 拒绝启动:PID=%d 存活 退出码 1 进程不存在 ftruncate + write(PID) fsync 持久化 主循环运行中 收到 SIGTERM/SIGINT → unlink(PID_FILE)

关键设计说明

本实现采用flock文件锁 + PID存活检查双重机制:文件锁用于防止并发启动时的竞态条件(两个进程同时检查PID文件),PID存活检查用于处理进程被kill -9强制终止后残留PID文件的场景。文件锁随进程退出自动释放,因此即使进程异常崩溃,锁也会被内核回收,但PID文件内容会残留——此时通过kill(pid,0)检测到原进程已不存在,即可安全覆盖。PID文件路径固定为/tmp/mydaemon.pid,实际项目中可改为/var/run/目录并使用O_EXCL标志增强安全性。