通信之道:解锁Linux进程间通信的无限可能(一)

目录

一:进程间通信介绍

1:进程间通信的概念

2:进程间通信的目的

​3:进程间通信的本质

4:进程间通信分类

二:管道

1:什么是管道

2:匿名管道

2.1:匿名管道的原理

2.2:pipe函数

2.3管道使用步骤

2.3.1:代码示例

2.4:管道读写规则

2.5:管道特点

2.6:管道的四种特殊情况

2.6.1:情况1

2.6.2:情况2

2.6.3:情况3

2.6.4:情况4

2.7:管道的大小

3:命名管道

3.1:原理

3.2:使用命令创建命名管道

3.3:创建一个命名管道

3.3.1:代码示例

3.4:命名管道的打开规则

[3.5:用命名管道实现serve & cilent通信](#3.5:用命名管道实现serve & cilent通信)

3.5.1:NamePiped.hpp

3.5.1.1:构造函数与析构函数的实现

3.5.1.2:管道开启模块

3.5.1.3:数据通信模块(读写数据)

读取数据

写入数据

3.5.1.4:总代码

3.5.2:Server.cpp

3.5.3:Cilent.cpp

3.5.4:测试运行

Server.cpp

3.5.5:用命名管道实现派发计算任务

Server.cpp

3.5.6:使用命名管道实现进程监控

Server.cpp

3.5.7:用命名管道实现文件拷贝

Server.cpp

Cilent.cpp

3.5.8:命名管道和匿名管道的区别

3.5.9:命令行当中的管道


一:进程间通信介绍

1:进程间通信的概念

进程间通信简称IPC(Interprocess communication),进程间通信就是在不同进程之间传播或交换信息.

2:进程间通信的目的

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

3:进程间通信的本质

进程间通信的本质就是,让不同的进程看到同一份资源.

  • 由于进程具有独立性,具有独立的数据结构与独立的代码和数据,因此各个进程之间要实现通信是非常困难的.
  • 各个进程之间若要实现通信,一定要借助第三方资源,这些进程就可以通过向第三方资源写入或者是读取数据,进而实现进程之间的通信,这个第三方资源实际上就是操作系统提供的一段内存区域.
  • 因此,进程间通信的本质就是,让不同的进程看到同一份资源(内存、文件内核缓冲等).由于这份资源可以由操作系统中的不同模块提供,因此出现了不同的进程间通信方式.

4:进程间通信分类

管道

  • 匿名管道pipe
  • 命名管道

System V IPC

  • System V 消息队列.
  • System V 共享内存.
  • System V 信号量.

POSIX IPC

  • 消息队列.
  • 共享内存.
  • 信号量.
  • 互斥量.
  • 条件变量.
  • 读写锁.

二:管道

1:什么是管道

管道是Unix中最古老的进程间通信的形式,我们把从一个进程连接到另一个进程的数据流称为一个"管道"。

上述命令中,who命令和wc命令都是两个程序,当它们运行起来后就变成了两个进程,who进程通过标准输出打到"管道"当中,wc进程再通过标准输入从"管道"当中读取数据,至此便完成了数据的传输,进而完成数据的进一步加工处理.

2:匿名管道

2.1:匿名管道的原理

匿名管道用于进程间通信,且仅限于本地父子进程之间的通信.

进程间通信的本质就是,让不同的进程看到同一份资源,使用匿名管道实现父子进程通信的原理就是,让两个父子进程先看到同一份被打开的文件资源,然后父子进程就可以对该文件进行写入或是读取操作,进而实现父子进程间通信.

举一个例子

  • 有一个父进程,有自己对应的task_struct,然后父进程分别以读方式与写方式打开磁盘中的hello.txt文件,首先先将文件从磁盘中加载到内存,形成对应的struct file,将该struct file链入到双向循环链表中,并将该结构体的首地址填入到fd_array数组当中下标为3的位置, 使得fd_array数组中下标为3的指针指向该struct file,由于父进程分别读方式和写方式打开磁盘中的hello.txt文件,因此fd_array下标为3的指针指向以读方式打开的struct file,下标为4的指针指向以写方式打开的struct file.

PS:struct file里面包括文件的inode(存储文件的所有属性)、操作方法集(每一个文件还需要有一张对应的表,叫做操作方法集)、内核级文件缓冲区(加载文件的内容)

  • 那么就存在一个问题,当第二次打开同一个文件时,需不需要将对应的inode,操作方法集以及内核级文件缓冲区包括文件的属性和内容再重新加载一次到操作系统里面呢?
  • 答案是不需要!因为打开的是同一个文件,虽然是以不同的方式打开的同一个文件,但是文件的inode,操作方法集,内核级缓冲区都是一样的,所以只有struct file会被单独创建两次,但文件只要加载一次.
  • 然后父进程创建了一个子进程,那么子进程就会创建自己的内核数据结构,并且拷贝父进程的struct files_struct,那么除了这些,还要不要拷贝struct file,inode,内核级缓冲区等等这一块数据.
  • 答案是不需要!因为进程具有独立性,但是struct file,inode,内核级缓冲区等等这一块数据是属于文件系统的,进程要保证独立性跟文件系统没有任何关系,文件系统不一定需要保持独立性.
  • 这里父子进程看到的同一份文件资源是由操作系统来维护的,所以当父子进程对该文件进行写入时,该文件缓冲区当中的数据并不会进行写时拷贝.
  • 父子进程看到了同一份文件的内存级文件缓冲区,把整个基于文件 + 文件缓冲区,让多个进程看到的同一份资源,叫做管道文件.

2.2:pipe函数

pipe函数用于创建匿名管道,pip函数的函数原型如下

pipe函数的参数是一个输出型参数,数组pipefd用于返回两个指向管道读端和写端的文件描述符.

|-----------------|----------------|
| 数组元素 | 含义 |
| pipefd[0] | 管道读端的文件描述符 |
| pipefd[1] | 管道写端的文件描述符 |

pipe函数调用成功时返回0,调用失败时返回-1

2.3管道使用步骤

  • 在创建匿名管道实现父子进程间通信的过程中,需要pipe函数和fork函数搭配使用,具体步骤如下:

1.父进程调用pipe函数创建管道.

2.父进程创建子进程.

3.父进程关闭写端,子进程关闭读端.

注意

  • 管道只能进行单向通信,因此当父进程创建完子进程后,需要确认父子进程谁读谁写,然后关闭相应的读写端.
  • 从管道写端写入的数据会被内核缓冲,直到从管道的读端被读取.

那么有的uu就会有问题,管道只允许单向通信,那如果想进行双向通信呢以及为什么管道只允许单向通信.

  • 如果想双向通信的话,创建两个管道即可.
  • 管道为什么要单向通信:为了简单,只让它进行单向通信.
    我们可以站在文件描述符的角度再来看看这三个步骤:
  • 1、父进程调用pipe函数创建管道.
  • 2、父进程创建子进程.
  • 3、父进程关闭写端(fd[0]),子进程关闭读端(fd[1])

了解的匿名管道的原理与使用步骤后,我们通过代码再演示一下

2.3.1:代码示例
cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <string>
#include <sys/types.h>
#include <sys/wait.h>
const int size = 1024;

std::string GetOtherMessage()
{
    static int cnt = 0;
    //获取信息的次数
    std::string Messageid = std::to_string(cnt);
    cnt++;
    //获取子进程的id
    pid_t id =getpid();
    std::string stringpid = std::to_string(id);

    std::string Message = " messageid: ";
    Message += Messageid;
    Message += " My Pid is :";
    Message += stringpid;

    return Message;
}

void SubProcessWrite(int Wfd)
{
    int PipeSize = 0;
    char c ='A';
    std::string Message = "Father,I am your son";
    while(true)
    {
        //子进程发给父进程的消息
        std::string Information = Message + GetOtherMessage();
        //直接通过系统调用进行文件写入
        //使用write函数,将Information.c_str()位置开始向后size个字节的数据写入文件描述符为Wfd的文件
        write(Wfd,Information.c_str(),Information.size());
        sleep(1);
    }
    std::cout << "child quit...."<<std::endl;
}

void FatherProcessRead(int Rfd)
{
    char Inbuffer[size];
    while(true)
    {
        //sleep(500);
        //从文件描述符为Rfd的文件读取sizeof(Inbuffer)字节的数据到Inbuffer位置当中。
        ssize_t n = read(Rfd,Inbuffer,sizeof(Inbuffer));
        if(n > 0)
        {
            Inbuffer[n] = 0;
            std::cout << "Father Get Message: " << Inbuffer << std::endl;
        }
        //如果read的返回值为0,表示写端直接关闭了,我们读到了文件的结尾.
        else if(0 == n)
        {
            std::cout << "Client Quit,Father Ger Return val: "<< n <<" Father Quit Too"<<std::endl;
            break;
        }

                //读取失败
        else if(n < 0)
        {
            std::cerr << "Read Error" << std::endl;
            break;
        }
        sleep(1);
        //读一次消息以后直接退出
        break;

    }
}
int main()
{
    //1:创建管道
    int PipeFd[2];
    //输出型参数
    int n = pipe(PipeFd);
    if(n != 0)
    {
        std::cerr <<"errno" << errno << ":" << "errstring :" << strerror(errno) << std::endl;
        return 1;
    }
    //PipeFd[0]--->0---->永远保存的是读端---->read(嘴巴:读)  PipeFd[1]--->1---->永远保存的是写端---
    std::cout << "PipeFd[0]:"<<PipeFd[0] << ", PipeFd[1]" << std::endl;
    sleep(2);

    //2:创建子进程
    pid_t id = fork();
    if(id == 0)
    {
        std::cout<<"子进程关闭了不需要的fd了,准备发消息了"<<std::endl;
        sleep(2);
        //子进程---只write,关闭读端
        close(PipeFd[0]);
        SubProcessWrite(PipeFd[1]);
        close(PipeFd[1]);
        exit(0);
    }

    //父进程---只进行read数据,因此关闭写端
    close(PipeFd[1]);
    FatherProcessRead(PipeFd[0]);
    std::cout << "5s Father Close Rfd"<<std::endl;
    sleep(5);
    close(PipeFd[0]);
    int Status = 0;
    //等待子进程退出,0表示是阻塞等待,如果子进程没退出,父进程会被阻塞
    pid_t Rid = waitpid(id,&Status,0);
    if(Rid > 0)
        std::cout << "Wait ChildProcess Success And Exit Sig "<<(Status&0x7f)<<std::endl;
    return 0;
}

2.4:管道读写规则

pipe2函数与pipe函数类似,也是用于创建匿名管道,其函数原型如下:

cpp 复制代码
int pipe2(int pipefd[2], int flags);

pipe2函数的第二个参数用于设置选项.

1、当没有数据可读时:

  • O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来为止.
  • O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN.

2、当管道满的时候:

  • O_NONBLOCK disable:write调用阻塞,直到有进程读走数据.
  • O_NONBLOCK enable:write调用返回-1,errno值为EAGAIN.

3、如果所有管道写端对应的文件描述符被关闭,则read返回0.

4、如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出.

5、当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性。

6、当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。

2.5:管道特点

1.管道内部自带同步与互斥机制

  • 将一次只允许一个进程使用的资源,称为临界资源.管道在同一时刻是只允许一个进程对其进行写入或是读取操作,因此管道也就是一种临界资源.
  • 临界资源是需要被保护的,如果我们不对管道这种临界资源进行任何保护机制,那么就可能出现同一时刻多个进程对同一管道进行操作的情况,进而导致同时读写、交叉读写以及读取到的数据不一致等问题.
  • 为了避免这些问题,内核会对管道操作进行同步与互斥:
    • 同步:两个或两个以上的进程在运行过程中协同步调,按预定的先后次序运行.比如说:A任务的运行依赖于B任务产生的数据.
    • 互斥:一份公共资源同一个时刻只能被一个进程使用,多个进程不能同时使用公共资源.
  • 实际上,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。对于管道的场景来说,互斥就是两个进程不可以同时对管道进行操作,它们会相互排斥,必须等一个进程操作完毕,另一个才能操作,而同步也是指这两个不能同时对管道进行操作,但这两个进程必须要按照某种次序来对管道进行操作。
  • 也就是说,互斥具有唯一性和排它性,但互斥并不限制任务的运行顺序,而同步的任务之间则有明确的顺序关系。

2.管道的生命周期随进程

管道本质上是通过文件进行通信的,也就是说管道依赖于文件系统,那么当所有打开该文件的进程都退出后,该文件也就会被释放掉,所以说管道的生命周期随进程.

3.管道提供的是流式服务

对于进程A写入管道当中的数据,进程B每次从管道读取的数据的多少是任意的,这种被称为流式服务,与之相对应的是数据包服务:

  • **流式服务:**数据没有明确的分割,不划分成固定的报文段,发送方可以连续发送任意长度的数据,接收方以字节流的形式接收.
  • 数据报服务: 具有明确的分割,以独立的数据报(报文段)为单位进行传输,每个数据报都包含完整的地址信息和数据内容,接收方按报文段接收。

4、管道是半双工通信的

在数据通信中,数据在线路上的传送方式可以分为以下三种:

  • 单工通信 (Simplex Communication):单工模式的数据传输是单向的。通信双方中,一方固定为发送端,另一方固定为接收端。
  • 半双工通信 (Half Duplex):半双工数据传输指数据可以在一个信号载体的两个方向上传输,但是不能同时传输。
  • 全双工通信(Full Duplex):全双工通信允许数据在两个方向上同时传输,它的能力相当于两个单工通信方式的结合。全双工可以同时(瞬时)进行信号的双向传输。

管道的通信方式是半双工通信,数据只能向一个方向流动,需要双方通信时,建立起两个管道.

2.6:管道的四种特殊情况

2.6.1:情况1

如果管道内部是空的并且写端的文件描述符Wfd没有关闭,读取条件不具备,那么读端进程就会被挂起,直到管道里面有数据后,读端进程才会被唤醒.

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/wait.h>

const int size = 1024;

void FatherProcessRead(int Rfd)
{
    char Inbuffer[size];

    std::cout << "Father: start read (will block if pipe empty and write end open)..."<< std::endl;

    ssize_t n = read(Rfd, Inbuffer, sizeof(Inbuffer));

    if (n > 0)
    {
        Inbuffer[n] = 0;
        std::cout << "Father Get Message: " << Inbuffer << std::endl;
    }
    else if (n == 0)
    {
        std::cout << "Write end closed, read returns 0" << std::endl;
    }
    else
    {
        std::cerr << "Read Error: " << strerror(errno) << std::endl;
    }
}

int main()
{
    int PipeFd[2];

    if (pipe(PipeFd) != 0)
    {
        std::cerr << "pipe error: " << strerror(errno) << std::endl;
        return 1;
    }

    pid_t id = fork();

    if (id == 0)
    {
        // 子进程:只保留写端,但不写
        close(PipeFd[0]);

        std::cout << "Child: keep write end open but do NOT write..." << std::endl;

        // 一直睡,保持写端打开
        while (true)
        {
            sleep(5);
        }

        close(PipeFd[1]);
        exit(0);
    }

    // 父进程:只读
    close(PipeFd[1]);

    FatherProcessRead(PipeFd[0]);

    close(PipeFd[0]);
    pid_t Rid = waitpid(id, nullptr, 0);
    if(Rid > 0)
        std::cout << "Wait ChildProcess Success"<<std::endl;

    return 0;
}
2.6.2:情况2

读端进程不读,写端进程一直在写,那么当管道被写满了以后,对应的写端进程会被挂起,直到管道中的数据被读端进程读取后,写端进程才会被唤醒.

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <string>
#include <sys/types.h>
#include <sys/wait.h>
const int size = 1024;

std::string GetOtherMessage()
{
    static int cnt = 0;
    //获取信息的次数
    std::string Messageid = std::to_string(cnt);
    cnt++;
    //获取子进程的id
    pid_t id =getpid();
    std::string stringpid = std::to_string(id);

    std::string Message = " messageid: ";
    Message += Messageid;
    Message += " My Pid is :";
    Message += stringpid;

    return Message;
}

//子进程负责写入
void SubProcessWrite(int Wfd)
{
    int PipeSize = 0;
    std::string Message = "Father,I am your son";
    while (true)
    {
        // //子进程发给父进程的消息
        // std::string Information = Message + GetOtherMessage();
        // //直接通过系统调用进行文件写入
        // //使用write函数,将Information.c_str()位置开始向后size个字节的数据写入文件描述符为Wfd的文件中.
        // write(Wfd,Information.c_str(),Information.size());//写入管道的时候,没有必要写入'\0'
        // sleep(2);
        char c ='A';
        write(Wfd,&c,1);
        std::cout <<"PipeSize: " << ++PipeSize << std::endl;
    }
    
}


//父进程负责读取
void FatherProcessRead(int Rfd)
{
    char Inbuffer[size];
    while (true)
    {
        sleep(500);
        //从文件描述符为Rfd的文件读取sizeof(Inbuffer)字节的数据到Inbuffer位置当中。
        ssize_t n = read(Rfd,Inbuffer,sizeof(Inbuffer));
        if(n > 0)
        {
            Inbuffer[n] = 0;
            std::cout << "Father Get Message: " << Inbuffer << std::endl;
        }
    }
    
}
int main()
{
    //1:创建管道
    int PipeFd[2];
    //输出型参数
    int n = pipe(PipeFd);
    if(n != 0)
    {
        std::cerr <<"errno" << errno << ":" << "errstring :" << strerror(errno) << std::endl;
        return 1;
    }
    //PipeFd[0]--->0---->永远保存的是读端---->read(嘴巴:读)  PipeFd[1]--->1---->永远保存的是写端---->write(钢笔:写)
    std::cout << "PipeFd[0]: " << PipeFd[0] <<", PipeFd[1]: " << PipeFd[1] <<std::endl;
    sleep(2);

    //2:创建子进程
    //3:关闭不需要的fd
    pid_t id = fork();
    if(id == 0)
    {
        std::cout<<"子进程关闭了不需要的fd了,准备发消息了"<<std::endl;
        sleep(2);
        //子进程---只write
        close(PipeFd[0]);
        SubProcessWrite(PipeFd[1]);
        close(PipeFd[1]);
        exit(0);
    }
    std::cout<<"父进程关闭了不需要的fd了,准备读消息了"<<std::endl;
    sleep(2);
    //父进程---只read
    close(PipeFd[1]);
    FatherProcessRead(PipeFd[0]);
    close(PipeFd[0]);
    //等待子进程退出,并且是阻塞等待
    pid_t Rid =waitpid(id,nullptr,0);
    if(Rid > 0)
        std::cout << "Wait ChildProcess Success"<<std::endl;
    return 0;
}
2.6.3:情况3

管道一直在读并且写端关闭了wfd,读端的返回值会读到0,表示读到了文件末尾.

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <string>
#include <sys/types.h>
#include <sys/wait.h>
const int size = 1024;

std::string GetOtherMessage()
{
    static int cnt = 0;
    //获取信息的次数
    std::string Messageid = std::to_string(cnt);
    cnt++;
    //获取子进程的id
    pid_t id =getpid();
    std::string stringpid = std::to_string(id);

    std::string Message = " messageid: ";
    Message += Messageid;
    Message += " My Pid is :";
    Message += stringpid;

    return Message;
}

//子进程负责写入
void SubProcessWrite(int Wfd)
{
    int PipeSize = 0;
    char c ='A';
    std::string Message = "Father,I am your son";
    while (true)
    {
        // //子进程发给父进程的消息
        // std::string Information = Message + GetOtherMessage();
        // //直接通过系统调用进行文件写入
        // //使用write函数,将Information.c_str()位置开始向后size个字节的数据写入文件描述符为Wfd的文件中.
        // write(Wfd,Information.c_str(),Information.size());//写入管道的时候,没有必要写入'\0'
        // sleep(2);
     
        write(Wfd,&c,1);
        std::cout <<"PipeSize: " << ++PipeSize <<"Write Charator Is :"<< c <<std::endl;
        c++;
        if(c == 'F')
            break;
        sleep(1);
    }
    std::cout << "child quit...."<<std::endl;
    
}


//父进程负责读取
void FatherProcessRead(int Rfd)
{
    char Inbuffer[size];
    while (true)
    {
        //sleep(500);
        //从文件描述符为Rfd的文件读取sizeof(Inbuffer)字节的数据到Inbuffer位置当中。
        ssize_t n = read(Rfd,Inbuffer,sizeof(Inbuffer));
        if(n > 0)
        {
            Inbuffer[n] = 0;
            std::cout << "Father Get Message: " << Inbuffer << std::endl;
        }
        //如果read的返回值为0,表示写端直接关闭了,我们读到了文件的结尾.
        else if(0 == n)
        {
            std::cout << "Client Quit,Father Ger Return val: "<< n <<" Father Quit Too"<<std::endl;
            break;
        }
        //读取失败
        else if(n < 0)
        {
            std::cerr << "Read Error" << std::endl;
            break;
        }
  
    }
    
}
int main()
{
    //1:创建管道
    int PipeFd[2];
    //输出型参数
    int n = pipe(PipeFd);
    if(n != 0)
    {
        std::cerr <<"errno" << errno << ":" << "errstring :" << strerror(errno) << std::endl;
        return 1;
    }
    //PipeFd[0]--->0---->永远保存的是读端---->read(嘴巴:读)  PipeFd[1]--->1---->永远保存的是写端---->write(钢笔:写)
    std::cout << "PipeFd[0]: " << PipeFd[0] <<", PipeFd[1]: " << PipeFd[1] <<std::endl;
    sleep(2);

    //2:创建子进程
    //3:关闭不需要的fd
    pid_t id = fork();
    if(id == 0)
    {
        std::cout<<"子进程关闭了不需要的fd了,准备发消息了"<<std::endl;
        sleep(2);
        //子进程---只write
        close(PipeFd[0]);
        SubProcessWrite(PipeFd[1]);
        close(PipeFd[1]);
        exit(0);
    }
    std::cout<<"父进程关闭了不需要的fd了,准备读消息了"<<std::endl;
    sleep(2);
    //父进程---只read
    close(PipeFd[1]);
    FatherProcessRead(PipeFd[0]);
    //等待子进程退出,并且是阻塞等待
    close(PipeFd[0]);
    pid_t Rid =waitpid(id,nullptr,0);
    if(Rid > 0)
        std::cout << "Wait ChildProcess Success"<<std::endl;
    return 0;
}
2.6.4:情况4

Rfd直接关闭,写端Wfd一直在进行写入,那么此时操作系统会将写端进程杀掉.

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <string>
#include <sys/types.h>
#include <sys/wait.h>
const int size = 1024;

std::string GetOtherMessage()
{
    static int cnt = 0;
    //获取信息的次数
    std::string Messageid = std::to_string(cnt);
    cnt++;
    //获取子进程的id
    pid_t id =getpid();
    std::string stringpid = std::to_string(id);

    std::string Message = " messageid: ";
    Message += Messageid;
    Message += " My Pid is :";
    Message += stringpid;

    return Message;
}

//子进程负责写入
void SubProcessWrite(int Wfd)
{
    int PipeSize = 0;
    char c ='A';
    std::string Message = "Father,I am your son";
    while (true)
    {
        //子进程发给父进程的消息
        std::string Information = Message + GetOtherMessage();
        //直接通过系统调用进行文件写入
        //使用write函数,将Information.c_str()位置开始向后size个字节的数据写入文件描述符为Wfd的文件中.
        write(Wfd,Information.c_str(),Information.size());//写入管道的时候,没有必要写入'\0'
        sleep(1);
     
    }
    std::cout << "child quit...."<<std::endl;
    
}


//父进程负责读取
void FatherProcessRead(int Rfd)
{
    char Inbuffer[size];
    while (true)
    {
        //sleep(500);
        //从文件描述符为Rfd的文件读取sizeof(Inbuffer)字节的数据到Inbuffer位置当中。
        ssize_t n = read(Rfd,Inbuffer,sizeof(Inbuffer));
        if(n > 0)
        {
            Inbuffer[n] = 0;
            std::cout << "Father Get Message: " << Inbuffer << std::endl;
        }
        //如果read的返回值为0,表示写端直接关闭了,我们读到了文件的结尾.
        else if(0 == n)
        {
            std::cout << "Client Quit,Father Ger Return val: "<< n <<" Father Quit Too"<<std::endl;
            break;
        }
        //读取失败
        else if(n < 0)
        {
            std::cerr << "Read Error" << std::endl;
            break;
        }
        sleep(1);
        //读一次消息以后直接退出
        break;
    }
    
}
int main()
{
    //1:创建管道
    int PipeFd[2];
    //输出型参数
    int n = pipe(PipeFd);
    if(n != 0)
    {
        std::cerr <<"errno" << errno << ":" << "errstring :" << strerror(errno) << std::endl;
        return 1;
    }
    //PipeFd[0]--->0---->永远保存的是读端---->read(嘴巴:读)  PipeFd[1]--->1---->永远保存的是写端---->write(钢笔:写)
    std::cout << "PipeFd[0]: " << PipeFd[0] <<", PipeFd[1]: " << PipeFd[1] <<std::endl;
    sleep(2);

    //2:创建子进程
    //3:关闭不需要的fd
    pid_t id = fork();
    if(id == 0)
    {
        std::cout<<"子进程关闭了不需要的fd了,准备发消息了"<<std::endl;
        sleep(2);
        //子进程---只write
        close(PipeFd[0]);
        SubProcessWrite(PipeFd[1]);
        close(PipeFd[1]);
        exit(0);
    }
    std::cout<<"父进程关闭了不需要的fd了,准备读消息了"<<std::endl;
    sleep(2);
    //父进程---只read
    close(PipeFd[1]);
    FatherProcessRead(PipeFd[0]);
    std::cout << "5s Father Close Rfd"<<std::endl;
    sleep(5);
    close(PipeFd[0]);
    int Status = 0;
    //等待子进程退出,并且是阻塞等待
    pid_t Rid =waitpid(id,&Status,0);
    if(Rid > 0)
        std::cout << "Wait ChildProcess Success And Exit Sig "<<(Status&0x7f)<<std::endl;
    return 0;
}
  1. 写端进程不写,读端进程一直读,那么此时会因为管道里面没有数据可读,对应的读端进程会被挂起,直到管道里面有数据后,读端进程才会被唤醒。
  2. 写端进程一直写,读端进程不读,那么当管道被写满后,对应的写端进程会被挂起,直到管道当中的数据被读端进程读取后,写端进程才会被唤醒。
  3. 写端进程将数据写完后将写端关闭,那么读端进程将管道当中的数据读完后,就会继续执行该进程之后的代码逻辑,而不会被挂起。
  4. 读端进程将读端关闭,而写端进程还在一直向管道写入数据,那么操作系统会将写端进程杀掉。
  • 其中前面两种情况能够很好的说明,管道是自带同步与互斥机制的,读端进程和写端进程是有一个步调协调的过程的,不会说当管道没有数据了读端还在读取,而当管道已经满了写端还在写入。读端进程读取数据的条件是管道里面有数据,写端进程写入数据的条件是管道当中还有空间,若是条件不满足,则相应的进程就会被挂起,直到条件满足后才会被再次唤醒。
  • 第三种情况也很好理解,读端进程已经将管道当中的所有数据都读取出来了,而且此后也不会有写端再进行写入了,那么此时读端进程也就可以执行该进程的其他逻辑了,而不会被挂起。
  • 第四种情况也不难理解,既然管道当中的数据已经没有进程会读取了,那么写端进程的写入将没有意义,因此操作系统直接将写端进程杀掉。而此时子进程代码都还没跑完就被终止了,属于异常退出,那么子进程必然收到了某种信号。

2.7:管道的大小

管道的容量是有限的,如果管道已满,那么写端将阻塞或失败,那么管道的最大容量是多少呢?

方法一:使用man手册

bash 复制代码
man 7 pipe

根据man手册,在2.6.11之前的Linux版本中,管道的最大容量与系统页面大小相同,从Linux 2.6.11往后,管道的最大容量是65536字节。

方法二:使用ulimit命令

bash 复制代码
ulimit -a

根据显示,管道的最大容量是 512 × 8 =4096 字节。

方法三:自行测试

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <string>
#include <sys/types.h>
#include <sys/wait.h>
const int size = 1024;

std::string GetOtherMessage()
{
    static int cnt = 0;
    //获取信息的次数
    std::string Messageid = std::to_string(cnt);
    cnt++;
    //获取子进程的id
    pid_t id =getpid();
    std::string stringpid = std::to_string(id);

    std::string Message = " messageid: ";
    Message += Messageid;
    Message += " My Pid is :";
    Message += stringpid;

    return Message;
}

//子进程负责写入
void SubProcessWrite(int Wfd)
{
    int PipeSize = 0;
    std::string Message = "Father,I am your son";
    while (true)
    {
        // //子进程发给父进程的消息
        // std::string Information = Message + GetOtherMessage();
        // //直接通过系统调用进行文件写入
        // //使用write函数,将Information.c_str()位置开始向后size个字节的数据写入文件描述符为Wfd的文件中.
        // write(Wfd,Information.c_str(),Information.size());//写入管道的时候,没有必要写入'\0'
        // sleep(2);
        char c ='A';
        write(Wfd,&c,1);
        std::cout <<"PipeSize: " << ++PipeSize << std::endl;
    }
    
}


//父进程负责读取
void FatherProcessRead(int Rfd)
{
    char Inbuffer[size];
    while (true)
    {
        sleep(500);
        //从文件描述符为Rfd的文件读取sizeof(Inbuffer)字节的数据到Inbuffer位置当中。
        ssize_t n = read(Rfd,Inbuffer,sizeof(Inbuffer));
        if(n > 0)
        {
            Inbuffer[n] = 0;
            std::cout << "Father Get Message: " << Inbuffer << std::endl;
        }
    }
    
}
int main()
{
    //1:创建管道
    int PipeFd[2];
    //输出型参数
    int n = pipe(PipeFd);
    if(n != 0)
    {
        std::cerr <<"errno" << errno << ":" << "errstring :" << strerror(errno) << std::endl;
        return 1;
    }
    //PipeFd[0]--->0---->永远保存的是读端---->read(嘴巴:读)  PipeFd[1]--->1---->永远保存的是写端---->write(钢笔:写)
    std::cout << "PipeFd[0]: " << PipeFd[0] <<", PipeFd[1]: " << PipeFd[1] <<std::endl;
    sleep(2);

    //2:创建子进程
    //3:关闭不需要的fd
    pid_t id = fork();
    if(id == 0)
    {
        std::cout<<"子进程关闭了不需要的fd了,准备发消息了"<<std::endl;
        sleep(2);
        //子进程---只write
        close(PipeFd[0]);
        SubProcessWrite(PipeFd[1]);
        close(PipeFd[1]);
        exit(0);
    }
    std::cout<<"父进程关闭了不需要的fd了,准备读消息了"<<std::endl;
    sleep(2);
    //父进程---只read
    close(PipeFd[1]);
    FatherProcessRead(PipeFd[0]);
    close(PipeFd[0]);
    //等待子进程退出,并且是阻塞等待
    pid_t Rid =waitpid(id,nullptr,0);
    if(Rid > 0)
        std::cout << "Wait ChildProcess Success"<<std::endl;
    return 0;
}

可以看到,在读端进程不进行读取的情况下,写端进程最多写65536字节的数据就被操作系统挂起了,也就是说,我当前Linux版本中管道的最大容量是65536字节。

3:命名管道

3.1:原理

  • 匿名管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间的通信,通常,一个管道由一个进程创建,然后该父进程调用fork,此后父子进程可以通过该管道进行通信.
  • 如果要实现两个毫不相关进程之间的通信,可以使用命名管道来做到,命名管道是一种特殊类型的文件,两个进程通过命名管道的文件名打开同一个管道文件(文件都有唯一的文件路径),此时这两个进程也就看到了同一份资源,进而就可以进行通信了.

注意

  • 普通文件是很难做到通信的,即便要做到通信也无法解决一些安全问题.
  • 命名管道和匿名管道一样,都是内存文件,只不过命名管道在磁盘有一个简单的映像,但这个映像的大小永远为0,因为命名管道和匿名管道都不会将通信数据刷新到磁盘当中.

3.2:使用命令创建命名管道

我们可以使用mkfifo命令创建一个命名管道

  • 使用这个命名管道文件,就能实现两个进程之间的通信了.我们在一个进程(进程A)中用shell脚本每秒向命名管道写入一个字符串,在另一个进程(进程B)当中用cat命令从命名管道当中进程读取.
  • 现象就是当进程A启动后,进程B会每秒从命名管道中读取一个字符串打印到显示器上。这就证明了这两个毫不相关的进程可以通过命名管道进行数据传输,这样子就基于文件级的管道实现了一次通信.

3.3:创建一个命名管道

在程序中创建命名管道使用mkfifo函数,mkfifo函数的函数原型如下:

cpp 复制代码
int mkfifo(const char *pathname, mode_t mode);
  • 第一个参数为pathname,表示要创建的命名管道文件.

    • 若pathname以路径的方式给出,则将命名管道文件创建在pathname路径下.
    • 若pathname以文件名的方式给出,则将命名管道文件默认创建在当前路径下。(注意当前路径的含义)
  • 第二个参数为mode,表示创建命名管道权限.

    • 但实际上创建出来文件的权限值还会受到umask(文件默认掩码)的影响,实际创建出来文件的权限为:mode&(~umask)。umask的默认值一般为0002,当我们设置mode值为0666时实际创建出来文件的权限为0664。
    • 若想创建出来命名管道文件的权限值不受umask的影响,则需要在创建文件前使用umask函数将文件默认掩码设置为0。
      mkfido函数返回值
  • 命名管道创建成功,返回0。

  • 命名管道创建失败,返回-1。

3.3.1:代码示例

使用以下代码即可在当前路径下,创建出一个名为myfifo的命名管道。

cpp 复制代码
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>

#define FileName "myfifo"

int main()
{
    umask(0);
    int Ret = mkfifo(FileName,0666);
    if(Ret < 0)
    {
        perror("mkfifo");
        return 1;
    }
        
    return 0;
}

运行代码后,命名管道myfifo就在当前路径下被创建了.

3.4:命名管道的打开规则

1、如果当前打开操作时为读而打开FIFO时.

  • O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO.
  • O_NONBLOCK enable:立刻返回成功.

2、如果当前打开操作时为写而打开FIFO时.

  • O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO.
  • O_NONBLOCK enable:立刻返回失败,错误码为ENXIO.

3.5:用命名管道实现serve & cilent通信

  • 实现服务端(server)和客户端(client)之间的通信之前,我们需要先让服务端运行起来,我们需要让服务端运行后创建一个命名管道文件,然后再以读的方式打开该命名管道文件,之后服务端就可以从该命名管道当中读取客户端发来的通信信息了.
  • 那么既然服务端和客户端之间是通过命名管道来进行通信的,因此我们可以先描述二者通信的命名管道然后将其封装到一个类里面.
3.5.1:NamePiped.hpp
cpp 复制代码
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <string>
#include <cerrno>
#include <cstdio>
#include <unistd.h>
#include <fcntl.h>

// 创建的公共路径
const std::string Comm_Path = "./myfifo";
#define Creator 1
#define User 2
#define Write O_WRONLY
#define Read O_RDONLY
#define DefaultFd -1
#define BaseSize 4096

class NamePiped
{

private:
    // 打开管道
    bool OpenNamedPipe(int mode);
public:
    NamePiped(const std::string &path, int who);
    //读取管道数据
    int ReadNamedPipe(std::string * out);
    //写入管道数据
    int WriteNamedPipe(const std::string & In);

    //  读端
    bool OpenForRead();
    bool OpenForWrite();
    ~NamePiped();
private:
    const std::string _FiFo_Path;
    int _id;
    int _fd;
};
  • 我们使用NamePiped这个类将命名管道进行封装,用于简化服务端与客户端进行通信的使用.
  • 封装 FIFO(命名管道)的创建、打开、读写和关闭操作;这样子我们可以像操作对象一样进行 创建管道、打开读/写端、读数据、写数据。
  1. Comm_Path = "./myfifo"------->表示默认管道路径(公共通信文件).
  2. Creator 和 User分别表示创建管道的一方与使用管道的一方.
  3. Write与Read是通过封装系统调用open()的模式------>写端只能够写,读端只能够读.
  4. DefaultFd:表示命名管道文件的文件描述符.
  5. BaseSize:表示命名管道文件的大小.
  6. _FiFo_Path:表示创建的命名管道文件的文件路径.
  7. _id:表示身份
  • const & : const std::string 输入型参数.
  • * :std::string * 输出型参数.
  • & :std::string & 输入输出型参数.
3.5.1.1:构造函数与析构函数的实现
cpp 复制代码
    //走初始化列表
    NamePiped(const std::string &path, int who)
    :_FiFo_Path(path)
    ,_id(who)
    ,_fd(DefaultFd)    
    {
        //为创建者创建管道
        if(_id == Creator)
        {
            int Result = mkfifo(_FiFo_Path.c_str(),0666);
            if(Result != 0)
                perror("mkfifo");
            std::cout << "Creater Create Named Pipe" << std::endl;
        }
    }
  • 初始化列表 :将路径、身份和默认的文件描述符(-1)进行初始化。

  • 创建权限控制 :通过判断 _id == Creator,确保只有"创建者"才会在系统底层真正生成这个 FIFO 文件。

  • mkfifo 系统调用0666 表示管道文件默认权限(读写),但实际权限还会受系统 umask 掩码影响

cpp 复制代码
    ~NamePiped()
    {
        if(_id == Creator)
        {
            int Result = unlink(_FiFo_Path.c_str());
            if(Result != 0)
                perror("unlink");
            std::cout << "Creater Free Named Pipe" << std::endl;
        }
        if(_fd != DefaultFd)
            close(_fd);
    }
  • 清理文件资源:依然通过Creator身份限制,调用unlink从Linux文件系统中删除该命名管道文件,防止遗留垃圾文件.
  • 清理进程级资源 :无论是 Creator 还是 User,只要打开过管道(_fd != -1),就会自动调用 close(_fd) 关闭文件描述符,防止文件描述符泄漏。
3.5.1.2:管道开启模块
cpp 复制代码
    // 打开管道
    bool OpenNamedPipe(int mode)
    {
        _fd = open(_FiFo_Path.c_str(),mode);
        if(_fd < 0)
            return false;
        return true;
    }

    
    //  读端
    bool OpenForRead()
    {
        return OpenNamedPipe(Read);
    }
    //写端
    bool OpenForWrite()
    {
         return OpenNamedPipe(Write);
    }
  • 底层封装:私有函数OpenNamePipe对系统的open进行了封装,并记录下返回的文件描述符_fd.
  • 接口隔离:向外部暴露了明确的OpenForRead和OpenForWrite.
  • 阻塞机制 :调用OpenForRead,进程会阻塞(卡住),直到另一个进程调用 OpenForWrite.
3.5.1.3:数据通信模块(读写数据)
读取数据
cpp 复制代码
   //读取管道数据
    int ReadNamedPipe(std::string * out)
    {
        char Buffer[BaseSize];
        int Result = read(_fd,Buffer,sizeof(Buffer));
        if(Result > 0)
        {
            // 放置字符串结束符 \0
            Buffer[Result] = 0;
            // C风格字符串转为 C++ std::string(隐式类型转换)
            *out = Buffer;
        }
        return Result;
    }
  • **输出型参数 *out:**使用指针代表这是用来接收数据的输出参数。

  • 处理逻辑: 调用底层的 read 系统调用,将读到的字节流塞入 Buffer

写入数据
cpp 复制代码
    //写入管道数据
    int WriteNamedPipe(const std::string & In)
    {
        return write(_fd,In.c_str(),In.size());
    }
  • 输入型参数 const &:保证数据不被修改的同时,避免了字符串拷贝的性能消耗。

  • 处理逻辑 :调用 write,将 std::string 转换为 C风格字符数组后写入管道。

3.5.1.4:总代码
cpp 复制代码
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <string>
#include <cerrno>
#include <cstdio>
#include <unistd.h>
#include <fcntl.h>

// 创建的公共路径
const std::string Comm_Path = "./myfifo";
#define Creator 1
#define User 2
#define Write O_WRONLY
#define Read O_RDONLY
#define DefaultFd -1
#define BaseSize 4096


/*
const & : const std::string 输入型参数.
std::string *  输出型参数.
:std::string & 输入输出型参数.
 */
class NamePiped
{

private:
    // 打开管道
    bool OpenNamedPipe(int mode)
    {
        _fd = open(_FiFo_Path.c_str(),mode);
        if(_fd < 0)
            return false;
        return true;
    }
public:
    //走初始化列表
    NamePiped(const std::string &path, int who)
    :_FiFo_Path(path)
    ,_id(who)
    ,_fd(DefaultFd)    
    {
        //为创建者创建管道
        if(_id == Creator)
        {
            int Result = mkfifo(_FiFo_Path.c_str(),0666);
            if(Result != 0)
                perror("mkfifo");
            std::cout << "Creater Create Named Pipe" << std::endl;
        }
    }
    //读取管道数据
    int ReadNamedPipe(std::string * out)
    {
        char Buffer[BaseSize];
        int Result = read(_fd,Buffer,sizeof(Buffer));
        if(Result > 0)
        {
            // 放置字符串结束符 \0
            Buffer[Result] = 0;
            // C风格字符串转为 C++ std::string(隐式类型转换)
            *out = Buffer;
        }
        return Result;
    }
    //写入管道数据
    int WriteNamedPipe(const std::string & In)
    {
        return write(_fd,In.c_str(),In.size());
    }

    //  读端
    bool OpenForRead()
    {
        return OpenNamedPipe(Read);
    }
    //写端
    bool OpenForWrite()
    {
         return OpenNamedPipe(Write);
    }
    ~NamePiped()
    {
        if(_id == Creator)
        {
            int Result = unlink(_FiFo_Path.c_str());
            if(Result != 0)
                perror("unlink");
            std::cout << "Creater Free Named Pipe" << std::endl;
        }
        if(_fd != DefaultFd)
            close(_fd);
    }
private:
    const std::string _FiFo_Path;
    int _id;
    int _fd;
};
3.5.2:Server.cpp

实现服务端(server)和客户端(client)之间的通信之前,我们需要先让服务端运行起来,我们需要让服务端运行后创建一个命名管道文件,然后再以读的方式打开该命名管道文件,之后服务端就可以从该命名管道当中读取客户端发来的通信信息了.

服务端的代码如下:

cpp 复制代码
// 负责读取
#include "NamedPipe.hpp"

int main()
{
    NamePiped fifo(Comm_Path, Creator);
    if (fifo.OpenForRead())
    {
        std::cout << "Server Open NamedPipe Done" << std::endl;
        sleep(3);
        while (true)
        {
            std::string Message;
            int n = fifo.ReadNamedPipe(&Message);
            if (n > 0)
                std::cout << "Cilent Say:> " << Message << std::endl;
            //写端关闭,读端在读取,会读到0并且一直读到文件末尾
            else if(n == 0)
            {
                std::cout << "Cilent Quit,Server Too!" << std::endl;
                break;
            }
            else
            {
                std::cout <<" fifo.ReadNamedPipe Error" << std::endl;
                break;
            }
        }
    }

    return 0;
}
3.5.3:Cilent.cpp

而对于客户端而言,由于服务端已经运行了,并且创建了命名管道,所以客户端只需要以写的方式打开该命名管道,之后客户端就可以将通信信息写入到命名管道文件当中,进而实现和服务端的通信.

cpp 复制代码
// 负责写入

#include "NamedPipe.hpp"

int main()
{
    NamePiped fifo(Comm_Path, User);
    if (fifo.OpenForWrite())
    {
        std::cout << "Cilent Open Named Pipe Done" << std::endl;
        while (true)
        {
            std::cout << "Please Enter> ";
            std::string Message;
            // 从标准输入获取到Message
            std::getline(std::cin, Message);
            fifo.WriteNamedPipe(Message);
        }
    }
    return 0;
}
3.5.4:测试运行

先将服务端进程运行起来,之后我们就能在客户端看到这个已经被创建的命名管道文件。

之后再将客户端运行起来,此时我们从客户端写入的信息被客户端写入到命名管道中,服务端再从命名管道当中将信息读取出来打印在服务端的显示器上,该现象说明服务端是能够通过命名管道获取到客户端发来的信息的,换句话说,服务端和客户端之间是能够通信的.

当客户端和服务端运行起来时,我们还可以通过ps命令查看这两个进程的信息,可以发现这两个进程确实是两个毫不相关的进程,因为它们的PID和PPID都不相同。也就证明了,命名管道是可以实现两个毫不相关进程之间的通信的。

服务端和客户端之间的退出关系.

当客户端退出后,服务端将管道当中的数据读完后就再也读不到数据了,那么此时服务端也就会去执行它的其他代码了(在当前代码中是直接退出了)。

通信是在内存当中进行的.那么有的uu就会有个小疑问,如果只让客户端向管道写入数据,而服务端不从管道读取数据,那么这个管道文件的大小会不会发生变化呢?

这里我们需要简单修改下服务端的代码,其他部分还是不变的.

Server.cpp
cpp 复制代码
// 负责读取
#include "NamedPipe.hpp"

int main()
{
    NamePiped fifo(Comm_Path, Creator);
    if (fifo.OpenForRead())
    {
        std::cout << "Server Open NamedPipe Done" << std::endl;
        sleep(3);
        while (true)
        {
            ;
        }
    }

    return 0;
}
  • 我们可以清晰地看到,服务端不读取管道当中的数据,但是管道当中的数据并没有被刷新到磁盘,使用ll命令看到命名管道的大小依旧为0。
  • 说明了双方进程之间的通信依旧是在内存当中进行的,和匿名管道通信是一样的。
3.5.5:用命名管道实现派发计算任务

两个进程之间的通信,并不是简单的发送字符串而已,服务端是会对客户端发送过来的信息进行处理的.

  • 这里我们以客户端向服务端派发计算任务为例,客户端通过管道向服务端发送双操作数的计算请求,服务端接收到客户端的信息后需要计算出相应的结果。
  • 这里我们无需更改客户端的代码,只需改变服务端处理通信信息的逻辑即可。
Server.cpp
cpp 复制代码
// 负责读取
#include "NamedPipe.hpp"
#include <string.h>
int main()
{
    NamePiped fifo(Comm_Path, Creator);
    if (fifo.OpenForRead())
    {
        std::cout << "Server Open NamedPipe Done" << std::endl;
        sleep(3);
        while (true)
        {
            std::string Message;
            //读取信息
            int n = fifo.ReadNamedPipe(&Message);
            if (n > 0)
            {
                Message[n] = '\0';
                std::cout << "Cilent Say:> " << Message << std::endl;
                //服务端进行计算任务
                char * Symbol = "+-*/%";
                const char * p = Message.c_str();
                int Flag = 0;
                while (*p)
                {
                    switch (*p)
                    {
                    case '+':
                        Flag = 0;
                        break;
                    case '-':
                        Flag = 1;
                        break;
                    case '*':
                        Flag = 2;
                        break;
                    case '/':
                        Flag = 3;
                        break;
                    case '%':
                        Flag = 4;
                        break;
                    }
                    p++;
                }
                char * Data1 = strtok((char * )Message.c_str(), "+-*/%");
                char * Data2 = strtok(NULL, "+-*/%");
                int Number1 = atoi(Data1);
                int Number2 = atoi(Data2);
                int Result = 0;
                switch(Flag)
                {
                case 0:
                    Result = Number1 + Number2;
                    break;
                case 1:
                    Result = Number1 - Number2;
                    break;
                case 2:
                    Result = Number1 * Number2;
                    break;
                case 3:
                    Result = Number1 / Number2;
                    break;
                case 4:
                    Result = Number1 % Number2;
                    break;
                }
                printf("%d %c %d = %d\n",  Number1,Symbol[Flag], Number2, Result); //打印计算结果
            }

            //写端关闭,读端在读取,会读到0并且一直读到文件末尾
            else if(n == 0)
            {
                std::cout << "Cilent Quit,Server Too!" << std::endl;
                break;
            }
            else
            {
                std::cout <<" fifo.ReadNamedPipe Error" << std::endl;
                break;
            }
        }
    }

    return 0;
}
3.5.6:使用命名管道实现进程监控
  • 还有一个有意思的玩法,我们可以通过一个进程来控制另一个进程的行为,比如我们从客户端输入命令到管道当中,再让服务端将管道当中的命令读取出来并执行。
  • 下面博主只实现了让服务端执行不带选项的命令,若是想让服务端执行带选项的命令,可以对管道当中获取的命令进行解析处理。这里的实现非常简单,只需让服务端从管道当中读取命令后创建子进程,然后再进行进程程序替换即可。
  • 这里也无需更改客户端的代码,只需改变服务端处理通信信息的逻辑即可.
Server.cpp
cpp 复制代码
// 负责读取
#include "NamedPipe.hpp"
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
    NamePiped fifo(Comm_Path, Creator);
    if (fifo.OpenForRead())
    {
        std::cout << "Server Open NamedPipe Done" << std::endl;
        sleep(3);
        while (true)
        {
            std::string Message;
            //读取信息
            int n = fifo.ReadNamedPipe(&Message);
            if (n > 0)
            {
                Message[n] = '\0';
                std::cout << "Cilent Say:> " << Message << std::endl;
                if(fork() == 0)
                {
                    //子进程
                    //进程程序替换
                    execlp(Message.c_str(),Message.c_str(),NULL);
                    exit(1);
                }
                waitpid(-1,NULL,0);
            }

            //写端关闭,读端在读取,会读到0并且一直读到文件末尾
            else if(n == 0)
            {
                std::cout << "Cilent Quit,Server Too!" << std::endl;
                break;
            }
            else
            {
                std::cout <<" fifo.ReadNamedPipe Error" << std::endl;
                break;
            }
        }
    }

    return 0;
}

