通过之前的学习我们已经能使用对应的套接字实现不同主机之间的通信,并且在之前我们还实现了自定义协议,在协议当中实现了序列化和反序列化的的功能,在上一篇章当中我们还基于Jsoncpp库当中提供的方法实现了网络版本计算器的项目,那么接下来在本篇当中将带来一些关于进程当中的一些补充知识,了解进程组、会话、守护进程等概念的含义以及原理,最终需要将我们之前实现的网络版本计算器实现为在对应的Xshell退出之后还能正常的运行。接下来就开始本篇的学习吧!!!
1.进程组
在初识进程的时候我们就已经了解过进程的概念,但是在此我们要映入一个进程组的概念。实际上每个进程除了拥有一个PID之外还属于一个进程组。
在一个进程组当中存在一个或者是多个的进程,进程组就是一个或者多个进程的集合。
每个进程组都和进程一样有对应的唯一的标识ID,称为PGID,在此该ID也是使用一个整数表示。
在此我们使用ps指令查看进程信息的时候距可以查看到进程所属的进程组ID是什么。

1.1 组长进程
在一个进程组当中我们知道了对应的PGID都是一样的,在此就再引入组长进程的概念。组长进程是指进程组当中对应的PID对于进程组PGID的进程。
例如以下示例:


以上我们通过管道同时创建出三个执行sleep指令的进程,那么以上就会发现这个三个进程的PGID是一样的,也就是说明这三个进程是属于同一个进程组的,并且三个进程当中有一个进程的PID是和进程组的ID是一样的,那么这时候就称该进程为该进程组的组长进程。
进程组组长的作用:进程组组长可以创建一个进程组或者创建该组中的进程
• 进程组的生命周期:从进程组创建开始到其中最后一个进程离开为止。
注意:主要某个进程组中有一个进程存在, 则该进程组就存在, 这与其组长进程是否已经终止无关。
2. 会话
以上我们就了解了什么是进程组,那么接下来就继续的来了解会话的概念。
实际上会话也是由对于的进程组来组成的,在一个的会话当中可以同时的存在或者多个的进程组。在每个会话当中也会存在一个唯一的会话ID。

例如以下的示例:
将三个的sleep指令执行的进程使用管道集连为一个进程组,并且在后台运行,那么运行之后使用ps命令查看对应的进程属性就可以看到在该进程组当中所有的进程的会话ID也是一样的。

在此会话当中实际上还存在一个话首进程的概念,话首进程是具有唯一进程ID的单进程,那么这时就可以将话首进程的ID称为会话ID。
注意:会话ID在一些的地方特被称为会话首进程ID,因为会话首进程总是一个进程的组长进程,所以两者是等价的。
3.控制终端
通过之前的学习我们知道当我们使用一个Xshel连接远程的服务器的适合,这时服务器就会创建出一个Shell进程,那么这时这个终端就会成为Shell进程的控制终端,在该终端当中会存储对应的进程的PCB信息,之后在该终端下创建对应的子进程之后子进程当中的PCB信息就是从该终端下获取的。默认终端当中都是没有进行重定向的,那么这时标准输入、标准输出、标准错误等都是指向控制终端。
在一个会话当中是能同时的存在前台进程组和后台进程组的,但是在一个会话当中只能存在一个前台进程组,而后台进程组是可以存在多个的。并且在一个会话当中默认的前台进程组就是对应的控制终端。

4. 作业
以上我们了解了进程组以及会话的概念,那么接下来就再来了解一个新的概念------作业。
实际上以上提到的进程组和之前就学习到的进程都是内核当中提供的概念,那对应用户来说这些概念还是过于的底层,因此在Shell当中就引入了作业的概念。
作业是针对用户来讲, 用户完成某项任务而启动的进程, 一个作业既可以只包含一个进程, 也可以包含多个进程, 进程之间互相协作完成任务, 通常是一个进程管道。
实际上Shell分前后台的不是进程或者进程组而是作业,一个前台作业可以由多个进程组成,一个或者多个后台作业也可以由多个进程组成,一个Shell可以同时运行一个加一个或者多个后台作业,此陈伟作业控制。
实际上在之前我们学习信号相关的概念的适合就了解到有前后台进程的概念,但是实际上之前我们的了解不是很全面,实际上之前了解的一个个的进程本质上就是不同的作业。使用jobs可以查看当前后台作业的信息。
例如以下示例:
将一个进程组中的sleepc程序都转到后台当中运行,那么这时这些的进程都是属于一个进程组,这时整个的进程组在Shell当中也就是一个作业。

通过以上就可以看出该作业的作业号是1,此时要将对应的作业提到前台就使用到fg 作业号例如以上要将作业1提到前台就使用指令fg 1

那么如果要将作业又提回后台呢?那么这时就需要使用到CTRL+Z,这时对应的作业就会被终止,作业同时就变成了后台作业。此时要将该作业在后台当中重新的运行起来就需要使用到bg 作业号。

