小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
linux系列专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!
在这里插入图片描述



前言

【linux】进程间通信(一)匿名管道,pipe——书接上文 详情请点击<——
【linux】进程池小程序——书接上文 详情请点击<——,本文会在上文的基础上进行讲解,所以对上文不了解的读者友友请点击前方的蓝字链接进行学习
本文由小编为大家介绍——【linux】进程间通信(二)命名管道


一、命名管道原理

  1. 在之前的文章中,小编已经讲解了具有血缘关系的进程进行进程间通信使用匿名管道,那么如果毫不相关的进程进程进程间通信呢?要使用命名管道
  2. 如果两个不同的进程,打开同一个文件的时候,在内核中,操作系统会打开几个文件?
  3. 在内核中,操作系统会打开一个文件,即在内核中,仅有一个文件的inode结构体,文件的files_operations结构体,一个内核级别的文件缓冲区
  4. 那么由于这两个进程可能会有不同的打开文件的方式,并且两个文件的执行流不同,读取写入到文件的位置可能也不同,所以分别会给这两个进程各自分配一个文件打开对象
    在这里插入图片描述
  5. 由于进程一和进程二看到同一份资源的方式是打开同一个文件,并不和匿名管道中子进程继承父进程的关系一样,在匿名管道中父进程需要以只读和只写的方式打开两次文件,然后子进程继承,父进程和子进程根据读写端关闭对应相反的读写端,而在命名管道这里,两个毫不相关的进程一和进程二并不需要关闭对应的读写端,而是根据需求以对应的读或者写的方式打开相同的文件即可
  6. 那么两个进程如何知道它们两个打开的是同一个文件?因为进行打开的时候是采用的是同路径下的同一个文件名 = 路径 + 文件名 = 具有唯一性
  7. 同样的,这个命名管道也管道它也具有管道的特征,即单向通信,并且不需要将缓冲区的内容刷新到磁盘上,命名管道同样也是一个文件,这个文件是内存级文件

二、命名管道的特征

如何证明,匿名管道文章中有进行讲解,小编就将匿名管道的内容直接拿来用了
如果想要了解如何证明详情请点击<——

  1. 一般来讲,毫不相关的进程通常采用命名管道来进行进程间通信
  2. 管道只能单向通信
  3. 父子进程是会进行协同的,同步与互斥的,目的是为了保护数据的安全
  4. 管道是面向字节流的。简单理解,管道就像水龙头一样,管道中的数据是以一个一个字节为基本单位,管道并不会维护写入时的格式,读取时的格式,即进行读取的时候管道并不会让你以数据写入时的大小格式进行读取,如何读取,写入而是需要由用户端进行约定,即进程间就要指定读写的协议,采用固定的大小进行读写,关于协议小编后面的文章会进行讲解
  5. 管道的基于文件的,文件的生命周期随进程,当进程结束的时候,自然而然管道文件就会被关闭,释放

三、命名管道的情况

如何证明,匿名管道文章中有进行讲解,小编这里就将匿名管道的内容直接拿来用了
如果想要了解如何证明详情请点击<——

  1. 读写端正常,管道为空,读端就要阻塞,可以有效保护数据安全
  2. 读写端正常, 管道满了,写端就要阻塞,可以有效保护数据安全
  3. 读端正常读,写端关闭,读端就会读到0,代表读到了文件(pipe)结束,读端退出,不会阻塞
  4. 写端正常写,读端关闭,操作系统就要通过13号信号杀掉正在写入的写端进程

