【Linux】进程间通信(三):命名管道

📝前言:

这篇文章我们来讲讲Linux 进程间通信(三)------命名管道

🎬个人简介:努力学习ing

📋个人专栏:Linux

🎀CSDN主页 愚润求学

🌄其他专栏:C++学习笔记C语言入门基础python入门基础C++刷题专栏


这里写目录标题

一,命名管道介绍

之前我们介绍过了匿名管道,匿名管道通常用于有血缘关系的进程。那如果两个毫无关系的进程之间要进行通信,这时候就需要用到我们的命名管道。

1. 原理介绍

首先,想要进行进程间通信,就要让两个进程看到同一份资源!这份资源就是文件!

命名管道的原理和匿名管道基本相同,都是要让两个进程看到同一份文件。那两个没有血缘关系的进程是如何做到的呢?

  • 当两个进程打开同一路径下的同一个文件的时候,对应的文件描述符会被写入对应进程的文件描述符表中
  • 而对应文件的struct file其实是会被拷贝一份的(尽管两个struct file描述的都是c.txt文件,但是两个进程可能会在文件中有不同的读写位置不同的struct file就会记录)
  • 但是文件的 inode 和 文件内核缓冲区(这是文件在内存中真正的主体)是不会拷贝的,因为都是同一个文件的。
  • 即:因为路径具有唯一性,两个进程利用路径的唯一性看到了同一个文件(资源)。因为文件有文件名,所以叫做命名管道。

问题是,对于普通文件来说,IO会往磁盘刷新!

所以,OS提供了一种特殊的文件:管道文件,这种文件不会往磁盘刷新。这样两个进程就可以通过读写这个文件来实现通信!

2. 简单创建

我们用mkfifo来创建管道文件,命令和库函数同名

命令:

库函数:

  • 参数:(文件路径,创建文件时的权限)
  • 返回值:成功:0,失败:-1

示例(命令行创建):

bash 复制代码
mkfifo fifo.txt

这个p就代表是管道文件

示例(库函数创建):

cpp 复制代码
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
int main()
{
    int ret = mkfifo("fifo2.txt", 0666);
    if(ret == 0)
        std::cout << "创建管道文件fifo2.txt成功" << std::endl;
    return 0;
}


当命名管道创建好后,和普通文件的使用放个一模一样。先打开,然后再读写操作。

3. 简单使用

命令行输入:echo "hello world" > fifo.txt

结果:

就像被卡柱一样,直到我们cat fifo.txt才有反应:

这是因为,管道文件有特殊的读写规则。

4. 管道文件特殊的读写规则

管道文件的读写端必须都打开了才能工作,不然一段会端塞。

以上面的例子为例,

  • 当使用 echo "hello world" > fifo.txt 往管道写数据时,必须有另一个进程在管道的读端等待读取数据,否则写操作会被阻塞

  • 同理,如果先打开读端,但是写端没有打开,读端也会阻塞等待写端打开。

5. 命名管道删除

使用unlink

当然,命令行也可以用rm

系统调用删,常用于代码中

二,使用示例

错误和退出宏

  • 当一个操作错误时,C语言会设置对应的错误码在errno,我们可以通过perror来打印对应错误码的错误信息。
  • 如果我们想在打印错误信息后立刻终止程序,可以添加exit操作

上述两个操作,我们可以用一个宏 + do...while(0)来包装一下

