8. 守护进程.md 12 KB

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() — 创建新会话

#include <unistd.h>

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:基础守护进程(五步法完整实现)

#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)

示例2:带完整错误处理的守护进程模板

#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

示例3:使用 syslog 的守护进程(生产级写法)

#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)

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 不可用,可写入文件日志。