💻 Computer Basics
1、计算机网络
1.1 传输层:TCP和UDP
1.1.1 三次握手
1.1.2 四次挥手
1.1.3 流量控制
1.1.4 拥塞控制
1.1.5 TCP和UDP的区别
1.1.6 TCP如何保证传输的可靠性
1.1.7 TCP长连接和短连接
1.1.8 应用层提高UDP协议可靠性的方法
1.1.9 UDP和IP的首部结构
1.2 应用层:HTTP和HTTPS
1.2.1 HTTP和HTTPS的区别
1.2.2 GET和POST的区别
1.2.3 Session与Cookie的区别
1.2.4 从输入网址到获得页面的过程(越详细越好)
1.2.5 HTTP请求有哪些常见的状态码
1.2.6 什么是RIP,算法是什么
1.2.7 HTTP1.1和HTTP2.0的主要区别
1.2.8 DNS
1.2.9 HTTPS加密和认证过程
1.2.10 常见网络攻击
1.2.11 REST
1.3 计算机网络体系结构
1.4 网络层协议
1.4.1 IP地址的分类
1.4.2 划分子网
1.4.3 什么是ARP协议
1.4.4 NAT协议
2、操作系统
2.1 进程和线程
2.1.1 进程和线程的区别
2.1.2 进程间通信方式
2.1.3 同步原语
2.1.4 进程状态
2.1.5 进程调度策略
2.1.6 僵尸进程和孤儿进程
2.1.7 协程
2.1.8 异常控制流
2.1.9 IO多路复用
2.1.10 用户态和内核态
2.2 死锁
2.3 内存管理
2.3.1 分段与分页
2.3.2 虚拟内存
2.3.3 页面置换算法
2.3.4 局部性原理
2.3.5 缓冲区溢出
2.4 磁盘调度
-
+
游客
注册
登录
进程间通信方式
## 基本概念 ### 通信模型 1. 通信模型指**进程间通信**(Inter-Process Communication, IPC)**中参与通信的进程以及他们的通信方式**(如后面介绍的单向/双向、同步/异步等),本文所涉及的简单的 IPC 只涉及两个进程,分别为**发送者进程**和**接收者进程**。 2. **通信的过程由发送者发起**,**发送者将一段定长的数据发送给接收者**,**之后发送者会等待接收来自接收者的返回数据**,**接收者在收到数据后**,**会根据自己的应用逻辑来处理数据**,**并将处理结果返回给发送者**,**整个通信的过程从发送者进程发起通信开始**,**到发送者进程接收到返回结果结束**。 3. 在不考虑进程间通信时,两个进程是被内核隔离开的,他们处在不同的地址空间内,看到的系统资源(如内存数据)都是互相隔离的,为了实现一个简单的 IPC,此处假设**内核已为两个进程映射了一段共享内存**,如下图所示,**在两个进程的地址空间中**,**分别有一段虚拟地址区间是映射到同一段物理内存的**,有了共享内存,两个进程相当于有了一个可以交流的**中间缓冲区**,**可以通过这个缓冲区进行通信**。 ![](https://notebook.ricear.com/media/202207/2022-07-08_155241_549261.png) ### 传递数据 1. 为了进程有效的协同,进程间通信往往伴随着数据的传递,一种常见的通信数据的抽象是**消息**。 2. **发送者进程通过发出一个或者多个消息**,**将数据传递给接收者进程**,**接收者进程以类似的方式将通信的结果等数据**,**通过消息返回给发送者进程**。 3. 消息一般包含一个**头部**和一个**数据段**,头部中通常包含如**魔数**、**消息长度**、**校验码**等信息,而数据段中既可以包含**纯数据**(如字符串),也可以包含**系统资源**(如文件描述符),简单的 IPC 方案中,消息的头部包含长度、状态等基本信息,而数据段仅包含纯数据,数据段的长度固定为 500 字节。 ![](https://notebook.ricear.com/media/202207/2022-07-08_155255_113936.png) ### 通信过程 1. 通信过程一般指**通信的进程间具体的通信发起**、**回复等过程**。 2. 简单的 IPC 设计中,假设共享内存刚好可以存放两个消息,一个消息归发送者来发起通信(称为发送者消息),一个消息归接收者来响应通信和返回结果(称为接收者消息)。 3. 具体的通信过程如下: 1. **开始时**,**两个消息的状态都是无效的**(见消息头部中的状态)。 2. **发送者会将要传输的数据内容拷贝到发送者消息上**,**然后依次设置头部中的状态**(设置为准备就绪)**等信息**, **发送者一发送完消息**,**就开始轮询接收者消息的状态信息**,**等待其变成准备就绪**。 3. **接收者会不停地轮询发送者消息的状态信息**,**一旦观测到状态信息为准备就绪**,**就表示发送者发出了一个消息**,**接收者在读取发送者的消息后**,**处理请求**,**并在接收者消息上准备返回结果**。 4. **当发送者观测到接收者消息的状态为准备就绪后**,**即表示收到了返回的结果**,**这就完成了简单 IPC 设计中的通信发起和回复的两个过程**。 4. 虽然简单 IPC 设计可以支持基本的通信,但是整个方案中有很多问题值得深入思考: 1. 通信带宽和时延表现如何,如何能够优化带宽和时延。 2. 消息数据段设置过长会带来内存浪费问题,过短则会导致频繁多次的拆分和通信,如何解决这个问题。 3. 对消息的轮询会导致 CPU 资源的大量浪费,这样的开销在通信不频繁的场景下格外突出,是否有更高效的通知方式。 对于上面提到的这些问题,后面会一一进行解答。 ## 需要考虑的问题 ### 数据传递 1. 从简单 IPC 方案中可以看出,进程间通信的一个重要功能是**在进程间传递数据**。 2. **消息传递**是 IPC 中常用的数据传递方式,其**将数据抽象成一个一个的消息进行传递**,不同的 IPC 设计有不同的消息抽象,如后面将介绍的消息队列中带类型的消息结构体抽象,消息传递往往需要一个**中间人**(如共享内存)。 3. 消息传递的方式主要有以下几种: 1. **基于共享内存的消息传递**: 1. 从上面提到的简单 IPC 方案中,我们已经看到共享内存是如何来帮助两个进程进行通信的。 2. 基于共享内存的消息传递的一个特点是**操作系统在通信过程中不干预数据传输**,从操作系统的角度来看,**共享内存为两个**(或多个)**进程在他们的虚拟地址空间中映射了同一段物理内存**,**这些进程基于这段共享内存设计通信的方案**,**操作系统通常不参与后续的通行过程**。 > 共享内存和基于共享内存的消息传递有什么区别? > > 1. 基于共享内存的消息传递是**以共享内存为媒介进行消息的传输**,**其核心的通信抽象仍然是消息**。 > 2. 共享内存的另一种使用方法是**直接在两个**(或多个)**进程间建立共享区域**,**然后在共享区域上建立数据结构**,**进程可以直接使用该共享区域上的数据**,**而不存在消息的抽象**。 > 3. 然而,直接使用共享内存上的数据结构存在不少问题: > 4. 由于共享内存的虚拟地址在不同进程的地址空间中可能是不同的,这会导致指针以及与指针相关的数据无法使用。 > 5. 该用法通常还假设共享内存的进程是互相信任的,而这与多进程的隔离性优势存在冲突。 > 以上这些问题限制了共享内存的使用场景。 2. **操作系统辅助的消息传递**: 1. 操作系统辅助的消息传递指**内核对用户态提供通信的接口**(如 `Send` 和 `Recv` 等),**进程可以直接使用这些接口**,**将消息传递给另一个进程**,**而不需要建立共享内存和轮询内存数据等操作**。 2. 当两个进程 $P$ 和 $Q$ 希望通过内核接口进行通信时,他们通常需要: 1. **通过特定内核接口建立一个通信连接**。 2. **通过 `Send`**、**`Recv` 接口进行消息传递**,**这里的建立通信连接的过程和通过内核建立共享内存区域是类似的**,**只是这里更多地强调在抽象意义上建立连接**,如内核可以维护一个数据结构来记录建立好连接的进程对。 > 共享内存和操作系统辅助传递的对比: > > 1. 从**数据传递的性能**来看: > 2. **共享内存可以实现理论上的零内存拷贝的传输**,这里的内存拷贝是指**将数据从内存中的一块区域拷贝到另一块区域**,**通常通过 CPU 访存指令来实现**。 > 3. **操作系统辅助传递方式下通常需要将数据先从发送者用户态内存拷贝到内核内存**,**再从内核内存拷贝到接收者用户态内存**,**这个过程包含两次拷贝**。 > 4. 操作系统辅助传递同样有优于共享内存的地方: > 5. **操作系统辅助传递的抽象更简单**,**内核可以保证每一次通信接口的调用都是一个消息被发送或接收**(或者出现异常错误),**并且能够较好地支持变长的消息**,**而共享内存则需要用户态软件封装来实现这一点**。 > 6. **操作系统辅助传递的安全性保证通常更强**,并且**不会破坏发送者和接收者进程的内存隔离性**。 > 7. **在多方通信时**,**在多个进程间共享内存区域是复杂且不安全的**,**而操作系统辅助传递可以避免此问题**。 ### 控制流转移 1. 对于操作系统来说,实现消息传递机制除了考虑数据的传输外,往往还会附带**控制流转移**的功能,**当一个通信发生时**,**内核将控制流从发送者进程切换到接收者进程**(返回的过程类似),前面介绍的共享内存方案中,如果不结合内核的消息传递或者其他通知机制,进程通常只能依赖于轮询内存数据来检查是否有消息到来,而这可能会浪费大量系统 CPU 计算资源。 2. **用户态进程其实是运行在操作系统抽象出来的时间片上的**,**并且进程可以有多种状态**,如运行状态、阻塞状态等,**IPC 中的控制流转移通常是利用内核对进程的运行状态和运行时间的控制来实现的**。 3. 一个常见的控制流转移过程如下图所示: ![](https://notebook.ricear.com/media/202207/2022-07-08_201548_291691.png) > 深色表示进程正在用户态运行,浅色表示进程处于阻塞中;a)进程 2 处于阻塞状态,等待新的请求到来;b)进程 1 发起 IPC 请求,陷入内核处理;c)内核将进程 2 唤醒去处理请求,而进程 1 处于阻塞状态等待执行结果。 1. 首先,**接收者进程完成初始化后将自己阻塞起来等待消息的到来**(如执行阻塞的 Recv)。 2. 之后,**发送者进程发起通信**,在处理该操作时,内核**首先将发送者发送的消息传递给接收者**,**然后让发送者进程进入阻塞状态**,**等待接收者进程的回复消息**,**并将接收者进程从阻塞状态中唤醒到预备状态**。 3. **对接收者而言**,**会看到阻塞的 Recv 返回了一个消息**,**表明接收到了来自发送者的一个消息**。 4. 上面的通信过程展示了控制流如何在进程间转移的,可以看到,**结合内核中的调度以及对进程**(或线程)**的调度状态的修改**,**控制流转移可以避免轮询操作**,**高效地将消息的到来和发出告知进程**。 ### 单向和双向 1. IPC 通常包含三种可能的方向,分别为**仅支持单向通信**、**仅支持双向通信**、**单向和双向通信均可**,三者的对比如下图所示: ![](https://notebook.ricear.com/media/202207/2022-07-08_203310_787911.png) 1. **单向通信通常指消息在一个连接上只能从一端发送到另一端**,**双向通信则允许双方互相发送消息**,**而单向和双向均可的方式则会根据通信中具体的配置选项等来判断是否需要支持单向或双向的通信**。 2. 通常来说,**单向通信其实是系统软件实现 IPC 的一个基本单元**,**双向通信是可以基于单向 IPC 来搭建的**。 3. 在接口上,**如果通信的两端在连接建立后**,**分别只能使用 `Send` 及 `Recv`**,**那么这通常对应于单向通信**,RPC 接口就是一个具有代表性的双向通信的例子,他要求接收者在处理好发送者发送的消息后返回一个消息,从而完成整个通信过程,即这个过程中会涉及两次单向通信。 4. 实际中,很多系统选择的是单向和双向均可的策略,这样可以比较好地支持各种场景,当然,如管道、信号等只支持单向通信的机制在实际中同样有较多的应用。 ### 同步和异步 1. 进程间通信的另一种分类是**同步 IPC**和**异步 IPC**。 2. 简单来看,**同步 IPC 指他的 IPC 操作**(如 `Send`)**会阻塞进程直到该操作完成**,而**异步 IPC 则通常是非阻塞的**,**进程只要发起一次操作即可返回**,而**不需要等待其完成**。 3. 同步 IPC 的 RPC 操作如下图所示: ![](https://notebook.ricear.com/media/202207/2022-07-08_205846_035291.png) 1. 同步 IPC 的 RPC 操作可以看成一个**线性的控制流**。 2. **调用者发起 RPC 请求**,**然后控制流切换到被调用者**,**被调用者处理请求时**,**调用者会处于阻塞的状态**。 3. **当被调用者执行完任务后**,**控制流会切回调用者**,**调用者拿到返回的结果后才可以继续执行**。 4. **从调用者的角度来看**,**当 RPC 返回后**,**请求消息的发送和结果的接收都已经完成了**。 4. 异步 IPC 的 RPC 操作如下图所示: ![](https://notebook.ricear.com/media/202207/2022-07-08_210429_271645.png) 1. 异步 IPC 是**多个并行的控制流**。 2. **当调用者发起 IPC 后**,**被调用者接收到通信的数据和请求后开始响应**,同时,**调用者的 IPC 调用不会等待被调用者的执行**,**而是直接返回**。 3. 异步 IPC 通常**通过轮询内存状态或注册回调函数**(如果内核支持)**来获取返回结果**。 5. **同步 IPC 往往是双向 IPC**,即**发送者需要等待返回结果**,不过**也存在单向 IPC 是同步的**,在这种场景下,虽然**发送者不会阻塞等待接收返回结果**,但是**发送者会阻塞等待接收者接收**,考虑一个具体的场景,假设操作系统支持多方通信,允许一个连接上有多个发送者,但不支持多个接收者,在这种场景下,一个可能的情况是,发送者发送消息的时候接收者正在接收和处理其他发送者的消息,在同步的设计下,此时该发送者需要进行一定的等待,而异步的设计则会通过如内核缓冲区等方式暂存消息,避免等待。 6. 在早期的微内核系统中,同步 IPC 往往是唯一的 IPC 方式,这是因为相比异步而言,同步 IPC 有着**更好的编程抽象**,如使用同步 IPC 时,**调用者可以将进程间通信看成一种函数调用**,**调用返回时也就意味着结果返回了**,**主要的不同是在 IPC 的场景下执行函数的可能是另外一个进程**。 7. 然而同步 IPC 在操作系统的发展中,逐渐表现出一些不足,一个典型的问题是**并发**,**当一个服务进程要响应很多客户进程的通信**,比如一个微内核中的用户态文件系统时,**在同步 IPC 的实现下服务进程为了性能往往需要创建大量工作线程去响应不同的客户进程**,**否则有可能出现阻塞客户请求的情况**,然而,这往往带来一个权衡,即**过少的工作线程会导致大量客户进程被阻塞**,**过多的工作线程会浪费系统资源**,**而使用异步 IPC 则可以在并发通信时避免这类问题**,当然,在后续的一些设计中,就通过**线程池**的模型克服了同步 IPC 下的并发挑战,总的来看,目前大部分操作系统内核都会选择**同时实现同步和异步 IPC**,**以满足不同的应用需求**。 ### 超时机制 1. 进程间的隔离性为通信带来的一个问题是**通信的双方很难确认对方的状态**: 1. 对于**同步 IPC**来说,当控制流从调用者切换到被调用者后,如果**被调用者恶意地不返回到调用者**,那么就会使得**调用者无法继续执行**。 2. 对于**异步 IPC**来说,即使控制流不会被调用者恶意地抢占,调用者仍然有可能**花费过长的时间来等待一个请求的处理**。 实际上,即使调用者并不是恶意的,也有可能因为调度、被调用者过于忙碌(需要处理大量其他请求)或者某些错误导致一些请求被丢失,从而出现上述的问题。 2. 为了解决这个问题,IPC 的设计中引入了**超时机制**: 1. 超时机制**扩展了 IPC 通信双方的接口**,**允许发送者/接收者指定他们发送/接收请求的等待时间**,例如,一个应用程序可以花费 5 秒等待文件系统进程的 IPC 请求的处理操作,如果超过 5 秒仍然没有反馈,则由操作系统内核结束这次 IPC 调用,返回一个超时的错误。 2. 超时机制**允许进程为一次通信的等待时间设置一个上限**,从而**避免类似于拒绝服务的供给情况出现**。 3. 然而在实际情况中,**大部分进程很难决定一个合理的超时**,例如前文 5 秒超时的例子,当进程间传输的数据量十分大,或者文件系统本身没有能够获得足够的时间片来处理请求时,其可能就无法及时完成任务,**定义过短的超时可能会导致调用者频繁地重试某一个 IPC 调用**,而**定义过长的超时则可能无法及时察觉到被调用者的异常**,因此,目前内核常常引入两个特殊的超时选择,分别为**永不返回**和**立即返回**: 1. **永不返回其实和引入超时之前的机制是类似的**,而**立即返回则是说只有当前被调用者处于可以立即响应的状态才会真的发起通信**,**否则就直接返回**。 2. 发送者进程会根据需求进行选择,**更加注重安全性的往往选择立即返回**,而**更加注重功能性的则倾向于选择永不返回**。 ### 通信连接管理 1. 通信连接的建立在前文中已被多次提到: 1. 对于**基于共享内存的进程间通信方案**,**通信连接的建立通常是在建立共享区域的一瞬间完成的**。 2. 对于**涉及内核的控制流转移的通信**而言,**通信连接管理是内核 IPC 模块的很重要的一部分**。 2. 通信连接管理和前文介绍的 IPC 的各种概念是息息相关的: 1. 连接如果能够**用于多于 2 个的进程**,就意味着该 IPC 设计是支持**多方通信**的。 2. 连接能否**缓冲数据**(消息)在一定程度上决定着 IPC 设计是**异步的还是同步**的。 3. 连接本身的**单向和双向**则直接关联着 IPC 的**方向性**。 3. 实际的系统中建立连接的方式有两种,分别为**直接通信**和**间接通信**: 1. **直接通信**: 1. 直接通信是指**通信的进程一方需要显式地标识另一方**,以 `Send` 和 `Recv` 为例,需要将他们完善成发送消息 `Send(P, message)`,给进程 $P$ 发送一个消息;接收消息 `Recv(Q, message)`,从进程 $Q$ 处接受一个消息。 2. 直接通信下**连接的建立是自动的**,**在具体交互时通过标识的名称完成**,这就意味着**一个连接会唯一地对应一对进程**,而**一对进程之间也只会存在一个连接**,**连接本身可以是单向的**,**又可以是双向的**(更常见)。 2. **间接通信**: 1. 间接通信**需要经过一个中间的信箱来完成通信**。 2. **每个信箱有自己唯一的标识符**,而**进程间通过共享一个信箱来完成交换信息**,也就是说,**进程间连接的建立发生在共享一个信箱时**,而**每对进程可以通过共享多个信箱的方式来建立多个连接**,**连接同样可以使单向的或双向的**。 > 在后面介绍中我们将会看到两类方式都有系统在使用: > > 1. 管道就属于间接通信的方式,通信的进程双方并不知道对方是哪一个特定的进程,他们只知道双方共享这一管道,管道本身肩负着类似信箱的任务。 > 2. 而信号则不同,信号的发送方需要显式地指定接收信号的进程号(或者进程组),这属于直接通信的方式。 ## 典型的进程间通信方式 典型的进程间通信方式主要有**管道**、**System V 中的消息队列**、**信号量**、**共享内存**、**Linux 信号机制**,以及**套接字机制**(socket)。 ### 管道 #### 含义 1. 管道是**两个进程间的一条通道**,**一端负责投递**,**另一端负责接收**: 1. 一个简单的例子是,我们经常会通过 `ps aux | grep target` 来查看当前是否有关键字 `targegt` 相关的进程在运行。 2. 这里其实是两个命令,通过 Shell 的管道符号 `|`,将第一个命令的输出投递到一个管道中,而管道对应的出口是第二个命令的输入。 3. 通过管道这种方式,`ps` 和 `grep` 这两个命令对应的进程进行了一次协同合作。 2. 管道是**单向的 IPC**,**内核中通常有一定的缓冲区来缓冲消息**,而**通信的数据**(消息抽象)**是字节流**,**需要应用自己去对数据进行解析**。 3. 一个管道**有且只能有两端**,而**这两端一定是一个负责输入**(发送数据),**一个负责输出**(接收数据)。 ![](https://notebook.ricear.com/media/202207/2022-07-10_155825_3535980.06066911973948963.png) 4. 管道在 UNIX 系列的系统中会被**当做一个文件**,**内核会为用户态提供代表管道的文件描述符**,**让其可以通过文件相关的系统调用来使用**,管道的特殊之处在于**他的创建会返回一组**(两个)**文件描述符**,然后**使用内存作为数据的一个缓冲区**,这是因为**管道的本质是为了通信**,一方面**对可持久化没有要求**,另一方面**还需要保证数据传输的高性能**。 5. **管道的行为和 FIFO 队列非常像**,**最早传入的数据会被最先读出来**,**当一个进程输入数据后**,**另一个进程可以通过管道读到数据**。 > 如果还没有数据写入,输出端的进程就开始尝试读数据会发生什么情况? > > 1. 第一种情况是如果系统发现**当前没有任何进程有这个管道的写端口**,则会看到**EOF**(End-of-File),之所以存在这种情况(即没有任何进程有该管道的写端口)是因为**管道的两个端口在 UNIX 系统的内核中是以两个独立的文件描述符存在的**,**写端口有可能被进程给主动关闭了**。 > 2. 另外一种情况是**输出端的进程会阻塞在这个系统调用上**,**直到数据到来**,针对这种情况,进程可以**通过配置非阻塞选项来避免阻塞**。 #### 分类 1. 在经典的 UNIX 实现中,管道通常有两类,分别为**匿名管道**和**命名管道**,主要区别于他们的**创建方式**: 1. **匿名管道**: 1. 匿名管道**是通过 `pipe` 的系统调用创建的**,**在创建的同时进程会拿到读写的端口**(两个文件描述符),由于整个管道没有全局的名字,因此只能通过这两个文件描述符来使用他。 2. 在这种情况下,通常**结合 `fork` 的使用**,即**用继承的方式来建立父子进程间的连接**: 1. **父进程首先通过 `pipe` 创建好对应的管道的两端**,**然后通过 `fork` 创建出子进程**,由于**子进程是可以继承文件描述符的**,因此**父子进程相当于通过 `fork` 的继承完成了一次 IPC 权限的分发**,然后**父子进程就可以通过管道来进行进程间的通信**。 2. 需要注意的是: 1. **在完成继承后**,其实**父子进程都会同时拥有管道的两端**,此时**需要父子进程主动地关闭多余的端口**,**否则可能会导致通信出错**。 2. 这种方式**对于父子进程等有着创建关系的进程间通信比较方便**,但是**对于两个关系较远的进程就不太适用**。 3. 优缺点: 1. 优点: 1. **简单方便**。 2. 缺点: 1. **只能在具有亲缘关系的进程之间通信**。 2. **命名管道**: 1. 命名管道**是由另一个命令 `mkfifo` 来创建的**,**在创建的过程中会指定一个全局的文件名**,**由这个文件名**(如/tmp/namedpipe)**来指代一个具体的管道**(即管道名)。 2. 通过这种方式,**只要两个进程通过一个相同的管道名进行创建**(并且都拥有对其的访问权限),**就可以实现在任意两个进程间建立管道的通信连接**。 3. 优缺点: 1. 优点: 1. **可以实现任意关系的进程间通信**。 2. 缺点: 1. **长期存在于文件系统中**,**使用不当容易出错**。 ### System V 消息队列 #### 为什么需要消息队列 1. 相比于其他的进程间通信机制,消息队列是**唯一一个以消息**(内核提供的)**为数据抽象的通信方式**,**内核应用可以通过消息队列来发送消息以及接收消息**,**发送和接收的接口是内核提供的**。 2. 消息队列是**一种非常灵活的通信机制**,他**支持同时存在多个发送者和多个接收者**,并且**Linux 为消息队列中的每个消息提供了类型的抽象**,**使得消息的发送者和接收者可以根据类型来选择性地处理消息**。 #### 消息队列的结构 ![](https://notebook.ricear.com/media/202207/2022-07-10_171149_2331500.21442941835166995.png) 1. 消息队列**在内核中的表示是队列的数据结构**。 2. 当**创建新的消息队列**时,**内核将从系统内存中分配一个队列数据结构**,**作为消息队列的内核对象**,这个对象中有**权限**以及**消息头部指针**,**队列的消息由这个头部指针引出**,**每个消息都会有指向下一个消息的指针**(或者为空),这是一个常见的队列的链表设计。 3. 在消息的结构体中,除了下一个指针之外,就是**消息的内容**,消息内容包含两部分,分别为**类型**和**数据**: 1. 数据是**一段内存数据**,**和管道中的字节流相似**。 2. 类型是**用户态程序为每个消息指定的**,在消息队列的设计中,**内核不需要知道类型的语义**,**仅仅只是保存**,**以及基于类型进行简单的查找**,**类型的具体意义需要用户态程序自己来管理**。 > 上图中第一个消息的类型是 1,这可能代表消息中的数据是一个字符串,也可能是一个结构体。 #### Linux 内核实现 1. 首先,**一旦一个队列被创建**,**除非内核重新启动或者该队列被主动删除**,**否则其数据都是会被保留的**。 2. 其次,**消息队列的内存空间是有限制的**,系统管理员通常**可以配置单个消息的最大空间**、**单个消息队列的最大空间**,**以及全系统的消息队列个数等信息**,通常**建议使用共享内存机制来传递长消息**,**而非使用消息队列**。 3. 最后,**消息在用户态和内核态之间传递时**,**会有拷贝的开销**: 1. **发送消息时**,**内核会通过 `copy_from_user` 来将数据从用户态搬移到内核空间**。 2. **接收消息时**,**内核会通过 `copy_to_user` 将数据搬移回用户态**。 #### 优缺点 ##### 优点 1. **可以实现任意进程间的通信**。 2. **通过系统调用函数来实现消息发送和接收之间的同步**,**无需考虑同步问题**。 ##### 缺点 1. **信息的复制需要额外消耗 CPU 的时间**,**不适宜信息量大或操作频繁的场合**。 > 消息队列和管道的对比: > > 1. **匿名管道是跟随进程的,消息队列是跟随内核的**,也就是说,**进程结束之后,匿名管道就死了**,**但是消息队列还会存在,除非显示调用函数销毁**。 > 2. **管道是文件,存放在磁盘上**,访问速度慢,**消息队列是数据结构,存放在内存**,访问速度快。 > 3. **管道是流式读取,消息队列是数据块式读取**。 ### System V 信号量 #### 为什么需要信号量 1. 和消息队列这样明确的传递消息的方案不同,信号量在实际的使用中**主要用作进程间的同步**。 2. 有些场景下,**多个进程需要依赖于进程间通信来同步彼此的状态**,如**执行的顺序**等,此时,管道、消息队列这些**能够传递数据但是不提供强制同步机制**的方案是不太满足要求的,信号量能够很好地满足此类场景的需求。 3. 信号量本身**传递的数据量很少**,一般来说**仅有一个共享的整型计数器**,该计数器通常**由内核维护**,而**对信号量的操作则需要经过内核系统调用**。 #### 含义 1. 信号量是用来 **辅助控制多个线程访问有限数量的共享资源** 的。 2. 信号量主要有三个操作,分别为初始化操作、 $P$ 操作和 $V$ 操作: 1. 信号量的初始值应当设置为 **共享资源的初始数量**。 2. $P$ 缩写自荷兰语 Probeer(尝试),表示**尝试一个操作**(在信号量中通常是**将一个计数器减 1**),**该操作的失败会将当前进程切换到阻塞的状态**,**直到其他进程执行了 $V$ 操作**,一般 **尝试消耗共享资源的线程应当调用 $P$ 操作来等待资源就绪**。 3. $V$ 缩写自荷兰语 Verhoog(增加),在信号量中是**将一个计数器加 1**,**该操作可能会唤醒一个因 $P$ 操作而陷入阻塞的进程**,一般 **产生或释放共享资源的线程应当调用 $V$ 操作来通知资源就绪**。 3. 信号量的一个简单设计是**限制器计数器值**,**使其只在 0 和 1 这两个数字之间变化**: 1. 当执行 $P$ 操作时,会试图将计数器减 1,如果这个操作会**将计数器减为负数**,那么就**阻塞该进程**,**直到减 1 操作能够顺利完成**。 2. 当执行 $V$ 操作时,会将计数器加 1,但是**最终的计数不能超过 1**(如果超过 1 就忽略该操作)。 > 需要注意的是 $p$**和**$V$**操作都是原子的**。 4. 该设计足够支持简单的进程间同步的需求,比如,对于两个进程 $A$ 和 $B$,希望能够在 $A$ 执行完相关代码后,$B$ 再执行: 1. 此时,$A$ 和 $B$ 可以共享一个信号量,使其初始值为 0。 2. $A$ 进程会在执行完代码后,执行一个对共享信号量的 $V$ 操作,而 $B$ 进程会在执行代码前,执行一个对共享信号量的 $P$ 操作。 3. 假设内核先调度了 $B$,使其 $P$ 操作最先发生,由于此时 $P$ 操作会导致信号量的结果为-1,而这是不被允许的,因此内核将阻塞 $B$ 进程。 4. 当 $A$ 进程执行完自己的代码后,执行 $V$ 操作,此时会将信号量的值更新为 1,同时,内核会发现此时 $B$ 的 $P$ 操作已经可以成功了,因此内核会唤醒 $B$,并执行 $B$ 的操作。 5. 通过以上方式,可以保证 $A$ 和 $B$ 的执行顺序。 #### 优缺点 ##### 优点 1. **可以同步进程**。 ##### 缺点 1. **信号量有限**。 ### System V 共享内存 #### 为什么需要共享内存 1. 对于此前介绍的机制,包括消息队列、信号量、管道等,内核都提供了完整的包括缓冲数据、接收消息、发送消息等一系列进程间通信的接口。 2. 虽然这些完善的抽象方便了用户进程的使用,但其中涉及的**数据拷贝**和**控制流转移**等处理逻辑**影响了这些抽象的性能**。 3. 共享内存的思路其实是**内核为需要通信的进程建立共享区域**,**一旦共享区域完成建立**,**内核就基本上不需要参与进程间通信**,通信的多方**既可以直接使用共享区域上的数据**,**也可以将共享区域当成消息缓冲区**。 #### 含义 1. 共享内存的核心思路是**允许一个或多个进程在其所在的虚拟地址空间中映射相同的物理内存页**,**从而进行通信**,这里以 Linux 中的设计为例,来介绍一些细节的设计和实现: ![](https://notebook.ricear.com/media/202207/2022-07-10_214216_0982650.15025278536568065.png) 1. **内核会为全局所有的共享内存维护一个全局的队列结构**,即上图中的**共享内存队列**,**这个队列的每一项**(`shmid_kernel` 结构体)**是和一个 IPC `key` 绑定的**,**各进程可以通过这样一个 `key` 来吵到并使用同一段共享物理内存区域**。 2. 虽然这样的 `key` 是全局唯一的,但是**能否使用这段共享内存**,**是通过 System V 的权限检查机制来判断的**,**只要进程有对应的权限**,**就能够通过内核接口**(`shm_at`)**将一段共享内存的区域映射到自己的虚拟地址空间中**。 3. **当两个进程**(进程 1 和进程 2)**分别对同一个共享内存建立了映射**(`shm_at`)**之后**,**内核会为他们分配两个 VMA**(Virtual Memory Area)**结构体**,**让他们都指向 `file`**,**这里的 VMA 会描述进程的一段虚拟地址空间的映射**。 4. **有了这两个 VMA 的建立**,**内核就能够从一个用户进程的虚拟地址找到对应的 VMA**,**从而知道这是一个共享内存的区间**。 5. **当进程不再希望共享内存时**,**可以取消共享内存和虚拟内存之间的映射**(`shm_dt` 接口),**这里取消映射的操作**(`detach` 操作),**只会影响当前进程的映射**,**其他仍在使用共享内存的进程是不受影响的**。 #### 优缺点 ##### 优点 1. 因为所有进程共享同一块内存,因此**共享内存在各种进程间通信方式中具有最高的效率**。 2. 访问共享内存区域和访问进程独有的内存区域一样快,并**不需要通过系统调用或者其他需要切入内核的过程来完成**。 3. 同时,共享内存**避免了对数据的各种不必要的复制**。 ##### 缺点 1. 因为**系统内核没有对访问共享内存进行同步**,因此我们**必须提供自己的同步措施**。例如,**在数据被写入之前不允许进程从共享内存中读取信息**,**不允许两个进程同时向同一个共享内存地址写入数据等**。解决这些问题的常用方法是通过**使用信号量进行同步**。 ### 信号 #### 为什么需要信号 1. 管道、消息队列、共享内存等方式主要关注在**数据传输设计**上,而信号的一个特点是其**单向的事件通知能力**。 2. **信号量也有通知能力**,**但需要进程主动去查询计数器状态或陷入阻塞状态来等待通知**,**使用信号**,**一个进程可以随时发送一个事件到特定的进程**、**线程或进程组等**,**并且接收事件的进程不需要阻塞等待该事件**,**内核会帮助其切户到对应的处理函数中响应信号事件**,**并且在处理完成后恢复之前的上下文**。 #### 含义 1. 信号是一种非常特殊的进程间通信的机制,他**传递的信息很短**,**只有一个编号**(信号编号)。 2. 信号在 Linux 系统中的使用非常广泛,一个简单的例子是用 `CTRL + C` 在 `Shell` 中终止一个执行中的程序,其背后的逻辑是 `Shell` 发出了一个 `SIGINT` 信号,从而导致默认信号处理函数结束了对应的进程。 3. 在通信的场景下,**一个进程会为一些特定的信号编号注册处理函数**,**当进程接收到对应的信号时**,**内核会自动地将该用户的控制流切换到对应的处理函数中**。 4. Linux 早期使用的信号有 31 个,后续 POSIX 标准又引入了编号从 32 到 64 的其他信号,Linux 传统信号被称为**常规信号**,而 POSIX 引入的信号被称为**实时信号**,主要用于**实时场景**,**一个进程如果多次收到某个常规信号事件**,**内核只会记录一次**,而**实时信号的多个相同信号事件通常不能丢弃**。 #### 实现 ##### 信号的发送 1. **信号的发送者可以是其他的用户态进程**,**也可以是内核**,**一个用户态进程可以通过内核提供的系统调用接口给一个进程或线程**(包括自己)**发送特定的信号事件**。 2. **内核会为每个进程和线程准备一个信号事件等待队列**,**一个进程内的多个线程共享该进程的信号事件等待队列**,**并拥有自己私有的等待队列**。 3. **内核通过不同的系统调用及其参数**,**来确定接收信号的目标进程或线程**,**将信号事件添加到其等待队列上**,**添加操作需要区别处理前面介绍的实时信号和常规信号**,**即当要发送的信号是非实时的信号**,**并且现在还未处理该信号时**,**内核会直接忽略过这个操作**。 ##### 信号的阻塞 1. Linux 提供了一个专门的系统调用 `sigprocmask`,来**允许用户程序设置对特定信号的阻塞状态**: 1. **当一个信号被阻塞后**,**Linux 将不会再触发这个信号对应的处理函数**,**直到该信号被解除阻塞状态**。 2. **信号阻塞并不阻止信号被添加到等待队列上**,**而当进程解除某个信号的阻塞状态时**,**其可能需要处理在阻塞期间收到的信号**。 2. 需要注意的是,**除了进程间通信外**,**信号还可用于进程管理**,**因此很多重要的信号是不能被阻塞的**,如 `SIGKILL`。 ##### 信号的响应和处理 1. **信号得到处理的时机通常是内核执行完异常**、**中断**、**系统调用等返回到用户态的时刻**,**此时内核会检查一个状态位来判断是否有信号需要处理**,**如果有**,**则先去处理该信号事件**,**这个状态位是在发送信号时设置的**。 2. 内核对信号的处理一般有下面三种方式: 1. **忽略**:**直接忽略对应的信号**。 2. **用户处理函数**:**调用用户注册的信号处理函数**。 3. **内核默认处理函数**:**调用默认的内核处理函数**(如果用户没有注册处理函数)。 > 内核的默认处理函数大多数情况下就是杀死进程或者直接忽略信号,而从通信角度来看,**只有用户注册了处理函数的信号才能够被用来通信**。 3. **用户注册的信号处理函数为用户态的代码**,**当内核在处理对应的信号时**,**需要返回到用户态去执行处理函数**,**内核需要有一套机制**,**能够在进入用户态执行处理函数后**,**恢复到之前的上下文**,具体实例如下: 1. **首先**,**一个用户态进程调用系统调用**,**进入了内核**,**内核在处理完系统调用后**,**发现有信号需要处理**,**于是切换到用户态处理函数位置**,**让其处理信号事件**。 2. **信号处理完成后**,**信号处理函数会通过系统调用 `sigreturn` 返回到内核**,**这个系统调用的主要作用就是辅助内核恢复到被信号打断之前的上下文**,**他不会返回到信号处理函数中**,**而是直接恢复到之前的用户态**(即下图中的步骤 4)。 3. 整个过程中,栈的处理是较为复杂的,这是因为,**Linux 内核一旦完成了从内核到用户态的切换**(即下图中的步骤 2),**这一次执行系统调用的相关栈状态和上下文就都被清空了**,这就意味着,**如果不进行特殊的处理**,**即使信号处理函数调用 `sigreturn` 进入内核**,**内核也不知道该如何恢复到此前的用户态上下文**。 4. 为了解决这个问题,**在跳转到信号处理函数前**,**Linux 会将系统调用的返回值和此前的用户上下文信息**(比如代码指针)**等保存在用户栈上**,**这样当内核接收到** `sigreturn`**的系统调用时**,**就会从用户栈上提取出这些上下文**,**然后恢复到之前的位置**。 ![](https://notebook.ricear.com/media/202207/2022-07-11_164933_7566950.9871454876368002.png) ### 套接字 #### 为什么需要套接字 1. 套接字是**一种既可用于本地**,**又可跨网络使用的通信机制**,**应用程序可以用相同的套接字接口来实现本地进程间通信和跨机器的网络通信**,因此,一些实际系统,如机器人操作系统(ROS),会大量地使用套接字作为进程间通信方案。 2. 在套接字进程通信下,**客户端进程通过一个特定的地址来找到要调用的服务端进程**,**这里的地址是进程运行时绑定的**,**套接字支持多种不同的地址类型**,例如**基于 IP 地址和端口组合的地址**或者**本地文件系统中的一个路径**。 3. 套接字进程间通信**可以使用不同的协议对通信进行控制**,如 TCP 和 UDP,通常而言,**TCP 的可靠性更好**,**该协议会负责数据重传**、**数据顺序维护等**,而**UDP 更为简单**,**在大多数场景下能够达到更好的传输性能**。 #### 含义 1. **Socket 是在应用层和传输层之间的一个抽象层**,他把 TCP/IP 层复杂的操作**抽象**为**几个简单的接口**,**供应用层调用实现进程在网络中的通信**。 ![](https://notebook.ricear.com/media/202105/2021-05-07_154807.png) #### 优缺点 ##### 优点 1. **传输数据为字节级**,传输数据**可自定义**,**数据量小,效率高**。 2. 传输数据**时间短**,**性能高**。 3. 适用于**客户端和服务器端之间信息实时交互**。 4. 可以**加密**,数据**安全性强**。 ## 参考文献 1. 《现代操作系统:原理与实现》 2. [进程线程面试题总结](https://zhuanlan.zhihu.com/p/135395279)。 3. [记一次阿里面试题:都有哪些进程间通信方式?麻烦你不要再背了](https://blog.csdn.net/hollis_chuang/article/details/107776832)。 4. [进程间通信方式总结](https://juejin.cn/post/6844903911556382728)。 5. [匿名管道和命名管道](https://blog.csdn.net/qq_33951180/article/details/68959819)。 6. [进程间通信的方式(三):消息队列](https://zhuanlan.zhihu.com/p/37891272)。 7. [进程间的通信方式(一):共享内存](https://zhuanlan.zhihu.com/p/37808566)。 8. [一文读懂 Socket 通信原理](https://zhuanlan.zhihu.com/p/109826876)。
ricear
2022年7月17日 20:23
©
BY-NC-ND(4.0)
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
分享
链接
类型
密码
更新密码