【Linux系统编程】进程间的通信——管道通信

目录

前言:

一,管道的认识

二,管道的深入了解

2-1,管道的特点

2-2,深入学习管道

 2-3,管道的特殊情况

三,匿名管道

四,Ubuntu系统和VSCode的使用

4-1,Ubuntu和VSCode的说明

4-2,VSCode远程连接云服务器Ubuntu

4-3,VSCode下操作主机用户

五,命令行管道

六,管道的场景应用——进程池

6-1,设计模式

6-2,管道信道和任务文件的设计

6-3,进程池的封装

6-4,程序入口的设计

七,命名管道

7-1,认识命名管道

7-2,命名管道的代码应用


前言:

        两个进程之间不能进行 “数据” 的直接传递,因为进程具有独立性,但两个进程之间的通信是必不可少的,进程之间往往需要多个进程协同,共同完成一些事情。

进程间通信的目的:

  1. 数据传输:一个进程需要将它的数据发送给另一个进程。
  2. 资源共享:多个进程之间共享同样的资源。
  3. 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事(如进程终止时要通知父进程)。
  4. 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另 一个进程的所有陷入和异常,并能够及时知道它的状态改变。

        什么是进程间的通信呢?本质是一个进程把自己的数据能够交给另一个进程。由于进程之间相互独立,所以这里需要操作系统(OS)充当第三者,负责提供专门的空间使两进程的数据进行交换。基于OS提供的空间有所不同,进而决定了多进程间有着不同的通信方式。下面我们先来学习进程间的通信方式之一——管道通信。


一,管道的认识

        管道是Unix/Linux中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。管道本质上是在内核中申请的一块固定大小的缓冲区,这块缓冲区可以被看作是一个内存中特殊存在的文件,具有文件描述符,但它并不属于其他任何文件系统,而是仅存在于内存中。

        注意:管道不是文件系统上的文件,因此没有独立的inode编号。文件系统上的文件通常具有持久的存储位置(如硬盘上的扇区),可以通过文件名进行访问,并且具有一系列的文件属性(如大小、权限、创建时间等)。而管道缓冲区是临时的,并且仅存在于内存中,用于实现进程间的通信。它并没有像文件系统文件那样的持久性和可访问性。


二,管道的深入了解

2-1,管道的特点
  1. 管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;常见与父子进程,即一个管道由一个进程创建,然后该进程调用fork创建子进程进行父子通信。
  2. 管道提供流式服务,是面向字节流(管道传输的数据是以字节为单位进行处理的)。
  3. 一般而言,进程退出,管道自动释放,所以管道的生命周期随进程。管道也属于文件,而文件的生命周期是随进程的。
  4. 管道自带同步机制(它在同一时间只能被一个进程用于写操作或被一个进程用于读操作。即当一个进程在写入管道时,其他进程不能同时写入;同样,当一个进程在读取管道时,其他进程不能同时读取或写入),且一般而言,内核会对管道操作进行同步与互斥。
  5. 管道是半双工的一种特殊情况,只能单向通信,即数据只能向一个方向流动,一端读入,一端写入;需要双方通信时,需要建立起两个管道,如下图:    

        最后说明一下,当向管道写端写入大量数据时,读端可一次读出全部数据,写入次数与读出次数没有直接的对应关系。

2-2,深入学习管道

        管道既然是一个特殊的内部文件,那么我们就从文件方面来具体了解管道。

        当类似于上面两进程间进行通信时,若父进程接收子进程的数据,父进程以可读权限的方式打开,子进程以可写权限的方式打开;若子进程接收父进程的数据,子进程以可读权限的方式打开,父进程以可写权限的方式发开。因此,父子进程必须分别以读写两种方式打开文件,其中一个进程若不用读或写,就把其中一个文件描述符关掉。这块的具体过程是先打开父进程,然后创建子进程,由于子进程可能要以读或写的方式打开文件,所以父进程一开始就要以读和写的方式打开两次以便子进程继承,最后,父子进程分别关闭相应的文件描述符,实现一个读一个写进而进行进程通信,过程如下图。

 2-3,管道的特殊情况

        1,当管道内部没有数据且专门进行写端的进程没有关闭自己的写端文件fd时,读端进程就要阻塞等待,直到管道中有数据。

        2,当管道内部被写满数据且读端进程没有关闭自己的读端文件fd时,写端进程写满后就要阻塞等待。

        3,对于写端进程而言,若关闭了管道,读端会将管道中的数据读完,最后将会读到返回值0,表示读结束。这类似于文件操作时读取到了文件的结尾EOF。

        4,对于读端进程而言,若关闭了管道,写端进程仍在写入,OS(操作系统)会直接终止写入的进程,通过信号13)SIGPIPE信号杀掉进程。