此时服务端接收到客户端的信息后,便进行进程程序替换,进而执行客户端发送过来的命令。

3.5.7:用命名管道实现文件拷贝

这里我们再用命名管道实现一下文件的拷贝。

需要拷贝的文件是file.txt,该文件当中的内容如下:

我们需要做的是,让客户端将file.txt文件通过管道发送给服务端,在服务端创建一个file-bat.txt文件,并将从管道获取到的数据写入到file-bat.txt文件当中,这样子就实现了file.txt文件的拷贝.

Server.cpp
cpp 复制代码
// 负责读取
#include "NamedPipe.hpp"
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
    NamePiped fifo(Comm_Path, Creator);
    int FdOut = open("file-bat.txt",O_CREAT | O_WRONLY,0666);
    if (FdOut < 0){
		perror("open");
		exit(-1);
	}
    if (fifo.OpenForRead())
    {
        std::cout << "Server Open NamedPipe Done" << std::endl;
        sleep(3);
        while (true)
        {
            std::string Message;
            //读取信息
            int n = fifo.ReadNamedPipe(&Message);
            if (n > 0)
            {
                write(FdOut,Message.c_str(),n);
            }

            //写端关闭,读端在读取,会读到0并且一直读到文件末尾
            else if(n == 0)
            {
                std::cout << "Cilent Quit,Server Too!" << std::endl;
                break;
            }
            else
            {
                std::cout <<" fifo.ReadNamedPipe Error" << std::endl;
                break;
            }
        }
    }
    close(FdOut);

    return 0;
}
Cilent.cpp
cpp 复制代码
// 负责写入

