🎬 胖咕噜的稞达鸭 :个人主页
🔥 个人专栏 : 《数据结构》《C++初阶高阶》
《Linux系统学习》
《算法日记》
⛺️技术的杠杆,撬动整个世界!
本文记述网络计算器的代码增加守护进程的缘由以及如何加,详细代码移步我的gitee:
网络计算器代码的完整实现
为什么要在网络计算器项目的代码中加入守护进程?
在网络计算器中加入守护进程,核心是解决终端会话依赖导致的服务稳定性问题:
用户登录终端会建立会话,关闭终端(或注销)时会话销毁,会给关联的服务器进程发挂断信号,导致服务器像普通命令一样中断;此时若有客户端还在连接,后续再想连就会失败。而守护进程能让服务器脱离终端会话的控制:一方面终端关闭 / 用户注销都不会影响进程运行,保证持续处理客户端的计算请求;另一方面还能规避 Ctrl+C 等终端信号的干扰,同时符合 Linux 后台服务的运行规范,让网络计算器能稳定、独立地提供服务。
将网络计算器加入守护进程,核心是为了让这个网络服务满足后台运行的核心需求:
1. 脱离终端独立后台运行,终端关闭(如退出 SSH)也不会终止服务,保证持续处理客户端计算请求;
2. 避免终端信号(如 Ctrl+C)、IO 关联等干扰,提升服务稳定性;
3. 符合 Linux 后台服务的运行规范,便于服务管理。
具体解决我们自己的代码会受到终端异常退出+ (ctrl+c) + (ctrl+z )等信号的干扰的方式:
解决server_netcal代码执行时受终端异常退出的影响;
核心思路:把server_netcal代码作为一个进程组,在 bash 进程打开时建立新会话,将其放入这个独立的新会话中,以此隔离终端退出的影响。
这种具有新会话的进程就是守护进程。

