进程间通信的⽅式及应⽤场景
开头
每个进程的⽤户地址空间都是独⽴的,进程与进程之间,内部空间是隔离的,进程 A 不可能直接使⽤进程 B 的变量名的形式得到进程B 中变量的值。但内核空间是每个进程都共享的,所以进程之间要通信必须通过内核。实现进程与进程之间的通信,常⽤的⽅式主要有:管道、消息队列、共享内存、信号量、信号、socket等等。
⼀、管道
在 Linux 命令中,常见的“|”符号就是⼀种管道。⽐如:
ps auxf | grep mysql
上⾯的命令中,“|”的功能是将前⼀个命令(ps auxf)的输出,作为后⼀个命令(grep mysql)的输⼊。这种管道没有名字,匿名管道,⽤完就销毁。命名管道也被叫做 FIFO,因为数据的传输⽅式是先进先出(first in first out)。
管道传输数据是单向的,如果想相互通信,需要创建两个管道才⾏。
管道创建、写⼊、读取
创建
mkfifo myPipe
myPipe 是新创建的管道的名称,基于 Linux ⼀切皆⽂件的理念,管道也是以⽂件的⽅式存在,可以⽤ ls 看到⽂件类型是 p,也就是pipe(管道)的意思:
$ ls -l
prw-r--r--. 1 root root 0 Jul 1702:45 myPipe
echo "hello" > myPipe # 将数据写进管道。程序会阻塞,只有当管道⾥的数据被读完后,程序才会正常继续。
管道写⼊数据
cat < myPipe # 读取管道⾥的数据
# hello
管道读取数据
管道的优缺点
缺点:管道的通信⽅式效率低,不适合进程间频繁地交换数据。
优点:简单。
⼆、消息队列
前⾯说到管道的通信⽅式效率很低,因此管道不适合进程间频繁地交换数据。
对于这个问题,消息队列可以解决。⽐如,A 进程要给 B 进程发送消息,A 进程将数据存⼊消息队列,B 进程只需要读取数据即可。反之亦如此。
消息队列的本质是保存在内核中的⼀种消息链表,在发送数据时,会分成独⽴的数据单元,也就是消息体(数据块)。消息体是⽤户⾃定义的数据类型,消息的发送⽅和接收⽅必须约定好消息体的数据类型,所以每个消息体都是固定⼤⼩的存储块,不像管道是⽆格式的字节流数据。如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。
消息队列⽣命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列会⼀直存在,⽽前⾯提到的匿名管道的⽣命周期,是随进程的创建⽽建⽴,随进程的结束⽽销毁。
消息这种模型,两个进程之间的通信就像平时发邮件⼀样,你来⼀封,我回⼀封,可以频繁沟通。但邮件的通信⽅式存在不⾜的地⽅有两点:⼀是通信不及时,⼆是附件也有⼤⼩限制,这同样也是消息队列通信不⾜的点。
消息队列的优缺点
缺点:
1. 通信不及时
2. 不适合⽐较⼤数据的传输,因为在内核中每个消息体都有⼀个最⼤长度的限制,同时所有队列所包含的全部消息体的总长度也是有上
限。在 Linux 内核中,会有两个宏定义 MSGMAX 和 MSGMNB,它们以字节为单位,分别定义了⼀条消息的最⼤长度和⼀个队列的最⼤长度。
3. 消息队列通信过程中,存在⽤户态与内核态之间的数据拷贝开销,因为进程写⼊数据到内核中的消息队列时,会发⽣从⽤户态拷贝数
据到内核态的过程,同理另⼀进程读取内核中的消息数据时,会发⽣从内核态拷贝数据到⽤户态的过程。
优点:
1. 可以频繁地交换数据
2. 可以⾃定义数据类型
三、共享内存
消息队列的读取和写⼊的过程,都会有发⽣⽤户态与内核态之间的消息拷贝过程。⽽共享内存就很好的解决了这⼀问题。
进程间通信效率最高的方式是 现代操作系统,对于内存管理,采⽤的是虚拟内存技术,也就是每个进程都有⾃⼰独⽴的虚拟内存空间,不同进程的虚拟内存映射到不同的物理内存中。所以,即使进程 A 和进程 B 的虚拟地址是⼀样的,其实访问的是不同的物理内存地址,对于数据的增删查改互不影响。
共享内存的机制,就是拿出⼀块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写⼊的东西,另外⼀个进程马上就能看到,⼤⼤提⾼了进程间通信的速度。
四、信号量
⽤了共享内存通信⽅式,带来新的问题:如果多个进程同时修改同⼀个共享内存,很有可能发⽣冲突。例如两个进程都同时写⼀个地址,先写的进程会的内容会被覆盖。
为了防⽌多进程竞争共享资源⽽造成的数据错乱,需要⼀种保护机制,使得共享的资源在任意时刻只能被⼀个进程访问。信号量就实现了这⼀保护机制。
信号量本质是⼀个整型的计数器,主要⽤于实现进程间的互斥与同步,⽽不是⽤于缓存进程间通信的数据。
信号量表⽰资源的数量,控制信号量的⽅式有两种原⼦操作:
P 操作:将信号量减去 -1,相减后如果信号量 < 0,则表明资源已被占⽤,进程需阻塞等待;相减后如果信号量 >= 0,则表明还有资源可使⽤,进程可正常继续执⾏。
V 操作:将信号量加上 1,相加后如果信号量 <= 0,则表明当前有阻塞中的进程,于是将该进程唤醒运⾏;相加后如果信号量 > 0,则表明当前没有阻塞中的进程;
P 操作是⽤在进⼊共享资源之前,V 操作是⽤在离开共享资源之后,这两个操作是必须成对出现的。
具体过程:
进程 A 在访问共享内存前,先执⾏ P 操作,由于信号量的初始值为 1,故在进程 A 执⾏ P 操作后信号量变为 0,表⽰共享资源可⽤,于是进程 A 就可以访问共享内存。
若此时,进程 B 也想访问共享内存,执⾏了 P 操作,结果信号量变为 -1,意味着临界资源已被占⽤,因此进程 B 被阻塞。
进程 A 访问完共享内存,执⾏ V 操作,使得信号量恢复为 0,接着就会唤醒阻塞中的线程 B,使得进程 B 可以访问共享内存,最后完成共享内存的访问后,执⾏ V 操作,使信号量恢复到初始值 1。
信号初始化为1,代表着是互斥信号量,它可以保证共享内存在任何时刻只有⼀个进程在访问,这就很好的保护了共享内存。
另外,在多进程⾥,每个进程并不⼀定是顺序执⾏的,它们基本是以各⾃独⽴的、不可预知的速度向前推进,但有时候我们⼜希望多个进程能密切合作,以实现⼀个共同的任务。
例如,进程 A 是负责⽣产数据,⽽进程 B 是负责读取数据,这两个进程相互合作、相互依赖,进程 A 必须先⽣产了数据,进程 B 才能
读取到数据,所以执⾏是有前后顺序的。这时候,就可以⽤信号量来实现多进程同步的⽅式,我们可以初始化信号量为0。
具体过程:
如果进程 B ⽐进程 A 先执⾏了,那么执⾏到 P 操作时,由于信号量初始值为 0,故信号量会变为 -1,表⽰进程 A 还没⽣产数据,于是进程 B 就阻塞等待;
接着,当进程 A ⽣产完数据后,执⾏了 V 操作,就会使得信号量变为 0,于是就会唤醒阻塞在 P 操作的进程 B;
最后,进程 B 被唤醒后,意味着进程 A 已经⽣产了数据,于是进程 B 就可以正常读取数据了。
可以发现,信号初始化为0,就代表着是同步信号量,它可以保证进程 A 应在进程 B 之前执⾏。
五、信号
上⾯说的进程间通信,都是常规状态下的⼯作模式。对于异常情况下的⼯作模式,需要⽤信号的⽅式来通知进程。
信号跟信号量虽然名字相似,但两者⽤途完全不⼀样,就好像 Java 和 JavaScript 的区别。
在 Linux 操作系统中,为了响应各种各样的事件,提供了⼏⼗种信号,分别代表不同的意义。可以通过 kill -l 命令查看所有的信号.
运⾏在 shell 终端的进程,我们可以通过键盘输⼊某些组合键的时候,给进程发送信号。例如
Ctrl+C 产⽣ SIGINT 信号,表⽰终⽌该进程;
Ctrl+Z 产⽣ SIGINTSIGTSTP 信号,表⽰停⽌该进程,但还未结束;
如果进程在后台运⾏,可以通过 kill 命令的⽅式给进程发送信号,但前提需要知道运⾏中的进程 PID 号,例如:
kill -9 1050 ,表⽰给 PID 为 1050 的进程发送SIGKILL信号,⽤来⽴即结束该进程;
所以,信号事件的来源主要有硬件来源(如键盘 Cltr+C )和软件来源(如 kill 命令)。
信号是进程间通信机制中唯⼀的异步通信机制,因为可以在任何时候发送信号给某⼀进程,⼀旦有信号产⽣,我们就有下⾯这⼏种,⽤户进程对信号的处理⽅式。
1. 执⾏默认操作。Linux 对每种信号都规定了默认操作,例如,上⾯列表中的 SIGTERM 信号,就是终⽌进程的意思。Core 的意思是
Core Dump,也即终⽌进程后,通过 Core Dump 将当前进程的运⾏状态保存在⽂件⾥⾯,⽅便程序员事后进⾏分析问题在哪⾥。
2. 捕捉信号。我们可以为信号定义⼀个信号处理函数。当信号发⽣时,就执⾏相应的信号处理函数。
3. 忽略信号。当我们不希望处理某些信号的时候,就可以忽略该信号,不做任何处理。有两个信号是应⽤进程⽆法捕捉和忽略的,即
SIGKILL 和 SEGSTOP,它们⽤于在任何时候中断或结束某⼀进程。
六、socket
⽹络通信
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论