引言
进程间通信(Inter-Process Communication,IPC)是指在不同进程之间传递数据或信号的机制。由于进程有独立的地址空间,操作系统必须提供专门的机制来实现进程间通信。
1. 本机通信
1.1 管道
管道是最古老的IPC形式,用于父子进程或兄弟进程间的通信。
c
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main() {
int fd[2]; // fd[0]: 读端, fd[1]: 写端
pid_t pid;
char buffer[100];
// 1. 创建管道
if (pipe(fd) == -1) {
perror("pipe failed");
return 1;
}
pid = fork(); // 创建子进程
if (pid < 0) {
perror("fork failed");
return 1;
}
if (pid > 0) { // 父进程
close(fd[0]); // 关闭读端
char *message = "Hello from parent!";
write(fd[1], message, strlen(message) + 1);
close(fd[1]);
wait(NULL); // 等待子进程
} else { // 子进程
close(fd[1]); // 关闭写端
read(fd[0], buffer, sizeof(buffer));
printf("Child received: %s\n", buffer);
close(fd[0]);
}
return 0;
}
管道的特点:
| 特性 | 说明 |
|---|---|
| 单向通信 | 数据只能单向流动 |
| 亲缘关系 | 只能用于父子/兄弟进程 |
| 字节流 | 无消息边界 |
| 缓存有限 | 通常 4KB-64KB |
| 生命周期 | 随进程结束而销毁 |
1.2 命名管道(FIFO)
命名管道突破了管道只能用于亲缘进程的限制。
c
// writer.c
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
int main() {
const char *fifo_path = "/tmp/myfifo";
// 创建命名管道
mkfifo(fifo_path, 0666);
int fd = open(fifo_path, O_WRONLY);
write(fd, "Hello FIFO!", 12);
close(fd);
return 0;
}
// reader.c
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main() {
const char *fifo_path = "/tmp/myfifo";
char buffer[100];
int fd = open(fifo_path, O_RDONLY);
read(fd, buffer, sizeof(buffer));
printf("Received: %s\n", buffer);
close(fd);
unlink(fifo_path); // 删除FIFO文件
return 0;
}
1.2 消息队列(Message Queue)
消息队列是内核维护的消息链表,支持多对多通信。
(note: 这里和后端常用中间件如kafaka等消息队列不一样
c
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
struct msg_buffer {
long msg_type;
char msg_text[100];
};
int main() {
key_t key = ftok("progfile", 65); // 生成key
int msgid = msgget(key, 0666 | IPC_CREAT);
struct msg_buffer message;
// 发送消息
message.msg_type = 1;
strcpy(message.msg_text, "Hello Message Queue!");
msgsnd(msgid, &message, sizeof(message), 0);
// 接收消息
msgrcv(msgid, &message, sizeof(message), 1, 0);
printf("Received: %s\n", message.msg_text);
// 删除消息队列
msgctl(msgid, IPC_RMID, NULL);
return 0;
}
消息队列特点:
- 独立于进存在
- 支持不同类型的消息
- 异步通信
- 内核持久化
1.4 共享内存(Shared Memory)
最快的IPC方式,多个进程映射到同一物理内存。
c
#include <sys/shm.h>
#include <stdio.h>
#include <string.h>
int main() {
// 1. 创建共享内存段
key_t key = ftok("shmfile", 65);
int shmid = shmget(key, 1024, 0666 | IPC_CREAT);
// 2. 附加到进程地址空间
char *str = (char*)shmat(shmid, NULL, 0);
// 3. 写入数据
printf("Write Data: ");
fgets(str, 1024, stdin);
printf("Data written in memory: %s\n", str);
// 4. 分离共享内存
shmdt(str);
// 5. 另一个进程可以读取
// 6. 删除共享内存
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
1.5 信号量
信号量(Semaphore)用于进程同步,控制对共享资源的访问。
c
#include <sys/sem.h>
#include <stdio.h>
#include <unistd.h>
int main() {
key_t key = ftok("semfile", 65);
int semid = semget(key, 1, 0666 | IPC_CREAT);
// 初始化信号量为1
union semun arg;
arg.val = 1;
semctl(semid, 0, SETVAL, arg);
pid_t pid = fork();
if (pid == 0) { // 子进程
struct sembuf wait_op = {0, -1, SEM_UNDO}; // P操作
printf("Child waiting for semaphore...\n");
semop(semid, &wait_op, 1);
printf("Child entered critical section\n");
sleep(2); // 模拟临界区操作
struct sembuf signal_op = {0, 1, SEM_UNDO}; // V操作
semop(semid, &signal_op, 1);
printf("Child left critical section\n");
} else { // 父进程
sleep(1); // 让子进程先运行
struct sembuf wait_op = {0, -1, SEM_UNDO};
printf("Parent waiting for semaphore...\n");
semop(semid, &wait_op, 1);
printf("Parent entered critical section\n");
sleep(1);
struct sembuf signal_op = {0, 1, SEM_UNDO};
semop(semid, &signal_op, 1);
printf("Parent left critical section\n");
}
// 清理
if (pid > 0) {
wait(NULL);
semctl(semid, 0, IPC_RMID);
}
return 0;
}
1.6 信号(Signal)
软件中断,用于通知进程事件发生。
c
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void signal_handler(int sig) {
printf("Received signal %d\n", sig);
}
int main() {
// 注册信号处理函数
signal(SIGINT, signal_handler); // Ctrl+C
signal(SIGTERM, signal_handler); // 终止信号
signal(SIGUSR1, signal_handler); // 用户自定义信号
printf("Process PID: %d\n", getpid());
printf("Send signals with: kill -SIGUSR1 %d\n", getpid());
// 发送信号
raise(SIGUSR1); // 给自己发送信号
// 等待信号
while(1) {
printf("Waiting...\n");
sleep(1);
}
return 0;
}
| 信号 | 值 | 描述 | 默认动作 |
|---|---|---|---|
| SIGHUP | 1 | 终端挂起 | 终止 |
| SIGINT | 2 | 中断(Ctrl+C) | 终止 |
| SIGQUIT | 3 | 退出(Ctrl+) | 终止+core |
| SIGKILL | 9 | 强制终止 | 终止 |
| SIGTERM | 15 | 软件终止 | 终止 |
| SIGUSR1 | 10 | 用户自定义1 | 终止 |
| SIGUSR2 | 12 | 用户自定义2 | 终止 |
1.7 套接字(Socket)
最通用的IPC方式,支持网络通信和本地通信。
UNIX域套接字(本地通信)
c
// server.c
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdio.h>
int main() {
int server_fd, client_fd;
struct sockaddr_un server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
char buffer[100];
// 创建UNIX域套接字
server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
// 绑定地址
server_addr.sun_family = AF_UNIX;
strcpy(server_addr.sun_path, "/tmp/mysocket");
unlink(server_addr.sun_path); // 删除已存在的socket文件
bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
// 监听
listen(server_fd, 5);
// 接受连接
client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
// 接收数据
read(client_fd, buffer, sizeof(buffer));
printf("Server received: %s\n", buffer);
// 发送响应
write(client_fd, "Hello from server!", 19);
close(client_fd);
close(server_fd);
unlink("/tmp/mysocket");
return 0;
}
// client.c
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdio.h>
int main() {
int sock_fd;
struct sockaddr_un server_addr;
char buffer[100];
sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
server_addr.sun_family = AF_UNIX;
strcpy(server_addr.sun_path, "/tmp/mysocket");
connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
write(sock_fd, "Hello from client!", 19);
read(sock_fd, buffer, sizeof(buffer));
printf("Client received: %s\n", buffer);
close(sock_fd);
return 0;
}
网络套接字(跨机器通信)
c
// TCP服务器示例
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main() {
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(8080);
bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
listen(server_fd, 10);
printf("Server listening on port 8080...\n");
while(1) {
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
char buffer[1024];
read(client_fd, buffer, sizeof(buffer));
printf("Received: %s\n", buffer);
char *response = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, World!";
write(client_fd, response, strlen(response));
close(client_fd);
}
close(server_fd);
return 0;
}
1.8 内存映射文件(Memory-Mapped File)
c
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main() {
const char *filename = "shared.txt";
const int size = 4096;
// 打开文件
int fd = open(filename, O_RDWR | O_CREAT, 0666);
ftruncate(fd, size); // 设置文件大小
// 内存映射
char *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// 写入数据
sprintf(ptr, "Hello from process 1!");
// 同步到磁盘
msync(ptr, size, MS_SYNC);
// 另一个进程可以读取相同文件
printf("Data: %s\n", ptr);
// 清理
munmap(ptr, size);
close(fd);
return 0;
}
小结
| IPC方式 | 通信方向 | 进程关系 | 速度 | 使用复杂度 | 数据量 | 适用场景 |
|---|---|---|---|---|---|---|
| 管道 | 单向 | 亲缘进程 | 中 | 低 | 小 | 父子进程通信 |
| 命名管道 | 单向 | 任意进程 | 中 | 低 | 小 | 持久化进程通信 |
| 消息队列 | 双向 | 任意进程 | 中 | 中 | 中 | 结构化消息传递 |
| 共享内存 | 双向 | 任意进程 | 最快 | 高 | 大 | 高性能数据共享 |
| 信号量 | 同步 | 任意进程 | 快 | 中 | 小 | 进程同步控制 |
| 信号 | 单向 | 任意进程 | 快 | 低 | 极小 | 事件通知 |
| 套接字 | 双向 | 任意进程(可跨网络) | 慢 | 高 | 大 | 网络通信/分布式 |
| 内存映射 | 双向 | 任意进程 | 快 | 中 | 大 | 文件共享 |
| 场景 | 推荐IPC方式 | 理由 |
|---|---|---|
| 父子进程简单通信 | 匿名管道 | 简单、高效 |
| 无亲缘关系进程通信 | 命名管道/UNIX套接字 | 支持任意进程 |
| 高性能数据共享 | 共享内存+信号量 | 最快速度 |
| 结构化消息传递 | 消息队列 | 支持消息类型 |
| 进程同步 | 信号量 | 专门用于同步 |
| 事件通知 | 信号 | 轻量级通知 |
| 网络通信 | 套接字 | 支持跨机器 |
| 文件数据共享 | 内存映射文件 | 高效文件访问 |
2. 现代编程语言中的IPC实现
python
# 1. 管道
import os, sys
def pipe_example():
r, w = os.pipe()
pid = os.fork()
if pid > 0: # 父进程
os.close(r)
os.write(w, b"Hello from parent")
os.close(w)
else: # 子进程
os.close(w)
data = os.read(r, 100)
print(f"Child received: {data.decode()}")
os.close(r)
# 2. 命名管道(FIFO)
import os
from multiprocessing import Process
def fifo_writer():
if not os.path.exists("/tmp/myfifo"):
os.mkfifo("/tmp/myfifo")
with open("/tmp/myfifo", "w") as f:
f.write("Hello FIFO")
def fifo_reader():
with open("/tmp/myfifo", "r") as f:
print(f.read())
# 3. 共享内存(multiprocessing模块)
from multiprocessing import Process, Value, Array
def worker(n, arr):
n.value += 1
for i in range(len(arr)):
arr[i] *= 2
if __name__ == '__main__':
num = Value('i', 0) # 共享整数
arr = Array('d', [1.0, 2.0, 3.0]) # 共享数组
p = Process(target=worker, args=(num, arr))
p.start()
p.join()
print(num.value) # 1
print(arr[:]) # [2.0, 4.0, 6.0]
java
// 1. 管道
import java.io.*;
public class PipeExample {
public static void main(String[] args) throws IOException {
PipedOutputStream output = new PipedOutputStream();
PipedInputStream input = new PipedInputStream(output);
new Thread(() -> {
try {
output.write("Hello Pipe".getBytes());
output.close();
} catch (IOException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
int data;
while ((data = input.read()) != -1) {
System.out.print((char) data);
}
input.close();
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
// 2. 内存映射文件
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
public class MappedFileExample {
public static void main(String[] args) throws Exception {
RandomAccessFile file = new RandomAccessFile("shared.dat", "rw");
FileChannel channel = file.getChannel();
// 映射100MB文件到内存
MappedByteBuffer buffer = channel.map(
FileChannel.MapMode.READ_WRITE, 0, 100 * 1024 * 1024);
// 写入数据
buffer.put("Hello Mapped File".getBytes());
// 另一个进程可以读取相同文件
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
System.out.println(new String(data));
channel.close();
file.close();
}
}
go
package main
import (
"fmt"
"net"
"os"
"time"
)
// 1. 管道(Go的管道是channel)
func goPipe() {
ch := make(chan string)
go func() {
ch <- "Hello from goroutine"
}()
msg := <-ch
fmt.Println(msg)
}
// 2. UNIX域套接字
func unixSocket() {
socketPath := "/tmp/go.sock"
// 服务器
go func() {
os.Remove(socketPath)
l, err := net.Listen("unix", socketPath)
if err != nil {
panic(err)
}
defer l.Close()
conn, err := l.Accept()
if err != nil {
panic(err)
}
defer conn.Close()
buf := make([]byte, 512)
n, err := conn.Read(buf)
if err != nil {
panic(err)
}
fmt.Printf("Server received: %s\n", buf[:n])
}()
time.Sleep(time.Second)
// 客户端
conn, err := net.Dial("unix", socketPath)
if err != nil {
panic(err)
}
defer conn.Close()
_, err = conn.Write([]byte("Hello UNIX socket"))
if err != nil {
panic(err)
}
}
// 3. 共享内存(通过mmap)
import "golang.org/x/exp/mmap"
func sharedMemory() {
// 打开内存映射文件
reader, err := mmap.Open("shared.dat")
if err != nil {
panic(err)
}
defer reader.Close()
// 读取数据
data := make([]byte, 100)
reader.ReadAt(data, 0)
fmt.Println(string(data))
}