进程间通信方式有哪些?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 知识面广度:面试官不想听你背名词,想听的是你能不能系统地说出主流 IPC 方式,讲清楚各自的适用场景。

  2. 原理理解深度:管道为什么要走内核缓冲区?共享内存为什么是最快的 IPC?这些 “为什么” 才是拉开差距的地方。

  3. 联系实际的意识:这道题答完,面试官大概率会顺势追问 “Java 里进程间怎么通信”,考察你能不能把操作系统知识和实际开发串起来。

核心答案

先说结论:Linux 中的进程间通信方式主要有 7 种

通信方式 数据流向 关键特性 适用场景
匿名管道 pipe 半双工 依赖亲缘关系(父子进程) 父子进程间简单字节流通信
命名管道 FIFO 半双工或双工(开两个) 文件系统中有名字,无亲缘限制 任意本机进程间通信
消息队列 双向可建 消息为单位,有类型、有格式 异步、解耦的少量数据传输
共享内存 直接读写 最快的 IPC,需配合同步机制 大数据量、高性能传输
信号量 不传数据 同步互斥原语 配合共享内存做保护
信号 signal 单向异步 唯一的异步机制,通知事件为主 进程杀死(kill -9)、异常通知
Socket 双向 唯一可跨主机的 IPC 网络通信、本机跨进程(Unix 域套接字)

一句话兜底:前四种传数据,信号量管同步,信号发通知,Socket 跨网络

进程间通信方式总览
进程间通信方式总览

深度解析

一、管道:最古老但也最常用

管道分两种:匿名管道和命名管道。

匿名管道就是 shell 里那个 | 符号,比如 cat log.txt | grep "ERROR"。它的特点是:

  • 半双工:数据只能单向流动,想双向通信就得开两根管道
  • 亲缘限制:只存在于父子进程或兄弟进程之间,因为管道的文件描述符需要通过 fork 继承传递
  • 字节流:没有消息边界,读出来的是一连串字节,需要自己解析

命名管道(FIFO)就是来解决亲缘限制的:它在文件系统里有个名字,任何进程都能通过路径打开它,比如 /tmp/myfifo

管道通信的本质是什么?看这张图:

管道内核缓冲区
管道内核缓冲区

图里的关键在于那个内核缓冲区

  • 进程 A 调用 write(),数据从 A 的用户空间拷贝进内核缓冲区
  • 进程 B 调用 read(),数据再从内核缓冲区拷贝到 B 的用户空间
  • 两边各不直接接触对方的内存,内核做中转

记住了 “两次拷贝” 这个细节,后面理解共享内存为什么快就顺理成章了。

管道消息队列区别
管道消息队列区别

另外管道还有个容量限制:Linux 下管道缓冲区默认是 64KB(16 页 × 4KB,Linux 2.6.11 起),写满了 write 会阻塞,读空了 read 会阻塞,所有 reader 都关闭时写端会收到 SIGPIPE 信号。想调整的话用 fcntl(fd, F_SETPIPE_SZ),不过非特权进程能调的上限受 /proc/sys/fs/pipe-max-size 约束,默认 1MB。

二、消息队列:有格式的管道

消息队列可以理解为 “升级版管道”,它是内核中一条消息的链表,克服了管道的几个短板:

  • 有消息边界:管道是字节流,消息队列以 “一条消息” 为单位,写入什么格式读出来还是什么格式
  • 有类型过滤msgrcv 可以按消息类型 type 选择性接收,不用按顺序死等
  • 异步解耦:A 进程发完消息就可以走人,B 进程想什么时候读就什么时候读,不要求两边同时在线

消息队列本身也有两种体系,面试提到能加分:

  • System V 消息队列msgget / msgsnd / msgrcv 这一套,比较老
  • POSIX 消息队列mq_open 这一套,API 更现代,支持消息优先级

不过消息队列也有硬伤:每次收发消息都要经过内核,数据要在用户态和内核态之间来回拷贝,而且单条消息有大小上限(System V 下默认 8192 字节,/proc/sys/kernel/msgmax 可查),大块数据传输就别指望它了。

三、共享内存:性能之王

重点来了。面试官问到 IPC,共享内存是必考点,尤其是 “为什么它最快” 这个追问。

原理:两个进程的虚拟地址空间,映射到同一块物理内存。A 写进去的东西,B 立刻就能看到,全程不经过内核中转。

共享内存映射
共享内存映射

  • 进程 A 和进程 B 各自的虚拟地址可以不一样,但通过各自的页表,最终指向同一块物理内存
  • 写入方一旦写入,读取方立即可见,没有中间商赚差价
  • 对比管道的 “用户态 → 内核 → 用户态” 两次拷贝,共享内存一次拷贝都不需要(建立映射后)

当然,天下没有免费的午餐。共享内存不提供任何同步机制,两个进程同时读写同一块内存就是数据竞争,所以它几乎总是和信号量搭配出现:信号量管 “谁能读写、什么时候能读写”,共享内存管 “数据放哪”。

共享内存相关的经典 API 就是这一套:shmget 创建、shmat 挂接(attach)、shmdt 脱离、shmctl 控制。

四、信号量:不传数据的 IPC

严格说,信号量本身不传输数据,它是一个同步互斥原语,核心就是那个 P/V 操作:

  • P 操作(wait):信号量值减 1,减完小于 0 就挂起等待
  • V 操作(signal):信号量值加 1,有等待者就唤醒它

