Linux守护进程是独立于终端运行的后台进程,不会受到用户登录注销的影响,广泛服务于系统级功能如日志服务、定时任务等场景。编写符合规范的守护进程需要遵循特定的流程,避免进程异常退出或资源占用问题。

守护进程的核心特性
守护进程和普通进程相比有三个核心区别,这些特性决定了它的实现逻辑:
- 脱离终端控制,不会收到终端发出的信号如SIGINT
- 会话和进程组独立,不会成为其他进程组的成员
- 工作目录通常设置为根目录,避免挂载点无法卸载
标准守护进程实现步骤
按照POSIX标准,创建守护进程需要依次完成以下步骤:
- 调用
fork创建子进程,父进程退出,让子进程成为孤儿进程被init收养 - 调用
setsid创建新会话,让子进程成为会话首进程,脱离原终端 - 再次调用
fork并让父进程退出,避免进程重新获得控制终端 - 将当前工作目录切换为根目录
/ - 重设文件权限掩码,避免继承的掩码影响文件创建
- 关闭标准输入、输出、错误流,重定向到
/dev/null或日志文件
完整实例代码分析
以下是一个符合规范的守护进程实例,实现了一个每10秒向日志文件写入时间戳的后台服务:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <time.h>
#include <string.h>
#include <signal.h>
// 日志文件路径
#define LOG_FILE "/tmp/daemon_demo.log"
// 信号处理函数,处理SIGTERM退出信号
void signal_handler(int sig) {
if (sig == SIGTERM) {
// 打开日志文件追加退出信息
int fd = open(LOG_FILE, O_WRONLY | O_CREAT | O_APPEND, 0644);
if (fd != -1) {
time_t now = time(NULL);
char buf[128];
snprintf(buf, sizeof(buf), "守护进程收到退出信号,退出时间:%s", ctime(&now));
write(fd, buf, strlen(buf));
close(fd);
}
exit(0);
}
}
int main() {
pid_t pid;
int fd;
time_t now;
char time_buf[128];
// 第一步:第一次fork,父进程退出
pid = fork();
if (pid < 0) {
perror("fork failed");
exit(1);
}
if (pid > 0) {
// 父进程退出,子进程成为孤儿进程
exit(0);
}
// 第二步:创建新会话,脱离终端
if (setsid() < 0) {
perror("setsid failed");
exit(1);
}
// 第三步:第二次fork,避免重新获得控制终端
pid = fork();
if (pid < 0) {
perror("second fork failed");
exit(1);
}
if (pid > 0) {
exit(0);
}
// 第四步:切换工作目录到根目录
if (chdir("/") < 0) {
perror("chdir failed");
exit(1);
}
// 第五步:重设文件权限掩码
umask(0);
// 第六步:关闭标准输入、输出、错误流,重定向到/dev/null
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
// 重定向标准输入到/dev/null
fd = open("/dev/null", O_RDONLY);
if (fd != STDIN_FILENO) {
dup2(fd, STDIN_FILENO);
close(fd);
}
// 重定向标准输出和错误到日志文件
fd = open(LOG_FILE, O_WRONLY | O_CREAT | O_APPEND, 0644);
if (fd != STDOUT_FILENO) {
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
close(fd);
}
// 注册信号处理
signal(SIGTERM, signal_handler);
// 守护进程核心逻辑:每10秒写入一次时间戳到日志
while (1) {
now = time(NULL);
snprintf(time_buf, sizeof(time_buf), "当前守护进程运行时间:%s", ctime(&now));
// 此时stdout已经重定向到日志文件,直接输出即可
printf("%s", time_buf);
fflush(stdout);
sleep(10);
}
return 0;
}
代码逻辑解析
上述代码严格按照守护进程创建步骤实现:
- 两次
fork操作确保进程完全脱离终端,第一次fork后父进程退出,子进程被init进程收养;第二次fork后再次退出父进程,避免子进程成为会话首进程后重新打开控制终端。 setsid调用创建新会话,让进程成为新会话的首进程,同时脱离原进程组和终端。- 文件描述符重定向部分,先关闭标准输入、输出、错误流,再将标准输入指向
/dev/null,标准输出和错误指向日志文件,避免后台运行时输出到终端导致问题。 - 信号处理部分注册了
SIGTERM的处理函数,保证进程收到终止信号时可以正常记录退出日志再退出。
编译与运行验证
编译上述代码可以使用gcc命令:
gcc -o daemon_demo daemon_demo.c
运行生成的可执行文件后,可以通过ps -ef | grep daemon_demo查看进程状态,会发现进程没有关联的终端(TTY列显示为?),父进程是init进程(PID为1)。查看/tmp/daemon_demo.log文件可以看到每隔10秒写入的时间戳信息。如果需要停止守护进程,可以使用kill -SIGTERM 进程PID命令,进程会正常退出并写入退出日志。
常见注意事项
- 不要在守护进程中输出调试信息到标准输出,因为标准输出已经被重定向,可能导致日志混乱
- 如果守护进程需要读取配置文件,要注意工作目录切换为根目录后,配置文件的路径需要使用绝对路径
- 多进程或多线程的守护进程需要额外处理进程间通信和资源同步问题,避免资源竞争
Linux守护进程daemon_process后台服务进程管理修改时间:2026-07-22 10:03:36