目录
[1. 进程组](#1. 进程组)
[1.1 什么是进程组](#1.1 什么是进程组)
[1.2 组长进程](#1.2 组长进程)
[2. 会话](#2. 会话)
[2.1 什么是会话](#2.1 什么是会话)
[3. 守护进程](#3. 守护进程)
之前的UDP不用解决字节流的问题,UDP也是需要序列与反序列化,之前写的聊天室就可以在客户端和服务端定义 Message,Message 结构体中可以包含昵称,发的内容,头像等等信息,用的是json串,可用udp来做。
第一次接触面向字节流是在管道中,写进去简单,但是读取的话是很难受的,比较麻烦,因为缺了序列化和反序列化的部分。所以很多的配置文件很多都是json,这是因为vscode软件本身就是通过读取json串,反序列化,根据key读取value,得到你的配置信息。
其实一个真正的服务器是不能直接这样跑的:

正儿八经的一个服务器是要以守护进程的样式去运行。
输入指令:ps ajx | grep ssh


要了解守护进程就先要了解进程间关系:
1. 进程组
输入指令:sleep 100

再次输入其它的指令没有反应,这种在命令行当中直接启动的,导致shell没办法直接解析的,sleep 100 这种进程叫做前台进程
输入指令:sleep 100 &

不影响后续指令的运行,这种指令称为后台进程,后台进程的特点就是在命令后面带上 & 符号,后台进程,不影响bash,用户可以在进程运行期间,继续访问Linux。一般执行耗时的任务时就会启动后台进程。

输入指令:sleep 1000 | sleep 2000 | sleep 3000 &
会有三个子进程:

188736 就是父进程。

PPID:表示的是父进程,PID:表示的是子进程,PGID:表示的就是进程组ID。进程之间存在兄弟、父子关系,兄弟和父子之间有可能还会存在其它的关系:进程组关系。以上三个进程属于同一个组,进程组的组ID一般是以多个进程当中的第一个被创建的进程的PID为进程组ID。所以,命令:sleep 1000 | sleep 2000 | sleep 3000 & 就是一个进程组,是启动一个进程组,共同完成一个任务。还存在一个概念:任务。

再次执行命令:sleep 1000 | sleep 2000 | sleep 3000 &
确实是后台进程,因为在状态中S后面没有 + 号,(有 + 就在前台,没有 + 号 就在后台)



此时再启动一个命令:sleep 4000 | sleep 5000 | sleep 6000 &

所以此时就存在两个任务。
- 进程组:就是第一个进程的PID
- 任务:从1开始的
- 进程组和任务之间的关系:进程组是手段,完成某种任务是目的(某一个进程组完成某项任务)
所以,当我在操作命令行的时候,启动一个进程组(至少一个进程),完成一种任务
查看后端任务的命令:jobs

将任务提到前台的命令:fg 1(1:任务号)

在输入:ctrl+c,终止该任务

任务前后台切换与管理:
- 将任务2放到前台:fg 2
- 又不想终止任务2,又想放回后台:ctrl + z
- jobs 查的时候发现:ctrl + z 是把任务暂停了,自动放到后台了,本来是Running的
- 想在后台继续运行起来:bg 2
- 再次 jobs 可以看见2号任务再次Running

1.1 什么是进程组
其实每⼀个进程除了有⼀个进程ID(PID)之外 还属于⼀个进程组。进程组是⼀个或者多个进程的集合, ⼀个进程组可以包含多个进程。每⼀个进程组也有⼀个唯⼀的进程组ID(PGID), 并且这个PGID类似于进程ID, 同样是⼀个正整数, 可以存放在pid_t数据类型中。
sy@iZ2vc2ummd1vkuxyf2vgvkZ:~/linux/lesson51/NetCal$ ps -eo pid,pgid,ppid,comm | grep sleep
188838 188838 188736 sleep
188839 188838 188736 sleep
188840 188838 188736 sleep
-e 选项表⽰every的意思, 表⽰输出每⼀个进程信息
-o 选项以逗号操作符(,)作为定界符, 可以指定要输出的列
1.2 组长进程
每⼀个进程组都有⼀个组⻓进程。 组⻓进程的ID等于其进程ID。(进程之间是兄弟关系的第一个是组长进程,进程之间是父子关系的话,父进程就是组长进程).
进程组组⻓的作⽤: 进程组组⻓可以创建⼀个进程组或者创建该组中的进程。
进程组的⽣命周期: 从进程组创建开始到其中最后⼀个进程离开为⽌。注意: 只要某个进程组中有⼀个进程存在, 则该进程组就存在, 这与其组⻓进程是否已经终⽌⽆关。

从上面可以看见如果组长退了,PGID也是不变的。
2. 会话

以上3个进程属于一个会话,SID的值对应的是当前进程bash进程对应的ID。
2.1 什么是会话
会话其实和进程组息息相关, 会话可以看成是⼀个或多个进程组的集合, ⼀个会话可以包含多个进程组。每⼀个会话也有⼀个会话ID(SID)

通常我们都是使⽤管道将⼏个进程编成⼀个进程组。 如上图的进程组2和进程组3可能是由下列命令形成的:
$ proc2 | proc3 &
$ proc4 | proc5 | proc6 &
&表⽰将进程组放在后台执⾏
首次登录Linux机器的时候,登陆成功之后,Linux 操作系统首先要为该用户创建一个bash进程组,完成命令行解释任务(其实只有一个进程),所以就会看见命令行提示符这样的东西,不管是前台还是后台,启动的所有任务都是以进程组的形式体现的,哪怕只有一个进程,Linux执行ls、pwd,所以会构建成同时存在其它进程组,bash进程将终端文件打开就有 0,1,2 了,创建进程组都是bash进程的子进程,子进程继承文件描述符表,所以在Linux系统中,任何进程默认打开012

如何证明?

云服务器没有键盘,所以将012都放入/dev/pts/0中


如何证明上面的进程组属于同一个会话?------ 让它们的SID相同就行了!!!
mei新建一个渠道,都会有一个新建的 bash


每一次登录新建一个tty和bash,bash内部可以新建一个进程,所以每一次登录,Linux系统会为当前用户创建一次会话,每一个会话都有会话ID

将以上一次会话放大:

所以,在启动服务器的时候,./Server 8080 属于是该会话内部的任务

登陆时要创建会话,退出的时候就会将整个会话干掉,整个会话内部的进程组就会被跟着退出,注意:老系统中会退出,但是现在的系统是不会退出的。
所以不让这个任务放在会话内部,应该将该任务单独拿出来,自己形成一个和 bash 并行的独立会话!!!独立会话的后端进程叫做守护进程!!!
结论:
- 一次会话中,只允许一个前台进程组(任务),可以同时存在多个(0)后台进程组(任务)。
- 前后台,都可以向终端文件进行写入。但是只有前台进程,能够从标准输入(终端文件中)获取数据!后台进程是获取不了数据的!

将新起的任务放在了后台,而我们可以同时存在多个后台任务,依旧属于前台,所以就可以获取键盘上的任务,可以进行命令的执行

问题1:为什么一次会话中只允许一个前台任务??
因为前台任务需要和用户交互,需要获取用户的标准输入,(键盘)标准输入只有一个,前台进程必须只有一个!!!
区分前后台的标准:
进程组能从键盘这个标准输入流获取数据,能获取数据,就是前台的,不能获取数据就是后台的。
所以 ctrl +c 为什么只能终止前台进程??因为只有前台进程能够收到键盘的输入,所以只能终止前台进程。但是后台进程依旧能够向显示器打印,说明前后台进程,都没有彻底脱离终端!!!(想脱离将012可以关闭掉)
所以 :指令 sleep 1000执行后,成为前台进程,bash成为后台进程,再次输入pwd类似的命令后,是没有反应的。当ctrl + c 退出当前的前台进程后,bash进程此时就会变为前台进程,再次输入pwd、ls命令的时候就会有反应。
例子:
有时 windows 电脑会变得比较卡,在老式的电脑中还有注销的选项,点击注销这个选项也是可以解决卡顿的问题。

当登陆windows时,windows就会创建一个会话,这个会话只有一个前台进程组,就是图像化界面,在图形化界面永远都是单机双击,登录就是新建会话的过程,访问机器的时候,在这个会话中创建了很多作业 or 任务,后面变得卡顿的原因就是启动的任务过多或者是启动的任务失控了,但是依旧属于这个会话,所以可以采取注销,所谓的注销就是将这次的会话删掉,会话删掉就会将会话所启动的任务自动退出。有时,启动的任务会自动的变为独立的守护进程,脱离对应的会话,此时便只能重启,有些重启也没有用,因为被设置为开机启动,所以得将开机启动项关闭。

3. 守护进程
如何让我的进程(组),成为守护进程呢???首先的是一个独立的会话,如何成为一个独立的会话呢?? --------- 调用setsid
setsid:创建一个会话并且设置一个会话中的进程组ID

- 返回值:

- 成功调用 setsid 的要求:调用 setsid 的进程,创建新的会话的进程一定不能是进程组的组长。

此时的进程
,就是组长,因为这个组里面只有它一个。如何解决让自己不是组长呢??------ fork() > 0; exit(0) -> 让子进程执行后续的代码。组长就是之前的父进程,父进程退出,子进程不是组长,让子进程调用setsid就可以了调用成功了。
所以:守护进程本质就是一种孤儿进程!!!
/dev/null 属于字符类的设备文件:

/dev/null 的好处就是:/dev/null 跟垃圾桶一样,写入数据时自动被丢弃,允许写入,但是会被丢弃掉;同样也允许你读,读到的数据什么也没有。

cpp
// Deamon.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
void Deamon()
{
// 1.忽略可能导致进程退出的信号
signal(SIGCHLD, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
// 2. 让进程不要成为组长
if (fork() > 0)
exit(0);
// ???
// 3.将自己设计成为新的会话
setsid();
// 4. 如何看待标准输入,标准输出,标准错误
// a. 关闭 b. 标准输入、标准输出、标准错误 -> 重定向 -> /dev/null(最佳实践)
int fd = open("/dev/null",O_RDWR); // 以读写的方式打开
if(fd >= 0)
{
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
}
}

此时再运行,就不会被卡住了:


将服务终止 ctrl + c

系统中的守护进程:

将 xshell 关掉,换一台云服务器(我这里用的是Centos7的虚拟机)同样也是可以进行运行的。
在 Linux 系统中,当你通过 Xshell 连接到服务器并运行程序时,程序进程是 SSH 会话的子进程,它们之间存在进程树层级关系。当 Xshell 关闭时,SSH 会话终止,系统会向该会话中的所有子进程发送 SIGHUP 信号,默认行为是终止这些进程,因此普通运行的程序会随着 Xshell 关闭而停止。守护进程的核心目标就是摆脱这种对终端会话的依赖,而信号处理是实现这一目标的重要技术手段之一。
代码中的 signal(SIGCHLD, SIG_IGN) 用于忽略子进程终止信号,这告诉系统当子进程结束时不需要向父进程发送通知,由系统自动回收子进程资源,避免产生僵尸进程,这在守护进程 fork 子进程处理并发连接时尤为重要,确保系统资源不会被耗尽。而 signal(SIGPIPE, SIG_IGN) 则忽略管道破裂信号,在网络通信中,当客户端突然断开连接而服务器仍在尝试写入数据时,系统会发送 SIGPIPE 信号导致程序崩溃退出,忽略该信号后写操作会返回错误而非终止程序,大大增强了守护进程的健壮性,特别是在长时间运行且需要处理大量网络连接的服务程序中。
那么如何关掉守护进程呢??




查看一下守护进程的详细信息:


查看 fd:ls /proc/208121/fd -l

上图中的4的对应的就是套接字。

bash
# Makefile
.PHONY:all
all:Server Client
Client::Client.cc
g++ -o $@ $^ -std=c++17 -ljsoncpp
Server:Main.cc
g++ -o $@ $^ -std=c++17 -ljsoncpp
.PHONY:clean
clean:
rm -rf Server Client output
.PHONY:output
output:
mkdir -p output/bin
mkdir -p output/lib
mkdir -p output/conf
mkdir -p output/log
cp Server output/bin
cp Client output/bin
make output:

所以在Windows下载软件的时候,下载的不是源代码,而是别人给你打包好的软件,直接解压,install,等待点击 exe,正在安装中,实际上是在做拷贝!!实际上现在的很多软件,下载的就只有安装脚本。

cpp
// Daemon.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
void Deamon()
{
// 1.忽略可能导致进程退出的信号
signal(SIGCHLD, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
// 2. 让进程不要成为组长
if (fork() > 0)
exit(0);
// 3. 更改守护进程的工作路径,建议/,访问内置的配置文件的时候,用绝对路径来访问
chdir("/");
// 4.将自己设计成为新的会话
setsid();
// 5. 如何看待标准输入,标准输出,标准错误
// a. 关闭 b. 标准输入、标准输出、标准错误 -> 重定向 -> /dev/null(最佳实践)
int fd = open("/dev/null",O_RDWR); // 以读写的方式打开
if(fd >= 0)
{
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
}
}



log下面就是我们对应的日志:

此时对应的日志就会往系统中写入:


守护进程可以手动写,也可以不用自己写,系统中存在一个系统调用:daemon()

- nochdir:是否更改路径
- nochdir:是否关闭文件描述符
上面参数都为0,对应的就是我们写的如上的Daemon.hpp