
大家好,我是蟹老板~
搞后端、搞嵌入式、搞系統開發,只要你的程序跑在 Linux 上,遲早得面對面試官的一個靈魂拷問:你的進程之間怎么實現通信?
一個進程負責采集數據,一個進程負責存數據庫;一個進程負責UI界面,一個進程負責后臺計算;瀏覽器和渲染引擎要通信,Nginx和業務程序要通信,Docker里的服務和宿主機也要通信。
說白了,Linux系統表面上看是一堆進程同時運行,本質上其實就是:一群互相隔離的進程,想盡辦法交換數據。
而解決這個問題的機制,就是IPC(Inter Process Communication,進程間通信)。
今天這一篇,我準備把 Linux/UNIX 里最核心、最常用、面試官最愛問、項目里必會的 IPC 方式,一次性給大家講明白。
整理出來,不是為了方便大家背八股啊。
而是為了大家想在選型的時候,別一拍腦袋就用個 Socket 去傳本機兩個進程的幾 KB 數據,畢竟,選錯 IPC,加班調優火葬場。
看完這一篇,你基本就有數了。
Linux 最早的 IPC 方案就是管道。 本質上就是內核提供的一個環形緩沖區。它的設計哲學就是 "一切皆文件",你可以把它理解成: 兩個進程之間架了一根水管。一個只管寫,一個只管讀,數據走單行道。 簡單粗暴。

匿名管道是最基礎、最常用的父子進程通信方式。它的本質是半雙工通信。
大白話來講就是:數據只能單向流動,要么父寫子讀,要么子寫父讀,不能同時雙向傳輸。而且匿名管道沒有名字,只能在有親緣關系的進程(父子、兄弟進程)之間使用,因為子進程會繼承父進程打開的文件描述符。
最經典的例子:
ps aux | grep nginx這里的|其實就是匿名管道,前面的進程輸出數據,后面的進程接收數據,整個過程完全不需要臨時文件。是不是很絲滑?
它的本質是在內核中開辟一塊環形緩沖區,然后給你返回兩個文件描述符:fd[0] 專門讀,fd[1] 專門寫。數據在管道里是無結構的字節流,單向流動,流完就沒了,不能倒帶,也不能重復讀。使用時一定要記住:不需要的那一端必須關閉!不然會導致進程永遠阻塞在 read 或者 write 上。

匿名管道只能給親戚用之痛,催生了命名管道(FIFO)。顧名思義,它有一個文件系統路徑作為名字
它和匿名管道的內核機制一脈相承,也是字節流、單向的,但它有一個在文件系統中可見的文件名。這意味著,就算張三進程和李四進程八竿子打不著,只要它們知道這個文件路徑(通常在 /tmp/ 下),就能通過 open() 打開它,然后開始讀寫通信。不需要親緣關系,這是它最大的突破。
但注意,這個文件只是個“入口地址”,數據根本不落盤,還是走內存。
你可以用mkfifo命令創建一個命名管道:
mkfifo my_fifo然后你會看到一個類型為p的特殊文件:
ls -l my_fifo
prw-r--r-- 1 user user 0 Jun 8 10:00 my_fifo任何進程,只要有權限,都可以打開這個文件進行讀寫。和匿名管道一樣,FIFO默認也是半雙工的,而且讀寫操作默認是阻塞的:如果一個進程以只讀方式打開FIFO,它會一直阻塞到有另一個進程以只寫方式打開同一個FIFO為止,反之亦然。

