当前位置: 首页 > news >正文

Linux操作系统:进程间关系

大家平时遇见的服务端,比如微信的服务端,难道就是要保证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:

可以看见终端文件的名字与所绑定的会话号都已经打印了出来。

5、终端与会话

在 UNIX / Linux 系统中,用户通过终端登录系统后得到一个 Shell 进程,这个终端成为 Shell进程的控制终端。控制终端是保存在 PCB 中的信息,我们知道 fork 进程会复制 PCB中的信息,因此由 Shell 进程启动的其它进程的控制终端也是这个终端。

默认情况下没有重定向,每个进程的标准输入、标准输出和标准错误都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。另外会话、进程组以及控制终端还有一些其他的关系,我们在下边详细介绍一下:

  • 一个会话可以有一个控制终端,通常会话首进程打开一个终端(终端设备或伪终端设备)后,该终端就成为该会话的控制终端。(这一点我们刚刚提到过),建立与控制终端连接的会话首进程被称为控制进程。
    • 当 Bash 作为登录 Shell 启动时:它自己创建一个新会话SID=Bash_PID),随后成为该会话的首进程(Session Leader),并将自己设为前台进程组PGID=Bash_PID
  • 一个会话中的几个进程组可被分成一个前台进程组以及一个或者多个后台进程组。这个什么意思呢?就是说,我的前台进程组同时只能存在一个。在平时我们的前台程序组是bash所在的进程组,我们的标准输入是被定向到了这个前台进程组中。但是当我们执行了一个程序(以前台方式执行),我们的前台进程组就会替换为该程序,bash将把自己替换为后台进程组里。这也就是为什么,在执行sleep,以及其他程序时,我们无法通过输入输出流给bash发送命令的原因。同样的,当一个进程在后台执行时,我们不能直接ctrl c终止掉它,因为此时的输入流是在前台进程组中,我们发送的信号到达不了后台进程组。
  • 如果终端接口检测到调制解调器(或网络)已经断开,则将挂断信号发送给控制进程(会话首进程)

在这里插入图片描述

6、作业控制

作业(job)是针对用户来讲,用户完成某项任务而启动的进程,一个作业既可以只包含一个进程,也可以包含多个进程,进程之间互相协作完成任务, 通常是一个进程管道。

Shell 分前后台来控制的不是进程而是作业或者进程组。一个前台作业可以由多个进程组成,一个后台作业也可以由多个进程组成,Shell 可以同时运⾏一个前台作业和任意多个后台作业,这称为作业控制。

我们之前说过,放在后台执⾏的程序或命令称为后台命令,可以在命令的后面加上&符号从而让Shell 识别这是一个后台命令,后台命令不用等待该命令执⾏完成,就可立即接收新的命令,另外后台进程执行完后会返回一个作业号以及一个进程号(PID):

在这里插入图片描述

作业号是 Shell(如 Bash)用来跟踪和管理后台任务挂起任务的标识符。通过作业号,我们可以对执行当前作业的进程组进行管理,这是因为一个任务号与该进程组的PGID关联起来了。


+-符号是 Shell(如 Bash)在作业控制(Job Control)中用来标识作业优先级的标记,它们帮助用户快速识别和管理作业.

默认作业(+):是 Shell 中最近被操作最近被放入后台的作业。当用户执行fgbgkill等命令不指定作业号时,默认操作的就是这个作业。

候选默认作业(-):是下一个可能成为默认作业的作业。当当前默认作业(+)终止或被移除后,-作业会自动升级为+


我们可以过以下命令来控制一个作业:

场景

命令组合

启动后台作业

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 100001表示任务号,+表示是默认任务,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、首先,守护进程一般会屏蔽特定的异常信号:

代码语言:javascript

AI代码解释

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、已经变成守护进程啦,不需要和用户的输入输出,错误进行关联了

代码语言:javascript

AI代码解释

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(文件结束符)。

:

代码语言:javascript

AI代码解释

#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命令了:


http://www.jsqmd.com/news/507459/

相关文章:

  • Qwen3-ForcedAligner-0.6B GPU部署实战:解决CUDA out of memory常见报错
  • 为什么是你来做?面试中犀利问题的底层逻辑是什么和标准回答模版
  • 便携式生理信号采集系统开发小结
  • C++map容器
  • GitHub_Trending/we/WeChatMsg架构解析:核心组件设计与交互逻辑
  • Qwen3-32B-Chat开源模型对比评测:Llama3-70B/Qwen3-32B/DeepSeek-V3推理效率PK
  • C++ stack 容器适配器-栈
  • FPGA动态部分重配置技术的三大实现方案对比
  • Rancher容器网络深度剖析:从基础概念到高级配置
  • 别再傻傻分不清了!从摄像头RAW到屏幕RGB,图像格式转换保姆级指南
  • 大小端的计算公式
  • Linux网络编程:TCP初体验
  • Qt 线程
  • CosyVoice 实战部署全攻略:从云端实例到本地服务,5步打造专属语音克隆应用
  • python中class与C++class的区别和联系
  • 终极指南:MS-DOS批处理变量使用与早期脚本参数传递技巧
  • 基频检测算法总结
  • Zig核心特性深度解析:为何它能替代C成为系统编程新宠
  • 如何轻松实现微信聊天记录从JSON到PDF的完整转换:GitHub_Trending/we/WeChatMsg终极指南
  • 深入解析Python的glob.glob()函数:高效递归匹配文件与目录的实战技巧
  • 海康威视DS-2CD2T2HY-LP1刷机固件包|含专用刷机工具+通用版固件|支持强刷救砖|终身可重复使用
  • Navicat Premium连接Oracle 11g保姆级教程(附instantclient配置避坑指南)
  • BackInTime 开源项目安装与使用指南
  • UR5机械臂实战:不依赖MoveIt的直接ROS控制方法(Python示例)
  • 100套前端可视化模板合集:支持HTML与Vue双架构,集成高德地图+百度ECharts图表
  • TF-IDF vs Word2Vec:如何根据你的项目需求选择合适的文本表示方法?
  • 探秘UI宝盒:18个顶级UI片段让你的前端开发效率提升300%
  • Discord 图片日志记录器使用教程
  • Dioxus国际化方案:构建多语言支持的全球应用
  • Postgres与Mybatis高效批量操作实战:从基础到高级冲突处理