网络计算器(3)增加守护进程开发

🎬 胖咕噜的稞达鸭个人主页
🔥 个人专栏 : 《数据结构《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关闭进程 这些信号的影响

如何解决:

  1. fork()出来一个新的进程(守护进程);
  2. 屏蔽所有可能致使进程结束的信号
  3. 但是还是需要显示器,键盘,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() 后已经:
  1. 脱离了原终端会话(创建了新会话,和你敲命令的终端彻底解绑);
  2. 不再属于终端的「前台进程组」,终端发的 SIGINT/SIGTSTP 信号根本传不到它身上;
  3. 甚至守护进程的父进程是 init/systemd(PID=1),和你操作的终端毫无关系。
    👉 简单说:你在终端按 Ctrl+C,就像你在客厅喊「关灯」,但卧室的灯(守护进程)根本听不到 ------ 信号发错了对象。

正确结束守护进程的方法(实操步骤)

  1. 找到守护进程的 PID(核心第一步):
bash 复制代码
# 方法1:按进程名找ps -ef | grep server_netcal
# 方法2:如果进程绑定了端口,按端口找netstat -tulpn | grep 端口号  # 比如你的服务端端口是8080
  1. 输出示例(PID 是第二列数字):
bash 复制代码
root      12345     1  0 10:00 ?        00:00:05 ./server_netcal
  1. 终止进程(根据需求选):
  • 正常终止(推荐,让进程收尾):
bash 复制代码
kill 12345  # 发 SIGTERM 信号,进程可捕获并清理资源
  • 强制终止(进程无响应时):
bash 复制代码
kill -9 12345  # 发 SIGKILL 信号,系统直接干掉进程,无法捕获
  1. 验证是否终止:
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.信号屏蔽:屏蔽终端相关终止 / 暂停信号(如 SIGINTSIGTSTP);
3.IO 重定向:通过dup2()将标准输入 / 输出 / 错误(0/1/2)重定向到/dev/null,丢弃终端打印输出,但不影响核心业务逻辑;
4.工作目录切换:推荐切换到根目录/(daemon()函数nochdir=0),避免挂载目录卸载导致路径失效

核心疑问解答

重定向后仍有应答:应答是服务端通过 socket/IPC 向客户端返回的业务结果,与终端打印(stdout/stderr)无关,仅终端日志被丢弃,业务通信不受影响;

无法用 Ctrl+C/Z 终止守护进程:这类信号仅发送给当前终端会话的前台进程组,守护进程已脱离该会话,需通过ps找 PID,再用kill(正常终止)/kill -9(强制终止)结束进程

相关推荐
安科士andxe8 小时前
深入解析|安科士1.25G CWDM SFP光模块核心技术,破解中长距离传输痛点
服务器·网络·5g
寻寻觅觅☆10 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
YJlio11 小时前
1.7 通过 Sysinternals Live 在线运行工具:不下载也能用的“云端工具箱”
c语言·网络·python·数码相机·ios·django·iphone
fpcc11 小时前
并行编程实战——CUDA编程的Parallel Task类型
c++·cuda
l1t11 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
CTRA王大大11 小时前
【网络】FRP实战之frpc全套配置 - fnos飞牛os内网穿透(全网最通俗易懂)
网络
小白同学_C11 小时前
Lab4-Lab: traps && MIT6.1810操作系统工程【持续更新】 _
linux·c/c++·操作系统os
今天只学一颗糖11 小时前
1、《深入理解计算机系统》--计算机系统介绍
linux·笔记·学习·系统架构
赶路人儿11 小时前
Jsoniter(java版本)使用介绍
java·开发语言
testpassportcn12 小时前
AWS DOP-C02 認證完整解析|AWS DevOps Engineer Professional 考試
网络·学习·改行学it