#include "NamedPipe.hpp"

int main()
{
    umask(0);
    NamePiped fifo(Comm_Path, User);
    int FdIn = open("file.txt", O_RDONLY, 0666);
    if (FdIn < 0)
    {
        perror("open");
        exit(-1);
    }
    if (fifo.OpenForWrite())
    {
        std::cout << "Cilent Open Named Pipe Done" << std::endl;
        char Buffer[128];
        while (true)
        {
            int n = read(FdIn, Buffer, sizeof(Buffer));
            if (n > 0)
            {
                // 写入数据
                fifo.WriteNamedPipe(std::string(Buffer, n));
            }

            else if (n == 0)
            {
                printf("read end of file!\n");
                break;
            }
            else
            {
                printf("read error!\n");
                break;
            }
        }
    }
    close(FdIn);
    return 0;
}
3.5.8:命名管道和匿名管道的区别
  • 匿名管道由pipe函数创建并打开。
  • 命名管道由mkfifo函数创建,由open函数打开。
  • FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在于它们创建与打开的方式不同.
  • 无论是匿名管道还是命名管道,都是为了让不同的进程看到同一份资源,而命名管道则是通过文件路径来实现的.
3.5.9:命令行当中的管道

现有data.txt文件,文件当中的内容如下:

我们可以利用管道("|")同时使用cat命令和grep命令,进而实现文本过滤。