四、接口mkfifo

  1. 下面我们来认识一下命名管道的接口,在命令行中中,匿名管道是 | ,而命名管道的指令是mkfifo,mkfifo可以在当前路径下创建一个命名管道,同时我们还可以看到FIFOs叫做命名管道named pipes,即命名管道又叫做FIFOs,什么意思呢?
  2. FIFO其实是first in first out,先进先出,即这是队列的特性,换句话来讲管道也就是队列,管道是单向通信的,管道是基于字节流的,管道是以字节为单位的,即管道是一个基于字节的队列
    在这里插入图片描述
  3. 那么下面我们来使用一下mkfifo,即直接在命令行中使用mkfifo 你想要创建的命名管道名,就可以在当前路径下创建出一个命名管道,这个命名管道的文件属性的开头是p,p即pipe管道,即标识这是一个管道文件
    在这里插入图片描述
  4. 首先命名管道一定是要有两个进程进行通信的,在左侧,所以此时我们先使用一个进程作为读端,即当我们在命令行中输出指令echo "hello linux"的时候,此时这条指令就会以进程的形式去执行,那么我们就采用echo作为写端向命名管道中进行写入,那么如何写入,重定向即可,但是当我们重定向之后,此时左侧就卡住了,即陷入阻塞等待的状态,等待什么?等待读端进行读取命名管道中的数据
  5. 此时我们复制ssh渠道,在右侧,在同样的目录下查看这个命名管道,发现命名管道的文件属性中的文件大小是0,这很容易理解,因为管道文件并不会向磁盘中进行刷新,即管道文件仅仅是一个内存级文件,所以文件大小是0
    在这里插入图片描述
  6. 此时左侧就卡住了,即陷入阻塞等待,等待读端进行读取命名管道中的数据,那么我们在右侧的相同路径下,使用cat查看管道文件的内容,当cat运行起来同样也是一个进程,此时右侧的echo和左侧的cat就是两个不相关的进程,此时使用cat < myfifo,将命名管道的数据重定向给cat,那么cat的作用就是得到数据,将数据向显示器上输出,所以此时在右侧的屏幕上就会出现hello linux,那么此时echo本该打印到左侧屏幕上的数据就通过命名管道进行进程间通信,将数据通信给了cat,进而打印到了右侧显示器上
    在这里插入图片描述
  7. 并且我们还可以使用追加重定向,结合shell脚本命令持续不断的进行通信
//脚本语言
while :; do echo "hello linux"; sleep 1; done >> myfifo
  1. 我们使用脚本命令进行进程间通信,echo进行追加重定向的向myfifo进行写入,那么此时由于读端还没有进行读取,所以此时写端就会阻塞等待读端进行读取
    在这里插入图片描述
  2. 那么我们接下来在右侧使用cat < myfifo从管道文件myfifo中进行读取即可,此时echo和cat这两个进程,就会源源不断的进行进程间通信了
    在这里插入图片描述
  3. 此时我们复制ssh渠道,再查看myfifo的大小,确实仍然为0,说明管道文件的数据的确不会向磁盘中进行刷新,是一个内存级文件
    在这里插入图片描述
  4. 如果想要在当前路径下删除命名管道文件,那么使用unlink 命名管道文件名,即可删除命名管道文件
    在这里插入图片描述

五、命名管道编码实现

函数接口mkpipe介绍

  1. 在编码中,创建命名管道同样有对应的函数接口,这个函数接口就是mkfifo
    在这里插入图片描述
  2. mkfifo需要传参,第一个参数传参命名文件的路径,第二个传参命名文件的权限模式,mkfifo的返回值是一个int的变量,如果mkfifo创建命名管道成功,那么就会返回0,如果没有创建成功,创建失败了,那么就会返回-1,并且设置对应的错误码
  3. 同样的如果在代码中想要删除命名管道文件,那么同样使用unlink函数即可删除命名管道文件即可
    在这里插入图片描述

框架介绍

  1. 命名管道需要有两个毫不相关的进程,看到同一份命名管道文件进行通信,因此,小编设置两个可执行程序,server.cc作为服务端,client.cc作为客户端
  2. 服务端用于提供命名管道文件,并且以读的方式打开管道文件,读取管道文件的数据,客户端以写的方式打开管道文件,并且向管道中进行写入,那么两个进程要进行通信,首先就要有管道文件才可以进行通信,换句话来说要让服务端先运行创建管道文件才能为进程间通信创建管道文件,进而客户端后运行,才可以进行通信
  3. 并且我们提供一个头文件comm.hh,这个同文件中提供服务端和客户端所需要的错误码,创建命名管道文件的方法,以及清理命名管道文件的方法
  4. 我们还需要一个自动化构建工具makefile,将两个源文件进行编译形成各自的可执行程序,并且还可以清理可执行文件

