进程间通信目的
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止 时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另 一个进程的所有陷入和异常,并能够及时知道它的状态改变。
本质
让不同进程看到同一份资源。
"资源"是一种特定形式的内存空间。
这个资源一般是由操作系统提供,而不是两个进程的其中一个,因为假设由一个进程提供,那么这个资源属于谁,是这个进程独有,那么就会破坏进程的独立性。
我们进程访问这个空间进行通信,本质就是访问操作系统,进程代表的就是用户,资源从创建到使用(一般),再到释放,系统会提供一个系统调用接口来实现(从底层设计,从接口设计都要由操作系统独立设计)所以一般操作系统会有一个独立的通信模块,这个模块隶属于文件系统,这个模块叫做IPC通信模块。
IPC的作用:提供一种受控的机制,允许数据跨越进程边界流动,同时不破坏操作系统的隔离保护。
进程间通信发展
管道(基于文件级别的通信)-->System V进程间通信(本机内部通信)-->POSIX进程间通信(网络通信)
进程间通信分类
管道
概念
管道是Unix中最古老的进程间通信的形式。 我们把从一个进程连接到另一个进程的一个数据流称为一个"管道"
本质
具有"血缘关系"的进程(通常是父子进程或兄弟进程)间的单向字节流。
接口
pipe
基本语法
#include <unistd.h>
int pipe(int pipefd[2]);
参数
pipefd[2]:一个包含2个整数的数组
pipefd[0]:用于读取管道的文件描述符
pipefd[1]:用于写入管道的文件描述符
若成功则返回0,不成功则返回-1
创建pipe
#include <unistd.h>
#include <stdio.h>
int main() {
int pipefd[2];
if (pipe(pipefd) == -1) {
perror("pipe failed");
return 1;
}
printf("Pipe created successfully!\n");
printf("Read end: fd = %d\n", pipefd[0]);
printf("Write end: fd = %d\n", pipefd[1]);
// 使用管道...
// 记得关闭文件描述符
close(pipefd[0]);
close(pipefd[1]);
return 0;
}
特点
匿名管道
概念
通过pipe()系统调用创建,存在于内核中,没有文件系统入口。只能用于有亲缘关系的进程。
关键特性
单向性:数据只能从一端写入,从另一端读取。这形成了经典的"生产者-消费者"模型。
亲缘关系限制:通常由父进程创建,然后通过fork()将管道的文件描述符复制给子进程,从而实现通信。
字节流导向:不维护消息边界。写入端多次写入的"Hello"和"World",在读取端可能被一次读取为"HelloWorld"。应用层需要自己定义消息分隔协议。
生命周期随进程:当所有引用该管道的进程都终止后,管道资源会被内核自动回收。
工作原理
(1)创建管道:父进程调用 int pipe(int fd[2])系统调用。内核会创建一个管道,并返回两个文件描述符:fd[0]:用于读取管道。fd[1]:用于写入管道。
(2)创建子进程:父进程调用 fork()。此时,子进程继承了父进程打开的文件描述符表,因此它也拥有指向同一个内核管道的fd[0]和fd[1]。
(3)关闭不需要的端口:由于管道是单向的,为了让数据从父流向子:父进程关闭它的读端 close(fd[0])。子进程关闭它的写端 close(fd[1])。反之,如果想让数据从子流向父,则关闭相反的描述符。
(4)进行通信:父进程用 write(fd[1], buf, size)向管道写数据。子进程用 read(fd[0], buf, size)从管道读数据。
(5)通信结束:进程关闭所有描述符,当没有进程再持有管道的写端描述符时,读端会收到EOF。
站在文件描述符角度理解管道

站在内核角度理解管道