看一個簡單的示例,一個進程寫,另一個進程讀:
寫進程:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
int fd = open("my_fifo", O_WRONLY);
if (fd == -1) {
perror("open");
return 1;
}
const char *msg = "Hello from writer!";
write(fd, msg, strlen(msg));
close(fd);
return 0;
}讀進程:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd = open("my_fifo", O_RDONLY);
if (fd == -1) {
perror("open");
return 1;
}
char buf[1024];
ssize_t n = read(fd, buf, sizeof(buf));
printf("讀進程收到:%.*s\n", (int)n, buf);
close(fd);
return 0;
}FIFO的優點是可以在任意進程間通信,使用簡單;缺點還是半雙工,而且存在阻塞問題,性能一般,傳輸能力有限,大數據量場景基本不會考慮它。它適用于簡單的、無親緣關系的進程間通信,比如不同程序之間傳遞簡單的指令。
信號(Signal)是整個 Linux/UNIX IPC 里面唯一的異步通信機制,也是最特殊的一種IPC。它不是用來傳輸大量數據的,而是用來通知進程發生了某個事件。

內核可以給進程發信號,進程也可以給另一個進程發信號。當進程收到信號時,它可以有三種處理方式:
常見的信號有:
很多人喜歡用signal()函數來注冊信號處理函數,但我強烈建議你們用sigaction()!因為signal()在不同UNIX系統上的行為不一致,而且在處理信號時會自動重置信號處理函數為默認值,容易導致競態條件。
來看個用sigaction()注冊SIGINT處理函數的示例:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sigint_handler(int sig) {
printf("\n收到SIGINT信號,程序即將退出\n");
// 注意:信號處理函數里只能調用異步安全的函數!
// 絕對不能調用printf、malloc、free這些非異步安全的函數!
// 這里只是演示,實際項目中不要這么做!
_exit(0); // 用_exit而不是exit,因為exit會刷新緩沖區
}
int main() {
struct sigaction sa;
sa.sa_handler = sigint_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction");
return 1;
}
printf("程序運行中,按Ctrl+C退出\n");
while (1) {
sleep(1);
}
return 0;
}這里有個很坑的點:信號處理函數里只能調用異步安全的函數!就是這個函數在執行過程中被信號中斷,再次進入也不會出問題。像printf、malloc、free、fopen這些函數都不是異步安全的,在信號處理函數里調用它們會導致不可預知的后果,輕則程序崩潰,重則數據損壞。
信號的優點是輕量級、異步通知;缺點是只能傳遞簡單的信號編號,不能傳遞復雜數據,而且信號處理函數有很多限制。它主要用于進程間的事件通知,比如父進程等待子進程退出、進程收到終止信號時做清理工作。
POSIX IPC是IEEE標準化的進程間通信接口,包括消息隊列、共享內存和信號量三種。相比于古老的System V IPC,POSIX IPC接口更簡潔、更易用,是現代Linux編程的主流。

