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