管道的定义
管道是一种两个进程间单向通信的机制,因为管道传递数据的单向性,所以我们也称他为半双工管道。管道分为无名管道与命名管道
管道的特点
- 数据只能从一个进流出到一个进程流入(一个读端,一个写端);如果需要进行双工通信,那么就需要两个管道
- 无名管道,只能在父子进程之间进行通讯,只能用于有亲缘关系的进程之间通讯。
管道的命令
cmd1|cmd2
操作符是:”|”,它只能处理经由前面一个指令传出的正确输出信息,对错误信息信息没有直接处理能力。然后,传递给下一个命令,作为标准的输入。
管道的使用
#include<unistd.h>
int pipe(int filedes[2]) //成功返回0,失败返回-1
pipe函数用于创建一个无名管道,数组是传出参数,用于保存两个返回的文件描述符,两个返回的文件描述符以一种特殊的方式连接在一起。写在fd[1]里的数据可以从fd[0]里读出来。数据读取采用的是先进先出的原则。
一般无名管道的使用方式都是,父进程创建一个管道,然后fork一个子进程,由于子进程拥有父进程的副本,所以父子进程之间可以通过管道里通讯。
对于从父进程到子进程的管道,父进程关闭读端(fd[0]),子进程关闭写端(fd[1]);对于从子进程到父进程的管道,子进程关闭读端(fd[0]),父进程关闭写端(fd[1])。
当管道的一端被关闭的时候,会出现以下的情况
- 当读一个写端被关闭的管道时,在所有数据读完以后,read返回为0,表示文件结束,如果写端没有被关闭,那么读端便会读完数据后阻塞。
- 当写一个读端被关闭的管道时,则产生信号SIGPIPE,write返回为-1,error置为EPIPE,如果读端没有被关闭,那么写端在数据写满以后便会进入阻塞。
无名管道代码如下:
#include<stdio.h>
#include<assert.h>
#include<unistd.h>
#include<fcntl.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
int main()
{
int fd[2]={0};//定义文件描述符
pipe(fd);
pid_t pid =fork();//创建子进程
if(pid==0)//如果是子进程
{
char buff[128]={0};
close(fd[1]);//关闭写端
while(1)
{
read(fd[0],buff,127); //从管道中读取数据
printf("child:: buff=%s",buff);
}
close(fd[0]);//关闭读端
}
else
{
close(fd[0]);//关闭读端
char cuff[128]={0};
sleep(1);
while(1)
{
fgets(cuff,127,stdin);//标准输入
write(fd[1],cuff,6);//将数据写到管道
}
close(fd[1]);//关闭写端
}
}
Popen和Pclose函数
最简单的在两个进程之间传递数据的方法就是使用popen和pclose函数了
他们的原型如下:
#include <stdio.h>
// 成功返回标准文件I/O指针,失败返回NULL
FILE *popen(const char *command, const char *open_mod);
// 成功返回shell的终止状态,失败返回-1
int pclose(FILE *stream_close);
该函数创建一个管道,并fork一个子进程,该子进程根据popen传入的参数,关闭管道的对应端,然后执行传入的shell命令,然后等待终止。
调用进程和fork的子进程之间形成一个管道。调用进程和执行shell命令的子进程之间的管道通信是通过popen返回的FILE*来间接的实现的,调用进程通过标准文件I/O来写入或读取管道。
command字符串是要运行的 程序名和相应的参数 open_mod必须是“r”或者“w”。如果是“r”,被调用程序的输出就可以被调用程序使用。如果是“w”,调用程序的输入就可以被调用程序使用。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
FILE *fpr = NULL, *fpw = NULL;
char buf[256];
int ret;
// 执行完这行代码,标准输出就装满,这里这个标准输出标记为out1,
// 管道指向out1,fpr指向管道的读端。执行这句代码,会一直去读取标准输出,
// 若标准输出为空,则会阻塞,直到标准输出不为空,执行命令后又会去指
// 读取标准输出继续执行。这里这个标准输入标记为in2。
// 管道指向int2,fpw指向管道的写端。
fpr = popen("cat /etc/group", "r");
fpw = popen("grep root", "w");
// 从out1中读取256个字节数据,存放在buf中。
while ((ret = fread(buf, 1, sizeof(buf), fpr)) > 0)
{
// 将buf的数据写到int2(此时gerp命令一直在获取int2,直到进行退出)。
fwrite(buf, 1, ret, fpw);
}
pclose(fpr);
pclose(fpw);
return 0;
}
popen函数的优缺点:
- 优点:可以使用shell来分析命令字符串,启动非常复杂的shell命令;
- 缺点:不仅要启动一个新进程,还要启动一个shell,效率会比较低
命名管道(FIFO)
命名管道定义及特点
POSIX标准中的FIFO又名有名管道或命名管道。我们知道前面讲述的POSIX标准中管道是没有名称的,所以它的最大劣势是只能用于具有亲缘关系的进程间的通信。FIFO最大的特性就是每个FIFO都有一个路径名与之相关联,从而允许无亲缘关系的任意两个进程间通过FIFO进行通信。所以,FIFO的两个特性:
- 和管道一样,FIFO仅提供半双工的数据通信,即只支持单向的数据流;
- 和管道不同的是,FIFO可以支持任意两个进程间的通信
命名管道的使用
#include <sys/types.h>
#include <sys/stat.h>
/ /成功则返回0,失败返回-1
int mkfifo(const char *filename, mode_t mode);
int mknod(const char *filename, mode_t mode | S_IFIFO,(dve_t)0);
- filename:一个Linux路径名,它是FIFO的名字。即每个FIFO与一个路径名相对应;
- mode:指定的文件权限位,类似于open函数的第三个参数。即创建该FIFO时,指定用户的访问权限,有以下值:S_IRUSR,S_IWUSR,S_IRGRP,S_IWGRP,S_IROTH,S_IWOTH。
mkfifo函数默认指定O_CREAT | O_EXECL方式创建FIFO,如果创建成功,直接返回0。如果FIFO已经存在,则创建失败,会返回-1并且errno置为EEXIST。对于其他错误,则置响应的errno值;
当创建一个FIFO后,它必须以只读方式打开或者只写方式打开,所以可以用open函数,当然也可以使用标准的文件I/O打开函数,例如fopen来打开。由于FIFO是半双工的,所以不能够同时打开来读和写。
其实一般的文件I/O函数,如read,write,close,unlink都可用于FIFO。对于管道和FIFO的write操作总是会向末尾添加数据,而对他们的read则总是会从开头数据,所以不能对管道和FIFO中间的数据进行操作,因此对管道和FIFO使用lseek函数,是错误的,会返回ESPIPE错误。
mkfifo的一般使用方式是:通过mkfifo创建FIFO,然后调用open,以读或者写的方式之一打开FIFO,然后进行数据通信。
命名管道代码如下:
// FIFOwrite.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
int fd;
int ret;
ret = mkfifo("my_fifo", 0666); // 创建命名管道
if(ret != 0)
{
perror("mkfifo");
}
fd = open("my_fifo", O_WRONLY); // 等着只读
if(fd < 0)
{
perror("open fifo");
}
char send[100] = "Hello World";
write(fd, send, strlen(send)); // 写数据
printf("write to my_fifo buf=%s\n",send);
while(1); // 阻塞,保证读写进程保持着通信过程
close(fd);
return 0;
}
// FIFOread.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
int fd;
int ret;
ret = mkfifo("my_fifo", 0666); // 创建命名管道
if(ret != 0)
{
perror("mkfifo");
}
fd = open("my_fifo", O_RDONLY); // 等着只写
if(fd < 0)
{
perror("open fifo");
}
while(1)
{
char recv[100] = {0};
read(fd, recv, sizeof(recv)); // 读数据
printf("read from my_fifo buf=[%s]\n", recv);
sleep(1);
}
close(fd);
return 0;
}
当然 ,如果我们是在同一目录下用管道连接两个进程,我们还可以直接使用shell命令mkfifo filename 创建一个命名管道,然后在程序中open打开就可以了。
总结
- 管道是一个环形队列缓冲区,允许两个进程以生产者消费者模型进程通信。是一个先进先出(FIFO)队列,由一个进程写,而由另一个进程读。
- 管道在创建时获得一个固定大小的字节数。当一个进程试图往管道中写时,如果有足够的空间,则写请求立即被执行,否则该进程被阻塞。如果一个进程试图读取的字节数多于当前管道中的字节数,也将被阻塞。
- 操作系统强制实行互斥,只能有一个进程可以访问管道。
- 只有有血缘关系(父子关系)的进程才可以共享匿名管道,不相关的进程只能共享命名管道。
- 命名管道的用途主要有:(1)shell命名使用FIFO将数据从一条管道传送到另一条时,无须创建中间临时文件;(2)在客户进程和服务器进程间传送数据。
参考:https://blog.csdn.net/daaikuaichuan/article/details/82827994