一、什么是进程间通信?
说白了,进程间通信就是让两个或多个进程之间能互相传数据、发信号。你想想看,每个进程都有自己的独立地址空间,一个进程根本看不到另一个进程的变量。那它们怎么协作呢?
我刚开始学嵌入式Linux时,总觉得这玩意儿离我很远。直到有一次做多进程数据采集系统,主进程要等三个子进程都采集完数据才能做融合处理。嗯,那时候我才意识到——没有IPC,多进程就是个摆设。
为什么需要IPC?
- 数据共享:一个进程算出来的结果,另一个进程要用
- 任务协作:多个进程分工干活,最后汇总
- 资源互斥:防止两个进程同时写同一个文件
- 事件通知:一个进程干完了,告诉另一个进程可以开始了
核心要点:IPC不是锦上添花,而是多进程编程的刚需。没有IPC,进程之间就是孤岛。
二、IPC的分类全景图
我习惯把IPC分成两大类:同一台机器上的通信和跨机器的通信。前者包括管道、信号、共享内存、消息队列,后者就是套接字。
下面这张图帮你理清脉络:
三、五种IPC机制详解
1. 管道(Pipe)
管道是最古老的IPC方式。我当年学Linux编程时,第一个接触的就是它。说白了,管道就是一个内核缓冲区,一个进程往里写,另一个从里读。
两种管道:
- 匿名管道:只能用于父子进程或有亲缘关系的进程。用
pipe()创建。 - 命名管道(FIFO):通过文件系统中的一个特殊文件通信,无亲缘关系的进程也能用。
// 匿名管道示例
#include <unistd.h>
#include <stdio.h>
int main() {
int fd[2];
pid_t pid;
char buf[128];
if (pipe(fd) == -1) {
perror("pipe");
return 1;
}
pid = fork();
if (pid == 0) {
// 子进程:写
close(fd[0]);
write(fd[1], "Hello from child", 17);
close(fd[1]);
} else {
// 父进程:读
close(fd[1]);
read(fd[0], buf, sizeof(buf));
printf("Parent received: %s\n", buf);
close(fd[0]);
}
return 0;
}
避坑指南:我曾经在项目中忘记关闭不需要的管道端,结果子进程一直阻塞在read上。记住:写端用完要关,读端用完也要关。不用的端先关掉,这是铁律。
2. 信号(Signal)
信号是一种异步通信方式。进程收到信号时,会打断当前执行流,去处理信号。有点像硬件中断的软件版。
常见的信号:
SIGINT:Ctrl+C 触发,终止进程SIGKILL:强制杀死进程,不能捕获SIGUSR1/SIGUSR2:用户自定义信号
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handler(int sig) {
printf("Caught signal %d\n", sig);
}
int main() {
signal(SIGINT, handler);
while(1) {
printf("Running...\n");
sleep(1);
}
return 0;
}
注意:信号处理函数里不要调用 printf 这类不可重入函数。我踩过这个坑——信号处理时堆栈状态不确定,printf 可能把整个程序搞崩。建议只设置标志位,在主循环里处理。
3. 共享内存(Shared Memory)
共享内存是效率最高的IPC方式。为什么?因为它不需要数据拷贝。多个进程直接映射同一块物理内存,读写就像操作本地变量一样快。
我做过一个实时视频处理项目,用管道传数据太慢,换成共享内存后延迟从毫秒级降到了微秒级。嗯,差距就是这么大。
#include <sys/shm.h>
#include <stdio.h>
int main() {
key_t key = 1234;
int shmid = shmget(key, 1024, IPC_CREAT | 0666);
char *data = (char*)shmat(shmid, NULL, 0);
sprintf(data, "Hello from process");
printf("Data: %s\n", data);
shmdt(data);
return 0;
}
核心要点:共享内存本身不提供同步机制。多个进程同时写同一块内存,数据就乱了。必须配合信号量或互斥锁使用。
4. 消息队列(Message Queue)
消息队列是管道的一种升级版。它支持按消息类型读取,不像管道那样只能先进先出。每个消息有类型标识,接收方可以只读自己关心的类型。
| 特性 | 管道 | 消息队列 |
|---|---|---|
| 数据格式 | 字节流 | 结构化消息 |
| 读取方式 | FIFO | 按类型选择 |
| 生命周期 | 随进程 | 内核持久 |
| 最大长度 | 有限 | 可配置 |
#include <sys/msg.h>
#include <stdio.h>
struct msgbuf {
long mtype;
char mtext[100];
};
int main() {
key_t key = 5678;
int msqid = msgget(key, IPC_CREAT | 0666);
struct msgbuf msg = {1, "Hello"};
msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
printf("Message sent\n");
return 0;
}
5. 套接字(Socket)
套接字是IPC的终极方案。它不仅能用于同一台机器的进程通信,还能跨网络通信。你上网、发微信、看视频,底层都是套接字在干活。
套接字分两种:
- Unix域套接字:用于本地通信,效率高,比TCP快
- 网络套接字:用于跨网络通信,基于TCP/UDP
// 简化的TCP客户端
#include <sys/socket.h>
#include <netinet/in.h>
int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
connect(sock, (struct sockaddr*)&addr, sizeof(addr));
send(sock, "Hello", 5, 0);
close(sock);
四、如何选择?
我个人的选择原则很简单:
- 简单场景:父子进程传数据,用管道就够了
- 高性能场景:大量数据频繁交换,上共享内存
- 异步通知:信号是最轻量的方式
- 结构化通信:消息队列比管道好用
- 跨机器:别无选择,套接字
我的经验:初学者容易一上来就用共享内存,觉得它快。但其实90%的场景管道或消息队列就够用了。共享内存带来的同步问题,往往比性能提升更让人头疼。先选简单的,不够再升级。
好了,IPC的入门就讲到这里。记住一句话:IPC不是技术难题,而是设计选择。选对了,代码清爽;选错了,调试到天亮。