前言
本文主要介绍了Linux编程中进程间通信方式中的管道,内容包括管道的原理、作用,pipe()函数的使用以及注意事项等。通过图文和代码相结合的方式帮助大家更好地理解管道这一概念,以及如何更好更合理地去使用管道,希望能够对大家有所帮助。如文章有出现错误的地方,欢迎大家扶正错误,同时也欢迎大家多多提一些建议。
在了解管道之间,我们首先需要简单了解下进程之间的通信方式有哪些?这些进程间的通信方式各自的优势是什么?管道的作用是什么?管道的原理又是什么?等等问题。让我们带着这些问题往下学习,相信你会有更多的收获。下面我们就开始本文的正文部分。
进程间的通信
每个进程各自有不同的用户地址空间,任何一个进程的变量在另一个中都是无法看到的,数据相互独立。所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信,即IPC(interProcess communication)。
进程间完成数据传递需要借助操作系统提供特殊的方法,如:文件、管道、信号、共享内存、消息队列、套接字、命名管道等。随着计算机的发展,有一些方法由于自身设计缺陷被淘汰或者弃用,如今常用的方式有:
- 管道(使用最简单)
- 信号(开销最小)
- 共享映射区(无血缘关系)
- 本地套接字(最稳定)
管道
管道的概念
管道是一种最基本的IPC机制,把一个进程连接到另一个进程的一个数据流称为一个“管道”,通常是用作把一个进程的输出通过管道连接到另一个进程的输入,作用于有血缘关系的进程之间,完成数据传递,管道是内核空间中的一块缓冲区,默认大小为4096
字节。调用pipe
系统函数即可创建并打开一个管道。
举例说明
在shell
中执行命令,经常会将上一个命令的输出作为下一个命令的输入,由多个命令配合完成一件事情。而这就是通过管道来实现的。|
这个竖线就是管道符号
ls -l | grep string //grep是抓取指令
ls
命令(其实也是一个进程)会把当前目录中的文件都列出来,但它不会直接输出,而是把要输出到屏幕上的数据通过管道输出到grep
这个进程中,作为grep
这个进程的输入;然后这个进程对输入的信息进行筛选(grep
的作用),把存在string
的信息的字符串(以行为单位)打印在屏幕上。
如图所示:
管道的特性
- 其本质是一个伪文件(实为内核缓冲区)。
- 由两个文件描述符引用,一个表示读端,一个表示写端。
- 规定数据从管道的写端流入管道,从读端流出。
- 管道不是普通的文件,不属于某个文件系统,其只存在于内存中,俗称伪文件,不需要占用磁盘空间。
- 管道没有名字,只能在具有公共祖先的进程(父进程和子进程,或两个兄弟进程)之间使用
- 管道随进程,进程在管道在,进程消失管道对应的端口也关闭,两个进程都消失管道也消失。
管道的原理
管道实为内核使用环形队列机制,借助内核缓冲区实现。
管道的局限性
- 管道不允许进程自己写,自己读。
- 管道中数据不可反复读取。一旦读走,管道中数据不再存在。
- 采用半双工通信方式,数据在同一时刻只能在一个方向上流动(单向流动)。
- 只能再有公共祖先的进程间使用管道。
这里简单举例子介绍一下通信方式,方便大家理解:
单工:遥控器,一端只负责发射信号,一端只负责接收信号。
双向半双工:对讲机,两端都可以发射和接受信号,但是不允许同时进行。
双向全双工:手机,运行同时发射和接收信号。
管道的读写行为
读管道:
- 管道有数据,
read
返回实际读取到的字节数。 - 管道无数据
- 管道写端被全部关闭,
read
返回0。(类似读到文件尾) - 管道写端没有被全部关闭,
read
阻塞等待(不久可能有数据传达,此时会让出cpu)。
- 管道写端被全部关闭,
写管道:
- 管道读端全部被关闭,进程异常终止(也可捕捉SIGPIPE信号,使进程不终止)。
- 管道读端没有被全部关闭
- 管道已满,
write
阻塞。 - 管道未满,
write
将数据写入,并返回实际写入的字节数。
- 管道已满,
pipe()函数
用于创建并打开一个管道,实现进程间的通信。
SYNOPSIS
#include <unistd.h>
int pipe(int pipefd[2]);
参数
pipefd
:是文件描述符数组,其中fd[0]
表示读端,fd[1]
表示写端。
返回值
- 若函数顺利执行,则返回0。
- 若发生错误,则返回-1,并设置
errno
。
pipe()函数的基本用法
父子进程之间通过管道进行通信。
#include <stdio.h>
#include <sys/types.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main()
{
int ret;
int fd[2];//数组
pid_t pid;
char *str = "hello pipe\n";
char buf[1024];
//创建管道
ret = pipe(fd);
if(ret == -1)//当ret为-1时,说明pipe函数发生错误执行失败
{
perror("pipe error\n");
exit(1);
}
//创建子进程
pid = fork();
if(pid > 0)//父进程
{
close(fd[0]);//父进程关闭读端
write(fd[1],str,strlen(str));
close(fd[1]);
sleep(1);//休眠一秒,目的是为了使子进程先于父进程终止,避免出现孤儿进程。
}
else if(pid == 0)//子进程
{
close(fd[1]);//子进程关闭写端
ret = read(fd[0],buf,sizeof(buf));//子进程读取管道中的数据
write(STDOUT_FILENO,buf,ret);//写进标准输出中去
close(fd[0]);
}
else//进程创建错误
{
perror("fork error\n");
exit(1);
}
return 0;
}
运行结果如下:
[root@model function]# ./pipe
hello pipe
[root@model function]#
注意
创建管道应该在创建进程之前进行。这是因为管道的文件描述符需要在父进程中创建,然后通过 fork
传递到子进程(以确保子进程能够继承管道的文件描述符)。管道的本质是父子进程之间共享的通信通道,而这种共享是通过 fork
复制文件描述符表实现的。
如果在 fork
之后创建管道,子进程不会继承管道的文件描述符,因此无法使用它进行通信。
使用pipe函数实现父子进程间管道原理
使用
pipe、dup2、fork、execlp
函数实现ps aux | grep pipe
命令。
思路
- 创建管道
- 创建子进程
- 子进程中,关闭读端,重定向标准输入到管道写端。
- 父进程中,关闭写端,重定向标准输出到管道读端。
- 子进程关闭写端,再通过execlp函数实现
ps aux
命令,同时设置错误提示信息。- 父进程关闭读端,再通过execlp函数实现
grep pipe
命令,同时设置错误提示信息。
代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <string.h>
int main()
{
pid_t pid;
int fd[2];
int ret;
//创建管道,创建管道要在创建进程之前完成
ret = pipe(fd);
if(ret == -1)
{
perror("pipe error\n");
exit(1);
}
//创建子进程
pid = fork();
if(pid == 0)//子进程,子进程中执行ps aux命令
{
close(fd[0]);//关闭读端
dup2(fd[1],STDOUT_FILENO);//重定向标准输出到写端
close(fd[1]);
sleep(2);
//execlp函数实现ps aux命令,父进程中同理
execlp("ps","ps","aux",NULL);
//设置错误提示信息
perror("execlp error\n");
exit(1);
}
else if(pid > 0)//父进程,父进程中执行grep pipe命令
{
close(fd[1]);//关闭写端
dup2(fd[0],STDIN_FILENO);//重定向标准输入到读端
close(fd[0]);
//execlp函数实现grep pipe命令
execlp("grep","grep","pipe",NULL);
//设置错误提示信息
perror("execlp error\n");
exit(1);
//回收子进程
/*
int status;
waitpid(pid,&status,0);
if(WIFEXITED(status))
{
printf("child exited with status %d\n",WIFEXITED(status));
}
*/
}
else
{
perror("fork error\n");
exit(1);
}
return 0;
}
运行结果如下:
[root@model function]# ./temp
root 4784 0.0 0.0 112828 968 pts/0 R+ 12:54 0:00 grep pipe
[root@model function]#
注意
- 管道要在创建子进程之间创建,否则子进程无法继承父进程的管道,导致进程间通信不成功。
- 建议先在子进程中执行命令,然后通过管道输出,作为输入给父进程,再执行父进程中的命令。因为我在之前实验中,采用的是父进程先执行
ps aux
命令,通过管道,给子进程作为输入,但是无法达到预期的效果。个人以为出现这样的情况是,父进程先执行完之后,进程就终止了,同时因为传输的数据内容较少,子进程还没有来得及读取,父进程已经结束,父进程先于子进程结束,导致出现了孤儿进程,继而被init进程接替,所以无法实现预期的效果。这仅是我个人的猜想,目前还没有找到确切的理论验证。 - 通过写端在管道中写完数据之后,有一个关闭写端的操作,之前我疑惑关闭之后,会不会导致
execlp
函数的执行的结果无法写入到管道中去。但其实并不会阻止数据写入管道,因为是通过dup2
重定向了标准输出(STDOUT_FILENO)到管道的写端文件描述符,此时,标准输出和管道写端指向的是同一个内核管道对象,也就是说我们关闭的仅仅是原始的管道写端,而因为dup2
已经创建了一个新的指向同一资源的文件描述符,所以不会影响标准输出到管道写端中。读端也是同理。
pipe函数练习
使用
pipe、dup2、fork、execlp
函数实现父子进程间ls | wc -l
命令。
代码
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
int main()
{
pid_t pid;
int ret;
int fd[2];
//创建管道
ret = pipe(fd);
if(ret == -1)
{
perror("pipe error\n");
exit(1);
}
//创建子进程
pid = fork();
if(pid > 0) //父进程
{
close(fd[0]);
//重定向
dup2(fd[1],STDOUT_FILENO);
close(fd[1]);
//父进程实现执行ls命令
execlp("ls","ls",NULL);
perror("parent execlp error\n");
exit(1);
}
else if(pid == 0)//子进程
{
close(fd[1]);
//重定向
dup2(fd[0],STDIN_FILENO);
close(fd[0]);
//子进程实现执行wc -l命令
execlp("wc","wc","-l",NULL);
perror("child execlp error\n");
exit(1);
}
else
{
perror("fork error\n");
exit(1);
}
return 0;
}
运行结果如下:
[root@model function]# ./ls
[root@model function]# 55
问题及改进
以上代码执行后,会出现一个问题,不管在父子进程中如何操作,父进程都是先执行,先于子进程结束,这样也就会导致出现孤儿进程。所以我们为了避免这个情况出现,可以创建两个子进程来分别执行对应的功能命令,父进程只用负责回收两个子进程,也就是兄弟进程间通信。
代码
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
int main()
{
pid_t pid,wpid;
int ret;
int fd[2];
int i = 0;
//创建管道
ret = pipe(fd);
//设置管道创建错误信息提示
if(ret == -1)
{
perror("pipe error\n");
exit(1);
}
//循环创建两个子进程
for(i = 0; i<2; i++)
{
pid = fork();
//子进程中直接跳出次循环,目的是为了不让其继续创建子进程
if(pid == 0)
break;
//若发生错误,设置错误信息提示
else if(pid == -1)
{
perror("fork error\n");
exit(1);
}
}
if(i == 0) //第一个子进程
{
close(fd[0]);
//重定向
dup2(fd[1],STDOUT_FILENO);
close(fd[1]);
//父进程实现执行ls命令
execlp("ls","ls",NULL);
perror("parent execlp error\n");
exit(1);
}
else if(i == 1)//第二个子进程
{
close(fd[1]);
//重定向
dup2(fd[0],STDIN_FILENO);
close(fd[0]);
//子进程实现执行wc -l命令
execlp("wc","wc","-l",NULL);
perror("child execlp error\n");
exit(1);
}
else if(i == 2)//父进程
{
//父进程要关闭管道读写端,否则代码执行不成功,因为数据在管道中要求是单向流动。
//要关闭多余的读写段,保证数据单向流动
close(fd[0]);
close(fd[1]);
//通过循环回收两个子进程
while((wpid = waitpid(-1,NULL,0)) != -1)
{
printf("wait child ID is %d\n",wpid);
}
}
return 0;
}
运行结果如下:
[root@model function]# ./pipe_brother
wait child ID is 5529
55
wait child ID is 5530
注意
- 父进程一定要关闭管道读写端,否则代码执行不成功。这是因为数据在管道中要求是单向流动,我们要关闭多余的读写段,保证数据单向流动。
- 调用一次
waitpid
函数只能回收一个子进程,使用循环多次调用。 - 管道要先于子进程创建。