进程间通信基本知识
进程间通信的定义
进程间通信方式分类
匿名管道(pipe)
匿名管道介绍
- 创建方式:使用
pipe
系统调用创建,返回一对文件描述符(读端和写端)。 - 生命周期:匿名管道的生命周期与创建它的进程有关。当进程结束时,匿名管道会被销毁。
- 作用域:通常用于有亲缘关系的进程(如父子进程)之间的通信。
匿名管道常见用法:
- 创建管道:
pipe(pipefd)
,pipefd是我们自定义的一个数组,将数组传递给pipe函数是为了存储pipe创建匿名管道时提供的两个文件描述符,我们的pipefd
数组中的pipefd[0]
是读端文件描述符,pipefd[1]
是写端文件描述符。 - 写入数据:
write(pipefd[1], data, size)
,将数据写入管道。 - 读取数据:
read(pipefd[0], buffer, size)
,从管道读取数据。 - 关闭管道:使用
close(pipefd[0])
和close(pipefd[1])
关闭读端和写端,释放资源。
创建匿名管道看起来似乎用到的是管道文件来进行通讯,但实际上并不是的,这里和管道文件时没有什么关联的,管道文件是真实存在于文件系统中的一种类型文件,我们可以用ls,rm这些命令对其进行操作。
匿名管道通过操作系统内核维护一个内存缓冲区和一对文件描述符(分别对应于管道的读端和写端)来实现进程之间的通信。当你创建一个匿名管道时,操作系统内核会分配和管理这个内存缓冲区,以支持数据的传输。数据从写端写入缓冲区,另一个进程从读端读取数据。虽然这里的缓冲区不是文件,但是我们可以将匿名管道视作一个文件来对其进行操作,使用read,write,和两个文件描述符来对文件进行读写。
匿名管道的特点
亲缘关系:通常用于有亲缘关系的进程(如父子进程)之间的通信。管道的读端和写端在父进程和子进程之间共享文件描述符。
为什么使用匿名管道通信的进程必须是亲缘关系???请看下面图文解释:
- 当一个进程通过
pipe()
系统调用创建匿名管道时,内核会返回两个文件描述符(一个用于读,一个用于写,对应匿名管道的读端和写端)。这些文件描述符是进程在内核中访问管道的唯一标识。- 继承机制:在UNIX和Linux系统中,当一个进程通过
fork()
创建子进程时,子进程会继承父进程的所有文件描述符。这意味着子进程可以使用与父进程相同的文件描述符来访问管道,从而实现进程间的通信。- 无文件系统接口:匿名管道不会在文件系统中创建实际文件,所以只能依靠文件描述符来访问。如果进程之间没有亲缘关系(如没有使用
fork()
),则它们无法直接共享这些文件描述符。
一对一通信:每个匿名管道只能实现两个进程之间的单向通信。
匿名管道的设计本质上是一种“点对点”通信方式,即两个进程之间直接传输数据。虽然可以让多个进程共享匿名管道的文件描述符,但这会导致数据读取的竞争或写入的混乱,所以并不推荐这样使用。
单向通信:匿名管道是半双工的,这意味着数据只能在一个方向流动。你需要两个匿名管道来实现双向通信,其中一个用于从进程A到进程B的数据流,另一个用于从进程B到进程A的数据流。
匿名管道确实是半双工的,这意味着在给定时间内,数据只能在一个方向上流动。然而,半双工并不禁止你在不同的时间段内切换通信方向——你可以在一个时间段内让进程A写、进程B读,之后再让进程B写、进程A读。
尽管切换通信方式在理论上可行,但在实际开发中,由于其复杂性和潜在的同步问题,通常不会这样操作。更直观、简单的方法是使用两个管道或选择其他更合适的IPC机制。
匿名管道的使用
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
int main(void)
{
// 创建匿名管道
int fd[2];//保存匿名管道的两个文件描述符
pipe(fd);
pid_t pid=fork();
// 子进程
if(pid == 0)
{
// 向父进程打招呼
char *msg = "hello parent!";
//sleep(2);
write(fd[1], msg, strlen(msg));
exit(0);
}
// 父进程
if(pid > 0)
{
char buf[50];
bzero(buf, 50);
// 静静地等待子进程的消息
read(fd[0], buf, 50);
printf("来自子进程: %s\n", buf);
exit(0);
}
}
看到这个代码和结果你可能觉得是偶然,确实,由于父进程和子进程并发执行,前提是子进程先从匿名管道写端写入数据,父进程才能正确读到并打印子进程传输的消息。
所以这里不妨让子进程在写之前sleep(2);结果你会发现父进程仍然成功收到并且打印出了子进程传递信息,只不过是延迟了两秒,证明父进程被阻塞等待子进程传递信息过来,接下来让我们详细说一下匿名管道的读写规则。
匿名管道的读写规则
操作受到阻塞的情况(2)
读写端正常,若管道如果为空,读端就要阻塞;
读写端正常,若管道如果被写满,写端被阻塞;
操作正常执行的情况(4)
读写端正常,读的时候,管道中有数据;
读写端正常,写的时候,管道中未被写满;
读端正常,无写端,读的时候,管道中有数据;
读端正常,无写端,读的时候,管道中无数据,这个read操作返回0,说明管道缓冲区数据读完了,也算read正常执行;
操作不正常的情况(2)
只要没有读端,任何试图写入管道的操作都会触发 SIGPIPE
信号,而不论管道的缓冲区是否已满。
匿名管道无用的文件描述符及时关闭
一般而言,不需要用到的文件描述符都最好及时关闭,避免不必要的副作用或浪费系统资源。例如上述程序中,子进程只用到了管道的写端,因此它的fd[0]可以也应该要关闭,相反父进程只用到了管道的读端,因此它的fd[1]可以也应该关闭。代码可以改成:
int main(void)
{
// 创建匿名管道
int fd[2];
pipe(fd);// 子进程
if(fork() == 0)
{
// 关掉不必要的读端
close(fd[0]);...
}// 父进程
else
{
// 关掉不必要的写端
close(fd[1]);...
}
}
如果我们需要实现双向通信,则最好使用两个管道,然后将里面不必要的文件描述符都及时关闭。