三,匿名管道

        既然管道拥有自己独立的文件系统,不依赖于传统的文件系统结构,那么也就是说它无法通过磁盘文件打开。操作系统(OS)为了支持我们进行管道通信,特意提供了系统调用 pipe()。

        Linux中,通过pipe函数创建的管道是匿名管道。使用pipe函数创建的管道在文件系统中没有名字,因此它被称为匿名管道。该函数的具体介绍如下:

头文件

        #include <unistd.h>

函数原型

        int pipe(int fd[2]);  //参数是一个大小为2的数组

函数功能

        创建一无名管道

参数

        fd:文件描述符数组,其中fd[0]表示读端,fd[1]表示写端

返回值

        创建成功返回0,失败返回-1,并设置错误代码以指示错误原因

        pipe()函数会使参数fd返回一个文件描述符数组,其中fd[0]表示读端,存入的文件描述符是3;fd[1]表示写端,存入的文件描述符是4,进程通信时该管道通过文件描述符来访问,这些文件描述符类似于用于访问普通文件的文件描述符。但是,这些文件描述符并不直接映射到文件系统上的物理文件。

        pipe()函数会创建一个不需要向磁盘中刷新的管道(缓冲区),通常把该创建的管道也叫做内存级别的匿名文件或匿名管道,进程间可以通过这个管道进行数据的传递和通信。一个进程向管道的写端写入数据,另一个进程从读端读取数据,那么匿名管道又是如何让不同进程看到同一份资源的呢?匿名管道是通过创建子进程,子进程会继承父进程的相关属性信息而实现进程通信。这里其实也不是说非要父子进程才可以,只要是具有血缘关系的进程其实都具有相关的资源继承,都可以相互间通信,所以,匿名管道只能让具有血缘关系的进程进行通信。下面为测试样例:

#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <string.h>  
int main() 
{
    int pipefd[2];
    //1,输出管道创建文件描述符数组的读端和写端
    int n = pipe(pipefd);
    if (n < 0)
    {
        return 1;
    }
    printf("pipefd[0]: %d, pipefd[1]: %d\n", pipefd[0]/*read*/, pipefd[1]/*write*/);// 3, 4

    //2,查看父子进程间的通信
    pid_t id = fork();
    //父进程
    if (id > 0) 
    {
        //father: write
        close(pipefd[0]); //关闭读端
        const char* str = "Hello from parent";
        printf("Father Information: Hello from parent\n");
        write(pipefd[1], str, strlen(str) + 1); //写入管道  
        wait(NULL); 
    }
    //子进程  
    else 
    {
        //child: read
        char buffer[1024];
        close(pipefd[1]); //关闭写端  
        read(pipefd[0], buffer, sizeof(buffer) + 1); //从管道读取    
        printf("Child: %s\n", buffer);
    }
    return 0;
}


四,Ubuntu系统和VSCode的使用