那么有的uu就会好奇,在命令行当中的管道("|")到底是匿名管道还是命名管道呢?

  • 由于匿名管道只能用于有亲缘关系的进程之间的通信,而命名管道可以用于两个毫不相干的进程之间的通信,因此可以先看看命令行当中的管道("|")连接起来的各个进程之间是否具有亲缘关系.
  • 下面通过管道("|")连接了三个进程,通过ps命令查看这三个进程可以发现,这三个进程的PPID是相同的,也就是说它们是由同一个父进程创建的子进程。
  • 而它们的父进程实际上就是命令行解释器,这里为bash
  • 因此,由管道("|")连接起来的各个进程是有亲缘关系的,它们之间互为兄弟进程,所以命令行上的管道实际上是匿名管道。
相关推荐
Deitymoon2 小时前
linux——线程的概念
linux
郝学胜-神的一滴2 小时前
Pytorch自动微分模块:从原理到实战,解锁反向传播核心奥秘
服务器·人工智能·pytorch·python·深度学习·机器学习
eF06U766F2 小时前
Ubuntu Linux 上 固定P/E 核混合架构CPU频率
linux·ubuntu·架构
minji...2 小时前
Linux 多线程(三)线程控制,线程终止,线程中的异常问题
linux·运维·服务器·开发语言·网络·算法
zzzsde2 小时前
【Linux】进程间通信(1)管道&&进程池实现
linux·运维·服务器
Miki Makimura2 小时前
C++ 聊天室项目:Linux 环境搭建与问题总结
linux·开发语言·c++
Yiyi_Coding2 小时前
bat 脚本(真实项目可用):ftp取远程文件
运维·脚本·ftp
开开心心_Every2 小时前
实用PDF擦除隐藏信息工具,空白处理需留意
运维·服务器·网络·pdf·电脑·excel·依赖倒置原则
Hello World . .2 小时前
Linux:Linux命令行音视频播放器
linux·音视频