mq_open/mq_send)消息隊列是一種基于消息的通信方式,它解決了管道"無邊界數據流"的問題。在管道里,數據是一串連續的字節流,你不知道一條消息從哪里開始,到哪里結束;而在消息隊列里,數據是以獨立的消息為單位傳輸的,每個消息有自己的類型和長度。
消息隊列由內核維護,進程退出后消息仍然存在,直到被其他進程讀取或者隊列被刪除。它支持隨機讀取,你可以指定讀取特定類型的消息,而不用按照先進先出的順序。
基本操作函數:
mq_open():創建或打開一個消息隊列mq_send():發送一條消息mq_receive():接收一條消息mq_close():關閉消息隊列mq_unlink():刪除消息隊列
我們來看一個簡單的消息隊列示例:
發送端:
#include <stdio.h>
#include <fcntl.h>
#include <mqueue.h>
#include <string.h>
#define QUEUE_NAME "/my_queue"
#define MAX_MSG_SIZE 1024
int main() {
// 打開消息隊列,O_CREAT表示不存在則創建,0666是權限
mqd_t mq = mq_open(QUEUE_NAME, O_WRONLY | O_CREAT, 0666, NULL);
if (mq == (mqd_t)-1) {
perror("mq_open");
return 1;
}
const char *msg = "Hello from message queue!";
// 發送消息,優先級為0
if (mq_send(mq, msg, strlen(msg), 0) == -1) {
perror("mq_send");
return 1;
}
printf("消息發送成功\n");
mq_close(mq);
return 0;
}接收端:
#include <stdio.h>
#include <fcntl.h>
#include <mqueue.h>
#define QUEUE_NAME "/my_queue"
#define MAX_MSG_SIZE 1024
int main() {
mqd_t mq = mq_open(QUEUE_NAME, O_RDONLY);
if (mq == (mqd_t)-1) {
perror("mq_open");
return 1;
}
char buf[MAX_MSG_SIZE];
unsigned int prio;
// 接收消息
ssize_t n = mq_receive(mq, buf, MAX_MSG_SIZE, &prio);
if (n == -1) {
perror("mq_receive");
return 1;
}
printf("收到消息(優先級%d):%.*s\n", prio, (int)n, buf);
mq_close(mq);
// 用完記得刪除消息隊列,否則會一直存在于系統中
mq_unlink(QUEUE_NAME);
return 0;
}編譯的時候記得加-lrt鏈接實時庫:
gcc sender.c -o sender -lrt
gcc receiver.c -o receiver -lrt消息隊列的優點是有消息邊界、支持優先級、支持隨機讀取、內核維護;缺點是消息大小有限制(Linux默認最大8KB)、不適合傳輸大量數據。它適用于傳遞小消息、需要按優先級處理的場景,比如任務調度、事件通知。
shm_open + mmap)共享內存是 Linux 下所有 IPC 的性能天花板,沒有之一,單機數據交換的唯一真神。
內核分配一塊物理內存,然后多個進程通過mmap()把這塊內存映射到自己的虛擬地址空間,數據不需要在內核和用戶空間之間拷貝,直接在用戶空間就能讀寫。這樣,一個進程對這塊內存的修改,其他進程能立刻看到。完全避免了內核作為中間商的拷貝開銷。零拷貝,極致的快。數據庫的 Buffer Pool 共享、音視頻流的高效傳輸,底層全是這玩意兒。
但“快”也意味著“亂”,共享內存它本身不提供任何同步機制**!多個進程同時往白板上寫字,很容易把字寫重疊(競爭條件)。所以,用共享內存幾乎必然**要搭配“互斥鎖”或者“信號量”來維持秩序。
基本操作步驟:
shm_open():創建或打開一個共享內存對象ftruncate():設置共享內存的大小mmap():將共享內存映射到進程地址空間munmap():解除映射close():關閉共享內存對象shm_unlink():刪除共享內存對象
一起來看一個簡單的共享內存示例,兩個進程共享一個整數:
寫進程:
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>
#define SHM_NAME "/my_shm"
#define SHM_SIZE sizeof(int)
int main() {
// 創建共享內存對象
int fd = shm_open(SHM_NAME, O_RDWR | O_CREAT, 0666);
if (fd == -1) {
perror("shm_open");
return 1;
}
// 設置共享內存大小
if (ftruncate(fd, SHM_SIZE) == -1) {
perror("ftruncate");
return 1;
}
// 映射到進程地址空間
int *shared_int = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (shared_int == MAP_FAILED) {
perror("mmap");
return 1;
}
close(fd); // 映射完成后就可以關閉文件描述符了
// 寫入數據
*shared_int = 42;
printf("寫入數據:%d\n", *shared_int);
// 等待讀進程讀取
sleep(5);
// 解除映射
munmap(shared_int, SHM_SIZE);
// 刪除共享內存對象
shm_unlink(SHM_NAME);
return 0;
}讀進程:
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#define SHM_NAME "/my_shm"
#define SHM_SIZE sizeof(int)
int main() {
// 打開共享內存對象
int fd = shm_open(SHM_NAME, O_RDONLY, 0666);
if (fd == -1) {
perror("shm_open");
return 1;
}
// 映射到進程地址空間
int *shared_int = mmap(NULL, SHM_SIZE, PROT_READ, MAP_SHARED, fd, 0);
if (shared_int == MAP_FAILED) {
perror("mmap");
return 1;
}
close(fd);
// 讀取數據
printf("讀取數據:%d\n", *shared_int);
// 解除映射
munmap(shared_int, SHM_SIZE);
return 0;
}同樣,編譯的時候要加-lrt。
共享內存的優點是速度極快、適合傳輸大量數據;缺點是沒有同步機制、容易產生競態條件、調試困難。它是大數據量傳輸(比如圖像處理、數據庫緩存)的首選,但編程復雜度也是最高的,比如視頻處理、數據庫、高頻交易系統。
sem_open)注意了,我們這里聊的是 POSIX 命名信號量,很多人容易把信號量和前面的“信號”搞混,記住:它們倆半毛錢關系都沒有!
信號量(sem_open)不是用來傳數據的,它是**“交通警察”。本質上就是一個內核計數器**,專門用來做進程間的同步和互斥。它最經典的操作就是 P操作(進入,計數器減1) 和 V操作(退出,計數器加1)。
當計數器的值為 1 的時候,它就是一把分布式鎖。共享內存要想安全用,必須讓信號量在旁邊站崗:
// 偽代碼:共享內存加鎖的標準姿勢
sem_wait(sem); // P操作,搶鎖。搶不到就憋著
memcpy(shm_ptr, data, size); // 寫入共享內存
sem_post(sem); // V操作,釋放鎖。后面排隊的進程可以進來了如果沒有這層保護,在多核 CPU 高并發下,你的共享內存分分鐘變成車禍現場。