cpp 复制代码
#define ERR_EXIT(m) \
do { \
	perror(m); \
	exit(EXIT_FAILURE); \
} while(0)
  • do...while(0):只是为了提供一个能用于代码块包装的{}
  • 后续写代码时,我们直接调用ERR_EXIT(m)m为我们传入的字符串提示信息
  • 因为宏是要写到一行的,\是为了换行
  • EXIT_FAILURE 是标准库 <stdlib.h> 中已经定义的宏(通常值为 1

示例1

思路

重建两个进程,利用命名管道进行文件的拷贝:把file1.txt 的 内容拷贝到 file2.txt

基本思路:

  • 一个进程往命名管道里面写入file1.txt的内容
  • 另一个进程从命名管道读内容,写入file2.txt

代码

WriteN 文件:

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

using namespace std;

#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)

int main()
{
    // 创建命名管道
    int m = mkfifo("tp", 0666);
    if (m < 0)
        ERR_EXIT("mkfifo"); // 代表mkfifo这个行为发生错误

    // 打开原有文件
    int infd = open("file1.txt", O_RDONLY);
    if (infd < 0)
        ERR_EXIT("open");

    // 打开命名管道
    int outfd = open("tp", O_WRONLY);
    if (outfd < 0)
        ERR_EXIT("open");

    // 把原有文件的内容输出到命名管道
    char buffer[1024];
    int n;
    while ((n = read(infd, buffer, sizeof(buffer))) > 0) // 当还读到有数据时
    {
        write(outfd, buffer, n);
    }
    close(infd);
    close(outfd);
    return 0;
}

ReadN文件:

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

using namespace std;

#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)

int main()
{

    // 打开命名管道
    int infd = open("tp", O_RDONLY);
    if(infd < 0) 
        ERR_EXIT("open");

    // 打开要写入的文件
    int outfd = open("file2.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    if (outfd < 0)
        ERR_EXIT("open");
    
    // 把命名管道的内容写到要拷贝到的文件
    char buffer[1024];
    int n;
    while((n = read(infd, buffer, sizeof(buffer))) > 0) // 当还读到有数据时
    {
        write(outfd, buffer, n);
    }
    close(infd);
    close(outfd);

    // 删除命名管道
    unlink("tp");
    return 0;
}

运行

一个终端先运行writer,另一个终端再运行reader,就完成了文件拷贝工作

示例2

思路

  • 利用命名管道建立一个服务段和客户端对话的窗口。
  • 客户端往命名管道里面写数据。
  • 服务段从命名管道里面读数据。

代码

comm.hpp文件:

cpp 复制代码
#pragma once

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

#define PATH "."
#define FILENAME "fifo"

using namespace std;

#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)

class NamedPipe
{
public:
    NamedPipe(string path, string name)
        : _path(path), _name(name)
    {
        _fifoname = _path + "/" + _name;
        int m = mkfifo(_fifoname.c_str(), 0666);
        if (m < 0)
            ERR_EXIT("mkfifo");
        cout << "mkfifo " << _fifoname << "sucess" << endl;
    }

    ~NamedPipe()
    {
        int u = unlink(_fifoname.c_str());
        if (u < 0)
            ERR_EXIT("unlink");
        cout << "unlink " << _fifoname << "sucess" << endl;
    }

private:
    string _path;
    string _name;
    string _fifoname;
};

class Link // 服务端和用户端通过这个Link对象来进行对话
{
public:
    Link(const string& path, const string& name) // 这个构造只是为了把上面创建的命名管道的信息拿下来
        : _path(path), _name(name)
    {
        _fifoname = _path + "/" + _name;
        cout << "Link " << _fifoname << " -> sucess" << endl;
    }

    void LinkForWrite()
    {
        // 当命名管道创建好了以后,就当做普通文件一样访问
        _fd = open(_fifoname.c_str(), O_WRONLY | O_TRUNC);
        if (_fd < 0)
            ERR_EXIT("open");
        cout << "Now you can write something to server " << endl;
    }

    void LinkForRead()
    {
        _fd = open(_fifoname.c_str(), O_RDONLY);
        if (_fd < 0)
            ERR_EXIT("open");
        cout << "Now you can read from client " << endl;
    }

    void Write()
    {
        while (true)
        {
            string message;
            getline(cin, message);
            if(message == "quit")
            {
                break;
            }
            write(_fd, message.c_str(), message.size());
        }
    }

    void Read()
    {
        while (true)
        {
            char buffer[1024];
            int n = read(_fd, buffer, sizeof(buffer) - 1);
            buffer[n] = '\0';
            if (n > 0)
            {
                cout << buffer << endl;
            }
            else if (n == 0)
            {
                cout << "cilent quit... now server quit" << endl;
                exit(EXIT_SUCCESS);
            }
            else
            {
                ERR_EXIT("read");
            }
        }
    }

    void Close()
    {
        close(_fd);
    }

private:
    string _path;
    string _name;
    string _fifoname;
    int _fd;
};

client.cpp文件:

cpp 复制代码
#include "comm.hpp"

int main()
{
    Link Client(PATH, FILENAME);
    Client.LinkForWrite();
    Client.Write();
    Client.Close();
}

server.cpp文件

cpp 复制代码
#include "comm.hpp"

int main()
{
    // 创建命名管道
    // 构造时创建,进程结束时,自动析构
    NamedPipe fifo(PATH, FILENAME);

    Link Server(PATH, FILENAME);
    Server.LinkForRead();
    Server.Read();
    Server.Close();
}

Makefile文件:

bash 复制代码
.PHONY:all
all:client server
client:client.cpp
	g++ -o $@ $^ -std=c++11
server:server.cpp
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f client server
  • 伪目标all(不对应实际文件,仅用于执行命令),依赖于clientserver
  • make 执行all的时候,就会去执行clientserver目标

运行


🌈我的分享也就到此结束啦🌈

要是我的分享也能对你的学习起到帮助,那简直是太酷啦!

若有不足,还请大家多多指正,我们一起学习交流!

📢公主,王子:点赞👍→收藏⭐→关注🔍

感谢大家的观看和支持!祝大家都能得偿所愿,天天开心!!!

相关推荐
黑石云4 分钟前
P2P最佳网络类型
服务器·边缘计算·p2p
CodeWithMe7 分钟前
【C/C++】现代C++线程池:从入门到生产级实现
c++
33三 三like8 分钟前
MacOS安装软件后无法启动报错:“已损坏,无法打开,你应该将它移到废纸篓“
运维·服务器
lsnm11 分钟前
【LINUX操作系统】生产者消费者模型(下):封装、信号量与环形队列
linux·运维·服务器·开发语言·c++·ubuntu·centos
小灰兔的小白兔12 分钟前
【ubuntu】虚拟机连不上网,且网络中没有有线连接
运维·服务器·ubuntu
谢尔登16 分钟前
【Umi】项目初始化配置和用户权限
开发语言·javascript·ecmascript
chao_78917 分钟前
python八股文汇总(持续更新版)
开发语言·python·学习
爱喝水的鱼丶21 分钟前
SAP-ABAP:SAP的BAPI_PO_CHANGE功能详解
开发语言·sap·abap·bapi·采购订单修改
未来之窗软件服务26 分钟前
在 Excel 中使用东方仙盟软件————仙盟创梦IDE
开发语言·excel·excel插件·仙盟创梦ide
孤寂大仙v27 分钟前
【Linux笔记】——简单实习一个日志项目
java·linux·笔记