信号量的值大于 0 时表示可用资源数,等于 0 时表示资源耗尽。它管的是 “谁先谁后、谁进谁等” 这件事。面试时把信号量归类说清楚——“它解决的是进程间的同步与互斥,而不是数据传输”,这句话能让面试官知道你概念是清晰的。

共享内存信号量
共享内存信号量

五、信号:唯一的异步机制

信号是所有 IPC 方式中唯一的异步通信方式。进程不需要阻塞等待,信号随时可能到来,内核会中断进程当前的执行流,转去执行信号处理函数。

日常开发里你天天在用它,只是可能没意识到:

  • kill -9 <pid> 就是给进程发 SIGKILL 信号
  • Ctrl + C 是给前台进程发 SIGINT
  • kill -15 <pid> 是发 SIGTERM,进程可以捕获它做优雅退出——这也是为啥停服务建议用 -15 而不是 -9-9 无法被捕获,进程没有任何清理资源的机会

注意 SIGKILL(9)和 SIGSTOP(19)这两个信号不能被捕获、阻塞或忽略,属于内核的 “保底手段”。

信号异步通知
信号异步通知

六、Socket:唯一能跨主机的 IPC

前面所有方式都只能在本机内通信,只有 Socket 突破了这个限制——它不仅能跨进程,还能跨主机,网络通信靠的就是它。

很多人没注意到,Socket 还有个本机玩法:Unix 域套接字(Unix Domain Socket)。它不走网络协议栈,专门用于同一台主机上的进程通信,比走 TCP 回环接口快不少。像 MySQL、Docker 的守护进程监听的就是 /var/run/docker.sock 这种 Unix 域套接字。

Socket 跨主机通信
Socket 跨主机通信

对 Java 开发者来说,Socket 就更熟悉了。进程间通信在 Java 生态里的对应物:

  • 跨进程Socket / ServerSocket、HTTP 接口调用、RMI
  • 跨系统解耦:消息中间件(RocketMQ、Kafka),可以理解为分布式版的 “消息队列”
  • 本机协调:文件锁、RandomAccessFile + 文件通道

给一个最简单的 Java Socket 双向通信骨架:

// 服务端进程:监听 8080 端口
try (ServerSocket serverSocket = new ServerSocket(8080)) {
    Socket client = serverSocket.accept(); // 阻塞等待连接
    BufferedReader in = new BufferedReader(
            new InputStreamReader(client.getInputStream()));
    System.out.println("收到客户端消息: " + in.readLine());
}

// 客户端进程:连接服务端并发送一条消息
try (Socket socket = new Socket("127.0.0.1", 8080)) {
    PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
    out.println("hello from another process");
}

这就是两个独立 JVM 进程之间用 Socket 通信的最小实现,本质上和 C 里调用 socket API 是一回事,Java 只是把系统调用包装成了面向对象的接口。

七、一张表看懂怎么选

你的需求 推荐方式
父子进程间传简单数据 匿名管道
本机任意进程间传数据 命名管道 / Unix 域套接字
大数据量、高频通信、追求性能 共享内存 + 信号量
异步解耦、不要求实时 消息队列
只需要通知 “某事件发生了” 信号
跨主机分布式通信 Socket

面试高频追问

  1. 为什么共享内存是最快的 IPC 方式?

    因为数据不需要在内核和用户态之间来回拷贝。管道、消息队列都要经历 “发送方用户态 → 内核 → 接收方用户态” 的两次拷贝,而共享内存建立映射后,双方直接读写同一块物理内存,零拷贝。

  2. 进程间通信和线程间通信有什么区别?

    进程地址空间隔离,通信必须借助内核提供的机制;线程共享同一进程的地址空间,全局变量天然可见,通信的核心问题反而是 “同步互斥”(锁、wait/notify)。一个重在 “怎么传数据”,一个重在 “怎么防打架”。

  3. 共享内存有了,为什么还需要消息队列?

    共享内存快但不管同步、不管消息边界,需要自己实现协议;消息队列自带边界和类型,用起来简单安全。性能不是唯一标准,开发效率和正确性同样重要。

  4. Java 进程之间怎么通信?

    Socket / HTTP / RMI 直连,或者用 RocketMQ、Kafka 这类消息中间件做异步解耦。注意 Java 没有直接暴露共享内存 API,需要通过 JNI 或者依赖一些映射文件的方案(MappedByteBuffer 操作内存映射文件算是个近似玩法)。

常见面试变体

  • “管道和消息队列有什么区别?”
  • “共享内存为什么快?它有什么缺点?”
  • “信号和信号量的区别?”(这俩名字像,完全是两个东西:signal 是异步通知,semaphore 是同步计数器)
  • “介绍一下零拷贝?”(从共享内存和 sendfilemmap 的角度都能切入,属于这块的高阶延伸)

记忆口诀

七种 IPC:管道匿名靠父子,命名管道随便聊;消息队列有格式,共享内存速度高;信号量来管同步,信号异步把事报;Socket 唯一跨主机,本机还有域套接字。

总结

一句话:IPC 七种方式,管道传流、队列传消息、共享内存拼性能、信号量管同步、信号发通知、Socket 跨网络。把 “共享内存为什么快(免拷贝)” 和 “信号量只是同步工具不传数据” 这两个点说透,再顺势聊到 Java 里的 Socket 和 MQ,这道题就算答得漂亮了。