什么是守护进程(Daemon)?
守护进程是在后台运行的特殊进程,具有以下特征:
典型守护进程:sshd(SSH服务)、nginx(Web服务器)、syslogd(日志服务)、crond(定时任务)。
为什么需要守护进程?
守护进程与系统服务的关系
守护进程是系统服务的载体。一个系统服务(如 Web 服务器)以守护进程形式运行后,通过 socket/管道/D-Bus 与外界通信,而非通过终端 I/O。
#include <unistd.h>
pid_t setsid(void);
fork())终端 (tty)
└── 会话 (session)
└── 进程组 (process group)
└── 进程 (process)
守护进程需要脱离这个层级结构,即不在任何会话的终端关联中。
| 步骤 | 函数 | 目的 |
|---|---|---|
| 1 | fork() + exit() |
父进程退出,子进程被 init 收养 |
| 2 | setsid() |
创建新会话,脱离终端 |
| 3 | chdir("/") |
避免工作目录被卸载导致挂载问题 |
| 4 | umask(0) |
重置权限掩码,避免继承父进程的限制 |
| 5 | close() 所有 fd |
关闭继承的文件描述符,避免资源泄漏 |
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
#include <time.h>
#define PID_FILE "/var/run/my_daemon.pid"
static volatile sig_atomic_t running = 1;
static void signal_handler(int sig)
{
(void)sig;
running = 0;
}
static void write_pid_file(void)
{
FILE *fp = fopen(PID_FILE, "w");
if (fp) {
fprintf(fp, "%d\n", getpid());
fclose(fp);
}
}
static void remove_pid_file(void)
{
unlink(PID_FILE);
}
static void daemon_work(void)
{
time_t now;
FILE *log_fp;
while (running) {
time(&now);
log_fp = fopen("/tmp/daemon_demo.log", "a");
if (log_fp) {
fprintf(log_fp, "[%s] daemon is running, pid=%d\n",
ctime(&now), getpid());
fclose(log_fp);
}
sleep(5);
}
}
int main(void)
{
pid_t pid;
/* 步骤1: fork() 创建子进程,父进程退出 */
pid = fork();
if (pid < 0) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid > 0) {
/* 父进程退出,子进程成为孤儿进程,被 init/systemd 收养 */
exit(EXIT_SUCCESS);
}
/* 子进程继续执行 */
/* 步骤2: setsid() 创建新会话,脱离终端 */
if (setsid() < 0) {
perror("setsid");
exit(EXIT_FAILURE);
}
/* 步骤3: chdir("/") 避免工作目录问题 */
if (chdir("/") < 0) {
perror("chdir");
exit(EXIT_FAILURE);
}
/* 步骤4: umask(0) 重置文件权限掩码 */
umask(0);
/* 步骤5: 关闭所有继承的文件描述符 */
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
/* 重定向标准流到 /dev/null */
open("/dev/null", O_RDONLY); /* stdin → /dev/null */
open("/dev/null", O_WRONLY); /* stdout → /dev/null */
open("/dev/null", O_WRONLY); /* stderr → /dev/null */
/* 注册信号处理 */
signal(SIGTERM, signal_handler);
signal(SIGINT, signal_handler);
/* 写 PID 文件 */
write_pid_file();
/* 守护进程主循环 */
daemon_work();
/* 清理退出 */
remove_pid_file();
exit(EXIT_SUCCESS);
}
编译与运行:
gcc -o my_daemon my_daemon.c
sudo ./my_daemon
# 验证守护进程
ps aux | grep my_daemon
cat /var/run/my_daemon.pid
tail -f /tmp/daemon_demo.log
# 停止守护进程
kill $(cat /var/run/my_daemon.pid)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
static volatile sig_atomic_t g_running = 1;
static void on_signal(int sig)
{
(void)sig;
g_running = 0;
}
static int create_daemon(void)
{
pid_t pid;
int fd;
/* 第一次 fork */
pid = fork();
if (pid < 0) {
perror("[daemon] fork");
return -1;
}
if (pid > 0) {
_exit(EXIT_SUCCESS);
}
/* 创建新会话 */
if (setsid() < 0) {
perror("[daemon] setsid");
return -1;
}
/* 第二次 fork(确保不会获得控制终端) */
pid = fork();
if (pid < 0) {
perror("[daemon] second fork");
return -1;
}
if (pid > 0) {
_exit(EXIT_SUCCESS);
}
/* 设置文件权限创建掩码 */
umask(0022);
/* 切换工作目录 */
if (chdir("/") < 0) {
perror("[daemon] chdir");
return -1;
}
/* 关闭所有文件描述符 */
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
/* 重定向标准流 */
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);
}
return 0;
}
static int write_pid(const char *path)
{
FILE *fp = fopen(path, "w");
if (!fp) {
perror("[daemon] write pid");
return -1;
}
fprintf(fp, "%d\n", getpid());
fclose(fp);
return 0;
}
int main(int argc, char *argv[])
{
const char *pid_file = "/var/run/my_daemon2.pid";
if (create_daemon() < 0) {
exit(EXIT_FAILURE);
}
signal(SIGTERM, on_signal);
signal(SIGINT, on_signal);
signal(SIGHUP, SIG_IGN); /* 忽略终端断开信号 */
write_pid(pid_file);
while (g_running) {
/* 你的守护进程工作逻辑 */
sleep(10);
}
unlink(pid_file);
return 0;
}
编译与运行:
gcc -o my_daemon2 my_daemon2.c -Wall
sudo ./my_daemon2
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <syslog.h>
#include <string.h>
#include <errno.h>
#include <time.h>
static volatile sig_atomic_t g_running = 1;
static void on_signal(int sig)
{
(void)sig;
g_running = 0;
}
static int daemon_init(void)
{
pid_t pid;
int fd;
pid = fork();
if (pid < 0) return -1;
if (pid > 0) _exit(EXIT_SUCCESS);
if (setsid() < 0) return -1;
pid = fork();
if (pid < 0) return -1;
if (pid > 0) _exit(EXIT_SUCCESS);
umask(0022);
chdir("/");
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
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);
}
return 0;
}
int main(void)
{
openlog("my_daemon", LOG_PID, LOG_DAEMON);
if (daemon_init() < 0) {
syslog(LOG_ERR, "daemon init failed: %s", strerror(errno));
exit(EXIT_FAILURE);
}
syslog(LOG_INFO, "daemon started, pid=%d", getpid());
signal(SIGTERM, on_signal);
signal(SIGINT, on_signal);
signal(SIGHUP, SIG_IGN);
while (g_running) {
syslog(LOG_DEBUG, "daemon heartbeat, pid=%d", getpid());
sleep(30);
}
syslog(LOG_INFO, "daemon shutting down, pid=%d", getpid());
closelog();
return 0;
}
编译与运行:
gcc -o daemon_syslog daemon_syslog.c -Wall
sudo ./daemon_syslog
# 查看日志
sudo tail -f /var/log/syslog | grep my_daemon
# 或
sudo journalctl -u my_daemon -f
# 停止
sudo kill $(cat /var/run/my_daemon2.pid)
setsid() 要求调用者不是进程组 leader,而只有 fork 出的子进程才不是 leaderchdir("/") 必须做:如果守护进程的工作目录在一个挂载点上,该挂载点无法被卸载(umount 会失败)SIGHUP(终端断开),响应 SIGTERM(优雅退出)syslog() 写入系统日志syslogd,需确认日志目标或使用文件日志Q1:守护进程编写为什么要 fork 两次?
A:第一次 fork 使子进程获得一个非进程组 leader 的身份(父进程退出后),满足 setsid() 的调用条件。第二次 fork 确保最终进程的会话 ID ≠ 进程组 ID,从而彻底防止获得控制终端。如果只 fork 一次,子进程作为会话 leader,打开终端设备时可能自动成为控制终端。
Q2:守护进程为什么要关闭所有文件描述符?
A:守护进程从父进程继承了文件描述符,可能包括:
关闭所有 fd 并重定向到 /dev/null 是标准做法。
Q3:setsid() 的三个作用是什么?
A:setsid() 调用后,当前进程:
这就是守护进程"脱离终端"的核心系统调用。
Q4:嵌入式 Linux 中如何管理守护进程的生命周期?
A:常见做法:
/etc/init.d/ 下编写启动/停止脚本,使用 start-stop-daemon.service 文件,使用 systemctl 管理Q5:守护进程中为什么使用 syslog() 而不是 printf()?
A:守护进程没有控制终端(stdin/stdout/stderr 已关闭),printf() 输出无处可去。syslog() 是 Linux 系统日志接口,将消息写入系统日志守护进程(rsyslogd/syslogd),支持日志级别、日志文件轮转、远程日志等功能。嵌入式中如果 syslogd 不可用,可写入文件日志。