一、管道(pipe)
1、管道的定义和特点
管道是一种两个进程间进行单向通信的机制。因为管道传递数据的单向性,管道又称为半双工管道。管道的这一特点决定了器使用的局限性。管道是Linux支持的最初Unix IPC形式之一,具有以下特点:
- 数据只能由一个进程流向另一个进程(其中一个读管道,一个写管道);如果要进行双工通信,需要建 立两个管道;
- 管道只能用于父子进程或者兄弟进程间通信。,也就是说管道只能用于具有亲缘关系的进程间通信。
2、管道的命令
command1 | command2 | command3
操作符是:”|”,它只能处理经由前面一个指令传出的正确输出信息,对错误信息信息没有直接处理能力。然后,传递给下一个命令,作为标准的输入。
举个例子,在shell中输入命令:ls -l | grep string,我们知道ls命令(其实也是一个进程)会把当前目录中的文件都列出来,但是它不会直接输出,而是把本来要输出到屏幕上的数据通过管道输出到grep这个进程中,作为grep这个进程的输入,然后这个进程对输入的信息进行筛选,把存在string的信息的字符串(以行为单位)打印在屏幕上。
3、管道的使用
#include <unistd.h>
int pipe(int filedes[2]); //成功返回0,失败返回-1
pipe函数用来创建一个管道,fd是传出参数,用于保存返回的两个文件描述符,该文件描述符用于标识管道的两端,fd[0]只能由于读,fd[1]只能用于写。所以管道只能保证单向的数据通信。
一般管道的使用方式都是:父进程创建一个管道,然后fork产生一个子进程,由于子进程拥有父进程的副本,所以父子进程可以通过管道进程通信。这种使用方式如下图所示:
对于从父进程到子进程的管道,父进程关闭读端(fd[0]),子进程关闭写端(fd[1]);对于从子进程到父进程的管道,子进程关闭读端(fd[0]),父进程关闭写端(fd[1])。
当管道的一端被关闭后,会出现下面的几种情况:
1. 当读一个写端被关闭的管道时,在所有数据都被读取后,read返回0,表示文件结束;如果写端没有被关闭,但是没有数据,则读端读完数据后阻塞;
2. 当写一个读端被关闭的管道时,则产生信号SIGPIPE,write返回-1,errno设置为EPIPE;如果读端没有被关闭,写端写满数据后,则写端阻塞。
4、管道的demo
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
int main()
{
int fd[2]; // 定义文件描述符
pid_t pid;
char str[1024] = "hello\n";
char buf[1024];
if (pipe(fd) < 0) // 创建管道,成功返回0,失败返回-1
{
perror("pipe");
exit(1);
}
pid = fork(); // 创建一个子进程
// 功能:父写子读
if (pid > 0) // 父进程
{
close(fd[0]); // 父进程关闭读端
sleep(2);
write(fd[1], str, strlen(str)); // 向管道里写数据
wait(NULL); // 回收子进程
}
else if (pid == 0) // 子进程
{
close(fd[1]); // 子进程关闭写端
int len = read(fd[0], buf, sizeof(buf)); // 从管道里读数据
write(STDOUT_FILENO, buf, len); // 把读到的数据写到标准输出
}
else // 创建子进程失败
{
perror("fork");
exit(1);
}
return 0;
}
二、popen和pclose函数
作为关于管道的一个实例,就是标准I/O函数库提供的popen函数,该函数创建一个管道,并fork一个子进程,该子进程根据popen传入的参数,关闭管道的对应端,然后执行传入的shell命令,然后等待终止。
调用进程和fork的子进程之间形成一个管道。调用进程和执行shell命令的子进程之间的管道通信是通过popen返回的FILE*来间接的实现的,调用进程通过标准文件I/O来写入或读取管道。
#include <stdio.h>
// 成功返回标准文件I/O指针,失败返回NULL
FILE *popen(const char *command, const char *type);
// 成功返回shell的终止状态,失败返回-1
int pclose(FILE *stream);
- command:该传入参数是一个shell命令行,这个命令是通过shell处理的;
- type:该参数决定调用进程对要执行的command的处理,type有如下两种情况:type = “r”,调用进程将读取command执行后的标准输出,该标准输出通过返回的FILE*来操作;type = “w”,调用进程将写command执行过程中的标准输入。
#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的原理及优缺点】:
当调用popen运行一个新进程时,它首先启动shell,然后将command参数传递给它。
- 优点:可以使用shell来分析命令字符串,启动非常复杂的shell命令;
- 缺点:不仅要启动一个新进程,还要启动一个shell,效率会比较低。
三、命名管道(FIFO)
1、命名管道的定义和特点
POSIX标准中的FIFO又名有名管道或命名管道。我们知道前面讲述的POSIX标准中管道是没有名称的,所以它的最大劣势是只能用于具有亲缘关系的进程间的通信。FIFO最大的特性就是每个FIFO都有一个路径名与之相关联,从而允许无亲缘关系的任意两个进程间通过FIFO进行通信。所以,FIFO的两个特性:
- 和管道一样,FIFO仅提供半双工的数据通信,即只支持单向的数据流;
- 和管道不同的是,FIFO可以支持任意两个进程间的通信。
2、命名管道的使用
下面是FIFO的接口定义:
#include <sys/types.h>
#include <sys/stat.h>
/ /成功则返回0,失败返回-1
int mkfifo(const char *pathname, mode_t mode);
- pathname:一个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,然后进行数据通信。
3、命名管道的demo
// 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;
}
四、总结
1. 管道是一个环形队列缓冲区,允许两个进程以生产者消费者模型进程通信。是一个先进先出(FIFO)队列,由一个进程写,而由另一个进程读。
2. 管道在创建时获得一个固定大小的字节数。当一个进程试图往管道中写时,如果有足够的空间,则写请求立即被执行,否则该进程被阻塞。如果一个进程试图读取的字节数多于当前管道中的字节数,也将被阻塞。
3. 操作系统强制实行互斥,只能有一个进程可以访问管道。
4. 只有有血缘关系(父子关系)的进程才可以共享匿名管道,不相关的进程只能共享命名管道。
5. 命名管道的用途主要有:(1)shell命名使用FIFO将数据从一条管道传送到另一条时,无须创建中间临时文件;(2)在客户进程和服务器进程间传送数据。