管道,从名字上理解就知道它和数据传输有关。它是最基本的进程间通信机制,依据pipe
系统函数来创建,从而完成数据传输。从实现原理上来说,管道是内核使用环形队列机制借助内核缓冲区实现的,它也可以认为是一个伪文件,它由两个文件描述符引用,一个为读端用于读数据,一个为写端用于写数据。
我们根据数据流向将管道分为三类,一为单工管道,数据流向是单向的,只能由某一个人接收信息,另一个人发送信息。二为半双工管道,双方都可以进行接收和发送数据,但是不能同时进行。三为全双工管道,这种通信方式是双方可以同时发送和接收信息,在本文中不涉及这种通信。
下面我们列出关于管道的几个基本的函数。
功能 | 函数格式 | 参数含义 | 返回值 |
---|---|---|---|
打开管道 | FILE* popen (const char *command, const char *open_mode) |
1.command :打开的文件名 2. open_mode :访问该文件的模式(只读/只写) |
NULL->打开失败;非NULL->文件描述符 |
读取数据 | size_t fread ( void *buffer, size_t size, size_t count, FILE *stream) |
buffer :用于接收数据的内存地址 size :读取每个数据项的字节数 count : 数据项个数 stream :输入流 |
>count ->出错; 正数->真实读取的数据项个数 |
写入数据 | size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream) |
buffer :写入数据的内存地址 size :读取每个数据项的字节数 count : 数据项个数 stream :目标文件指针 |
>count ->出错; 正数->真实读取的数据项个数 |
关闭管道 | int pclose(FILE *stream); |
stream : 文件描述符 |
-1 -> 成功; 0 ->失败 |
单工指的就是单向的通信。
popen
会启动两个进程,首先会启动了一个shell命令,然后会开启我们传给popen
函数的命令进程。popen("./output","r")
:以读的方式打开可执行文件./a.outpopen("./output","w")
:以写的方式打开可执行文件./a.out- 相比较
exec
和system
函数,popen
可以进行进程间的通信,可以传输数据。- 数据不可以在管道中反复读取
此时相当于从终端读取数据
相关代码如下:
#include
#include
using namespace std;
int main(){
FILE* pf = popen("./output","r"); // 打开某个管道
if(pf != NULL){
char buff[30] = {'\0'};
fread(buff, 1, sizeof(buff), pf); // 读出数据
cout << "read:" << buff <<endl;
pclose(pf); // 关闭管道
pf = NULL;
}
}
此时就相当于写数据写到终端
#include
#include
using namespace std;
int main(){
FILE* pf = popen("./input","w");
if(pf != NULL){
char buff[] = "abcdef1234";
fwrite(buff, 1, sizeof(buff), pf);
cout << "read:" << buff <<endl;
pclose(pf);
pf = NULL;
}
}
半双工管道意思就是指,数据传输指数据可以在一个信号载体的两个方向上传输,但是不能同时传输。
函数格式 | 相关参数意义 | 该函数功能 | 返回值意义 |
---|---|---|---|
int pipe(int filedes[2]) |
filedes[0] ->读 ; filedes[1] ->写 |
创建管道,获取文件操作符 | -1 ->失败; 0 ->成功 |
size_t write(int fd, const void *buf, size_t nbyte) |
fd ->文件描述符;buf ->写入数据的内存单元;nbyte ->写入文件指定的字节数 |
读取数据 | -1 ->失败;正数 ->写入的字节数 |
size_t read(int fd, void *buf, size_t count) |
fd ->文件描述符;buf ->读取数据的内存单元; |
写入数据 | -1 ->失败;0 -> 无数据;正数 ->读取的字节数 |
int fcntl(int fd, int cmd, long arg) |
fd ->文件描述符;cmd ->控制管道命令;arg -> 描述符状态 |
控制设置是否进行阻塞 | |
close(filedes) |
filedes ->文件操作符 |
关闭管道 |
注意:
cmd
命令的种类有:F_GETFL:获取文件描述符状态;F_SETFL:设置文件描述符状态;- 描述符的状态有两种,O_NONBLOCK:非阻塞;O_BLOCK:阻塞
- 不需要启动额外的shell进程
- 可以理解为一次性启动两个管道,一个管道用于读,一个管道用于写
我们接下来的代码并不是直接给出实现半双工双向通信的代码,而是单向通信的功能开始,进行改进实现这种半双工双向的通信。
文件描述附用于读写数据,是系统用于提供操作文件的ID,一个文件文件描述符表示对一个文件的操作。
linux内核中使用三个关联的数据结构,从而打开文件描述符对应的文件,其中的文件表中存放的是文件的相关信息,V-节点表里面存放的是文件中真实的数据,具体如下:
比较特别的是,父子进程前打开的文件,对于父子进程而言关系如下:
首先我们实现一个简单功能:让父进程利用管道,读到了我们从终端键入的数据abcde
,并将其放入了字符串数组buff
中。
此时的管道通信图如下:
为了防止误用,我们常常会在父子进程中分别关闭不用的读写功能,管道示意图如下:
测试代码如下:
#include
#include
#include
#include
using namespace std;
int main(){
int fd[2];
pipe(fd); // 获得文件描述符,文件描述符用于操作通道
cout << getpid() << endl;
if(0 == fork()){
close(fd[0]);
cout << getpid() << ":";
string s;
cin >> s; // 阻塞,等待终端输入数据
write(fd[1], s.c_str(),s.size()+1);
close(fd[1]);
}else{
close(fd[1]);
char buff[30] = {'\0'};
read(fd[0],buff,sizeof(buff)); // 阻塞,等待管道写入数据
cout << getpid() << ":" << buff << endl;
close(fd[0]);
}
}
注意:这部分要注意阻塞的出现,当管道中没有数据的时候,
read
函数会发生阻塞,等待管道读入数据。
但是read
这里系统增添的阻塞使得进程在阻塞的过程中无法执行任何任务,为了提高效率,我们往往会取消这里的阻塞,让进程在等待的时间内处理其他的任务,使用一个while循环来进行轮询,代码如下:
#include
#include
#include
#include
using namespace std;
int main(){
int fd[2];
pipe(fd); // 获得文件描述符,文件描述符用于操作通道
cout << getpid() << endl;
if(0 == fork()){
cout << getpid() << ":";
string s;
cin >> s; // 阻塞,等待终端输入数据
write(fd[1], s.c_str(),s.size()+1);
}else{
fcntl(fd[0],F_SETFL, O_NONBLOCK); // 取消阻塞
char buff[30] = {'\0'};
while(-1 == read(fd[0],buff,sizeof(buff))){
sleep(1);
cout << "wait..." << endl;
}
cout << getpid() << ":" << buff << endl;
}
close(fd[0]);
close(fd[1]);
}
最终我们实现:让父进程读完数据之后写数据,然后让子进程读出该数据,子进程读出数据之后写数据让父进程进行读取。为了实现双方的相互通信,我们需要开启两套管道,管道通信示意如下:
实现代码如下:
#include
#include
#include
#include
using namespace std;
int main(){
int fd1[2]; // 无法使用一个管道实现多次半双工通信,
// 半双工:不是实事双向通信,在一方发送消息的时候另一方>再等待
int fd2[2];
pipe(fd1);
pipe(fd2);
cout << getpid() << endl;
if(0 == fork()){
for(;;){
cout << getpid() << ":";
string s;
cin >> s; // 阻塞,等待终端输入数据
write(fd1[1],s.c_str(),s.size()+1);
char buff[30] = {'\0'};
read(fd2[0],buff,sizeof(buff)); // 阻塞,等待管道写入数据
cout << getpid() << ":" << buff << endl;
}
}else{
for(;;){
char buff[30] = {'\0'};
while(-1 == read(fd1[0],buff,sizeof(buff))){ // 阻塞,等待>管道写入数据
sleep(1);
cout << "\r" << "wait..." << endl;
}
cout << getpid() << ":" << buff << endl;
cout << getpid() << ":";
string s;
cin >> s;
write(fd2[1], s.c_str(), s.size()+1);
}
}
close(fd1[0]);
close(fd1[1]);
close(fd2[0]);
close(fd2[1]);
}
在这部分,我们考虑到非亲缘进程的通信,为了能让两个没有亲缘关系的进程可以进行通信,我们首先要对管道起一个名字,从而使得这两个进程可以在同一个管道中进行读取数据,从而实现通信。
函数功能 | 函数格式 | 参数意义 |
---|---|---|
创建命名管道 | int mkfifo(pathname,mode) |
pathname ->文件路径(该文件必须不存在);mode ->该管道的访问权限 |
打开FIFO文件 | int open(const char *path, int mode) |
pathname ->文件路径;mode ->访问该管道的模式 |
注意:
- 访问权限:一般为0666,指的是对拥有者、拥有组和其他人都可以进行读写的权限,具体权限的内容可以看关于linux文件的权限表示的内容。
- FIFO文件:具有先进先出的性质。
- 管道文件生成的时候要求输入路径名的格式是文件路径。
我们需要创建三个文件,一个用于创建命名管道,另外两个分别生成为模拟两个没有亲缘进程在该管道中进行数据的读取操作,具体代码如下:
首先创建一个fifo
管道文件,用于后续通信。
#include
#include
using namespace std;
int main(){
string name;
cin >> name;
mkfifo(name.c_str(),0666);
}
#include
#include
#include
#include
#include
using namespace std;
int main(){
string file;
cin >> file;
cout << "before open" << endl;
int fd = open(file.c_str(),O_RDONLY);
if(-1 == fd){
perror("open pipe error");
return 1;
}
cout << "after open" << endl;
char buff[30] = {'\0'};
read(fd, buff, sizeof(buff));
cout << buff << endl;
close(fd);
}
#include
#include
#include
#include
#include
using namespace std;
int main(){
string file;
cin >> file;
cout << "before open" << endl;
int fd = open(file.c_str(),O_WRONLY);
if(-1 == fd){
perror("open pipe error");
return 1;
}
cout << "after open" << endl;
string s;
cin >> s;
write(fd,s.c_str(),s.size()+1);
close(fd);
}
我们需要在两个shell里来测试该代码,运行两个读写文件生成可执行文件,在命令行g++ write.cpp -o write
以及g++ read.cpp -o read
生成可执行文件,最终运行结果如下:
我们需要先运行l两个可执行文件:
在./write
的shell内输入想要传输的数据: