守护进程(Daemon Process)完全详解
守护进程(Daemon)是脱离控制终端、在后台长期运行、不受用户登录 / 退出影响 的特殊进程,是 Linux/Unix 系统中后台服务的核心形态(如nginx、sshd、crond、mysqld均为守护进程)。本文从核心概念、创建流程、实战代码、现代管理方式到避坑点,全面讲解守护进程。
一、守护进程核心特征
| 特征 | 说明 |
|---|---|
| 脱离终端 | 无控制终端(stdin/stdout/stderr 与终端解绑),不会被终端信号(如 SIGHUP)终止 |
| 后台运行 | 父进程退出后由init/systemd(PID=1)收养,成为孤儿进程但不退出 |
| 独立会话 / 进程组 | 通过setsid创建新会话,不再属于原用户会话,避免终端操作影响 |
| 长期稳定运行 | 无交互、低资源占用,持续提供服务(如网络监听、定时任务) |
| 特殊命名规范 | 传统守护进程名以d结尾(如sshd、crond),非强制但成惯例 |
| 权限可控 | 通常以root启动后切换到普通用户(如nginx→www-data),降低安全风险 |
二、守护进程创建的标准流程(经典 7 步)
Linux 下手动创建守护进程需遵循固定步骤,核心是 "脱终端、避信号、清资源",缺一不可:
步骤 1:fork 子进程,父进程退出
-
目的 :
- 父进程退出后,子进程成为 "孤儿进程",被
init/systemd收养; - 子进程不再是 "会话组长",为后续
setsid调用铺路(setsid要求调用者非会话组长)。
- 父进程退出后,子进程成为 "孤儿进程",被
-
代码示例 :
c
运行
pid_t pid = fork(); if (pid > 0) { // 父进程直接退出 exit(0); } else if (pid < 0) { // fork失败 perror("fork failed"); exit(1); } // 子进程继续执行后续逻辑
步骤 2:设置文件权限掩码(umask)
-
目的 :清除继承的
umask(默认 022),保证守护进程创建文件 / 目录时权限可控(如umask(0)让权限完全由open/mkdir的mode参数决定)。 -
代码示例 :
c
运行
umask(0); // 常用,也可设为022(拒绝其他用户写权限)
步骤 3:创建新会话(核心!setsid)
-
目的 :
- 子进程成为新会话的 "会话组长";
- 子进程成为新进程组的 "组长进程";
- 彻底脱离原控制终端(会话与终端解绑,终端关闭不影响进程)。
-
代码示例 :
c
运行
if (setsid() < 0) { // 创建新会话失败 perror("setsid failed"); exit(1); }
步骤 4:再次 fork 子进程,父进程(原会话组长)退出
-
可选但推荐:避免守护进程 "意外获取控制终端"(会话组长有权申请终端,再次 fork 后子进程非会话组长,彻底无法关联终端)。
-
代码示例 :
c
运行
pid_t pid2 = fork(); if (pid2 > 0) { exit(0); } else if (pid2 < 0) { perror("fork2 failed"); exit(1); }
步骤 5:切换工作目录到根目录(/)
-
目的 :避免守护进程的工作目录是 "挂载点"(如
/mnt/usb),导致该挂载点无法卸载。 -
代码示例 :
c
运行
if (chdir("/") < 0) { // 也可切换到自定义目录(如/var/run/mydaemon) perror("chdir failed"); exit(1); }
步骤 6:关闭所有不必要的文件描述符
-
目的:继承的 fd(0/stdin、1/stdout、2/stderr)仍关联原终端,关闭后释放资源,避免终端操作触发异常(如 SIGPIPE)。
-
代码示例 :
c
运行
// 方式1:关闭所有fd(推荐) int max_fd = sysconf(_SC_OPEN_MAX); // 获取进程最大可用fd for (int i = 0; i < max_fd; i++) { close(i); } // 方式2:仅关闭标准输入/输出/错误(简化版) // close(STDIN_FILENO); // close(STDOUT_FILENO); // close(STDERR_FILENO);
步骤 7:重定向标准 fd 到 /dev/null(或日志文件)
-
目的 :守护进程无终端,若代码中有
printf/cerr等输出,会触发SIGPIPE信号导致崩溃,需将 0/1/2 重定向到 "黑洞" 或日志文件。 -
代码示例 :
c
运行
// 重定向到/dev/null(丢弃所有输入输出) int fd = open("/dev/null", O_RDWR); dup2(fd, STDIN_FILENO); // 标准输入 → /dev/null dup2(fd, STDOUT_FILENO); // 标准输出 → /dev/null dup2(fd, STDERR_FILENO); // 标准错误 → /dev/null close(fd); // 关闭临时fd
三、守护进程关键补充:信号 / 日志 / PID 管理
1. 信号处理(避免异常退出)
守护进程需处理 / 忽略关键信号,否则易被意外终止:
c
运行
// 自定义退出处理函数(优雅关闭:清理资源、删除PID文件)
void daemon_exit(int sig) {
syslog(LOG_INFO, "daemon exit by signal %d", sig);
unlink("/var/run/mydaemon.pid"); // 删除PID文件
closelog(); // 关闭syslog
exit(0);
}
// 信号初始化
void init_signal() {
signal(SIGHUP, SIG_IGN); // 忽略终端关闭信号
signal(SIGPIPE, SIG_IGN); // 忽略管道破裂信号
signal(SIGTERM, daemon_exit);// 处理终止信号(kill默认发送)
signal(SIGCHLD, SIG_IGN); // 自动回收子进程,避免僵尸进程
}
2. 日志管理(必做!无终端需记录日志)
守护进程无终端输出,必须将日志写入文件或syslog(系统日志):
c
运行
// 初始化syslog(推荐)
void init_log() {
// 标识:mydaemon,选项:记录PID+日志到终端(备用),分类:守护进程日志
openlog("mydaemon", LOG_PID | LOG_CONS, LOG_DAEMON);
syslog(LOG_INFO, "daemon started, pid=%d", getpid());
}
// 业务中输出日志
syslog(LOG_DEBUG, "client connect success"); // 调试日志
syslog(LOG_ERR, "open file failed: %s", strerror(errno)); // 错误日志
3. PID 文件(管理守护进程的核心)
将守护进程 PID 写入/var/run/mydaemon.pid,方便启停 / 重启管理:
c
运行
void write_pid_file() {
FILE *pid_file = fopen("/var/run/mydaemon.pid", "w");
if (!pid_file) {
syslog(LOG_ERR, "create pid file failed");
exit(1);
}
fprintf(pid_file, "%d", getpid());
fclose(pid_file);
// 设置PID文件权限(仅root可读)
chmod("/var/run/mydaemon.pid", 0600);
}
4. 切换普通用户(降低安全风险)
避免守护进程以root运行,创建后切换到低权限用户(如nobody):
c
运行
void switch_user() {
struct passwd *pw = getpwnam("nobody"); // 获取普通用户信息
if (pw) {
setgid(pw->pw_gid); // 先切换组
setuid(pw->pw_uid); // 再切换用户
}
}
四、完整实战:手动实现守护进程
c
运行
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <signal.h>
#include <syslog.h>
#include <pwd.h>
#include <string.h>
// 全局变量:PID文件路径
#define PID_FILE "/var/run/mydaemon.pid"
// 优雅退出处理
void daemon_exit(int sig) {
syslog(LOG_INFO, "daemon exit (signal: %d)", sig);
unlink(PID_FILE);
closelog();
exit(0);
}
// 初始化信号
void init_signal() {
signal(SIGHUP, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
signal(SIGTERM, daemon_exit);
signal(SIGCHLD, SIG_IGN);
}
// 初始化日志
void init_log() {
openlog("mydaemon", LOG_PID | LOG_CONS, LOG_DAEMON);
syslog(LOG_INFO, "daemon log init success");
}
// 写入PID文件
void write_pid() {
FILE *fp = fopen(PID_FILE, "w");
if (!fp) {
syslog(LOG_ERR, "write pid file failed: %s", strerror(errno));
exit(1);
}
fprintf(fp, "%d", getpid());
fclose(fp);
chmod(PID_FILE, 0600);
}
// 切换普通用户
void switch_to_nobody() {
struct passwd *pw = getpwnam("nobody");
if (!pw) {
syslog(LOG_WARNING, "get nobody user failed, skip switch");
return;
}
if (setgid(pw->pw_gid) < 0 || setuid(pw->pw_uid) < 0) {
syslog(LOG_WARNING, "switch user failed: %s", strerror(errno));
}
}
// 创建守护进程核心逻辑
void create_daemon() {
// 步骤1:fork子进程,父退出
pid_t pid = fork();
if (pid > 0) exit(0);
if (pid < 0) { perror("fork"); exit(1); }
// 步骤2:设置umask
umask(0);
// 步骤3:创建新会话
if (setsid() < 0) { perror("setsid"); exit(1); }
// 步骤4:再次fork,避免会话组长
pid_t pid2 = fork();
if (pid2 > 0) exit(0);
if (pid2 < 0) { perror("fork2"); exit(1); }
// 步骤5:切换工作目录
if (chdir("/") < 0) { perror("chdir"); exit(1); }
// 步骤6:关闭所有fd
int max_fd = sysconf(_SC_OPEN_MAX);
for (int i = 0; i < max_fd; i++) close(i);
// 步骤7:重定向标准fd到/dev/null
int fd = open("/dev/null", O_RDWR);
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
close(fd);
// 初始化信号/日志/PID
init_signal();
init_log();
write_pid();
// 切换普通用户
switch_to_nobody();
}
int main() {
// 创建守护进程
create_daemon();
// 守护进程核心业务逻辑(无限循环)
while (1) {
syslog(LOG_DEBUG, "daemon running...");
sleep(5); // 模拟5秒执行一次任务
}
return 0;
}
编译与运行
bash
运行
# 编译
gcc -o mydaemon mydaemon.c
# 启动(需root,因/var/run需root权限)
sudo ./mydaemon
# 查看守护进程
ps -ef | grep mydaemon
cat /var/run/mydaemon.pid
# 停止守护进程
sudo kill -TERM $(cat /var/run/mydaemon.pid)
五、现代替代方案:systemd 管理(推荐)
Linux(CentOS7+/Ubuntu16+)已弃用传统init,推荐用systemd管理守护进程 ------ 无需手动写fork/setsid,支持自动重启、日志管理、权限控制,更易维护。
步骤 1:编写 systemd 服务文件
创建/lib/systemd/system/mydaemon.service:
ini
[Unit]
Description=My Custom Daemon
After=network.target # 网络启动后再启动
[Service]
Type=simple # 简单后台进程
User=nobody # 运行用户
Group=nobody # 运行组
ExecStart=/usr/local/bin/mydaemon # 守护进程路径
ExecStop=/bin/kill -TERM $MAINPID # 停止命令
Restart=on-failure # 失败时自动重启
RestartSec=5 # 重启间隔5秒
StandardOutput=syslog # 标准输出重定向到syslog
StandardError=syslog # 标准错误重定向到syslog
SyslogIdentifier=mydaemon # 日志标识
[Install]
WantedBy=multi-user.target # 多用户模式下启动
步骤 2:systemd 管理命令
bash
运行
# 重载配置(修改service文件后执行)
sudo systemctl daemon-reload
# 启动守护进程
sudo systemctl start mydaemon
# 设置开机自启
sudo systemctl enable mydaemon
# 查看状态
sudo systemctl status mydaemon
# 停止守护进程
sudo systemctl stop mydaemon
# 查看日志(systemd统一日志)
journalctl -u mydaemon -f # -f:实时查看
六、常见问题与避坑
1. PID 文件写入失败
- 原因:
/var/run是临时目录,普通用户无写入权限; - 解决:
root启动后切换用户,或把 PID 文件放到/var/log(普通用户可写)。
2. 日志丢失
- 原因:未重定向 stdout/stderr,且未用 syslog;
- 解决:必做 fd 重定向 + syslog,或直接用 systemd 的日志管理。
3. 进程重复启动
- 原因:未检查 PID 文件是否存在;
- 解决:启动时先判断
/var/run/mydaemon.pid是否存在,存在则退出。
4. 守护进程被意外杀死
- 原因:未忽略
SIGHUP信号,终端关闭时被终止; - 解决:
signal(SIGHUP, SIG_IGN)。
5. 工作目录导致挂载点无法卸载
- 原因:未执行
chdir("/"),工作目录是挂载点; - 解决:必须切换到非挂载点目录(如
/、/var/lib/mydaemon)。
七、核心总结
- 传统守护进程:核心是 7 步创建流程(fork→umask→setsid→fork→chdir→close fd→重定向),需手动处理信号、日志、PID;
- 现代方案:优先用 systemd 管理,无需手写底层逻辑,支持自动重启、权限控制、日志聚合;
- 关键避坑:必处理信号、必记录日志、必管理 PID 文件、切换普通用户、脱离挂载点目录。
守护进程的核心目标是 "后台稳定运行",无论用传统方式还是 systemd,都需保证 "资源不泄漏、信号不崩溃、权限够安全"。