【Linux网络#6】:进程间关系 与 守护进程

🔥个人专栏:Linux—登神长阶
⛺️ 欢迎关注:👍点赞 👂🏽留言 😍收藏 💞 💞 💞
进程间关系与守护进程目录
2.1 什么是作业(job)和作业控制(Job Control)?
1. 前言
还记得我们之前学的前台和后台任务嘛,如下:

我们一般把不需要交互的任务放在 后台,我们之前也说了,如下:

我们可以发现在前台任务执行时,输入其他指令也不会产生别的影响,而在后台任务中,我们输入的每个指令都会有相对应的输出,因此我们可以知道:
- 前台进程:用户直接与之交互的任务或程序。用户可以通过输入、点击等方式与这些任务进行实时的交互。通常会占用用户的注意力
- 后台进程:不需要用户直接交互,且可以在用户进行其他操作时继续运行的任务。用户不需要关注它们的进程
2. 进程间关系 -- 作业控制
2.1 什么是作业(job)和作业控制(Job Control)?
🧀 作业是针对用户来讲,用户完成某项任务而启动的进程
- 一个作业既可以只包含一个进程,也可以包含多个进程,进程之间互相协作完成任务, 通常是一个进程管道(之前上面有演示的)
Shell 分前后台来控制的不是进程,而是 作业 或者 进程组
- 一个前台作业可以由多个进程组成,一个后台作业也可以由多个进程组成
- Shell 可以同时运行一个前台作业和任意多个后台作业,这称为作业控制。
2.2 作业号
🔥 放在后台执行的程序或命令称为后台命令,可以在命令的后面加上&符号从而让 Shell 识别这是一个后台命令,后台命令不用等待该命令执行完成,就可立即接收新的命令
- 此外:后台进程执行完后会返回一个作业号以及一个进程号(PID)

第一行表示作业号和进程 ID, 可以看到作业号是 1, 进程 ID 是 3251046
2.3 作业状态
| 作业状态 | 含义 |
| 正在运行【Running】 | 后台作业(&),表示正在执行 |
| 完成【Done】 | 作业已完成,返回的状态码为 0 |
| 完成并退出【Done(code)】 | 作业已完成并退出,返回的状态码为 0 |
| 已停止【Stopped】 | 前台作业,当前被 Ctrl + Z 挂起 |
| 已终止【Terminated】 | 作业被终止 |
2.4 作业切回和挂起
作业切回
🐇 如果想将挂起的作业切回,可以通过 fg 命令,fg 后面可以跟作业号或作业的命令名称。如果参数缺省则会默认将作业号为 1 的作业切到前台来执行,若当前系统只有一个作业在后台进行,则可以直接使用 fg 命令不带参数直接切回。
具体的参数参考如下:

如下操作:

注意: 当通过 fg 命令切回作业时,若没有指定作业参数,此时会将默认作业切到前台执行,即带有 “+” 的作业号的作业
作业挂起
🐇 我们在执行某个作业时,可以通过 Ctrl+Z 键将该作业挂起,然后 Shell 会显示相关的作业号、状态以及所执行的命令信息。
例如我们把刚刚切回起来的作业挂起到后台:

- Ctrl + z 就相当于暂停前台,因为我们不能让一个暂停的任务来拥有终端,因此这个进程自动会被放到后台

- bg 1:让其在后台运行起来
2.5 查看后台执行或挂起的作业
我们可以直接通过输入 jobs 命令,查看本用户当前后台执行或挂起的作业
- 参数 -l 则显示作业的详细信息
- 参数 -p 则只显示作业的 PID

关于默认作业:对于一个用户来说,只能有一个默认作业(+),同时也只能有一个即将成为默认作业的作业(-),当默认作业退出后,该作业会成为默认作业。
- +: 表示该作业号是默认作业
- - : 表示该作业即将成为默认作业
- 无符号: 表示其他作业
2.6 作业控制的信号
上面提到了键入 Ctrl + Z 可以将前台作业挂起,实际上是将 STGTSTP 信号发送至前台进程组作业中的所有进程, 后台进程组中的作业不受影响。 在 unix 系统中, 存在 3 个特殊字符可以使得终端驱动程序产生信号, 并将信号发送至前台进程组作业, 它们分别是:
- Ctrl + C: 中断字符, 会产生 SIGINT 信号
- Ctrl + \: 退出字符, 会产生 SIGQUIT 信号
- Ctrl + Z:挂起字符, 会产生 STGTSTP 信号

🔥 结论:
- 任何时刻,只允许有一个前台进程,多个或者 0 个 后台进程,因为键盘(标准输入) 只有1 个
- 命令行启动任何进程,bash 会自动变成后台,直到前台进程接收
3. 进程组
💢 对于上面学的,我们基于管道(|)把所有的任务级联起来