例如以上的示例就可以看出对应的作业状态从Stopped变成了Running,这就说明作业在后台当中运行起来了。
5. 守护进程
以上当我们了解了进程组、会话、作业相关的概念之后,接下来就可以到本篇当中最重点的内容------守护进程。
实际上在之前的学习当中我们就了解到了僵尸进程以及孤儿进程等特殊的进程。那么在提到的守护进程又是是嘛呢?
实际上在之前我们使用UDP或者是TCP套接字进程主机之间的通信的适合就会发现一个问题,那就是我们编写的代码生成对应的可执行程序之后都是在对应Xshell终端下运行的,而我们运行的进程的生命周期是随着对应的Xshll的,那么这就使得Xshell程序退出的适合,我们运行的服务器程序也就终止了。那么这就有问题了,我们使用的抖音、微信等的客户端是可以7*24小时使用的,那么这就说明对应的服务器当中运行的进程的生命周期是不能随着Shell的。
为了解决以上的问题,在Linux当中映入了对应的解决方法就是,要让一直运行的进程存储在独立的会话当中,我们知道当Xshell对应的远程连接服务实际上就是维护在一个会话当中,那么这时就只需要将运行的进程从其会话当中剥离而到新的会话当中就可以实现一直运行的效果。

在Linux提供了一个名为setsid的函数来实现创建新会话的能力。函数如下所示:
cpp
#include <sys/types.h>
#include <unistd.h>
pid_t setsid(void);
参数:无
返回值:
创建成功返回创建的SID,失败返回-1
以上函数使用还存在要求就是调用该函数的进程不能是当前进程组的组长进程,因为该进程调用之后会创建出新的会话,并且将该进程置为新会话的会话首进程。
因此为了避免出现以上的问题在调用该函数之后都需要在当前的进程中通过fork()创建出子进程,之后再将父进程退出,此时子进程就变为了孤儿进程,那么这时该进程组的组长进程就是1号进程。此时就不会再出现以上的问题。
那么接下来就在函数Dameno当中实现实现守护进程的功能,实现的代码如下所示:
cpp
#include <sys/types.h>
#include <unistd.h>
#include <iostream>
#include "log.hpp"
#include <cstring>
#include "Common.hpp"
#include <signal.h>
#include <sys/stat.h>
#include <fcntl.h>
using namespace LogModule;
const std::string dev = "/dev/null";
// 函数参数nochdir标识是否要切换当前的工作路径,noclose标识是否要关闭对应的标准输入输出错误流
void Dameon(int ischdir, int isclose)
{
// 对信号进行处理
signal(SIGPIPE, SIG_IGN);
signal(SIGCHLD, SIG_IGN);
// 创建子进程,父进程退出
if (fork() > 0)
{
exit(0);
}
// 创建新的会话
setsid();
// 判断是否要切换当前的工作路径
if (ischdir == 1)
chdir("/");
// 判断是否要关闭标准输入输出错误流
if (isclose == 0)
{
// 打开/dev/null设备文件
int fd = ::open(dev.c_str(), O_RDWR);
if (fd < 0)
{
LOG(LogLevel::FATAL) << "open " << dev << " error";
exit(OPEN_ERROR);
}
else
{
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
close(fd);
}
}
else
{
close(0);
close(1);
close(2);
}
}
以上的函数就实现了对应将当前的进程修改为守护进程的功能,在此函数提供的选项第一个是是否要改变当前进程的工作路径,因为默认进程运行的工作路径是在当前进程所处的路径下的,但是在运行一些服务器的程序的时候需要保存一些的日志信息就可能需要从根目录开始进行路径的解析,因此一般是将要守护进程的工作路径切换为根目录。
除此之外函数的第二个参数是是否要将当前的进程的标准输入输出错误的文件描述符关闭,如果不需要就将对应的内容写入到/dev/null 路径下的null的文件。该文件当中是一个字符类型的文件,默认写入到该文件当中的内容都是默认被清理的。

在实现了以上的函数之后接下来就可以在原来我们实现的网络版本计算器当中将对应的服务器端守护进程话。Server.cc实现的代码如下所示:
cpp
#include "log.hpp"
#include "Protool.hpp"
#include "TcpServer.hpp"
#include <memory>
#include "NetCal.hpp"
#include "Dameon.hpp"
void Usage(std::string proc)
{
std::cerr << "Usage:" << proc << " port" << std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
// 守护进程化
Dameon(1, 0);
Enable_File_log_Strategy();
// 创建进行具体计算的对象
std::unique_ptr<Cal> cal = std::make_unique<Cal>();
// 创建进行报文
std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>([&cal](Requst &req) -> Response
{ return cal->Execute(req); });
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(std::stoi(argv[1]), [&protocol](std::shared_ptr<Socket> &sock, InetAddr &client)
{ protocol->GetRequst(sock, client); });
tsvr->Start();
return 0;
}
以上的代码就是基于原来的网路版本计算器的服务器的代码,以上通过调用Dameon函数就将对应的进程守护进程化。
运行程序之后,发现当前的前台命令行无反应,使用ps指令进行查询


通过使用以下的指令查看可发现工作路径发生了变化:

标准输入输出错误的指向文件也发生了变化:

在此将对应的服务器的进程进行守护进程化之后接下来将对应的服务器的Xshell关闭,那么接下来使用客户端对服务器进行连接,发现还是可以实现连接并且还能实现对应的运算,那么这就说明我们实现的代码是没问题的。

实际上除了自己实现对应的Daemon函数之外,在C库当中也提供了daemon的函数来实现守护进程。函数原型如下所示:
cpp
#include <unistd.h>
int daemon(int nochdir, int noclose);
参数: nodchdir:是否要切换工作路径
noclose:是否要关闭 stdin/stdout/stderr
返回值:创建守护进程成功返回0,失败返回-1
6.网络版本计算器完整代码
Tcp_Calculator_2 · 追梦卓/Linux - 码云 - 开源中国
以上就是本篇的所有内容了,在此我们就将套接字通信章节的内容全部学习完毕,那么接下来我们将进入到HTTP章节的学习,未完待续......