makefile

  1. 自动化构建工具makefile,将两个源文件进行编译形成各自的可执行程序,并且还可以清理可执行文件
.PHONY:all
all:server client

server:server.cc
	g++ $^ -o $@ -std=c++11

client:client.cc
	g++ $^ -o $@ -std=c++11

.PHONY:clean
clean:
	rm -f server client

comm.hpp

  1. 头文件comm.hpp,这个同文件中提供服务端和客户端所需要的错误码,创建命名管道文件的方法,以及清理命名管道文件的方法
  2. 服务端使用Init实例化一个类,那么此时这个类实例化的时候,此时就会自动调用构造函数,使用mkfifo创建命名管道文件,同时当这个类的离开main函数,生命周期结束,那么此时就会自动调用析构函数,使用unlink清理命名管道文件
  3. 同时我们进行了差错处理,即如果创建或者清理命名管道文件失败,那么此时就使用perror打印错误信息,并且终止程序,返回错误码
  4. 还应该包含宏定义的命名管道文件的路径FIFO_FILE,这个路径可以用于打开创建以及打开命名管道文件
#pragma once

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cstdlib>
#include <cstdio>
#include <fcntl.h>
#include <string>
#include <cstring>
#include <cerrno>

#define FIFO_FILE "./myfifo"
#define MODE 0664

enum
{
    FIFO_CREAT_ERR = 1,
    FIFO_DELETE_ERR, // 如果第二个不初始化,那么就会在第一个的基础上加1为2
    FIFO_OPEN_ERR    // 同理第三个变量在第二个变量的基础上加1为3
};

class Init
{
public:
    Init()
    {
        // 创建命名管道
        int n = mkfifo(FIFO_FILE, MODE);
        if (n == -1)
        {
            perror("mkfifo");
            exit(FIFO_CREAT_ERR);
        }
    }

    ~Init()
    {
        // 清理命名管道
        int m = unlink(FIFO_FILE);
        if (m == -1)
        {
            perror("unlink");
            exit(FIFO_DELETE_ERR);
        }
    }
};

server.cc

  1. 在服务端,那么我们首先使用Init实例化一个类,那么此时这个类实例化的时候,此时就会自动调用构造函数,使用mkfifo创建命名管道文件,同时当这个类的离开main函数,生命周期结束,那么此时就会自动调用析构函数,使用unlink清理命名管道文件
  2. 那么接下来有了命名管道文件,接下来就是要以只读的方式open打开命名管道文件,当打开成功后,打印打开管道文件结束的语句
  3. 当我们已经有了对应的open返回的命名管道文件对应的文件描述符fd之后,此时作为服务端我们就要死循环式的使用read从管道文件中读取信息,既然要读取数据,那么首先就要有一个缓冲区buffer用于存储数据
  4. 当read读取的返回值大于0,说明此时读取成功,紧接着由于管道文件,即文件中不存在字符串以’\0’结尾的规定,字符串以’\0’结尾仅仅是c语言的规定,所以我们要将read从管道文件中读取上来的数据当作字符串来处理,由于read会返回读取到的字节个数,所以我们就在缓冲区buffer的第n个位置处置为’\0’即可
  5. 当read的返回值为0,说明客户端已经退出,即写端已经退出,那么此时我读端也没有继续读取的必要了,所以打印退出信息后,退出即可
  6. 当read的返回值小于0,说明读取失败,同样退出
  7. 当退出死循环后,此时close关闭对应的文件描述符fd即可
#include "comm.hpp"

using namespace std;