- 这些进程级联起来想完成一个任务,我们就把这些进程 叫作 一个 进程组
- 他们的进程组的 PGID 都是一样的,即属于一个组,说明进程除了有兄弟、父子关系之外,还有组内关系
- 也就是说多个进程组如果通过管道级联或者通过某种方式让几个进程协作起来,那么这些进程它们的 PGID 所对应的值其实是多个进程当中创建的第一个进程 pid,以第一个进程作为自己的老大,即作为进程组 ID,这个老大其实也就是组长进程(下面我们会说的)
- 那 这个 PGID 是什么呢》其实就是我们进程 PCB 中包含的一个字段
补充:通常我们都是使用管道将几个进程编成一个进程组,如上面
下面我们来做个详细了解
3.1 进程组概念
💦 上面我们说到其实每一个进程除了有一个进程 ID(PID)之外 还属于一个进程组。
- 进程组是一个或者多个进程的集合, 一个进程组可以包含多个进程。
- 每一个进程组也有一个唯一的进程组 ID(PGID), 并且这个 PGID 类似于进程 ID, 同样是一个正整数, 可以存放在 pid_t 数据类型中。
- 进程组 是为了完成一个任务的 或 作业的
$ ps -eo pid,pgid,ppid,comm | grep test #结果如下 PID PGID PPID COMMAND 2830 2830 2259 test # -e 选项表示 every 的意思, 表示输出每一个进程信息 # -o 选项以逗号操作符(,)作为定界符, 可以指定要输出的列3.2 组长进程
每一个进程组都有一个组长进程。 组长进程的 ID 等于其进程 ID。我们可以通过 ps 命令看到组长进程的现象:
$ ps ajx | head -1 && ps ajx | grep sleep PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 3185939 3288744 3185939 3185939 ? -1 S 1001 0:00 sleep 180 3099626 3289414 3289414 3099626 pts/0 3289414 S+ 1001 0:00 sleep 10000 3099626 3289415 3289414 3099626 pts/0 3289414 S+ 1001 0:00 sleep 20000 3099626 3289416 3289414 3099626 pts/0 3289414 S+ 1001 0:00 sleep 30000 3203921 3289620 3289619 3203921 pts/1 3289619 S+ 1001 0:00 grep --color=auto sleep 从结果上看 sleep 10000 进程的 PID 和 PGID 相同, 那也就是说明 sleep 10000 进程是该进程组的组长进程, 该进程组包括 sleep 10000 、sleep 20000、sleep 30000 三个进程
- 进程组组长的作用: 进程组组长可以创建一个进程组或者创建该组中的进程
- 进程组的生命周期: 从进程组创建开始到其中最后一个进程离开为止。
注意:主要某个进程组中有一个进程存在, 则该进程组就存在, 这与其组长进程是否已经终止无关。
补充一句,在 Linux 中我们一般使用 ps 命令查看进程的,如下:
$ ps -o pid,ppid,pgid,sid,comm # 输出 PID PPID PGID SID COMMAND 3203921 3203920 3203921 3203921 bash 3292594 3203921 3292594 3203921 ps4. 会话
4.1 什么是会话
刚刚我们谈到了进程组的概念, 那么会话又是什么呢?
会话其实和进程组息息相关,会话可以看成是一个或多个进程组的集合, 一个会话可以包含多个进程组。每一个会话也有一个会话 ID(SID)

- 比如:上面中进程组中的 SID 都相同,并且与父进程 Id 一样,这个 SID 究竟是什么呢?
- 在操作系统中,SID(Session ID) 是会话标识符,用于标识一个会话(Session)
- 会话是一组进程的集合,通常与用户登录会话相关联。每个会话都有一个唯一的 SID,用于管理该会话中的进程
上边我们提到了会话 ID, 那么会话 ID 是什么呢? 我们可以先说一下会话首进程, 会话首进程是具有唯一进程 ID 的单个进程, 那么我们可以将会话首进程的进程 ID 当做是会话 ID。
- 注意:会话 ID 在有些地方也被称为 会话首进程的进程组 ID, 因为会话首进程总是一个进程组的组长进程, 所以两者是等价的
4.2 如何创建会话
可以调用 setsid 函数来创建一个会话, 前提是调用进程不能是一个进程组的组长。
#include <unistd.h> /* *功能:创建会话 *返回值:创建成功返回 SID, 失败返回-1 */ pid_t setsid(void);该接口调用之后会发生:
- 调用进程会变成新会话的会话首进程。 此时, 新会话中只有唯一的一个进程
- 调用进程会变成进程组组长。 新进程组 ID 就是当前调用进程 ID
- 该进程没有控制终端。 如果在调用 setsid 之前该进程存在控制终端, 则调用之后会切断联系
需要注意的是: 这个接口如果调用进程原来是进程组组长, 则会报错, 为了避免这种情况, 我们通常的使用方法是:先调用 fork 创建子进程, 父进程终止, 子进程继续执行, 因为子进程会继承父进程的进程组 ID, 而进程 ID 则是新分配的, 就不会出现错误的情况
5. 控制终端
先说下什么是控制终端?
🔥 在 UNIX 系统中,用户通过终端登录系统后得到一个 Shell 进程,这个终端成为 Shell 进程的控制终端。
- 控制终端是保存在 PCB 中的信息,我们知道 fork 进程会复制 PCB中的信息,因此由 Shell 进程启动的其它进程的控制终端也是这个终端。
- 默认情况下没有重定向,每个进程的标准输入、标准输出和标准错误都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。
另外会话、进程组以及控制终端还有一些其他的关系,我们在下边详细介绍一下:
- 一个会话可以有一个控制终端,通常会话首进程打开一个终端(终端设备或伪终端设备)后,该终端就成为该会话的控制终端。
- 建立与控制终端连接的会话首进程被称为 控制进程
- 一个会话中的几个进程组可被分成一个前台进程组以及一个或者多个后台进程组。
- 如果一个会话有一个控制终端,则它有一个前台进程组,会话中的其他进程组则为后台进程组。
- 无论何时进入终端的中断键(ctrl+c)或退出键(ctrl+\),就会将中断信号发送给前台进程组的所有进程
- 如果终端接口检测到调制解调器(或网络)已经断开,则将挂断信号发送给控制进程(会话首进程)
这些特性的关系如下图所示:

6. 守护进程
6.1 基本概念
如果我们想要一个不受会话影响的进程,该怎么做呢?
- 由 bash 创建一个 子进程 或者 进程组,如果进程组中只有一个进程,那么就是单进程的进程组
- 然后如果不想受登录退出的影响,那么就要把它独立形成一个新的会话,那么此进程与bash 的关系 从 包含 -> 并列关系,此后就不受退出登录的影响,此时就是一个独立的会话
- 守护进程 就是这样的,
守护进程也属于后台进程的一种,但是两者有个本质区别
- 后台进程仍然属于这个会话,而守护进程属于自己独立的会话
守护进程(Daemon Process) 是一种在后台运行的特殊进程,通常用于执行系统任务或服务,而不依赖于用户交互。理解守护进程的关键在于它的两个核心特性:
- 守护进程要脱离终端,以确保它不会受到终端关闭或用户注销的影响终端依赖问题:
- 普通进程可能会依赖于终端进行输入输出(例如,从终端读取输入或向终端打印输出)。如果终端关闭,这些进程可能会被终止或无法正常运行。
- 守护进程的目标:
守护进程通常用于提供系统服务(如网络服务、日志服务等),这些服务需要在后台长期运行,而不受用户登录或终端关闭的影响。
- 守护进程其实就是一个孤儿进程,它脱离了父进程和终端,由系统的 init 进程(PID 1) 接管
- 由于守护进程没有父进程,它不会受到用户注销或终端关闭的影响,可以在后台长期运行
6.2 如何将服务守护进程化
这里我们以我们之前写的 网络版计算器 为例
Daemon.hpp
#pragma once #include <iostream> #include <cstdlib> #include <signal.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> #define ROOT "/" #define devnull "/dev/null" void Daemon(bool ischdir, bool isclose) { // 1. 守护进程一般要屏蔽到特定的异常信号 signal(SIGCHLD, SIG_IGN); signal(SIGPIPE, SIG_IGN); // 2. 成为非组长, fork之后父进程直接退出, 子进程代替父进程向后运行 if (fork() > 0) exit(0); // 3. 建立新会话 setsid(); // 4. 每一个进程都有自己的CWD,是否将当前进程的CWD更改成为 / 根目录 if (ischdir) chdir(ROOT); // 5. 已经变成守护进程啦,不需要和用户的输入输出,错误进行关联了 if (isclose) { ::close(0); ::close(1); ::close(2); } else { int fd = ::open(devnull, O_WRONLY); if (fd > 0) { // 各种重定向 dup2(fd, 0); dup2(fd, 1); dup2(fd, 2); close(fd); } } }TcpServer.cc 修改如下:

结果如下:

- 在上面,我们可以看到 其 PGID = SID,表示其独立的进程了,终端为问号,TPGID = -1,表示其已经与终端去关联了
现在我们就可以把终端关掉了,然后该进程就在我们的云服务器上 24 小时运行了,此时我们也可以把它上传到网上进行使用的,来随时访问了

- 使用 killall 可执行文件 删掉守护进程
注意一下:这里当我们直接发送动态链接的可执行文件会出现阶层库的问题
- 静态链接是将库的代码直接嵌入到最终的可执行文件中。程序在编译时会将所有依赖的库代码复制到可执行文件中
- 动态链接是在程序运行时加载所需的库。可执行文件只包含对库的引用,而不包含库的实际代码
所以如果有问题的话,我们最好还是发送其静态链接最好,加个 -static,编译的时候
补充 -- 系统提供的 daemon

7. 共勉
【*★,°*:.☆( ̄▽ ̄)/$:*.°★* 】那么本篇到此就结束啦,如果有不懂 和 发现问题的小伙伴可以在评论区说出来哦,同时我还会继续更新关于【Linux】的内容,请持续关注我 !!