前台进程
- 前台进程会独占终端的输入输出(
stdin/stdout/stderr),终端的键盘输入会直接传给它,它的输出也会直接显示在终端上; - 终端退出(如关闭
bash窗口)时,会给前台进程发送SIGHUP信号,前台进程默认会被终止,这也是我们要解决的核心问题。
后台进程
- 后台进程是用&启动的进程(如
./server_netcal &),它不再独占终端输入,但依然属于当前终端的会话 / 进程组; - 它的输出仍会显示在终端上,且终端退出时,后台进程同样会收到
SIGHUP信号并终止 ------ 这也是为什么单纯用&启动server_netcal,终端退出后进程还是会挂掉。
守护进程
- 守护进程是完全脱离终端会话的进程(也是使用网络计算器代码中要实现的最终形态),它不属于任何终端的会话 / 进程组,终端的任何操作(退出、关闭)都不会影响它;
- 它会被系统进程(如
systemd)接管,即使所有终端都关闭,也能一直运行,且标准 IO 通常会重定向到日志文件或/dev/null,不再依赖终端。
如何创建会话:
可以调用 setsid 函数来创建一个会话,前提是调用进程(server_netcal)不能是一个进程组的组长。
为什么?
为什么不能让server_netcal成为新会话的组长:
原来server_netcal的进程组是从旧的bash会话中取过来到新的会话中的,但是server_netcal不可以作为这个新会话的组长,因为一个进程组只属于一个会话,因为server_netcal是从bash会话中加入到新会话中的,server_netcal带的是旧进程组,新会话的组长必须是创建新进程组的进程,而不是带着旧进程组的进程。举个例子:新会话要的是全新的部门和全新的经理,而不是把老公司的部门直接挪过来一个经理(server_netcal),老部门有自己的归属,挪过来就乱了。
cpp
#include <unistd.h>
* 功能: 创建会话
* 返回值: 创建成功返回ID,失败返回-1
*/pid_t setsid(void);
该接口调用之后会发生:
- 调用进程会变成新会话的会话首进程。此时,新会话中只有唯一的一个进程。
- 调用进程会变成进程组组长。新进程组 ID 就是当前调用进程 ID。
- 该进程没有控制终端。如果在调用 setsid 之前该进程存在控制终端,则调用之后会切断联系。
需要注意的是:这个接口如果调用进程原来是进程组组长,则会报错。
为了避免这种情况,我们通常的使用方法是先调用 fork 创建子进程,父进程终止,子进程继续执行,因为子进程会继承父进程的进程组 ID,而进程 ID 则是新分配的,就不会出现错误的情况。
守护进程也是孤儿进程的一种。
Daemon.hpp
这个文件的作用是守护进程:解决代码会受到终端异常关闭,ctrl+c ctrl+z关闭进程 这些信号的影响
如何解决:
- fork()出来一个新的进程(守护进程);
- 屏蔽所有可能致使进程结束的信号
- 但是还是需要显示器,键盘,
stdin,stdout,stderr这些关联的,守护进程,不从键盘输入,也不需要向显示器打印
怎么做到3?
方法一:关闭0,1,2(万一我们程序内部有stdin,stdout,stderr,会使程序自身都受到影响
方法二:打开/dev/null,重定向标准输入,标准输出,标准错误到/dev/null

用dup2()实现重定向,将0,1,2重定向。
在main()函数中实现调用Daemon(),然后调用日志(打印到控制台)。
目的是:
把进程的 标准输出(stdout)、标准错误(stderr) 重定向到 /dev/null(Linux 的「黑洞」,写入的内容会被直接丢弃);
这个操作只影响 printf()、cout、perror() 这类「往终端打印日志 / 信息」的行为,不影响进程的核心业务逻辑。
cpp
int main(int argc,char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
std::cout << "服务器已经启动,已经是一个守护进程了" << std::endl;
//守护进程
Daemon();
Enable_File_Log_Strategy();//这样一个操作我们在显示器中打印的就被重定向了
//1.顶层:处理业务
std::unique_ptr<Cal> cal = std::make_unique<Cal>();
//2.协议层
std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>([&cal](Request &req)->Response{
return cal->Execute(req);
});
//3.服务器层
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(std::stoi(argv[1]),
[&protocol](std::shared_ptr<Socket> &sock,InetAddr &client)
{
protocol->GetRequest(sock,client);
});
tsvr->Start();
}
问题:我们重定向了标准输入,标准输出和标准错误,一切往终端的写入都会被丢到/dev/null中,但是我们在启动客户端的时候,往终端输入的信息为什么会有应答?
应答:本质是 server_netcal 进程的业务功能反馈,和 终端打印 是两回事:
- 比如你的进程是网络服务端(
netcal),客户端发请求 → 进程处理请求 → 进程给客户端返回应答包------ 这个应答是通过网络套接字(socket) 发送的,不是通过终端输出; - 再比如进程是本地服务,接收管道 / 消息队列请求 → 返回处理结果 ------ 这个应答是通过进程间通信(IPC) 完成的,也和终端无关。
问题:如果我们想要结束客户端的进程,直接输入ctrl+c或者ctrl+z可以吗,不可以我们该如何结束这个,关闭客户端?
不可以!
为什么 Ctrl+C/Ctrl+Z 没法结束守护进程?
你在终端按 Ctrl+C/Ctrl+Z,本质是给当前终端会话下的前台进程组发信号:
Ctrl+C→ 发SIGINT(中断信号),默认终止进程;Ctrl+Z→ 发SIGTSTP(暂停信号),默认暂停进程。
但你的server_netcal是守护进程,它在调用Daemon()后已经:
- 脱离了原终端会话(创建了新会话,和你敲命令的终端彻底解绑);
- 不再属于终端的「前台进程组」,终端发的
SIGINT/SIGTSTP信号根本传不到它身上; - 甚至守护进程的父进程是
init/systemd(PID=1),和你操作的终端毫无关系。
👉 简单说:你在终端按Ctrl+C,就像你在客厅喊「关灯」,但卧室的灯(守护进程)根本听不到 ------ 信号发错了对象。
正确结束守护进程的方法(实操步骤)
- 找到守护进程的
PID(核心第一步):
bash
# 方法1:按进程名找ps -ef | grep server_netcal
# 方法2:如果进程绑定了端口,按端口找netstat -tulpn | grep 端口号 # 比如你的服务端端口是8080
- 输出示例(PID 是第二列数字):
bash
root 12345 1 0 10:00 ? 00:00:05 ./server_netcal
- 终止进程(根据需求选):
- 正常终止(推荐,让进程收尾):
bash
kill 12345 # 发 SIGTERM 信号,进程可捕获并清理资源
- 强制终止(进程无响应时):
bash
kill -9 12345 # 发 SIGKILL 信号,系统直接干掉进程,无法捕获
- 验证是否终止:
bash
ps -ef | grep server_netcal # 看不到对应 PID 就说明结束了
当然Linux中也是提供了守护进程的函数daemon()

用途:让进程在后端运行。noclose()是否关闭了stdin,stdout,stderr;
nochdir 的具体含义:
nochdir 是一个布尔型参数(0 或非 0),规则极其简单:

为什么要切换到根目录(即 nochdir=0)?
这是守护进程的最佳实践,核心原因是:
- 守护进程是长期运行的后台进程,如果它的工作目录是挂载的磁盘目录(比如
/mnt/usb),一旦这个磁盘被卸载,会导致该目录变成无效目录,守护进程可能出现无法创建文件、无法访问路径等问题; - 根目录 / 是 Linux 系统最基础、永远不会被卸载的目录,切换到这里能保证守护进程的工作目录【永远有效】

此时工作路径已经在根目录下面了。
然后在Makefile中添加以下:
cpp
.PHONY:output # 声明output是「伪目标」,不是实际文件/目录
output: # 定义名为output的构建目标
@mkdir output # 创建根输出目录output(@表示执行时不打印这条命令)
@mkdir -p output/bin # -p:如果目录已存在不报错,创建二进制文件目录
@mkdir -p output/conf # 创建配置文件目录(你这里没复制配置,预留用)
@mkdir -p output/log # 创建日志目录(预留日志输出用)
@cp ServerNetCald output/bin # 把编译好的服务端程序复制到bin目录
@cp netcal_tcpclient output/bin # 把客户端程序复制到bin目录
@cp test.conf output/conf
@tar czf output.tgz output //发布软件包
然后执行代码:
cpp
ubuntu@VM-0-4-ubuntu:~/test/linux_-learning2026/lesson31/NETCAL$ make
g++ -o ServerNetCald main.cc -std=c++17 -ljsoncpp
g++ -o netcal_tcpclient Tcpclient.cc -std=c++17 -ljsoncpp
ubuntu@VM-0-4-ubuntu:~/test/linux_-learning2026/lesson31/NETCAL$ make output
ubuntu@VM-0-4-ubuntu:~/test/linux_-learning2026/lesson31/NETCAL$ ll
total 380
drwxrwxr-x 4 ubuntu ubuntu 4096 Feb 8 22:14 ./
drwxrwxr-x 6 ubuntu ubuntu 4096 Feb 5 10:03 ../
-rw-rw-r-- 1 ubuntu ubuntu 539 Feb 8 20:31 Common.hpp
-rw-rw-r-- 1 ubuntu ubuntu 1908 Feb 8 21:42 Daemon.hpp
-rw-rw-r-- 1 ubuntu ubuntu 1823 Feb 7 11:00 InetAddr.hpp
-rw-rw-r-- 1 ubuntu ubuntu 6425 Feb 7 10:56 Log.hpp
-rw-rw-r-- 1 ubuntu ubuntu 1270 Feb 8 21:38 main.cc
-rw-rw-r-- 1 ubuntu ubuntu 432 Feb 8 22:09 Makefile
-rw-rw-r-- 1 ubuntu ubuntu 881 Feb 7 10:58 Mutex.hpp
-rw-rw-r-- 1 ubuntu ubuntu 1109 Feb 7 16:00 NetCal.hpp
-rwxrwxr-x 1 ubuntu ubuntu 125296 Feb 8 22:13 netcal_tcpclient*
drwxrwxr-x 5 ubuntu ubuntu 4096 Feb 8 22:14 output/
-rw-rw-r-- 1 ubuntu ubuntu 9410 Feb 8 11:19 Protocol.hpp
-rwxrwxr-x 1 ubuntu ubuntu 181888 Feb 8 22:13 ServerNetCald*
-rw-rw-r-- 1 ubuntu ubuntu 4033 Feb 8 10:16 Socket.hpp
-rw-rw-r-- 1 ubuntu ubuntu 1949 Feb 8 11:17 Tcpclient.cc
-rw-rw-r-- 1 ubuntu ubuntu 2224 Feb 7 16:12 TcpServer.hpp
-rw-rw-r-- 1 ubuntu ubuntu 0 Feb 8 22:10 test.conf
drwxrwxr-x 2 ubuntu ubuntu 4096 Feb 7 08:48 testJson/
这个形成的output就是我们的执行发布文件。

软件打包发到windows系统:sz output.tgz;
发送到Linux服务器:rz + 拉取
软件解包tar xzf output.tgz
这样我们的程序就可以一直在后台运行,就像微信,美团这些软件无论在任何时候都可以自由访问一样。
总结:
为网络计算器服务端(server_netcal)实现守护进程改造,核心解决终端依赖导致的服务稳定性问题:让服务端脱离终端会话控制,避免终端关闭 / 注销、Ctrl+C/Ctrl+Z 等信号干扰,实现后台长期稳定运行,符合 Linux 后台服务规范。
守护进程实现关键
1.创建新会话:调用
setsid()创建独立会话(需先fork子进程,避免原进程组组长调用报错),使服务端成为新会话 / 新进程组的首进程,切断与原终端的关联;
2.信号屏蔽:屏蔽终端相关终止 / 暂停信号(如SIGINT、SIGTSTP);
3.IO重定向:通过dup2()将标准输入 / 输出 / 错误(0/1/2)重定向到/dev/null,丢弃终端打印输出,但不影响核心业务逻辑;
4.工作目录切换:推荐切换到根目录/(daemon()函数nochdir=0),避免挂载目录卸载导致路径失效。
核心疑问解答
重定向后仍有应答:应答是服务端通过 socket/IPC 向客户端返回的业务结果,与终端打印(stdout/stderr)无关,仅终端日志被丢弃,业务通信不受影响;
无法用 Ctrl+C/Z 终止守护进程:这类信号仅发送给当前终端会话的前台进程组,守护进程已脱离该会话,需通过ps找 PID,再用kill(正常终止)/kill -9(强制终止)结束进程。