匿名管道
管道也叫无名 (匿名)管道,是 UNIX 系统中IPC(进程间通信)的最古老形式,所有的 UNIX系统都支持这种通信机制。匿名管道只能在具有公共祖先(父子进程或具有亲缘关系的进程)的进程之间使用。
管道其实是内核中的一块缓冲区,进行通信时要先把数据拷贝到内核中,然后通过管道传输后,到达另一端的内核态,再次拷贝到用户态,从而完成通信(通常这种机制也是管道效率较低的表现)。
为什么匿名管道只能用于亲缘关系间通信?
父进程调用fork() 创建的子进程会复制父进程的struct files_struct,同时在这里面fd的数组(也就是文件描述符集合)会复制一份,但是fd指向的struct file对于同一个文件还是只有一份,这样就做到了,两个进程各有两个fd指向同一个struct file的模式,两个进程就可以通过各自的fd写入和读取同一个管道文件实现跨进程通信了。
下面看看管道的有关接口:
#include <unistd.h>
int pipe(int pipefd[2]);
功能:创建一个匿名管道,用来进程间通信。
参数:int pipefd[2] 这个数组是一个传出参数。(会返回两个文件描述符,一个读端,一个写端)
pipefd[0] 对应的是管道的读端
pipefd[1] 对应的是管道的写端
返回值:
成功 0
失败 -1
用匿名管道实现父子进程的通信,子进程发送数据给父进程,父进程读取到数据输出:
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
int pipefd[2]; //先定义好两个文件描述符,作为创建管道函数的参数,0为读端,1为写端
pipe(pipefd); //创建一个匿名管道
// 创建子进程
pid_t pid = fork();
if(pid > 0) {
close(pipefd[1]);
printf("i am parent process \n "); // 父进程
char buf[1024] = {0};
int len = read(pipefd[0], buf , sizeof(buf));
printf("parent recive : %s\n " , buf);
} else if(pid == 0){ // 子进程
close(pipefd[0]);
printf("i am child process \n");
sleep(5); // 子进程睡眠十秒,父进程是读不到数据的,匿名管道默认是阻塞的
char * str = " hello, i am child";
write(pipefd[1] , str , strlen(str));
}
return 0;
}
编译运行后,可以看到运行结果:
补充一点:
上面写原理的时候也提到了,创建管道后父子进程是这个样子的:
仔细查看代码,可以发现父子进程内的代码都提前调用了close()关闭其中管道的其中一端,这是因为管道只能一端写入,另一端读出,如果基于上图这种模式会造成混乱,因为父进程和子进程都可以写入,也都可以读出,通常的方法是父进程关闭读取的fd,只保留写入的fd,而子进程关闭写入的fd,只保留读取的fd,变成下图:
总结一下匿名管道的特点:
read( )读管道时:
管道中有数据时:read( )返回实际读到的字节数。
管道中无数据时:
- 如果写端被全部关闭,read( )返回0,相当于读到文件的末尾。
- 如果写端没有完全关闭,read( )阻塞等待。write( )写管道时:
管道读端全部被关闭:进程异常终止,进程收到SIGPIPE信号。
管道读端没有全部关闭:
-管道已满,write( )阻塞。
-管道没有满,write( )将数据写入,并返回实际写入的字节数。
有名管道
有名管道也叫命名管道,或者叫FIFO文件,不同于匿名管道之处的是:提供了一个路径名与之关联,以 FIFO的文件形式存在于文件系统中,并且其打开方式与打开一个普通文件是一样的,即使与 FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO文件 相互通信。
有名管道可以用命令或者接口创建 :
//用命令mkfifo 创建一个有名管道
mkfifo 管道名字
//用函数创建有名管道
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
参数:
- pathname: 管道名称的路径
- mode: 文件的权限 , 是一个八进制的数
返回值:成功返回0,失败返回-1,并设置错误号
用命令mkfifo myfifo 创建一个有名管道文件 myfifo:
模拟一下两个进程的通信,一个进程往管道写数据write.c , 一个进程往管道读数据read.c,下面看看write.c 的实现:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
// 调用open()拿到管道文件的文件描述符,且以只写的方式打开管道 , 如果你要写 ,那就把读关掉
int fd = open("myfifo", O_WRONLY);
// 写数据
for(int i = 0; i < 100; i++)
{
char buf[1024];
sprintf(buf, "hello, %d\n", i);
printf("write data : %s\n", buf);
write(fd, buf, strlen(buf));
sleep(1);
}
close(fd);
return 0;
}
接下来的read.c 的实现:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
int fd = open("myfifo", O_RDONLY); //以只读的权限打开管道文件
// 读数据
while(1)
{
char buf[1024] = {0};
int len = read(fd , buf ,sizeof(buf));
if(len > 0) // 大于0则表示读取成功,返回的是读到的字节数数量。
{
printf("recv buf : %s\n" , buf);
}
else if(len == 0) //等于0证明管道的写端已经关闭了,数据也读完了。
{
printf("写端断开连接 \n");
break;
}
else if(len == -1) //调用失败
{
printf("异常退出 \n");
}
}
close(fd);
return 0;
}
编译运行一下,如果只运行write.c ,可以看到是处于阻塞等待的情况,因为读端还没有打开:
运行一下 read.c ,可以看到开始通信,写端在往管道写数据:
读端在往管道读数据:
关闭任意一方都会使得程序结束。
最后总结一下有名管道的特点(与匿名管道类似):
当一个进程以只读的方式打开一个管道时会阻塞,直到另外一个进程以只写的方式打开管道后才接触阻塞。(总之就是只有一方打开管道默认是阻塞的)
read( )读管道时:
管道中有数据,read返回实际读到的字节数。
管道中无数据:
-管道写端被全部关闭,read返回0,(相当于读到文件末尾)。
-写端没有全部被关闭,read阻塞等待。
write( )写管道时:
管道读端被全部关闭,进行异常终止(收到一个SIGPIPE信号)
管道读端没有全部关闭:
-管道已经满了,write会阻塞。
-管道没有满,write将数据写入,并返回实际写入的字节数。