Linux操作系统:进程间关系
摘要:本文介绍了Linux系统中的进程管理相关概念,包括前台/后台进程、进程组、会话、终端和作业控制。重点讲解了进程组是由多个相关联进程组成的集合,会话是进程组的更高层次组织单元,通常与终端绑定。文章还详细说明了终端如何与会话交互,以及如何通过作业控制命令管理后台任务。通过这些机制,系统可以更好地组织和管理进程,为后续实现守护进程提供理论基础。
前言
大家好,前几天我们已经基本完成的对于TCP套接字的内容的了解,并且以此让大家了解了序列化与反序列化的过程,实现了一个网络版的计算器。但其实我们的服务器仍然有些瑕疵。因为他现在仍然不配叫做一个服务端。我们想让我们的的客户端正常的运行,就必须保证运行服务端的Shell不能销毁,一旦Shell1销毁,他所执行的代码自然也就结束了。
大家平时遇见的服务端,比如微信的服务端,难道就是要保证24小时的终端打开吗?显然不是的,所以今天我们就为大家拓展介绍一下进程间的各种关系与守护进程的概念。
1、前台后台进程
我们之前已经初步接触到了前台与后台进程的概念,也曾提到过,我们在执行程序或命令时,默认都是在前台执行的
这个时候,如果我们想取消进程,可以直接输入ctrl c发送信号。
要想让其在后台执行,可以在后面加一个&表示让其在后台执行。
2、进程组
我们之前在学Linux系统部分的时候说过进程的概念,其实每一个进程除了有一个进程 ID(PID)之外,它还属于一个进程组。进程组是一个或者多个进程的集合, 用于管理一组相关联的进程,一个进程组可以包含多个进程。 每一个进程组也有一个唯一的进程组 ID(PGID), 并且这个 PGID 类似于进程 ID, 同样是一个正整数, 可以存放在 pid_t 数据类型中。
之前我们说过,每一个命令在Shell中其实都是转化为一个进程。那么如果我利用管道,连续执行三个sleep 10000命令,会发生什么?
我们这里返回了一行信息,这个的中括号中的1,我们后面会说到,这是作业号。而后面的数字1195338,是返回的最后一个指令(进程)的PID。
我们可以使用ps查看当前Shell中运行的进程:
可以看见,我们应该是先创建了1195386这个进程,随后依次创建另外两个sleep进程。
这里输出的信息有一列叫做:PGID,这个就是我们进程组的ID,一个进程组的ID等于它的组长的PID。而组长通常是第一个创建的进程,也就是我们的1195386这个进程。
值得注意的是, 即使组长进程终止,只要组内还有其他进程存在,进程组仍然存在,直到组内所有进程退出或加入其他进程组。组长退出,进程组的PGID也不会改变。
在Linux系统中,使用管道连接的命令通常会被分配到一个进程组里,这也就是我们三个sleep命令的关联之处。
3、会话
刚刚我们谈到了进程组的概念, 那么会话又是什么呢? 会话其实和进程组息息相关,会话可以看成是一个或多个进程组的集合, 它是 Unix/Linux 操作系统中的一个 进程管理单元,用于组织一组相关的 进程组(Process Groups)。一个会话可以包含多个进程组。每一个会话也有一个会话 ID(SID)
那么会话 ID 是什么呢? 我们可以先说一下会话首进程, 会话首进程是具有唯一进程 ID 的单个进程, 那么我们可以将会话首进程的进程 ID 当做是会话 ID。注意:会话 ID 在有些地方也被称为 会话首进程的进程组 ID, 因为会话首进程总是一个进程组的组长进程, 所以两者是等价的。
4、终端
说完会话,就不得不提一下终端的概念。终端(Terminal)是用户与计算机系统交互的入口,其核心功能是输入指令并显示输出结果。
我们的云服务使用的是伪终端的形式:
我们通过在终端中运行的Shell程序,实现了解析用户命令并调用操作系统内核执行的作用。
我们的一个会话通常会与一个终端进行绑定。用户通过 SSH 或终端模拟器登录 → 系统分配一个伪终端(如 /dev/pts/0
),随后启动 Shell(如 bash
)→ Shell 成为 会话首进程(Session Leader),并绑定该终端。
在我们的云服务器中,/dev/pts/目录下存储的就是我们的终端文件:
当我们新建几个终端后,可以看见终端文件又多了几个:
我们可以通过:echo "My terminal: $(tty); My session: $$"
来查看我们的当前终端的信息:
可以看见终端文件的名字与所绑定的会话号都已经打印了出来。
5、终端与会话
在 UNIX / Linux 系统中,用户通过终端登录系统后得到一个 Shell 进程,这个终端成为 Shell进程的控制终端。控制终端是保存在 PCB 中的信息,我们知道 fork 进程会复制 PCB中的信息,因此由 Shell 进程启动的其它进程的控制终端也是这个终端。
默认情况下没有重定向,每个进程的标准输入、标准输出和标准错误都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。另外会话、进程组以及控制终端还有一些其他的关系,我们在下边详细介绍一下:
- 一个会话可以有一个控制终端,通常会话首进程打开一个终端(终端设备或伪终端设备)后,该终端就成为该会话的控制终端。(这一点我们刚刚提到过),建立与控制终端连接的会话首进程被称为控制进程。
-
- 当 Bash 作为登录 Shell 启动时:它自己创建一个 新会话(
SID=Bash_PID
),随后成为该会话的 首进程(Session Leader) ,并将自己设为 前台进程组(PGID=Bash_PID
)
- 当 Bash 作为登录 Shell 启动时:它自己创建一个 新会话(
- 一个会话中的几个进程组可被分成一个前台进程组以及一个或者多个后台进程组。这个什么意思呢?就是说,我的前台进程组同时只能存在一个。在平时我们的前台程序组是bash所在的进程组,我们的标准输入是被定向到了这个前台进程组中。但是当我们执行了一个程序(以前台方式执行),我们的前台进程组就会替换为该程序,bash将把自己替换为后台进程组里。这也就是为什么,在执行sleep,以及其他程序时,我们无法通过输入输出流给bash发送命令的原因。同样的,当一个进程在后台执行时,我们不能直接ctrl c终止掉它,因为此时的输入流是在前台进程组中,我们发送的信号到达不了后台进程组。
- 如果终端接口检测到调制解调器(或网络)已经断开,则将挂断信号发送给控制进程(会话首进程)
6、作业控制
作业(job)是针对用户来讲,用户完成某项任务而启动的进程,一个作业既可以只包含一个进程,也可以包含多个进程,进程之间互相协作完成任务, 通常是一个进程管道。
Shell 分前后台来控制的不是进程而是作业或者进程组。一个前台作业可以由多个进程组成,一个后台作业也可以由多个进程组成,Shell 可以同时运⾏一个前台作业和任意多个后台作业,这称为作业控制。
我们之前说过,放在后台执⾏的程序或命令称为后台命令,可以在命令的后面加上&符号从而让Shell 识别这是一个后台命令,后台命令不用等待该命令执⾏完成,就可立即接收新的命令,另外后台进程执行完后会返回一个作业号以及一个进程号(PID):
作业号是 Shell(如 Bash)用来跟踪和管理 后台任务 或 挂起任务 的标识符。通过作业号,我们可以对执行当前作业的进程组进行管理,这是因为一个任务号与该进程组的PGID关联起来了。
+
和 -
符号是 Shell(如 Bash)在作业控制(Job Control)中用来标识作业优先级的标记,它们帮助用户快速识别和管理作业.
默认作业(+):是 Shell 中 最近被操作 或 最近被放入后台 的作业。当用户执行 fg
、bg
或 kill
等命令 不指定作业号 时,默认操作的就是这个作业。
候选默认作业(-):是 下一个可能成为默认作业 的作业。当当前默认作业(+
)终止或被移除后,-
作业会自动升级为 +
。
我们可以过以下命令来控制一个作业:
场景 | 命令组合 |
---|---|
启动后台作业 | cmd & |
挂起当前前台作业 | Ctrl+Z |
恢复挂起的作业到前台 | fg 或 fg %1 |
恢复挂起的作业到后台 | bg 或 bg %2 |
终止作业 | kill %1 |
查看所有作业(含 PID) | jobs -l |
让后台作业忽略 SIGHUP |
disown %1 |
以这组命令为例:
我们首先创建一个后台进程组,得到他的任务号为1。我们使用fg 命令,指定任务号为1的任务,使其变成一个前台进程组。此时打印出了我们的执行的命令:sleep 10000。
随后,我们可以使用 ctrl z使其被挂起,暂停,此时打印出的[1]+ Stopped sleep 10000
1表示任务号,+表示是默认任务,stopped表示任务状态,sleep 10000表示任务执行的命令。
我们可以新开一个终端,获取当前进行信息,发现其的状态为T。随后我们执行bg 1,将其又恢复到后台。
我们可以通过jobs命令令查看本用户当前后台执⾏或挂起的作业。
上面我们提到了键入 Ctrl + Z 可以将前台作业挂起,实际上是将 STGTSTP 信号发送至前台进程组作业中的所有进程, 后台进程组中的作业不受影响。 在 unix系统中, 存在 3 个特殊字符可以使得终端驱动程序产生信号, 并将信号发送至前台进程组作业, 它们分别是:
- Ctrl + C: 中断字符, 会产生 SIGINT 信号
- Ctrl + \: 退出字符, 会产生 SIGQUIT 信号
- Ctrl + Z:挂起字符, 会产生 STGTSTP 信号
终端的 I/O(即标准输入和标准输出)和终端产生的信号总是从前台进程组作业连接打破实际终端。
在我们的Windows系统中呢?
其实也是这样的。我们每一个用户都是一个会话,在电脑很卡时点击注销,回到登录页面,这个注销等于在Xshell中直接关闭链接,会话中的所有的进程都会被收到影响。
那么能不能创建一个子进程独立形成一个会话。使得这个进程不受用户登录注销的影响?
:守护进程就是这样的存在。
7、守护进程
守护进程是 Linux/Unix 系统中的一种长期运行的后台服务进程,独立于终端会话,通常在系统启动时自动运行,持续提供某种功能(如 Web 服务、日志管理等)。
不同于后台进程依旧属于当前会话,守护进程是属于一个独立的会话,他会脱离于当前终端,比如服务器,它的io是从网络中读取,日志写到磁盘文件里。
想要创建一个守护进程,有两种方式,第一种就是直接使用系统调用:
第二种方式就是手动的创建:
需要注意的是,我们想要创建的守护进程,不能是组长,也就是一个进程组里的第一个进程。
所以我们一般会先fork出一个进程,让父进程销毁,随后子进程变成一个孤儿进程,虽然与之前的父进程在同一个进程组,但是子进程不会是组长。
具体实现:
1、首先,守护进程一般会屏蔽特定的异常信号:
void Daemon()
{
//1、屏蔽特定异常信号
signal(SIGCHLD, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
}
其中,屏蔽第一个信号是为了防止守护进程因未处理的子进程退出而变成僵尸进程,屏蔽第二个信号是为了防止因向已关闭的管道或套接字写入数据导致进程意外终止。
第二步,保证为非组长,这里就要涉及之前说的使用子进程了: //2、设置不为组长 if(fork()>0) { exit(0);//使父进程退出,子进程变成孤儿,满足不是组长的条件 }
第三步,建立新会话,我们可以使用系统调用:
直接给当前执行函数的进程变为一个新的会话:setsid();
第四步,每一个进程都有自己的CWD,我们需要根据用户意愿,选择是否将当前进程的CWD更改成为 / 根目录,这是为了在使用文件时将其路径转化为了绝对路径,方便管理与使用。
//4、每一个进程都有自己的CWD,是否将当前进程的CWD更改成为 / 根目录 if(ischdir) { chdir(ROOT);//设置为根目录 }
第五步,此时该进程已经变成守护进程啦,不需要和用户的输入输出,错误进行关联了,但是我们要根据用户意愿选择是否将其取消关联。
如果不需要,就直接关闭0,1,2文件描述符。
如果需要输入输出,就把这些结果重定向到一个黑洞文件中:/dev/null
` //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);
}
}`
其中,/dev/null
是 Linux 系统中的空设备文件,其核心特性:
- 写入黑洞:所有写入它的数据会被直接丢弃(返回成功,但数据消失)。
- 读取空值:从它读取会立即返回 EOF(文件结束符)。
:
#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、设置不为组长
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);
}
}
}
当我们完成这个代码后,就可以把我们之前的网络版计算机的服务端,变成一个守护进程,这样,一旦我们启动服务端,就算把终端关闭,服务端进程也不会关闭,因为他已经在我们的服务器中运行起来了。
我们只需要添加该头文件,并在服务端main函数开始时调用Daemon函数,并把日志改成写入文件模式,就可以开启我们的守护进程模式的服务端:
运行代码:
可以看见,我们的服务端已经不在前台运行了,此时我们把终端关闭,不会影响它的运行。如果我们想关闭,就只能通过kill命令了:
更多推荐
所有评论(0)