【进程间通信1】使用管道实现进程间的通信(附C++实现代码)


什么是管道

管道,从名字上理解就知道它和数据传输有关。它是最基本的进程间通信机制,依据pipe系统函数来创建,从而完成数据传输。从实现原理上来说,管道是内核使用环形队列机制借助内核缓冲区实现的,它也可以认为是一个伪文件,它由两个文件描述符引用,一个为读端用于读数据,一个为写端用于写数据。

我们根据数据流向将管道分为三类,一为单工管道,数据流向是单向的,只能由某一个人接收信息,另一个人发送信息。二为半双工管道,双方都可以进行接收和发送数据,但是不能同时进行。三为全双工管道,这种通信方式是双方可以同时发送和接收信息,在本文中不涉及这种通信。

1. 单工管道

相关函数

下面我们列出关于管道的几个基本的函数。

功能函数格式参数含义返回值
打开管道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.out
  • popen("./output","w"):以写的方式打开可执行文件./a.out
  • 相比较execsystem函数,popen可以进行进程间的通信,可以传输数据。
  • 数据不可以在管道中反复读取

单工—读数据

此时相当于从终端读取数据

相关代码如下:

#include <iostream>
#include <cstdlib>

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 <iostream>
#include <cstdlib>

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;
    }
}

2. 半双工管道

半双工管道意思就是指,数据传输指数据可以在一个信号载体的两个方向上传输,但是不能同时传输。

相关函数

函数格式相关参数意义该函数功能返回值意义
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 <iostream>
#include <cstring>
#include <unistd.h>
#include <fcntl.h>

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 <iostream>
#include <cstring>
#include <unistd.h>
#include <fcntl.h>

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 <iostream>
#include <cstring>
#include <unistd.h>
#include <fcntl.h>

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]);
}

运行结果如下:
在这里插入图片描述

3. FIFO半双工

在这部分,我们考虑到非亲缘进程的通信,为了能让两个没有亲缘关系的进程可以进行通信,我们首先要对管道起一个名字,从而使得这两个进程可以在同一个管道中进行读取数据,从而实现通信。

相关函数

函数功能函数格式参数意义
创建命名管道 int mkfifo(pathname,mode)pathname->文件路径(该文件必须不存在);mode->该管道的访问权限
打开FIFO文件int open(const char *path, int mode)pathname->文件路径;mode->访问该管道的模式

注意:

  • 访问权限:一般为0666,指的是对拥有者、拥有组和其他人都可以进行读写的权限,具体权限的内容可以看关于linux文件的权限表示的内容。
  • FIFO文件:具有先进先出的性质。
  • 管道文件生成的时候要求输入路径名的格式是文件路径。

FIFO双向通信代码实现

我们需要创建三个文件,一个用于创建命名管道,另外两个分别生成为模拟两个没有亲缘进程在该管道中进行数据的读取操作,具体代码如下:

生成管道代码实现

首先创建一个fifo管道文件,用于后续通信。

#include <iostream>
#include <sys/stat.h>
using namespace std;

int main(){
    string name;
    cin >> name;
    mkfifo(name.c_str(),0666);
}

运行结果如下:
在这里插入图片描述

读出数据代码实现

#include <iostream>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>

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 <iostream>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>

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内输入想要传输的数据:

在这里插入图片描述
回车后我们就可以在./read的shell中看到我们刚才输入的数据了,如下:

在这里插入图片描述

  • 3
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
MFC是微软基于C++的一个应用程序框架,它可以帮助开发人员快速创建Windows应用程序。进程通信(IPC)是不同进程进行数据交换和通信的一种方法。管道是一种常见的IPC机制,它允许两个进程进行双向通信。 在MFC中,使用管道进行进程通信可以分为两个步骤:创建管道使用管道进行通信。 首先,需要创建一个管道。可以使用CreatePipe函数来创建匿名管道,它接受两个参数,第一个参数是用于接收管道句柄的指针,第二个参数是用于发送管道句柄的指针。成功创建管道后,你将获得两个句柄,一个用于读取数据,一个用于写入数据。 然后,可以使用ReadFile和WriteFile函数来读取和写入管道中的数据。这些函数可以传入一个管道句柄,一个缓冲区来存储数据以及数据的长度。通过这些函数,可以在两个进程传递数据。 如果需要实现双向通信,可以在每个进程使用一个管道来进行读取和写入操作。这样,两个进程就可以通过各自的管道进行双向通信了。例如,进程A使用管道A向进程B发送数据,进程B使用管道B向进程A发送数据。 总结起来,MFC可以使用管道进行进程通信,通过创建管道使用ReadFile和WriteFile函数来实现数据的读取和写入。如果需要双向通信,则可以在两个进程中分别创建管道来进行双向数据传输。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值