4-1,Ubuntu和VSCode的说明

        目前企业中,编译器很少使用本地环境VS,大多数情况下还要使用VSCode,而VS在最开始使用是因为它足够简单上手,运用简洁方便,至于常用的CentOS系统,它目前已经停止维护更新了,但很多市场却不一定停止使用CentOS系统平台,这也是为什么一开始建议最好使用CentOS系统后面要切换Ubuntu系统的原因。

        进入管道这块,我们将要使用Ubuntu云系统及VSCode工具。若是之前使用的CentOS云服务器或者其它云服务器的话,这里可直接在自己购买云服务器的个人平台上重装系统,将其切换成Ubuntu(建议切换成Ubuntu 20.04版本),但是需要注意,重装系统会导致原本系统上的文件等一系列资源全部被清除,因此在重装前必要的文件可先打包压缩,然后使用lrzsz工具将其传入到Windows系统上,再由Windows传入Ubuntu上。

        VSCode的下载安装和使用,这里有全面详细的讲解:VSCode基本使用全面讲解

        Ubuntu系统在使用上与CentOS系统大差不差,但也有略微区别,比如Ubuntu常用的安装软件工具指令,从CentOS上的yum改为apt(或apt-get。apt-get它是一个古老的效率低下的安装指令,没有apt效率高)。还有就是Ubuntu系统在安装过程中默认不设置root账户和密码,‌而是使用一个名为ubuntu的用户作为默认用户。‌这意味着,‌在启动Ubuntu云服务器时,不能使用root用户登入,因为系统没有设置root账户,‌用户需要创建root密码才能使用root账户进行操作,但进入系统后可直接su指令切换成root用户。

4-2,VSCode远程连接云服务器Ubuntu

        VSCode我们要的是它的编辑功能,拥有VSCode和Ubuntu系统后,首先要做的是将其连通。

        VSCode远程连接Ubuntu,要打开VSCode在其【扩展】(专门安装插件的功能)中安装一个插件【Remote - SSH】(只需搜索remot即可找到)。安装完后,VSCode最左边框栏中会有一个【远程资源管理器】,VSCode远程连接Ubuntu就是在【远程资源管理器】中进行连接的,具体的做法是在SSH中新建远程,之后会出现【输入SSH连接命令】,输入的格式是:ssh [用户名]@[主机公网IP]。如下图:

        设置完后下面会提示“已添加主机”,这时我们可选中【打开配置】查看文件config所写入进去的配置信息(config所在的路径如上图)。一般情况下我们都可不用管理,直接关掉此文件即可。

        配置完后,我们会在SSH下发现我们配置指定公网IP的主机,若没有出现需要在【远程】中不断刷新,直到出现为止,出现的这个公网IP主机就是远程连接的主机。

        下面,我们来登入连接的用户。首先,要鼠标右击连接的主机,选择【在当前窗口中连接】(也可以选择【新窗口连接】,只不过选择后会弹出一个新的VSCode窗口,除非特定情况下,否则一般建议在当前窗口连接),这时,上面的搜索栏中会出现几种系统平台的选择,这里选择Linux(注意:若选择系统平台后出现问题,这时可能要检查文件,有些电脑默认的xshell中config文件权限是有问题的),然后,系统会让我们输入Linux下登入指定用户的密码,输入成功后,远程连接的主机上面会打了个小对勾,显示已连接,如下图:

        连接成功后,我们在当前连接主机下的VSCode操作就是在系统主机下的操作,用户主机与当前VSCode是同步的,可在VSCode中实现远程代码操作。而用户主机下的家目录里,生成了一个专门写入VSCode配置信息的 .vscode-server 目录,表示同步的一些配置信息。

