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,都需保证 "资源不泄漏、信号不崩溃、权限够安全"。

相关推荐
liteblue1 小时前
DEB包解包与打包笔记
linux·笔记
minji...1 小时前
Linux 基础IO(一) (C语言文件接口、系统调用文件调用接口open,write,close、文件fd)
linux·运维·服务器·网络·数据结构·c++
赖small强1 小时前
【Linux内存管理】Linux虚拟内存系统详解
linux·虚拟内存·tlb
码龄3年 审核中1 小时前
Linux record 04
linux·运维·服务器
RisunJan1 小时前
Linux命令-ftptop命令(实时监控 ProFTPD 服务器连接状态)
linux·运维·服务器
虾..2 小时前
Linux 文件描述符,重定向及缓冲区理解
linux·运维·服务器
fengyehongWorld2 小时前
Linux lftp命令
linux
赖small强2 小时前
【Linux C/C++开发】Linux C/C++ 堆栈溢出:原理、利用与防护深度指南
linux·c语言·c++·stack·堆栈溢出
TracyCoder1232 小时前
在Ubuntu上搭建大模型最基础的应用环境
linux·运维·ubuntu