资讯动态

Linux守护进程(Daemon)完全指南:从原理到实战

发布时间:2026/8/19 14:49:10 来源:尧图企业网站定制
前言在服务端编程中我们经常听到“守护进程”Daemon这个词。它是 Linux/Unix 系统中一种长期运行在后台、没有控制终端、默默提供服务的进程。你有没有想过为什么像sshd、httpd、mysqld这些服务能在系统启动后一直运行而且不会因为用户退出终端而挂掉为什么我们写的网络服务器程序在终端关闭后也会随之消失如何让自己的程序变成像系统服务一样“打不死的小强”答案就是让程序守护进程化。一、什么是守护进程守护进程Daemon是在后台运行的特殊进程它独立于控制终端周期性地执行某种任务或等待处理某些发生的事件。常见的守护进程包括sshd、httpd、mysqld、crond等。1.1 守护进程 vs 普通后台进程特性普通后台进程 ()守护进程控制终端有继承父进程无TTY?会话关系属于原会话独立新会话用户注销影响收到SIGHUP可能终止不受影响进程组组长可能是必须是标准IO指向终端重定向到/dev/null关键区别后台进程只是在后台跑但守护进程是彻底脱离终端独立运行。简单来说守护进程就是割断了和一切终端、用户交互的“孤儿进程”但又被 init 系统或 systemd领养。二、前置知识进程组、会话与控制终端在深入守护进程之前必须理解三个核心概念2.1 进程组Process Group进程组是一个或多个进程的集合每个进程组有唯一的进程组IDPGID组长进程的PID等于PGID进程组的生命周期从创建到组内最后一个进程离开# 查看进程组信息 ps -eo pid,pgid,ppid,comm | grep test2.2 会话Session会话是一个或多个进程组的集合每个会话有唯一的会话IDSID通常由setsid()创建新会话2.3 控制终端Controlling Terminal与会话绑定的终端设备会话首进程打开终端后该终端成为会话的控制终端一个会话最多只能有一个控制终端前台进程组接收终端输入后台进程组不受影响三、守护进程创建五步曲Step 1忽略异常信号信号处理// 1. 忽略IO子进程退出等相关的信号 signal(SIGPIPE, SIG_IGN); signal(SIGCHLD, SIG_IGN);为什么需要忽略这些信号SIGPIPE - 管道破裂信号触发场景向一个已关闭的管道或Socket写入数据时默认行为终止进程守护进程处理SIG_IGN忽略原因网络服务中客户端可能随时断开连接。如果服务器向已关闭的连接写数据不忽略SIGPIPE会导致服务器进程直接崩溃SIGCHLD - 子进程退出信号触发场景子进程终止时向父进程发送默认行为忽略但子进程会变成僵尸进程守护进程处理SIG_IGN忽略原因在Linux中将SIGCHLD设置为SIG_IGN后内核会自动回收子进程资源不会产生僵尸进程。这对于需要频繁创建子进程处理请求的服务器至关重要。防御性编程守护进程需要长期稳定运行任何可能导致异常终止的信号都应该被妥善处理。Step 2fork() 创建子进程 父进程退出// 2. 父进程直接结束 if (fork() 0) exit(0);这一步的两个核心目的目的1让Shell认为命令已执行完毕当用户在Shell中执行./server时如果父进程不退出Shell会一直等待。父进程退出后Shell立即显示提示符子进程在后台继续运行。目的2确保子进程不是进程组组长这是最关键的一步setsid()有一个硬性要求调用进程不能是进程组组长。父进程通常是进程组组长PID PGID子进程继承父进程的PGID但拥有新的PID因此子进程PID ≠ PGID满足setsid()的调用条件孤儿进程被init收养// 父进程退出后// 子进程成为孤儿进程// 被 init/systemd (PID1) 收养// PPID 从原父进程变为 1Step 3setsid() 创建新会话核心步骤// 3. 只能是子进程孤儿了父进程就是1 setsid(); // 成为一个独立的会话setsid()三大作用作用说明创建新会话调用进程成为新会话的会话首进程Session Leader创建新进程组调用进程成为新进程组的组长PGID PID切断终端关联如果之前有控制终端联系被彻底切断关键验证点TTY?没有控制终端TPGID-1没有前台进程组SIDPIDPGID自己是会话首进程和进程组组长⚠️为什么必须先fork再setsid如果直接调用setsid()而调用进程恰好是进程组组长调用会失败返回-1。通过fork()确保子进程不是组长这是setsid()成功的前提条件。Step 4chdir(/) 切换工作目录// 4. 每一个进程都有自己的CWD是否将当前进程的CWD更改成为/根目录 if (nochdir 0) chdir(/);为什么要切换到根目录场景假设你的服务器程序在/mnt/usb/server/目录下启动如果不切换目录守护进程的CWD一直是/mnt/usb/server/这个目录位于USB设备上管理员想卸载USB设备umount /mnt/usb失败因为守护进程占用着该目录守护进程可能运行数月甚至数年USB设备一直无法卸载根目录的优势// 5. 方法2打开/dev/null重定向标准输入、标准输出标准错误到/dev/null if (noclose 0) { int fd ::open(dev.c_str(), O_RDWR); if (fd 0) { LOG(LogLevel::FATAL) open dev errno; exit(OPEN_ERR); } dup2(fd, 0); // stdin dup2(fd, 1); // stdout dup2(fd, 2); // stderr close(fd); }根目录总是存在且可访问不会被卸载守护进程可以在任何环境下运行参数nochdir的作用nochdir0表示需要切换目录默认行为nochdir1表示不切换目录特殊需求Step 5重定向标准IO到 /dev/null为什么要重定向不能直接用close(0/1/2)吗方案做法问题方案1直接关闭close(0); close(1); close(2);❌ 后续open()可能分配到0/1/2导致混乱方案2重定向到/dev/nulldup2(fd, 0/1/2)✅ 保持fd连续性安全静默/dev/null 是什么特殊字符设备文件写入操作数据被内核直接丢弃不占用磁盘空间读取操作立即返回EOF文件结束作用让守护进程有合法的fd 0/1/2但无实际IO为什么保持fd 0/1/2很重要库函数假设很多第三方库假设标准IO可用直接关闭可能导致崩溃文件描述符分配规则Linux总是分配最小的可用fd。如果0/1/2被关闭下次open()会返回0这可能被误认为是stdinSIGPIPE防护向已关闭的fd写入会触发SIGPIPE重定向到/dev/null则安全静默参数noclose的作用noclose0表示需要重定向推荐默认行为noclose1表示不重定向调试用保留终端IO四、执行流程详解五、完整代码解析#pragma once #include iostream #include cstdio #include sys/types.h #include unistd.h #include signal.h #include sys/types.h #include sys/fcntl.h #include Log.hpp #include Common.hpp using namespace LogModule; const std::string dev /dev/null; // 将服务进程守护进程的任务 void Daemon(int nochdir, int noclose) { // Step 1: 信号处理 // 忽略SIGPIPE防止向已关闭的socket/pipe写入导致进程终止 signal(SIGPIPE, SIG_IGN); // 忽略SIGCHLD让内核自动回收子进程避免僵尸进程 signal(SIGCHLD, SIG_IGN); // Step 2: fork() 父进程退出 // 目的1让Shell认为命令执行完毕 // 目的2确保子进程不是进程组组长为setsid()做准备 if (fork() 0) exit(0); // 父进程直接退出 // Step 3: setsid() 创建新会话 // 调用进程成为 // - 新会话的会话首进程 (SID PID) // - 新进程组的组长 (PGID PID) // - 切断与控制终端的所有联系 setsid(); // Step 4: 切换工作目录 // 防止占用可卸载的文件系统 // 确保守护进程在任何目录下都能运行 if (nochdir 0) chdir(/); // Step 5: 重定向标准IO // 守护进程不从键盘输入也不需要向显示器打印 // 网络服务的数据从网卡来服务器只要有主机即可 if (noclose 0) { // 打开/dev/null读写模式 int fd ::open(dev.c_str(), O_RDWR); if (fd 0) { LOG(LogLevel::FATAL) open dev errno; exit(OPEN_ERR); } else { // 将标准输入(0)、标准输出(1)、标准错误(2)重定向到/dev/null dup2(fd, 0); // stdin → /dev/null dup2(fd, 1); // stdout → /dev/null dup2(fd, 2); // stderr → /dev/null close(fd); // 关闭原始fddup2已复制 } } }六、如何使用守护进程函数// ./server port int main(int argc, char *argv[]) { if (argc ! 2) { std::cout Usage : argv[0] port std::endl; return 0; } uint16_t localport std::stoi(argv[1]); // 将当前进程守护进程化 // 参数1: nochdirfalse(0) → 切换工作目录到/ // 参数2: noclosefalse(0) → 重定向标准IO到/dev/null Daemon(false, false); // 创建服务器对象并启动事件循环 std::unique_ptrTcpServer svr(new TcpServer(localport, HandlerRequest)); svr-Loop(); return 0; }七、验证守护进程的方法7.1 使用 ps 命令查看# 查看守护进程信息 ps axj | grep server # 关键字段说明 # PPID: 1 ← 被init收养 # PID: 2831 ← 进程ID # PGID: 2831 ← 进程组ID等于PID说明是组长 # SID: 2831 ← 会话ID等于PID说明是会话首进程 # TTY: ? ← 无控制终端 # TPGID: -1 ← 无前台进程组 # STAT: S ← 睡眠状态后台运行7.2 查看文件描述符# 查看进程的文件描述符 ls -l /proc/2831/fd # 预期输出 # 0 - /dev/null # 1 - /dev/null # 2 - /dev/null # 3 - socket:[...] ← 服务器监听socket # ...7.3 查看工作目录# 查看进程工作目录 ls -l /proc/2831/cwd # 预期输出 # cwd - /

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价