4-3,VSCode下操作主机用户

        上面说到,VSCode一旦连接我们的主机后将会同步云端。VSCode的功能有很多,这里,也可在VSCode的终端下操作连接的主机。具体做法是,在VSCode终端中上的【查看】中,点击【终端】(快捷键【Ctrl + `】可直接选择终端或取消终端)即可在主机下的指令终端下进行,如下:

        至于代码的调试这里建议在云主机下调试,因为VSCode上面的调试有些复杂且复杂情况的调试容易出现很多问题。云主机的调试以前都是用gdb,但gdb上命令行模式的调试不便于观察,图形化界面的调试需要云服务器较高的配置,否则性能效率低,如今推荐使用便于观察的cgdb工具(指令:cgdb [可执行文件]),使用的方法跟gbd基本一致。安装的指令:sudo apt install -y cgdb

        最后说明一点,这里不建议在VSCode上安装过多插件,因为VSCode一旦连接上云端主机,两者之间同步,VSCode所有的插件安装相当于在云端主机下安装(安装在家目录下的 .vscode-server 配置目录中)。C/C++开发或研发整体量相对较轻,连接VSCode的目的时为了方便使用以及减轻在主机下安装过重的插件工具。


五,命令行管道

        我们曾经学习的“ | ”命令行管道,本质上就是上面说明的匿名管道。系统内部的指令一旦运行起来就要创建进程,那么我们观察下面的情况。

        上面sleep指令用两个 “ | ” 管道连接后,当执行时,这里可不是从左到右依次创建三个进程,而是一次性的把进程全部创建出。这里管道连接的原理是一号进程用一号管道的写端(标准输出重定向到管道的写端),二号进程用一号管道的读端(标准输入重定向到管道的读端),如此这两个进程就连接起来了,后面同理。此外,我们还可发现,上面用两个管道连接的三个进程具有相同的PPID,父进程都是bash,也就是说它们三个进程之间其实是兄弟关系,对应上面管道的特点:管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信。


六,管道的场景应用——进程池

6-1,设计模式

        进程池的本质是一次可以创建一批进程,管道在进程池这块的作用不可忽视。比如这里我们一次性创建5个进程,这5个进程通过一个主进程(这个主进程可以是父进程)控制管理,这时就需要一定的通信方案,所以,可以在主进程和每5个进程之间建立一条管道信道,主进程负载均衡地往5个管道中写入相应的指令数据,后面的5个进程从中读数据执行相应的任务,如下图:

        注意,若是运行程序时,可能会输入错误的指令或出现异常情况,因此,这里专门设计了错误指令的提示符和枚举了错误码。

//枚举错误码

enum

{

    UsageError = 1,

    ArgError,

    PipeError

};

//运行程序指令输入错误时的提示符

void Usage(const string& proc)

{

    cout << "Usage: " << proc << " subprocess-num" << endl;

}

6-2,管道信道和任务文件的设计

// 管理子进程的管道信道

class Channel

{

public:

    Channel(int pipefd, pid_t sub_id, const string& name)

        :_pipefd(pipefd), _sub_process_id(sub_id), _name(name)

    { }

    void PrintDebug()

    {

        cout << "_pipefd: " << _pipefd;

        cout << ", _sub_process_id: " << _sub_process_id;

        cout << ", _name: " << _name << endl;

    }

    string name() { return _name; }

    int wfd() { return _pipefd; }

    pid_t pid() { return _sub_process_id; }

    void Close() { close(_pipefd); }

    ~Channel()

    { }

private:

    int _pipefd;  //写端文件描述符

    pid_t _sub_process_id; //子进程的pid

    string _name;    //子进程的编号名称

};

        子进程执行的任务在文件task.hpp(.hpp表示把头文件和源文件合成一体,一般是写开源软件的)中。子进程执行的任务这里设计出了三种函数:PrintLog、ReloadConf、ConnectMysql。当子进程工作时,会调用worker函数,阻塞等待读取写端(父进程)的命令信息;当写端关闭且信息读取完毕时,子进程退出worker函数,最终进程退出并被父进程释放资源。下面代码注释会详细说明。

//此代码在task.hpp文件中

#pragma once

//.hpp是把头文件和源文件合成一体,一般是写开源软件的

#include <iostream>

#include <unistd.h>

using namespace std;

typedef void(*work_t)(int);  //函数指针类型

typedef void(*task_t)(int, pid_t);  //函数指针类型

void PrintLog(int fd, pid_t pid)

{

    cout << "sub process: " << pid << ", fd: " << fd<< ", task is : printf log task\n" << endl;

}

void ReloadConf(int fd, pid_t pid)

{

    cout << "sub process: " << pid << ", fd: " << fd<< ", task is : reload conf task\n" << endl;

}

void ConnectMysql(int fd, pid_t pid)

{

    cout << "sub process: " << pid << ", fd: " << fd<< ", task is : connect mysql task\n" << endl;

}

task_t tasks[3] = {PrintLog, ReloadConf, ConnectMysql}; //任务数组

//随机选择任务码

uint32_t NextTask()

{

    return rand() % 3;

}

//子进程处理管道中的任务

void worker(int fd)

{

    while(true)

    {

        //command_code实际上是子进程任务tasks数组的下标

        uint32_t command_code = 0; //uint32_t:一个无符号的32位整型数据类型

        //管道文件以重定向,将从管道中读取到的数据存储在command_code变量中。

        //这里,子进程将会阻塞等待read中,直到父进程调用write开始往管道中写数据

        ssize_t n = read(0, &command_code, sizeof(command_code)); //ssize_t:一个有符号的整型数据类型

        if(n == sizeof(command_code)) //当读取到的实际字节数等于最大读取字节数时程序正常

        {

            if(command_code >= 3) continue; //tasks任务数组大小3,当读取到的任务指令大于等于3时表示读取到了错误任务

            tasks[command_code](fd, getpid()); //任务指令读取成功后执行相应任务

        }

        //此时表示已经读到了文件末尾,写端也不再写入了,可以退出该进程并杀死

        else if(n == 0)

        {

            cout << "sub process: " << getpid() << " quit now..." << endl;

            break;

        }

    }

}

6-3,进程池的封装

        进程池中,我们主要封装了创建5个子进程及对应管道的CreateProcess函数(注意:由于进程的继承关系,每次创建子进程时子进程会继承父进程连接前几次管道的写端),父进程负载均衡轮流的选择管道NextChannel函数,父进程发送任务码的SendTaskCode函数,父进程回收子进程资源的KillAll函数。进程池封装后还需要一个控制进程池的CtrlProcessPool函数,具体代码和说明如下:

// 进程池

class ProcessPool

{

public:

    ProcessPool(int sub_process_num)

        : _sub_process_num(sub_process_num)

    { }

    int CreateProcess(work_t work) // 回调函数,让子进程开始工作

    {

        //注意:每次创建子进程时会继承父进程连接前面创建子进程管道的写段

        //fds存储每次连接管道的写端,用于后面关闭子进程继承父进程连接管道的写端

        vector<int> fds;

        // 依次创建每个子进程及对应的管道信道,并将其用管道信道channels管理起来

        for (int number = 0; number < _sub_process_num; number++)

        {

            int pipefd[2]{0};

            int n = pipe(pipefd);

            if (n < 0)

            {

                return PipeError;

            }

            pid_t id = fork();

 // child -> read。由于继承父进程文件描述符的关系,下面创建的所有子进程的读端全部是3

            if (id == 0)

            {

                if (!fds.empty())

                {

                    cout << "close w fd: ";

                    for (auto fd : fds)

                    {

                        close(fd);

                        cout << fd << " ";

                    }

                    cout << endl;

                }

                sleep(1);

                close(pipefd[1]);

                // 执行任务

                dup2(pipefd[0], 0); // 标准输入从管道中读取,即重定向管道文件

                work(pipefd[0]);

                exit(0);

            }

            // father -> write

            string cname = "channel-" + to_string(number);

            close(pipefd[0]);

            channels.push_back(Channel(pipefd[1], id, cname));

            // 把父进程的写端fd保存起来

            fds.push_back(pipefd[1]);

        }

        return 0;

    }

    // 轮流均衡地依次选择子进程对应的管道:0,1,2...n - 1(每个子进程及管道的编号实际上就是数组的下标)

    int NextChannel()

    {

        static int next = 0;

        int c = next;

        next++;

        next %= channels.size();

        return c;

    }

    // 父进程发送任务码

    void SendTaskCode(int index, uint32_t code)

    {

        cout << "send code: " << code << " to " << channels[index].name() << " sub prorcess id: " <<  channels[index].pid() << endl;

        write(channels[index].wfd(), &code, sizeof(code));

    }

    // 退出全部的子进程(只需要关闭对应信道管道Channel中的写端即可)并回收子进程的资源

    void KillAll()

    {

        for (auto &channel : channels)

        {

            //关闭管道写端文件描述符,子进程work中的read读到文件末尾(0)后将exit(0)退出

            channel.Close();

            //父进程阻塞等待子进程,回收所有已退出子进程的资源

            pid_t pid = channel.pid();

            pid_t rid = waitpid(pid, nullptr, 0);

            if (rid == pid)

            {

                cout << "wait sub process: " << pid << " success..." << endl;

            }

            cout << channel.name() << " close done" << " sub process quit now : " << channel.pid() << endl;

        }

    }

    void Debug()

    {

        for (auto &channel : channels)

        {

            channel.PrintDebug();

        }

    }

    ~ProcessPool()

    { }

private:

    int _sub_process_num;

    vector<Channel> channels;

};

//控制进程池

void CtrlProcessPool(ProcessPool *processpool_ptr, int cnt)

{

    while (cnt)

    {

        // a. 选择一个进程和通道

        int channel = processpool_ptr->NextChannel();

        // b. 你要选择一个任务

        uint32_t code = NextTask();

        // c. 发送任务

        processpool_ptr->SendTaskCode(channel, code);

        sleep(1);

        cnt--;

    }

}

6-4,程序入口的设计

        程序入口中,首先,我们要处理出入进来的指定,当指令不合格时直接返回对应设计的错误码;指令合格时这里分三步进行:1,创建通信信道和子进程。  2,控制子进程。  3,回收子进程资源。代码如下:

int main(int argc, char *argv[])

{

    if (argc != 2)

    {

        Usage(argv[0]);

        return UsageError;

    }

    int sub_process_num = stoi(argv[1]);

    if (sub_process_num <= 0) return ArgError;

    srand((uint64_t)time(nullptr));

    // 1. 创建通信信道和子进程

    ProcessPool *processpool_ptr = new ProcessPool(sub_process_num);

    processpool_ptr->CreateProcess(worker);

    processpool_ptr->Debug();

    // 2. 控制子进程

    CtrlProcessPool(processpool_ptr, 10); //设置发送10个任务

    cout << "task run done" << endl; //任务全部执行完毕的提示符

    // 3. 回收子进程要进行两步

    // 首先,让所有的子进程退出;然后,父进程回收已退出子进程的资源(wait/waitpid)

    processpool_ptr->KillAll();

    delete processpool_ptr;

    return 0;

}

        程序代码的总设计可进入此链接:Linux管道与进程池应用的总和代码

        运行程序时这里启动两个窗口分别观察5个进程情况和代码运行情况,如下图:


七,命名管道

7-1,认识命名管道

        认识命名管道前,首先来了解下不同进程打开同一份文件时的情况。当两个或多个不同进程打开同一份文件时,系统都需要为不同进程创建struct file对象,因为不同进程打开文件的方式可能不同,对文件的管理也可能不同,但是,系统没有把同一份文件的属性、内容在内部维护成两份或多份(即没有给不同进程复制文件的inode、指向的缓冲区等一系列东西),因为文件都是一样的(属性和内容),系统只需维护一份即可,没必要浪费多余的空间。

        上面提到不同进程打开同一份文件时所指向的缓冲区,即可理解为命名管道(原理如上面所说明),只不过这种文件是一种特殊的文件,即管道文件。管道文件不需要往磁盘文件中写入数据,因为我们的目的只是利用管道进行通信,没必要往磁盘中写入数据浪费空间。

        根据命名管道的原理可知,命名管道与匿名管道不同的是,命名管道可以在不相关的进程之间交换数据。系统指令中通过mkfifo函数来创建管道文件。此函数也可在代码程序中使用。如下:

从命令行上创建:

        格式:mkfifo [选项] [filename]    //mkfifo命令通常不需要额外的选项

        

从代码程序里创建:

        格式:int mkfifo(const char *pathname, mode_t mode);

        返回值:成功时,mkfifo返回0;失败时,mkfifo返回-1,并设置相应的错误码errno

参数说明:

        filename:特殊文件名称

        mode:指定文件的权限

       pathname:一个常量字符串,表示要创建文件的路径名。若没有包含任何路径信息(即没有/字符)直接写成文件名,那么这个文件名会被解释为相对于当前工作目录的路径。

7-2,命名管道的代码应用

        我们创建两个毫不相关的进程来使用命名管道进行通信,即负责从管道中读取数据的源文件PipeServer.cc,负责从管道中写入数据的源文件PipeClient.cc。有了匿名管道的相关知识,命名管道的代码实现这里不做过多说明,实现原理和代码说明注释里会解释。

        注意:不能在同一个路径下创建相同命名管道文件,这里在程序PipeServer.cc中创建管道文件。

        方便观察,我们设置头文件和源文件的组合Comm.hpp来管理命名管道。代码与说明如下:

//下面两行代码用于防止头文件被重复包含(使用宏定义)

#ifndef __COMM_HPP__

//#define定义的宏后面没有跟任何东西,表示正在定义一个名为 _COMM_HPP_ 的宏,但它本身不关联任何值或替换文本。

//此种写法通常与#ifndef结合使用,用于防止头文件被重复包含

#define _COMM_HPP_  

#include <iostream>

#include <string>

#include <cerrno>

#include <cstring>

#include <sys/types.h>

#include <sys/stat.h>

#include <unistd.h>

#include <fcntl.h>

using namespace std;

#define Mode 0666

#define Path "./fifo" // 表示当前路径下的管道文件fifo

//创建管道文件

class Fifo

{

public:

    //构造函数创建管道文件(注意:管道文件不可多次创建)

    Fifo(const string &path)

        : _path(path)

    {

        umask(0);

        int n = mkfifo(_path.c_str(), Mode);

        if (n == 0)

        {

            cout << "mkfifo success" << endl;

        }

        //创建管道文件失败时,输出错误码和错误信息

        else

        {

            cerr << "mkfifo failed, errno: " << errno << ",  errstring: " << strerror(errno) << endl;

        }

    }

    ~Fifo()

    {

        //析构函数使用unlink函数删除指定路径下的文件

        int n = unlink(_path.c_str());

        //返回0,删除成功

        if (n == 0)

        {

            cout << "remove fifo file: " << _path << " success" << endl;

        }

        //返回-1,删除失败

        else

        {

            cerr << "remove failed, errno: " << errno << ",  errstring: " << strerror(errno) << endl;

        }

    }

private:

    string _path; // 文件路径 + 文件名

};

#endif

写端PipeClient.cc程序:

//PipeClient.cc负责从管道中写入数据

#include "Comm.hpp"

int main()

{

    int wfd = open(Path, O_WRONLY);

    if (wfd < 0)

    {

        cerr << "open failed, errno: " << errno << ",  errstring: " << strerror(errno) << endl;

        return 1;

    }

    string inbuffer;

    while (true)

    {

        cout << "Please Enter Your Message# ";

        getline(cin, inbuffer);

        if(inbuffer == "quit") break;  //写入quit时表示终止进程通信

        ssize_t n = write(wfd, inbuffer.c_str(), inbuffer.size());

        if (n < 0)

        {

            cerr << "write failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;

            break;

        }

    }

    close(wfd);

    return 0;

}

读端PipeServer.cc程序:

//PipeServer.cc中创建管道文件,负责从管道中读取数据

#include "Comm.hpp"

int main()

{

    Fifo fifo(Path);

    // 如果我们的写端没打开,先读打开,open的时候就会阻塞,直到把写端打开,读open才会返回

    int rfd = open(Path, O_RDONLY);

    if (rfd < 0)

    {

        cerr << "open failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;

        return 1;

    }

    cout << "open success" << endl;

    char buffer[1024];

    while (true)

    {

        ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);

        if (n > 0)

        {

            buffer[n] = 0;

            cout << "client say : " << buffer << endl;

        }

        else if (n == 0)

        {

            cout << "client quit, me too!!" << endl;

            break;

        }

        else

        {

            cerr << "read failed, errno: " << errno << ",  errstring: " << strerror(errno) << endl;

            break;

        }

    }

    close(rfd);

    return 0;

}

        可进入git仓库中完整查看(里面使用Makefile进行编译):Linux命名管道的代码应用

        若先运行读端进程,写端进程没有发送信息读端进程会进入上面代码说明在open打开时就阻塞等待,直到写端进程写入数据,如下图:

  • 16
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值