当前位置: 首页 > 后端技术 > Java

进程间通信详解

时间:2023-04-01 21:46:03 Java

进程间通信方法介绍共享内存信号量信号量工作原理了解信号量管道匿名管道命名管道消息队列什么是消息队列?FeaturesSignals关于SignalsFeaturesSockets结语我的仓库已经收录文章:Java学习笔记和免费书籍分享进程间通信方式介绍在操作系统中,进程可以理解为一个关于计算机资源集合的运行活动,它是正在执行的程序的实例。从概念上讲,一个进程有自己的虚拟CPU和虚拟地址空间,任何一个进程都是相互独立的,这也引入了一个问题——如何让进程之间进行通信?由于进程相互独立,没有直接通信的手段,所以需要借助操作系统来辅助。举个通俗的例子,如果A和B是独立的,不能互相沟通,如果要沟通,可以依赖第三方C,比如A把信息交给C,C把信息交给C信息给B——是进程间通信的主要思想——共享资源。这里要解决的一个重要问题是如何避免竞争,即防止多个进程同时访问临界区的资源。共享内存共享内存是进程间通信最简单的方法之一。共享内存允许两个或多个进程访问同一内存。当一个进程改变这个地址的内容时,其他进程会注意到这个改变。你可能会想,我直接创建一个文件,然后进程就可以访问了?是的,但是这种方法有几个缺陷:访问文件需要陷入系统调用,从用户态切换到内核态,然后执行内核指令。这样做效率很低,而且不在用户手中。直接访问磁盘非常慢,比访问内存慢数百倍。从某种意义上说,这是共享磁盘而不是共享内存。在Linux下,使用共享内存的方式让进程完成对共享资源的访问。它将磁盘文件复制到内存中,并创建一个从虚拟地址到内存的映射,就好像资源已经在进程空间中一样。之后,我们就可以像局部变量一样对它们进行操作,实际写入磁盘的操作由系统自行选择最佳方式完成。例如,操作系统可以批处理和排序,从而大大提高IO速度。如上图所示,进程将共享内存映射到自己的虚拟地址空间,进程访问共享进程就像访问自己的虚拟内存一样,速度非常快。共享内存的模型应该比较容易理解:在物理内存中创建共享资源文件,进程将共享内存绑定到自己的虚拟内存上。这里要解决的问题之一就是如何将同一块共享内存绑定到自己的虚拟内存上。要知道在不同的进程中使用malloc函数会依次分配空闲内存,而不是分配同一块内存,那么如何解决这个问题呢?Linux操作系统找到了一种方法来帮助我们解决这个问题。在#include和#include头文件下,有如下shm系列函数:shmget函数:通过ftok()函数获取共享文件资源标识符(IPCkey),并以资源标识符为参数获取共享内存区域的唯一ID。ftok()函数是用来识别系统IPC资源的,比如这里的共享资源,下面的消息队列,管道……都属于IPC资源。IPC:Inter-ProcessCommunication,进程间通信),IPC是指两个进程之间的数据交互。shmat函数:通过shmget函数得到的标识符,建立共享内存到进程独立空间的映射。shmdt功能:释放映射。由于我们主要是在Java/Go开发岗位,在Linux系统下与这些C函数打交道的次数可能为0,所以在学习过程中就不详细描述函数的具体用法了。当我们使用它时,Bing搜索是可以的。通过以上函数,每个独立的进程只要有一个统一的共享内存标识,就可以建立一个虚拟地址到物理地址的映射,每个虚拟地址都会被翻译成指向共享区域的物理地址,从而实现对共享内存。还有一个使用mmap函数的类似实现。mmap通常是直接映射到磁盘——所以不认为是共享内存,存储容量很大,但是访问速度慢;shmat则相反,通常在内存中节省资源来创建映射,访问速度快,但存储容量小。与其他几种方法相比,共享内存是最高效的,因为它不需要进行多次复制,直接对内存进行操作,但需要注意的是,操作系统不保证任何并发问题,比如两个进程改变同一个内存区域,因为你和你的朋友在线编辑同一个文档中的同一个标题,这会导致一些不好的结果,所以我们需要使用信号量或者其他方法来完成同步。Semaphore信号量是Dijkstra首先提出的解决不同执行线程同步问题的方法。进程和线程在抽象上是相似的,所以信号量也可以用来同步进程间通信。信号量如何工作信号量s是一个具有非负整数值的全局变量,由称为P和V的两个特殊原子操作实现:P(s):如果s的值大于零,则将其减1,然后立即返回,过程继续。;如果它的值为零,则进程的执行被挂起,等待s再次变为非零。V(s):V操作将s的值递增1,如果有任何进程正在等待s的值变为非零,则V操作重新启动这些等待进程之一(随机),然后执行该进程通过P操作将s重置为0,其他等待进程将继续等待。重要的是要了解信号量信号量不是用来传输资源的,而是用来保护共享资源的。信号量的含义是允许同时最大访问资源的进程数。它是一个全局变量。让我们考虑上面的一个简单例子:两个进程同时修改并导致错误。我们不考虑读者,只考虑作者进程。在这个例子中,一个共享资源最多允许一个进程修改该资源,所以我们将s初始化为1。一开始,A是第一个写入资源的。这时,A调用P(s)将s减1。此时s=0,A进入共享区工作。这时进程B也想进入共享区修改资源。它调用P(s),发现此时s为0,于是挂起进程,加入等待队列。A完成其工作并调用V(s)。它发现s为0,检测到等待队列不为空,于是随机唤醒一个等待进程,给s加1,从而唤醒了这里的B。B被唤醒,继续执行P操作。此时s不为0,B成功执行并设置s为0,进入工作区。此时C要进入工作区...可以发现任何时候只有一个进程可以访问共享资源。这就是信号量的作用。他控制着进入共享区的最大进程数,这取决于Initializes的值。之后进入共享区前调用P操作,离开共享区后调用V操作。这就是信号量的思想。Linux下没有直接的P&V函数,但是我们需要封装这些基本的sem函数族:全局计数变量s为由ftok标识的共享资源,并返回一个唯一标识的信号量组ID。semop:该函数接受上述函数返回的信号量组ID和其他一些参数。根据参数的不同,有一些不同的操作。它会对信号量组ID绑定的全局计数变量s进行一些操作,P&V操作就是基于这个实现的。semctl:该函数接受上述函数返回的信号量组ID和其他一些参数,主要控制与信号量相关的信息,如删除信号量。顾名思义,管道就像生活中的管道,一端传输,另一端接收,双方不需要认识对方,只需要了解管道即可。管道是最基本的进程间通信机制。管道由pipe函数创建:调用pipe函数会在内核中开辟一块缓冲区,用于进程间通信。这个缓冲区称为管道,它有一个读端和一个写端。管道分为匿名管道和命名管道。匿名管道匿名管道是通过pipe函数创建的,接收一个长度为2的Int数组,返回1或0表示成功或失败:intpipe(intfd[2])该函数打开两个文件描述符,一个用于读取结束file,一个写端,分别存放在fd[0]和fd[1]中,然后可以作为参数调用write和read函数进行写或读。注意fd[0]只能读文件,而fd[1]只能写文件。你可能会有疑问,如何实现通信?其他进程不知道这个管道,因为进程是独立的,其他进程看不到某个进程做了什么。是的,“其他”进程不知道,但它的子进程知道!这就涉及到fork派生过程的相关知识。如果一个进程fork了一个子进程,子进程会复制父进程的内存空间信息。注意这里是复制而不是共享,也就是说父子进程仍然是独立的,但是在这一刻,他们所有的信息又是平等的了。因此,子进程也知道全局管道,也有两个链接到管道的文件描述符,所以匿名管道只能在具有亲和力的进程之间进行通信。另外注意匿名管道内部是用环形队列实现的,只能从写端到读端。由于设计技术问题,管道设计为半双工。如果一方要写,就必须关闭读描述符,如果一方要读,就必须关闭写描述符。所以我们说管道的消息只能单向传输。注意管道是阻塞的,阻塞到什么程度取决于读写进程是否关闭了文件描述符。假设读管道,如果读为空,假设此时写端口还没有完全关闭,那么操作系统就会假设还有数据要读,读进程就会阻塞,直到有新数据或写端口关闭;如果管道为空,写端口关闭,操作系统会认为没有什么可读的,直接退出并返回0。写端口在写管道时,如果管道满了,如果读端没有关闭,写端就会被阻塞;如果读端关闭,操作系统会认为这样的写管道没有意义,因为没有人接收到它。因此,将发送终止信号以使内部进程管道由内核管理。在半双工条件下,保证数据不会出现并发问题。命名管道了解了匿名管道之后,命名管道就很容易理解了。在匿名管道的介绍中,我们说过其他进程不知道管道和文件描述符的存在,所以匿名管道只适用于有亲缘关系的进程,而命名管道很好地解决了这个问题——现在管道有一个具有独特性的name,任何进程都可以访问这个管道。注意,操作系统将管道视为抽象文件,但管道不是普通文件。管道存在于内核空间,不放在磁盘上(命名管道文件系统上有标识符,无数据块),访问速度较快,但存储容量较小。管道是临时的,遵循流程。当进程销毁时,所有端口自动关闭。此时,管道不存在。操作系统把所有的IO抽象都看成是文件,比如网络中的一种文件,也就是说我们可以使用任何文件方式来操作管道。理解这种抽象很重要,命名管道利用了这种抽象。linux下用mkfifo函数创建,传入指定的'文件名',其他进程就可以调用open方法打开这个特殊文件,进行读写操作(必须是一个字节流,对)。注意,命名管道适用于任何进程,除了这个区别外,大部分与匿名管道相同。消息队列什么是消息队列?消息队列,又叫消息队列,又叫邮箱,是Linux的一种通信机制。这种通信机制传输的数据会被拆分成独立的数据块,也叫消息体,其中可以定义类型和数据,它克服了无格式字节流的缺陷(现在接收到void*就可以知道它的原始格式了)):structmsgbuf{longmtype;/*消息类型*/charmtext[1];/*消息正文*/};和管道类似,它有个缺点就是每条消息的最大长度是有限制的,整个消息队列的长度也是有限制的。内核为每个IPC对象维护了一个数据结构structipc_perm,其中有指向链表头和链表尾的指针,保证每次插入和移除都是O(1)时间复杂度。1.msgget函数:创建或访问消息队列原型:#include#include#includeintmsgget(key_tkey,intmsgflag);参数:key:消息队列的名称,由ftok()生成,消息队列是一个PIC资源,key标识消息队列,如果传入的key存在,则返回对应的消息队列ID,否则返回消息将被创建并返回队列ID。msgflag:有IPC_CREAT和IPC_EXCL两个选项,单独使用IPC_CREAT,消息队列不存在则创建,存在则打开并返回;单独使用IPC_EXCL是没有意义的;同时使用两者,如果消息队列不存在则创建它,如果存在则返回错误。返回值:返回一个非负整数,为消息队列的标识码,成功则返回-12。msgctl函数:消息队列的控制函数原型:#include#include#includeintmsgctl(intmsqid,intcmd,structmsqid_ds*缓冲器);参数:msqid:msgget函数返回的消息队列标识码cmd:有3个可选值,这里我们使用IPC_RMIDIPC_STAT将msqid_ds结构体中的数据设置为消息队列当前关联的值IPC_SET前提是进程有足够的权限,将消息队列当前关联值设置为msqid_ds数据结构IPC_RMID中给定的值删除消息队列返回值:成功返回0,失败返回-13。msgsnd函数:向消息队列添加一条消息原型:#include#include#includeintmsgsnd(intmsqid,constvoid*msgp,size_tmsgsz,intmsgflg);参数:msgid:msgget函数返回的消息队列标识码msgp:指向待发送消息的指针msgze:msgp指向的消息长度(不包括消息类型longint)msgflg:默认为0返回值:成功返回0,失败返回-14。msgrcv函数:接受来自消息队列原型的消息:ssize_tmsgrcv(intmsqid,void*msgp,size_tmsgsz,longmsgtyp,intmsgflg);参数:同msgsnd返回值:成功返回实际放入接收缓冲区的字符数,失败返回-1特性与管道不同,消息队列的生命周期取决于内核,不取决于进程销毁销毁,我们需要显示调用界面删除或者使用命令删除。消息队列可以双向通信。它克服了管道只能承载无格式字节流的缺点。信号关于信号一个进程可以向另一个进程发送信号。信号是一种消息,可用于通知进程组已发送某种类型的事件。进程组中的进程可以采用处理程序来处理事件。Linux下的unistd.h头文件定义了图中的常量。当你在shell命令行输入ctrl+c时,内核会向前台进程组中的每个进程发送一个SIGINT信号,终止进程。我们可以看到上面只有30个信号,所以操作系统会为每个进程维护一个int类型的变量sig,用30位来表示是否有对应的信号事件,每个进程也有一个int类型的变量block对应到sig,其30位表示是否阻塞相应的信号(不调用处理程序)。如果有多个相同的信号同时到达,冗余的信号通常会被放入一个等待队列中等待。我们需要了解什么是进程组。每个进程属于一个进程组,多个进程可以属于同一个组。每个进程都有一个进程ID,称为pid,每个进程组都有一个进程组ID,称为pgid。默认情况下,一个进程和它的子进程属于同一个进程组。软件(比如检测键盘输入是硬件方面的)可以使用kill函数发送信号。kill函数接受两个参数,进程ID和信号类型,它将信号类型发送给相应的进程。如果pid为0,它会发送给属于自己进程组的所有进程。接收者可以使用信号函数为相应的事件添加处理程序。一旦事件发生,如果没有被阻塞,就会调用处理程序。Linux下有一套完整的函数来处理信号机制。特征信号是在软件层面模拟中断机制,是一种异步通信方式。信号可以直接在用户空间进程和内核进程之间进行交互,内核进程也可以通过它来通知用户空间进程发生了哪些系统事件。如果进程当前未在执行,则信号由内核保存,直到进程恢复执行并传递给它;如果一个信号被设置为被进程阻塞,则信号的传递会被延迟,直到它的阻塞被清除时才传递给进程。信号有一个明确的生命周期,首先信号被产生,然后内核存储信号直到它可以被发送,最后内核在信号空闲时适当地处理信号。处理程序可以被另一个处理程序中断,因此这会导致并发问题,因此处理程序中的代码应该是线程安全的,通常通过设置块位图来阻塞所有信号。SocketSocket用于与网络中的不同主机进行通信。它主要用于客户端和服务器之间。Linux下还有一系列的C语言函数,如socket、connect、bind、listen和accept。对于原理还是分析一下Java中的socket源码比较好。结束语对于工作来说,这些操作我们可能一辈子都不会用到,但是作为对操作系统的学习,了解进程间如何通信还是很有必要的。在面试的时候,我们不需要把这些方法掌握的很深,但是一定要说说沟通的方法,这些方法有什么特点,适合什么条件,大概怎么操作,基本上足以让面试官对你很满意了。