内核与底层
缓冲区:管道在内核中有一个固定大小的缓冲区(通常为4KB或64KB,可通过fcntl设置)。写操作将数据复制到内核缓冲区,读操作从缓冲区复制数据到用户空间。
阻塞与非阻塞:
读空管道:如果管道为空,读操作默认阻塞,直到有数据写入。
写满管道:如果管道已满,写操作默认阻塞,直到有数据被读出腾出空间。
可以使用fcntl设置文件描述符为O_NONBLOCK来改为非阻塞模式。
同步与互斥:内核保证了管道读写的原子性。小于管道缓冲区大小(PIPE_BUF,通常是512字节或4KB)的写操作是原子的,即不会被其他写入操作的数据穿插。
优点
(1)简单高效:是系统调用,不涉及磁盘I/O,数据在内核和用户空间间复制一次。
(2)无需同步代码:内核自动处理读写同步(阻塞/唤醒)。
(3)资源自动管理。
缺点
(1)只能用于亲缘进程。
(2)单向通信。要实现双向通信,需要创建两个管道。
(3)传输的是字节流,无消息边界,对结构化数据不友好。
(4)生命周期短,随进程结束。
匿名管道代码
(1)父进程向子进程发送消息
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main() {
int pipefd[2];
pid_t pid;
char buffer[100];
if (pipe(pipefd) == -1) {
perror("pipe创建失败");
exit(EXIT_FAILURE);
}
printf("管道创建成功: fd[0]=%d, fd[1]=%d\n", pipefd[0], pipefd[1]);
pid = fork();
if (pid < 0) {
perror("fork失败");
exit(EXIT_FAILURE);
}
if (pid > 0) {
printf("=== 父进程 (PID=%d) ===\n", getpid());
close(pipefd[0]);
char *message = "Hello from parent process!";
printf("父进程准备发送消息: %s\n", message);
write(pipefd[1], message, strlen(message) + 1); // +1包含'\0'
printf("父进程已发送消息\n");
close(pipefd[1]);
printf("父进程关闭了写端 fd[1]\n");
wait(NULL);
printf("子进程已结束,父进程退出\n");
} else { // 子进程 (pid == 0)
printf("=== 子进程 (PID=%d) ===\n", getpid());
close(pipefd[1]);
ssize_t bytes_read = read(pipefd[0], buffer, sizeof(buffer));
printf("子进程接收到 %ld 字节数据\n", bytes_read);
if (bytes_read > 0) {
printf("子进程收到的消息: %s\n", buffer);
}
close(pipefd[0]);
printf("子进程关闭了读端 fd[0]\n");
printf("子进程退出\n");
}
return 0;
}
(2)父子进程互相发送消息
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main() {
int pipe1[2];
int pipe2[2];
pid_t pid;
char buffer[100];
if (pipe(pipe1) == -1 || pipe(pipe2) == -1) {
perror("管道创建失败");
exit(EXIT_FAILURE);
}
printf("pipe1: 父[%d]->子[%d]\n", pipe1[1], pipe1[0]);
printf("pipe2: 子[%d]->父[%d]\n", pipe2[1], pipe2[0]);
pid = fork();
if (pid < 0) {
perror("fork失败");
exit(EXIT_FAILURE);
}
if (pid > 0) {
printf("=== 父进程 (PID=%d) ===\n", getpid());
close(pipe1[0]);
close(pipe2[1]);
char *msg_to_child = "Hello child! This is your parent.";
printf("父进程发送消息: %s\n", msg_to_child);
write(pipe1[1], msg_to_child, strlen(msg_to_child) + 1);
ssize_t bytes = read(pipe2[0], buffer, sizeof(buffer));
if (bytes > 0) {
printf("父进程收到子进程消息: %s\n", buffer);
}
bytes = read(pipe2[0], buffer, sizeof(buffer));
if (bytes > 0) {
printf("父进程收到子进程回复: %s\n", buffer);
}
char *end_msg = "Goodbye child!";
write(pipe1[1], end_msg, strlen(end_msg) + 1);
close(pipe1[1]);
close(pipe2[0]);
wait(NULL);
printf("父进程退出\n");
} else {
printf("=== 子进程 (PID=%d) ===\n", getpid());
close(pipe1[1]);
close(pipe2[0]);
ssize_t bytes = read(pipe1[0], buffer, sizeof(buffer));
if (bytes > 0) {
printf("子进程收到父进程消息: %s\n", buffer);
}
// 回复父进程
char *reply1 = "Hi parent! I got your message.";
printf("子进程回复父进程: %s\n", reply1);
write(pipe2[1], reply1, strlen(reply1) + 1);
char *reply2 = "How are you today?";
write(pipe2[1], reply2, strlen(reply2) + 1);
bytes = read(pipe1[0], buffer, sizeof(buffer));
if (bytes > 0) {
printf("子进程收到父进程消息: %s\n", buffer);
}
close(pipe1[0]);
close(pipe2[1]);
printf("子进程退出\n");
}
return 0;
}
命名管道 (FIFO)
概念
通过mkfifo()创建,在文件系统中有一个路径名(如 /tmp/myfifo)。任何知道该名字的进程都可以打开它进行通信,突破了亲缘关系限制。
关键特性
可以用于任意进程间通信,不限于亲缘关系
遵循先进先出(FIFO)原则
数据在内核中缓冲,不实际写入磁盘
创建命名管道
(1)命令行创建
使用mkfifo命令
$ mkfifo mypipe
(2)使用C语言创建
#include <sys/types.h>
#include <sys/stat.h>
// 方法1:使用 mkfifo 函数
int mkfifo(const char *pathname, mode_t mode);
// 方法2:使用 mknod 函数(更通用)
int mknod(const char *pathname, mode_t mode, dev_t dev);
两个独立进程完整通信
(1)写入者
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#define FIFO_PATH "/tmp/myfifo"
int main() {
int fd;
char message[100];
printf("Writer Process (PID=%d)\n", getpid());
if (mkfifo(FIFO_PATH, 0666) == -1) {
if (errno != EEXIST) {
perror("mkfifo");
exit(EXIT_FAILURE);
}
}
printf("Opening FIFO for writing...\n");
fd = open(FIFO_PATH, O_WRONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
printf("FIFO opened successfully!\n");
for (int i = 1; i <= 5; i++) {
snprintf(message, sizeof(message),
"Message %d from writer (PID=%d)", i, getpid());
printf("Writing: %s\n", message);
ssize_t bytes = write(fd, message, strlen(message) + 1);
if (bytes == -1) {
perror("write");
break;
}
sleep(1);
}
write(fd, "END", 4);
close(fd);
printf("Writer finished.\n");
return 0;
}
(2)读取者
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#define FIFO_PATH "/tmp/myfifo"
int main() {
int fd;
char buffer[256];
printf("Reader Process (PID=%d)\n", getpid());
printf("Opening FIFO for reading...\n");
fd = open(FIFO_PATH, O_RDONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
printf("FIFO opened successfully!\n");
while (1) {
memset(buffer, 0, sizeof(buffer));
ssize_t bytes = read(fd, buffer, sizeof(buffer) - 1);
if (bytes <= 0) {
printf("EOF reached or error\n");
break;
}
printf("Received: %s\n", buffer);
if (strcmp(buffer, "END") == 0) {
printf("Received END signal, exiting...\n");
break;
}
}
close(fd);
unlink(FIFO_PATH);
printf("Reader finished.\n");
return 0;
}
命名管道与匿名管道区别
|--------|------------|---------------------|
| 特性 | 匿名管道 | 命名管道 |
| 文件系统可见 | 否 | 是 |
| 进程关系 | 必须有亲缘关系 | 任意进程 |
| 创建方式 | pipe()系统调用 | mkfifo()函数或 mknod() |
| 生命周期 | 随进程结束 | 持久存在,直至被删除 |
| 打开方式 | 通过继承的文件描述符 | 通过路径名打开 |