文章目录
前言
进程是系统资源分配的最小单位,每个进程拥有独立的地址空间。为了保证不同进程之间能够交换数据、同步状态、协同工作,操作系统提供了多种 IPC 机制。
1.管道(Pipe)
内核中开辟一个固定大小的缓冲区,一个进程写入,另一个进程读取。
- 半双工(数据单向流动)
- 只能在父子进程或兄弟进程间使用(因为有共同祖先)
- 读取时如果没有数据会阻塞,直到有数据或所有写端关闭
Java 中的体现:ProcessBuilder 启动子进程后,可以通过 Process.getInputStream() / getOutputStream() 获得管道流,实现父进程与子进程通信。
java
// 父进程与子进程通过管道通信示例
ProcessBuilder pb = new ProcessBuilder("grep", "java");
Process p = pb.start();
// 向子进程的标准输入写入数据
try (OutputStream os = p.getOutputStream()) {
os.write("java is fun\npython is fun".getBytes());
}
// 读取子进程的标准输出
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(p.getInputStream()))) {
reader.lines().forEach(System.out::println);
}
2.命名管道(FIFO)
与普通管道的区别:在文件系统中有一个路径名,不相关的进程可以通过该文件名进行通信。
- 特点:遵循先进先出原则,写入的数据被另一进程读取后即从内核缓冲区移除。
- 使用场景:需要长期存在的、无亲缘关系的进程间的数据流。
Java 标准库没有直接封装 FIFO,但可以通过 JNA 或直接操作 /tmp/myfifo 等文件路径,使用 RandomAccessFile 以读写方式打开(Linux 下需要配合 mkfifo 预先创建)
3.消息队列
即消息的链表,存储在内核中,每个消息有类型和正文。
优点:
- 解耦:发送方和接收方不需要同时运行
- 支持多条消息,有优先级
- 数据有边界,不像流式管道需要自己解析
缺点:消息大小通常有限制,内核空间拷贝数据有一定开销。
可以使用 Kafka、RabbitMQ 等消息中间件(用户态),或通过 java.nio.channels.Pipe 实现线程间通信(不是进程间)
4.共享内存
将同一块物理内存映射到多个进程的虚拟地址空间中,一个进程修改后,另一个进程直接可见。
- 优点:速度最快,没有内核介入的数据拷贝(只需一次页表映射)。
- 难点:需要同步机制(比如信号量)来避免竞争条件。
Java 实现方式包括:
- 使用 MappedByteBuffer + FileChannel 映射文件到内存,不同进程映射同一个文件(类似 mmap)。
- 使用第三方库(如 Chronicle Map)或 JNI 调用 POSIX 的 shmget / shmat。
java
// Java 中使用内存映射文件模拟共享内存
RandomAccessFile file = new RandomAccessFile("shared.dat", "rw");
FileChannel channel = file.getChannel();
MappedByteBuffer buffer = channel.map(
FileChannel.MapMode.READ_WRITE, 0, 1024);
// 进程 A 写入
buffer.putInt(0, 100);
// 进程 B 读取
int value = buffer.getInt(0);
5.信号量
一个整数计数器,支持 P(等待)和 V(释放)操作,用于控制多进程对共享资源的访问。
- 与互斥锁的区别:信号量可以允许多个线程同时访问(计数型),互斥锁只允许一个。
Java 中的相关类:java.util.concurrent.Semaphore 用于线程间,不直接用于进程间。若需进程间信号量,需使用 FileLock 或 JNI 调用系统信号量。
6.信号(Signal)
软件中断,异步通知进程某个事件发生。常见信号:SIGKILL(强制终止)、SIGTERM(请求终止)、SIGINT(Ctrl+C)。
- 特点:携带信息少,只用来通知事件,不传递数据。
Java 中的处理:通过 Runtime.getRuntime().addShutdownHook 捕获 SIGTERM 等信号;更细粒度的信号处理可以使用 sun.misc.Signal(非标准)或 jdk.internal.misc.Signal。
java
// 优雅关闭示例
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("收到终止信号,正在释放资源...");
}));
7.Socket
通过网络协议栈(TCP/UDP)通信,可以跨主机。本地进程间通信可使用 Unix Domain Socket(比 TCP 更快,不走网络协议栈)。
- 优点:通用性强,支持跨网络、跨语言。
Java 中的优势:java.net.Socket / ServerSocket 以及 NIO 提供了非常成熟的实现。本地通信可指定 localhost 127.0.0.1。
java
// 服务端
ServerSocket server = new ServerSocket(8888);
Socket client = server.accept();
// 客户端
Socket socket = new Socket("localhost", 8888);
面试问题
1.共享内存为什么最快?
- 其他IPC方式(管道、消息队列、Socket)通常需要将数据从用户空间拷贝到内核空间,再拷贝到接收方用户空间,共两次拷贝。而共享内存只在内核中建立映射,进程直接读写同一块物理内存,无需拷贝。
2.Java 中为什么很少直接用共享内存?
- Java 的内存模型屏蔽了物理内存的直接操作,跨 JVM 的共享内存需要依赖 MappedByteBuffer 或 JNI,且需要自行处理内存布局、字节序、同步等问题,复杂度高。通常使用 Redis、Kafka 等中间件来代替。
3.管道和消息队列的核心区别?
- 管道是无格式的字节流,没有消息边界;消息队列是有格式的独立消息,每个消息有类型和长度,接收方可以按类型读取。
4.信号量和互斥锁的区别?
- 互斥锁是二元信号量(0/1),用于保护临界区;信号量可以大于1,允许多个资源实例被并发访问。另外,互斥锁要求同一个线程释放,信号量可以由不同进程释放。