int main()
{
    Init init;

    int fd = open(FIFO_FILE, O_RDONLY);
    if(fd == -1)
    {
        perror("open");
        exit(FIFO_OPEN_ERR);
    }

    cout << "server open file  done" << endl;

    while(true)
    {
        char buffer[1024] = { 0 };
        int n = read(fd, buffer, sizeof(buffer));
        if(n > 0)
        {
            buffer[n] = '\0';
            cout << "clint say# " << buffer << endl;
        }
        else if(n == 0)
        {
            printf("client exit, me to, errno string: %s, errno code: %d\n", strerror(errno), errno);
            break;
        }
        else
            break;
    }

    close(fd);

    return 0;
}

client.cc

  1. 作为客户端,同样的,我们需要以读的方式打开命名管道文件即可,当打开成功后,给客户端一个回馈,打印客户端打开管道文件结束的语句
  2. 那么接下来就是打印一个语句提示用户进行输入,用户进行输入,例如输入what are you doing,那么此时字符串和字符串之间有空格进行分隔,所以我们不能使用常规的cin获取用户输入,因为cin是以空格或者换行进行分隔
  3. 这里我们期望的是以换行为分隔,所以我们使用getline,getline会从标准输入cin中进行读取,并且进行特殊处理当遇到换行的时候才结束读取,将读取的内容放到string类型的对象中,所以这里我们还需要一个string类型的对象line,并且使用getline应该包头文件#include <string>
    在这里插入图片描述
  4. 此时用户输入信息,我们获取到了用户输入的信息,那么使用write将信息到命名管道文件中即可,同时上述过程应该是死循环式的进行,因为客户会进行持续的交流,即我们要进行持续的通信
  5. 如何退出?用户按下ctrl + c即可退出,所以在最后,我们还应该使用close将命名管道文件进行关闭
#include "comm.hpp"

using namespace std;


int main()
{
    int fd = open(FIFO_FILE, O_WRONLY);
    if(fd == -1)
    {
        perror("open");
        exit(FIFO_OPEN_ERR);
    }

    cout << "client open file  done" << endl;

    string line;
    while(true)
    {
        cout << "Please Enter@ ";
        getline(cin, line);

        write(fd, line.c_str(), line.size());
    }

    close(fd);

    return 0;
}
测试
  1. 那么我们复制ssh渠道分别作为客户端与服务端
    在这里插入图片描述
  2. 此时我们就有了两个ssh渠道,并且这两个ssh渠道处于同一个路径下
    在这里插入图片描述
  3. 那么我们接下来编译,运行即可,注意这里一定要先运行服务端,观察下面左侧,小编已经将服务端运行起来了,此时服务端创建命名管道文件,可是奇怪的是服务端没有打开命名管道文件,因为我没有看到显示器上打印服务端打开命名管道文件结束的回馈信息,那么此时左侧的服务端在阻塞等待什么?
  4. 左侧的服务端在等待客户端启动,进而才会打开管道文件,这也不稀奇,因为管道文件本身就是要有一个读端,一个写端,当读端和写端同时存在的时候才可以建立信道进行进行间通信,这里仅仅有一个读端,自然无法建立信道,所以这里的阻塞等待是很正常的现象
    在这里插入图片描述
  5. 那么接下来小编运行客户端,此时一瞬间服务端与客户端就打开了对应的命名管道文件,此时两个相关的进程处于同一个路径并且可以看到同一份资源,建立起了信道,同时小编编写的回馈信息也被打印了
    在这里插入图片描述
  6. 那么接下来客户端与服务端就可以进行进程间通信了
    在这里插入图片描述
  7. ctrl + c退出客户端,此时写端关闭,自然而然的,服务端中的read就会返回0,表示已经读取到了文件结尾,写端已经关闭了,所以此时读端也会进行退出,关闭对应的命名管道文件,即关闭读端,此时进程间通信结束
    在这里插入图片描述

总结

以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!

Logo

葡萄城是专业的软件开发技术和低代码平台提供商,聚焦软件开发技术,以“赋能开发者”为使命,致力于通过表格控件、低代码和BI等各类软件开发工具和服务

更多推荐