套接字(Socket)是最通用的IPC方式,它不僅可以用于同一臺主機上的進程間通信,還可以用于不同主機上的進程間通信。

很多人以為Socket就是網絡通信,其實不完全對。
UNIX Domain Socket:這是本機進程通信的六邊形戰士,我個人最推薦的萬能方案。雖然它用的是網絡協議的編程接口(API),但傳輸層走的是操作系統內核,而不是網卡。
像MySQL、Nginx這些本地服務的高性能通信,很多都用的是UNIX域套接字。
它有兩種類型:
UNIX Domain Socket通過文件系統路徑來標識,支持權限控制,可以設置只有特定用戶的進程才能訪問。

一個簡單的UNIX Domain Socket流套接字示例:
服務端:
#include <stdio.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <string.h>
#define SOCKET_PATH "/tmp/my_unix_socket"
#define BUF_SIZE 1024
int main() {
int server_fd, client_fd;
struct sockaddr_un server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
char buf[BUF_SIZE];
// 創建UNIX域流套接字
server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (server_fd == -1) {
perror("socket");
return 1;
}
// 如果socket文件已經存在,先刪除
unlink(SOCKET_PATH);
// 綁定地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sun_family = AF_UNIX;
strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path) - 1);
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("bind");
return 1;
}
// 監聽
if (listen(server_fd, 5) == -1) {
perror("listen");
return 1;
}
printf("服務端等待連接...\n");
// 接受連接
client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
if (client_fd == -1) {
perror("accept");
return 1;
}
printf("客戶端已連接\n");
// 接收數據
ssize_t n = read(client_fd, buf, BUF_SIZE);
printf("收到:%.*s\n", (int)n, buf);
// 發送響應
const char *response = "Hello from server!";
write(client_fd, response, strlen(response));
close(client_fd);
close(server_fd);
unlink(SOCKET_PATH);
return 0;
}客戶端:
#include <stdio.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <string.h>
#define SOCKET_PATH "/tmp/my_unix_socket"
#define BUF_SIZE 1024
int main() {
int sockfd;
struct sockaddr_un server_addr;
char buf[BUF_SIZE];
// 創建套接字
sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket");
return 1;
}
// 連接服務端
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sun_family = AF_UNIX;
strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path) - 1);
if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("connect");
return 1;
}
// 發送數據
const char *msg = "Hello from client!";
write(sockfd, msg, strlen(msg));
// 接收響應
ssize_t n = read(sockfd, buf, BUF_SIZE);
printf("收到響應:%.*s\n", (int)n, buf);
close(sockfd);
return 0;
}UNIX Domain Socket的優點是效率高、可靠、支持全雙工、支持權限控制;缺點是只能在同一臺主機上使用。它是同一主機上復雜進程間通信的首選,比如Web服務器和應用服務器之間的通信、數據庫客戶端和服務器之間的通信。
這個大家最熟了,跨主機通信的絕對標準,互聯網的基石。AF_INET + TCP/UDP。用于不同主機上的進程間通信。
把它也放在 IPC 里,是因為很多時候我們用它來做本機進程間的通信(127.0.0.1 環回地址)。而且開發調試非常方便,將來把服務拆到多臺機器上,代碼基本不用改。
它有兩種主要類型:
網絡Socket的編程接口和UNIX Domain Socket幾乎一樣,只是地址結構不同。TCP需要三次握手建立連接,四次揮手斷開連接,提供可靠的數據傳輸;UDP不需要建立連接,速度快,但可能丟包、亂序。
網絡Socket的優點是跨主機、通用性強、支持全雙工;缺點是有網絡開銷、開發調試復雜。它是分布式系統、網絡應用的基礎。

