# 8. 守护进程 ## 8.1 完整概念讲解 **什么是守护进程(Daemon)?** 守护进程是在后台运行的特殊进程,具有以下特征: - **脱离终端控制**:不响应终端信号(如 Ctrl+C、Ctrl+Z) - **生命周期长**:系统启动时创建,关机时才终止 - **以 root 或特殊用户运行**:通常执行系统级服务 - **没有控制终端**(TTY 为空) 典型守护进程:`sshd`(SSH服务)、`nginx`(Web服务器)、`syslogd`(日志服务)、`crond`(定时任务)。 **为什么需要守护进程?** 1. 服务器程序需要 7x24 小时运行,不能因终端关闭而终止 2. 嵌入式设备需要后台监控进程持续运行 3. 系统服务(网络、日志、定时任务)需要在无人值守时工作 **守护进程与系统服务的关系** 守护进程是系统服务的载体。一个系统服务(如 Web 服务器)以守护进程形式运行后,通过 socket/管道/D-Bus 与外界通信,而非通过终端 I/O。 ## 8.2 核心API/语法 ### setsid() — 创建新会话 ```c #include pid_t setsid(void); ``` - 调用进程成为新会话的 leader(session leader) - 调用进程成为新进程组的 leader(process group leader) - 调用进程脱离控制终端 - **注意**:调用者不能是进程组 leader(所以必须先 `fork()`) - 返回值:成功返回新会话ID,失败返回 -1 ### 会话与终端的关系 ``` 终端 (tty) └── 会话 (session) └── 进程组 (process group) └── 进程 (process) ``` 守护进程需要脱离这个层级结构,即不在任何会话的终端关联中。 ### 守护进程编写五步法 | 步骤 | 函数 | 目的 | | ---- | ------------------- | ---------------------------------- | | 1 | `fork()` + `exit()` | 父进程退出,子进程被 init 收养 | | 2 | `setsid()` | 创建新会话,脱离终端 | | 3 | `chdir("/")` | 避免工作目录被卸载导致挂载问题 | | 4 | `umask(0)` | 重置权限掩码,避免继承父进程的限制 | | 5 | `close()` 所有 fd | 关闭继承的文件描述符,避免资源泄漏 | ## 8.3 代码示例 ### 示例1:基础守护进程(五步法完整实现) ```c #include #include #include #include #include #include #include #include #include #include #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); } ``` **编译与运行:** ```bash 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) ``` ### 示例2:带完整错误处理的守护进程模板 ```c #include #include #include #include #include #include #include #include #include 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; } ``` **编译与运行:** ```bash gcc -o my_daemon2 my_daemon2.c -Wall sudo ./my_daemon2 ``` ### 示例3:使用 syslog 的守护进程(生产级写法) ```c #include #include #include #include #include #include #include #include #include #include #include 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; } ``` **编译与运行:** ```bash 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) ``` ## 8.4 注意事项与易错点 1. **必须先 fork() 再 setsid()**:`setsid()` 要求调用者不是进程组 leader,而只有 fork 出的子进程才不是 leader 2. **第二次 fork 的意义**:第一次 fork 后子进程继承父进程的会话ID,可能再次获得控制终端。第二次 fork 确保新进程的会话ID ≠ 进程组ID,彻底杜绝控制终端 3. **必须关闭所有文件描述符**:继承的 fd 可能持有终端、socket、锁文件等资源,不关闭会导致资源泄漏或安全问题 4. **`chdir("/")` 必须做**:如果守护进程的工作目录在一个挂载点上,该挂载点无法被卸载(umount 会失败) 5. **PID 文件管理**:守护进程应写入 PID 文件便于管理,退出时删除。注意处理 PID 文件已存在的情况(进程是否仍在运行) 6. **信号处理**:守护进程应忽略 `SIGHUP`(终端断开),响应 `SIGTERM`(优雅退出) 7. **使用 syslog 而非 printf**:守护进程没有终端,日志应通过 `syslog()` 写入系统日志 8. **嵌入式注意**:嵌入式系统可能没有 `syslogd`,需确认日志目标或使用文件日志 ## 8.5 面试要点(3-5个核心Q&A) **Q1:守护进程编写为什么要 fork 两次?** A:第一次 fork 使子进程获得一个非进程组 leader 的身份(父进程退出后),满足 `setsid()` 的调用条件。第二次 fork 确保最终进程的会话 ID ≠ 进程组 ID,从而**彻底防止获得控制终端**。如果只 fork 一次,子进程作为会话 leader,打开终端设备时可能自动成为控制终端。 **Q2:守护进程为什么要关闭所有文件描述符?** A:守护进程从父进程继承了文件描述符,可能包括: - 终端 stdin/stdout/stderr(导致与终端关联) - 其他打开的文件/管道/socket(资源泄漏) - 锁文件(可能导致锁冲突) 关闭所有 fd 并重定向到 `/dev/null` 是标准做法。 **Q3:`setsid()` 的三个作用是什么?** A:`setsid()` 调用后,当前进程: 1. 成为新**会话**的 leader(session leader) 2. 成为新**进程组**的 leader(process group leader) 3. **脱离控制终端**(与原终端断开关联) 这就是守护进程"脱离终端"的核心系统调用。 **Q4:嵌入式 Linux 中如何管理守护进程的生命周期?** A:常见做法: 1. **编写脚本**:在 `/etc/init.d/` 下编写启动/停止脚本,使用 start-stop-daemon 2. **systemd 服务**(较新系统):编写 `.service` 文件,使用 `systemctl` 管理 3. **PID 文件**:守护进程写入 PID 文件,管理脚本通过 PID 发送信号 4. **看门狗**:监控脚本定期检查进程是否存活,重启异常退出的守护进程 **Q5:守护进程中为什么使用 `syslog()` 而不是 `printf()`?** A:守护进程没有控制终端(stdin/stdout/stderr 已关闭),`printf()` 输出无处可去。`syslog()` 是 Linux 系统日志接口,将消息写入系统日志守护进程(rsyslogd/syslogd),支持日志级别、日志文件轮转、远程日志等功能。嵌入式中如果 `syslogd` 不可用,可写入文件日志。