目录
进程组
进程组概念
之前我们提到了进程的概念, 其实每⼀个进程除了有⼀个进程ID(PID)之外 还属于⼀个进程组。进程组是⼀个或者多个进程的集合, ⼀个进程组可以包含多个进程。 每⼀个进程组也有⼀个唯⼀的进程组ID(PGID), 并且这个PGID类似于进程ID, 同样是⼀个正整数, 可以存放在pid_t数据类型中。

组长进程
每⼀个进程组都有⼀个组⻓进程。 组⻓进程的ID等于其进程ID。我们可以通过ps命令看到组⻓进程的现象:

从结果上看ps进程的PID和PGID相同, 那也就是说明ps进程是该进程组的组⻓进程, 该进程组包括ps和cat两个进程。
• 进程组组⻓ 的作⽤: 进程组组⻓可以创建⼀个进程组 或者创建该组中的进程
•进程组 的**⽣命周期** : 从进程组创建开始到其中最后⼀个进程离开为⽌。注意: 只要某个进程组中有⼀个进程存在, 则该进程组就存在, 这与其组⻓进程是否已经终⽌⽆关。
会话
会话概念
刚刚我们谈到了进程组的概念, 那么会话⼜是什么呢? 会话其实和进程组息息相关, 会话 可以看成是 ⼀个或多个进程组的集合 , ⼀个会话可以包含多个进程组。每⼀个会话也有⼀个会话ID(SID)


通常我们都是使⽤管道将⼏个进程编成⼀个进程组。 如上图的进程组2和进程组3可能是由下列命令形成的:

demo:


从上述结果来看3个进程对应的PGID相同, 即属于同⼀个进程组。
前台进程和后台进程
一次会话中,只允许一个前台进程组(因为标准输入只有一个),可以同时存在多个后台进程组。
前后台进程组,都可以向终端文件进行写入,但只有前台进程组能够从标准输入(终端文件)中获取数据,前台进程负责和用户交互,获取用户的标准输入。
我们在xshell中输入命令后,命令行执行的命令变成前台进程,bash进程自动变成后台进程,那么bash就无法再从标准输入中获取数据了。所以这也可以解释,为什么执行sleep命令后,在sleep期间输入的其他命令都无效了,因为bash无法从标准输入流中获取这些数据,自然就无法解析了。
同时也解释了为什么ctrl+c只能终止前台进程,因为只有前台进程才能获取键盘的输入。

会话ID
上边我们提到了会话ID, 那么会话ID是什么呢? 我们可以先说⼀下会话⾸进程, 会话⾸进程是具有唯⼀进程ID的单个进程, 那么我们可以将会话⾸进程的进程ID当做是会话ID。注意:会话ID在有些地⽅也被称为 会话⾸进程的进程组ID ,因为会话⾸进程总是⼀个进程组的组⻓进程, 所以两者是等价的。
会话首进程,就是创建会话的进程,通常是登陆shell
控制终端
先说⼀下什么是控制终端?与会话关联的终端设备。
在UNIX系统中,**⽤⼾通过终端登录系统后得到⼀个Shell进程,这个终端成为Shell进程的控制终端。控制终端是保存在PCB中的信息,我们知道fork进程会复制PCB中的信息,因此由Shell进程启动的其****它进程的控制终端也是这个终端。**默认情况下没有重定向,每个进程的标准输⼊、标准输出和标准错误都指向控制终端,进程从标准输⼊读也就是读⽤⼾的键盘输⼊,进程往标准输出或标准错误输出写也就是输出到显⽰器上。另外会话、进程组以及控制终端还有⼀些其他的关系,我们在下边详细介绍⼀下:
◦ ⼀个会话可以有⼀个控制终端,通常会话⾸进程打开⼀个终端(终端设备或伪终端设备)后,该终端就成为该会话的控制终端。
◦ 建⽴与控制终端连接的会话⾸进程被称为控制进程。
◦ ⼀个会话中的⼏个进程组可被分成⼀个前台进程组 以及⼀个或者多个后台进程组。
◦ 如果⼀个会话有⼀个控制终端,则它有⼀个前台进程组,会话中的其他进程组则为后台进程组。
◦ ⽆论何时进⼊终端的中断键(ctrl+c)或退出键(ctrl+\),就会将中断信号发送给前台进程组的所有进程。
◦ 如果终端接⼝检测到调制解调器(或⽹络)已经断开,则将挂断信号发送给控制进程(会话⾸进程)。
这些特性的关系如下图所⽰:

守护进程
守护进程概念
守护进程是Linux/Unix系统中一种特殊的后台服务进程 ,它独立于控制终端并且周期性地执行某种任务或等待处理某些事件的发生。守护进程通常在系统启动时运行,在系统关闭时终止。

守护进程这么强,必须将我们的服务端改造成守护进程,这样服务端就可以"24小时挂机不掉线"了
即使不小心关闭了SSH窗口,或者电脑重启,或者误关终端,服务器也照常进行,就像微信后台服务器那样,你退出后仍然可以收到消息弹窗。
如何将一个进程变成守护进程?
首先要想成为守护进程,进程得先从原本的会话独立出来,成为一个独立会话,为此我们需要setsid函数
setsid函数

作用:创建一个全新的会话,且该会话sid,进程组id,都等于进程id。
返回值:成功返回进程id,否则返回-1错误信息被设置。
注意:创建新会话的进程,不能是当前 会话进程组的 组长进程。

否则就会有两个会话的sid都相同,这显然是矛盾的。
创建守护进程
**首先,**为了保证创建会话的进程不是组长进程,我们可以使用fork
fork>0 exit(0) ->子进程执行后续代码。所以,守护进程的本质就是一种孤儿进程。
**此外,**守护进程因为脱离终端,标准输入/输出/错误(0/1/2)失去实际意义,若不处理会导致:
写入崩溃 :若代码误向stdout/stderr写日志(如printf),会触发SIGPIPE(管道破裂)导致进程意外终止
读取阻塞 :若误从stdin读取(如scanf),会永久阻塞等待不存在的输入
资源泄漏:继承的无效文件描述符占用系统资源(每个进程默认有1024个描述符上限)
为此,我们可以将标准文件描述符重定向到/dev/null(最佳实践)
而不是直接写关闭标准文件描述符,某些库函数或第三方代码可能隐式使用标准I/O(如printf、perror),若描述符已关闭,调用这些函数会触发EBADF错误(错误码9),导致程序异常终止。
描述符指向/dev/null后,写入操作会静默丢弃数据(返回成功),读取操作立即返回EOF,程序行为更可控。


同时,我们前面提及,守护进程是一种特殊的后台服务进程 ,它独立于控制终端 并且周期性地执行某种任务或等待处理某些事件的发生。它通常在系统启动时运行,在系统关闭时终止。为了不影响其他文件操作,比如我把守护进程放在了/tmp路径下,我现在需要卸载掉tmp,因为守护进程一直在运行,我tmp就删不掉。**所以我们还要把守护进程放在根目录下,**这样守护进程就不会影响任何文件夹,系统的维护可以随时做,守护进程就像开了上帝模式一样,一直在平稳的运行而不干扰系统操作。
这一点使用chdir("/");就可以轻松做到。
Daemon.hpp
cpp
#pragma once
#include<iostream>
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>
#include<sys/stat.h>
#include<fcntl.h>
void Daemon()
{
//1.守护进程需要屏蔽可能会导致进程退出的信号
signal(SIGCHLD,SIG_IGN);
signal(SIGPIPE,SIG_IGN);
//2.避免组长进程
if(fork()>0)
exit(0);
//3.更改守护进程的工作路径,建议/
chdir("/");
//4.子承父业,子进程将自己设计成为新的会话
setsid();
//5.重定向标准文件描述符->/dev/null (最佳实践)
int fd=open("/dev/null",O_RDWR);
if(fd>=0)
{
dup2(fd,0);
dup2(fd,1);
dup2(fd,2);
}
}
然后在服务器开头调用一下我们自己写的Daemon函数就行

daemon函数
其实守护进程的代码是不需要手搓的,在unistd.h头文件中包含了daemon函数
两个参数都为0的时候就和我们手搓的代码一样。


此篇完,感谢你看到这里。