上面講的都是Linux內核提供的原生IPC方式,它們功能強大但接口比較底層,在復雜項目中,直接用裸的共享內存加信號量,開發效率低不說,還容易出 bug。
接下來的這些它們是封裝了上面那些底層協議的高級封裝組件。,它們提供了更友好的接口、更豐富的功能,能大大提高開發效率。
當你不想自己造輪子的時候,這些東西就香了。

這是 Linux 桌面環境的消息總線,IPC 的中央調度中心,
在 Linux 桌面環境(比如 Ubuntu 的 GNOME/KDE)或者汽車車載系統(AGL、GENIVI)里,D-Bus 是絕對的統治者。
它不是一個簡單的協議,而是一個總線系統。它支持信號訂閱(Publish/Subscribe)和遠程方法調用(RPC)。
一個進程往 D-Bus 總線上丟一個廣播,所有關注這個事件的進程都能收到,非常適合用來做復雜的系統級狀態管理(比如:電池電量低了,通知屏幕變暗、通知電源管理進程進低功耗、通知UI彈窗)。但由于它層層封裝,性能相對比較弱,不適合傳海量大數據。

如果你在搞分布式微服務,或者嵌入式設備需要跟云端、上位機進行復雜的結構化業務交互,Socket已經不夠用了,直接上 gRPC(谷歌出品)或者 Thrift(Apache出品)。
它們是標準的 RPC(遠程過程調用)框架。底層基于 HTTP/2(gRPC)或者自研高效傳輸協議,數據用 Protocol Buffers 等格式壓縮,體積小、速度快,而且自帶強類型定義,自動生成多語言代碼。你像調用本地函數一樣去調用另一個進程的函數就行,完全不需要自己去解析字節流。
gRPC使用Protocol Buffers作為序列化格式,支持多種編程語言,性能優異,是現在微服務架構的首選。Thrift是Facebook開發的,功能類似,也被廣泛使用。
它們適用于跨語言、跨主機的進程間通信,特別是微服務架構中的服務調用。

ZeroMQ和MQTT是消息傳遞和消息隊列框架,它們提供了更高級的通信模式,簡化了分布式系統的開發。
ZeroMQ 可以理解為 Socket 的超級增強版,一個高性能異步消息庫。它不是中間件,而是一個庫。你把它嵌入到程序里,它能用極簡的代碼實現復雜的消息模式(請求-應答、發布-訂閱、推-拉),底層可以跑在 TCP、IPC 甚至內存里。性能極高,但需要學習它那套“無 broker”的思維模式。
MQTT是一個輕量級的發布-訂閱消息協議,它天生是為低帶寬、不可靠網絡設計的,發布/訂閱模式,心跳保活,QoS 控制。一個在火星上跑的機器人,和一個在地球上的控制中心,它們之間用 MQTT 傳控制和遙測數據,就比用 TCP 裸協議要可靠得多,也省電得多。智能家居、工業物聯網、車聯網基本繞不開MQTT。

我的經驗是:
