Linux42 守护进程

守护进程(Daemon Process)完全详解

守护进程(Daemon)是脱离控制终端、在后台长期运行、不受用户登录 / 退出影响 的特殊进程,是 Linux/Unix 系统中后台服务的核心形态(如nginxsshdcrondmysqld均为守护进程)。本文从核心概念、创建流程、实战代码、现代管理方式到避坑点,全面讲解守护进程。

一、守护进程核心特征

特征 说明
脱离终端 无控制终端(stdin/stdout/stderr 与终端解绑),不会被终端信号(如 SIGHUP)终止
后台运行 父进程退出后由init/systemd(PID=1)收养,成为孤儿进程但不退出
独立会话 / 进程组 通过setsid创建新会话,不再属于原用户会话,避免终端操作影响
长期稳定运行 无交互、低资源占用,持续提供服务(如网络监听、定时任务)
特殊命名规范 传统守护进程名以d结尾(如sshdcrond),非强制但成惯例
权限可控 通常以root启动后切换到普通用户(如nginxwww-data),降低安全风险

二、守护进程创建的标准流程(经典 7 步)

Linux 下手动创建守护进程需遵循固定步骤,核心是 "脱终端、避信号、清资源",缺一不可:

步骤 1:fork 子进程,父进程退出

  • 目的

    1. 父进程退出后,子进程成为 "孤儿进程",被init/systemd收养;
    2. 子进程不再是 "会话组长",为后续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/mkdirmode参数决定)。

  • 代码示例

    c

    运行

    复制代码
    umask(0); // 常用,也可设为022(拒绝其他用户写权限)

步骤 3:创建新会话(核心!setsid)

  • 目的

    1. 子进程成为新会话的 "会话组长";
    2. 子进程成为新进程组的 "组长进程";
    3. 彻底脱离原控制终端(会话与终端解绑,终端关闭不影响进程)。
  • 代码示例

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

七、核心总结

  1. 传统守护进程:核心是 7 步创建流程(fork→umask→setsid→fork→chdir→close fd→重定向),需手动处理信号、日志、PID;
  2. 现代方案:优先用 systemd 管理,无需手写底层逻辑,支持自动重启、权限控制、日志聚合;
  3. 关键避坑:必处理信号、必记录日志、必管理 PID 文件、切换普通用户、脱离挂载点目录。

守护进程的核心目标是 "后台稳定运行",无论用传统方式还是 systemd,都需保证 "资源不泄漏、信号不崩溃、权限够安全"。

相关推荐
摇滚侠12 小时前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush412 小时前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行52012 小时前
Linux 11 动态监控指令top
linux
不会C语言的男孩14 小时前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
古城小栈14 小时前
Unix 与 Linux 异同小叙
linux·服务器·unix
凡人叶枫15 小时前
Effective C++ 条款42:了解 typename 的双重意义
java·linux·服务器·c++
2601_9618752415 小时前
决战申论100题2026|最新|范文
linux·容器·centos·debian·ssh·fabric·vagrant
java_cj15 小时前
深入kube-apiserver认证机制:从Bearer Token到mTLS的完整认证链解析
linux·运维·服务器·云原生·容器·kubernetes
lsyeei16 小时前
linux 系统目录详解
linux·运维·服务器
森G16 小时前
75、服务器源码解析---------云视频服务项目
linux·服务器·网络·c++·qt