当前位置: 首页 > 科技观察

终于有人把进程间通信解释清楚了

时间:2023-03-20 21:38:45 科技观察

多进程协同主要有以下三个优势。模块化功能以避免重新发明轮子。增强模块间的隔离性,提供更强的安全性。提高应用程序的容错能力。进程间通信(IPC)是多进程协作的基础。一般来说,IPC至少需要两方(比如两个进程)参与。根据信息流的方向,这两方通常被称为发送方和接收方。在实际使用中,IPC往往用于服务调用,所以参与IPC的双方也称为调用者和被调用者,或者客户端和服务端。图7-1是一个简单的IPC设计。假设内核为两个进程映射了一块共享内存,共享内存只能存放两条消息(发送消息和接收消息)。01进程间通信的重要功能1.数据传递消息传递(messagepassing)是IPC中常用的一种数据传递方式,它将数据抽象成一个个消息进行传递。不同的IPC设计有不同的消息抽象,消息传递往往需要“中间人”(如共享内存)。2.控制流转移当通信发生时,内核将控制流从发送进程切换到接收进程(返回过程类似)。IPC中的控制流传递通常是通过内核控制进程的运行状态和运行时间来实现的。02进程间通信的分类1.单向IPC、双向IPC、单向/双向IPC单向IPC通常是指消息只能在一个连接上从一端发送到另一端,而双向IPC允许双方相互发送消息。单向/双向IPC会根据通信中的具体配置选项判断是否需要支持单向或双向通信。实践中很多系统选择单向/双向IPC,可以更好的支持各种场景。当然,管道、信号等只支持单向IPC的机制在实际中也有很多应用。2、同步IPC和异步IPC简单的说,同步IPC就是它的IPC操作(比如Send)会阻塞进程,直到操作完成;而异步IPC通常是非阻塞的,进程只要发起一个操作就可以返回,不需要等待它完成。与异步相比,同步IPC具有更好的编程抽象。但是,在操作系统的发展过程中,同步IPC也逐渐显露出了一些不足。一个典型的问题是并发。一般来说,目前大多数操作系统内核都选择同时实现同步和异步IPC来满足不同的应用需求。03进程间通信相关机制1.超时机制超时机制扩展了IPC通信双方的接口,允许发送方/接收方指定其发送/接收请求的等待时间。例如,应用程序可能会花费5秒等待文件系统进程处理IPC请求。如果超过5秒仍然没有反馈,操作系统内核结束本次IPC调用并返回超时错误。2.通信连接管理对于基于共享内存的进程间通信方案,通信连接的建立通常在建立共享区的瞬间完成;对于涉及内核控制流传递的通信,通信连接管理是内核IPC模块中非常重要的一个部分。虽然在实际系统中有多种实现方式,但大多数可以归为两类——直接通信和间接通信。直接通信是指通信过程中的一方需要明确识别另一方。间接通信需要经过一个中间邮箱才能完成通信。每个邮箱都有自己唯一的标识符,进程之间通过共享一个邮箱来交换消息。3.权限检查进程间通信通常依赖于一套权限检查机制来保证连接的安全。例如,seL4等微内核系统中的Capability机制将所有通信连接抽象为单个内核对象。每个进程对内核对象的访问权限(以及可以对内核对象进行的操作)用Capability来表征。当一个进程试图与其他进程通信时,内核将检查该进程是否具有Capability,是否有足够的权限访问连接对象以及该对象是否指向目标进程。同样,一个宏内核,比如Linux系统,通常会复用其有效用户/有效组的文件权限来描述进程对某个连接的权限。4.命名服务命名服务就像一个全局的看板,可以协调服务器进程和客户端进程之间的信息。简单来说,服务器进程可以告诉命名服务进程它提供的服务。例如,文件系统进程可以注册一个“文件系统服务”,网络系统进程可以注册一个“网络服务”。客户端进程可以到命名服务中查询当前服务,并选择自己要建立连接的服务尝试获取权限。具体地,是否给相应的客户端进程分配权限是由命名服务和相应的服务器进程根据特定的策略来决定的。04宏内核进程间通信宏内核下典型的进程间通信机制包括管道、消息队列、信号量、SystemV中的共享内存、Linux信号机制、套接字机制(socket)。宏内核操作系统中的进程间通信更多的是应用程序之间的交互。因此,设计的重点通常放在界面的易用性和稳定性上。图7-5显示了典型宏内核进程间通信机制的比较。可以看出,虽然几种方案在IPC的几个设计方面有相同点和不同点,但它们之间的主要区别在于数据抽象。在实际应用中,虽然有多种IPC解决方案可以作为通信选择,但应用往往会根据数据抽象的需求来选择具体的解决方案。05微内核进程间通信由于进程间通信对微内核系统性能的重要性,大多数微内核操作系统都会从性能的角度优先设计和实现进程间通信。1.Mach:早期微内核进程间通信设计Mach通过两个基本抽象——端口和消息,设计并实现了一个间接通信IPC:通信的双方不需要显式指定另一个,而是通过端口进行通信(对应到“邮箱”)。通过端口在进程之间流动的数据就是消息。作为早期的微内核系统,Mach系统的性能与当时的宏内核系统(如UNIX)相比还有很大差距。其中一个原因是为了实现大量的目标,如可裁剪性、可移植性等,Mach具有复杂的内核和大量的代码。但是,Mach的IPC设计仍然对后来的很多系统产生了非常重大的影响。Mach中端口和消息的设计将进程间通信与特定进程隔离开来。一个进程只要拥有某个端口,就可以通过这个端口与“另一端”的进程通信。后续的微内核系统设计,无论是借鉴其设计,还是以其缺陷为戒,大多都考虑了Mach的思想。2.L4:围绕进程间通信优化设计的微内核系统基于Mach的经验,Liedtke等研究人员开始开发L4系列微内核系统。L4系列微内核系统的一个突出思想是:进程间通信是微内核的核心功能,需要围绕通信来完成整个系统的设计和实现。L4仍然是一个非常主流的微内核系统,尤其是在衍生出各种变体和相关系统之后。在L4微内核中,内核只保留基本功能,包括地址空间、线程、进程间通信等,并没有考虑兼容性等需求,而是选择针对特定硬件进行性能优化。这样做的好处是内核中的代码量非常少,可以尽可能支持少量的功能。3.LRPC:Migrationthreadmodel迁移线程(threadmigration)是一种比较“极端”的性能优化IPC设计。到目前为止,我们了解到IPC性能优化的大部分工作会集中在两个部分:优化控制流切换性能和优化数据传输性能。迁移线程认为,其他的IPC设计可以看成是把需要处理的数据发送给另一个进程,让其处理。这就是为什么控制流切换和数据传输会成为主要瓶颈的原因。如果我们换个角度,将另一个进程的数据处理代码拉到当前进程中,是否可以避免控制流切换(仍在当前进程中处理)和数据传输(数据已经在当前进程中准备好)?迁移线程是围绕这个新视角设计的。迁移线程方案在LRPC、Mach(优化版)等系统中都有使用,是目前纯软件进程间通信优化中最好的设计之一。迁移线程的基本原则是:简化控制流切换,让客户端线程执行“服务端代码”;简化数据传输,共享参数栈和寄存器;简化接口,减少序列化开销;优化并发性并避免共享全局数据结构。其中,前两个原则是基于“本地拉代码”的新视角。图7-13为迁移线程IPC与主流IPC设计的对比。要实现“本地拉代码”,迁移线程首先需要对线程结构进行解耦,明确线程的哪些部分在通信请求的处理中起到了关键作用。然后,这部分让被调用者(负责处理请求的逻辑)运行在调用者的上下文中,将跨进程调用变成更接近函数调用的形式。如果使用迁移线程模型,内核在进程间通信时不会阻塞调用者线程,而是让调用者线程执行被调用者的代码。整个过程并没有被被调用线程唤醒,相反,被调用方更像是一个“代码提供者”。另外,内核并不进行完整的上下文切换,只是切换地址空间(页表)和其他与请求处理相关的系统状态。其中,不会涉及线程和优先级的切换,也不会调用调度器。迁移线程的好处是减少了内核调度的时间,简化了内核中的IPC处理。在多核场景下,迁移线程方案也可以避免跨核通信带来的开销。06案例分析:AndroidBinder在Android场景下,进程间通信在大多数情况下其实就是“远程过程调用”。服务器进程负责提供具体的服务,客户端进程通过进程间通信发起服务请求,并获取服务器进程处理后的结果。BinderIPC中除了通信进程外,还引入了一个ContextManager进程。ContextManager提供命名服务,其任务是建立通信连接。在BinderIPC的核心设计中,提供了句柄(handle)的抽象来表示IPC对象(即通信连接)。句柄其实和我们熟悉的文件描述符非常相似。用户通过句柄的操作发起与特定进程的通信。与以往的进程间通信设计不同的是,BinderIPC采用了“线程池”服务器模型。也就是说在服务端,Binder的用户态和内核都会有一个响应线程池的概念。当客户端进程发起通信时,内核从(服务器端)线程池中选择一个可用的线程进行响应。这种设计可以在同步进程间通信的情况下更好的处理并发通信请求。