Linux进程间通信机制
[toc]
# 引言
操作系统是以进程为基本单位运行,涉及进程间通信势必涉及共享内存空间、磁盘文件或者网络,这其中就涉及一些可见性、有序性、正确性的问题,而本文将针对进程间通信展开深入探究。因为线程是进程内的执行单元,共享进程的资源,所以关于进程间的通信很多理念也都适用于线程间通信,希望对你有所启发。
我是 SharkChili ,Java 开发者,Java Guide 开源项目维护者。欢迎关注我的公众号:写代码的SharkChili,也欢迎您了解我的开源项目 mini-redis:https://github.com/shark-ctrl/mini-redis (opens new window)。
为方便与读者交流,现已创建读者群。关注上方公众号获取我的联系方式,添加时备注加群即可加入。
# 详解进程通信的几种方式
# 基于信号通知
通知也是通信中的一种情况,一般情况下涉及进程之间通知通常采用信号(signal)机制,对应的我们可以通过kill -l指令查看Linux系统下的各种信号:
sharkchili@DESKTOP-xxxxx:/tmp$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
2
3
4
5
6
7
8
9
10
11
12
13
14
这种方式只能传递一些特定的信号通知,对于复杂的信息传递则有些力不从心,例如我们要通过shell进程终止其他进程时,通常使用kill -9传递SIGKILL信号来强制关闭进程。
# socket套接字环回口传输
于是就考虑通过网络传输的方式进行同一操作系统内的进程间通信,也就是我们常说的socket套接字。因为同一台机器上的进程间通信本质上都是通过loopback环回接口进行通信,数据包在协议栈中就会被解析,不会经过物理网卡:

这种进程通信方式可以直接通过Wireshark抓包观察,可以看到数据包通过环回接口传输,在协议栈中完成数据传输,而不会像跨网络通信那样经过物理网络层:

为了同一操作系统内的进程间通信而采用环回接口进行协议栈通信传输显得有些繁琐,因此在实际应用中通常不推荐使用这种方式。
# 匿名管道和命名管道
所以就有了匿名管道,主要用于父子进程间的通信。对应fork创建的子进程可以共享父进程的内存空间,子进程可以直接继承父进程的消息管道。同时考虑到管道是单向通信的,所以父子进程间如果需要双向通信,需要创建两个管道,并约定好双方的读写端口:

在操作系统中,进程通信的管道设计来自于一种高级的同步原语——管程,因为管程通信涉及通信双方单通道的读写,所以在设计管程之初就提出单位时间内只有一个活跃进程操作的特性,为了保证数据读写的正确性,操作系统会采用互斥量(可以理解为Java中的synchronized关键字)或者信号量(生产者消费者模型)的方式管理临界资源,确保单位时间内只有一个进程能够访问管道。
但是管道通信也有着一定的局限性:
- 无法保证消息可靠性,即父子进程无法感知对方是否正常运行,如果有一方因为异常而下线,而另一方持续发送消息即有可能导致发送消息阻塞,进而导致内存溢出或者进程阻塞异常。即使下线的进程再次启动也无法定位到原来的匿名管道
- 匿名管道仅仅支持父子进程通信
所以,设计者在匿名管道基础上增加了命名管道,命名管道通过文件系统中的特殊文件进行标识,通信双方只需知晓管道名称即可正确定位到通道并准确发送消息。
# 内核消息队列
为了保证消息可靠性,操作系统内核中引入了以消息块为单位的消息队列作为多进程间的通信机制。基于该数据结构,接收方通过发送确认消息告知发送方可以发送消息,发送方收到确认后将要发送的消息写入消息队列中,由此双方通过这种可靠传输的方式完成进程间可靠通信:

对应的我们也以Go语言作为代码示例演示一下这种通信过程。需要补充说明的是,这种消息队列也可以不指定缓冲区大小,也就是无缓冲区队列。这种方式必须保证消息一发送消费者就得完成消息消费才能继续投递消息,反之则双方阻塞,所以这种方案也常常被称为同步通信。该模式本质上也可以通过无缓冲区队列进行说明,逻辑比较简单笔者就不多做演示:
package main
import (
"log"
"sync"
)
func main() {
producerCh := make(chan struct{}, 1)
consumerCh := make(chan string, 1)
var wg sync.WaitGroup
wg.Add(2)
go func() {
//1. 向生产者写入空消息
producerCh <- struct{}{}
wg.Done()
//3. 消费者消费消息
msg := <-consumerCh
log.Println("consumerCh receive message:", msg)
wg.Done()
}()
go func() {
//生产者信箱收到消息后,写入消息给消费者
<-producerCh
log.Println("producerCh received signal")
consumerCh <- "hello golang"
wg.Done()
}()
wg.Wait()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 映射共享内存
最后为了提升通信效率,多进程之间采用内存映射的方式,即进程双方进行通信时,各自映射同一块内存空间,保证双方读写操作都在同一块内存空间中进行。但是为了保证进程间通信的同步,则需要引入信号量的概念,确保单位时间内进程消息读写的正确性。对应的笔者采用Java代码的方式演示了一下基于信号量互斥保证共享数据读写的案例:
import java.util.LinkedList;
import java.util.concurrent.Semaphore;
public class SharedMemoryExample {
public static void main(String[] args) {
//模拟共享内存
LinkedList<String> messages = new LinkedList<>();
Semaphore semaphore = new Semaphore(1);
//拿到信号量写消息
new Thread(() -> {
try {
semaphore.acquire();
messages.add("hello world");
semaphore.release();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
//拿到信号量读消息
new Thread(() -> {
try {
semaphore.acquire();
String s = messages.pollFirst();
if (s != null)
System.out.println("poll message:"+s);
semaphore.release();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 小结
本文以进程为单位简单介绍了基于信号通知、socket套接字、管道、消息队列和共享内存的几种进程间通信方式,希望对你有所启发。
我是 SharkChili ,Java 开发者,Java Guide 开源项目维护者。欢迎关注我的公众号:写代码的SharkChili,也欢迎您了解我的开源项目 mini-redis:https://github.com/shark-ctrl/mini-redis (opens new window)。
为方便与读者交流,现已创建读者群。关注上方公众号获取我的联系方式,添加时备注加群即可加入。
# 参考
《趣话计算机底层技术》 《现代操作系统》
极简的go语言channel入门:https://sharkchili.blog.csdn.net/article/details/139911961 (opens new window) Macos下的wireshark抓包权限不足问题:https://blog.csdn.net/csdnmmd/article/details/123638070 (opens new window)