【Linux】命名管道

之前说过的匿名管道只能实现有"血缘关系"进程之间的通信,如果想要实现进程和进程之间的通信就要用命名管道。进程和进程之间的通信也是需要让不同的进程看到同一份资源,使⽤FIFO⽂件来做这项⼯作。

1.命名管道的创建和删除

1.1 命令行创建和删除

命名管道可以从命令⾏上创建,命令⾏⽅法是使⽤下⾯这个命令:

  • mkfifo 文件名:创建命名管道
  • unlink 文件名:删除管道文件

1.2 程序⾥创建和删除

首先创建两个文件server.cc和client.cc,这两个文件是不同的两个进程还有一个comm.hpp文件,里面提供一个路径,让server.cc和client.cc都能看到。

cpp 复制代码
//comm.hpp文件
#pragma once
#include <iostream>
using namespace std;
#define FIFO_FILE "fifo"

​
cpp 复制代码
//server.cc文件
#include "comm.hpp"
int main()
{
 
    return 0;
}
cpp 复制代码
//client.cc文件
#include "comm.hpp"
int main()
{
    return 0;
}
bash 复制代码
//Makefile文件
.PHONY:all
all:client server
client:client.cc
	g++ -o $@ $^
server:server.cc
	g++ -o $@ $^

.PHONY:clean
clean:
	rm -f client server

创建FIFO文件用函数mkfifo,第一个参数在指定路径下创建fifo文件,第二个参数是创建这个管道文件时的起始权限,一般为666,创建成功返回0,创建失败返回-1。

我们只需要一方创建出命名管道就可以了,这里选择让server创建。

cpp 复制代码
//server.cc文件
#include <sys/stat.h>
#include <sys/types.h>
#include "comm.hpp"
int main()
{
    int n = mkfifo(FIFO_FILE, 0666);
    if(n < 0) return 1;
    return 0;
}

但是我们会发现这个fifo文件的权限并不是666,而是664,因为系统的umask会屏蔽other的写权限,可以把umask设为0。

cpp 复制代码
int main()
{
    umask(0);
    int n = mkfifo(FIFO_FILE, 0666);
    if(n < 0) 
    {
        cout << "fifo创建失败" << endl;
        return 1;
    }
    cout << "创建成功" << endl;
    return 0;
}

删除管道用函数unlink,把指定路径的管道文件删除,删除成功返回0,删除失败返回-1.

cpp 复制代码
int main()
{
    umask(0);
    //创建
    int n = mkfifo(FIFO_FILE, 0666);
    if(n < 0) 
    {
        cout << "fifo创建失败" << endl;
        return 1;
    }
    cout << "创建成功" << endl;

    //删除
    n = unlink(FIFO_FILE);
    if(n < 0) 
    {
        cout << "fifo删除失败" << endl;
        return 1;
    }
    cout << "删除成功" << endl;
    return 0;
}

2.进程间通信

2.1 server进程

我们让server进程从管道里读,步骤就是先打开文件,然后读数据,然后关闭文件。

cpp 复制代码
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include "comm.hpp"
int main()
{
    umask(0);
    //创建fifo
    int n = mkfifo(FIFO_FILE, 0666);
    if(n < 0) 
    {
        cout << "fifo创建失败" << endl;
        return 1;
    }
    cout << "fifo创建成功" << endl;

    //只读打开
    int fd = open(FIFO_FILE, O_RDONLY);
    if(fd < 0) 
    {
        cout << "fifo打开失败" << endl;
        exit(1);
    }
    cout << "fifo打开成功" << endl;

    char buffer[1024];
    while(1)
    {
        int m = read(fd, buffer, sizeof(buffer)-1);
        if(m > 0)
        {
            buffer[m] = 0;
            cout << "从client读取到的内容为:" << buffer << endl;
        }
        else if(m == 0)
        {
            cout << "读取完成" << endl;
            break;
        }
        else 
        {
            cout << "读取失败" << endl;
            break;
        }
    }
   
    
    //关闭文件
    close(fd);

    //删除fifo
    n = unlink(FIFO_FILE);
    if(n < 0) 
    {
        cout << "fifo删除失败" << endl;
        return 1;
    }
    cout << "fifo删除成功" << endl;
    return 0;
}

2.2 client进程

管道已经在server里创建好了,所以 client只要对文件进行操作就行了。

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

int main()
{
    int fd = open(FIFO_FILE, O_WRONLY); //写方式打开
    if(fd < 0)
    {
        cout << "client打开fifo失败";
        exit(1);
    }

    string message;
    int num = 1;
    while(true)
    {
        cout << "请输入:" << endl;
        getline(cin, message);
        message += (", message num:" + to_string(num++) + ", pid:" + to_string(getpid()));
        write(fd, message.c_str(), message.size());
    }

    close(fd);

    return 0;
}

所有头文件都放在comm.hpp里的,两个文件准备好之后,直接make,然后一个shell运行server进程,另一个运行client进程。

先运行server。

当我们只运行server时,进程会卡在fifo创建之后,open之前,阻塞住了,他在等client启动,与client进行通信。

再运行client。

client一运行,server进程的fifo就打开成功了。

结论:write方(此处的client进程)没有执行open时,read方(此处的server进程)就要在open内部阻塞住,直到有人把管道文件打开了,open才会返回。如果读方的进程先启动,会阻塞住,因为写方没启动时读方自己启动没有意义。

fifo打开之后就可以通信了,client发消息,server接收消息。

至此我们就基本实现了两个进程之间的通信。

当我们把client的进程ctrl+c直接结束掉时,也就是写端关闭,写端关闭后读端会读到文件的结束,返回0。

到这里进程之间的通信其实就实现好了,我们现在可以对这个代码进行包装一下。

3.代码面向对象化

NamedPipe类

首先就是fifo文件的创建和删除操作,这里只需要知道FIFO文件在哪创建,创建的fifo文件叫什么名字,所以类成员就要有两个,path和name,为了方便mkfifo函数的传参,我们把path和name合并成一个字符串,中间用"/"连接。

cpp 复制代码
//comm.hpp文件
#pragma once
#include <iostream>
#include <unistd.h>
#include <string>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.h>
using namespace std;

#define FIFO_FILE "fifo"

class NamedPipe
{
public:
    NamedPipe(const string& path, const string& name)
        :_path(path), _name(name)
    {
        _fifo_name = _path + "/" + _name;
    }

    ~NamedPipe()
    {
       
    }
private:
    string _path;
    string _name;
    string _fifo_name;
};

在构造和析构函数内进行创建和删除fifo文件。

cpp 复制代码
class NamedPipe
{
public:
    NamedPipe(const string& path, const string& name)
        :_path(path), _name(name)
    {
        _fifo_name = _path + "/" + _name;
        umask(0);
        int n = mkfifo(_fifo_name.c_str(), 0666);
        if(n < 0) 
        {
            cout << "fifo创建失败" << endl;
            exit(1);
        }
        cout << "fifo创建成功" << endl;
    }

    ~NamedPipe()
    {
        int n = unlink(_fifo_name.c_str());
        if(n < 0) 
        {
            cout << "fifo删除失败" << endl;
            exit(1);
        }
        cout << "fifo删除成功" << endl;
    }
private:
   string _path;
   string _name;
   string _fifo_name;
};

ERR_EXIT宏替换

当程序返回值不符合我们的预期的时候,比如前面的n<0的情况,我们一般会打印错误信息然后让程序退出,这里可以使用一个宏替换,要包含头文件stdio.h,如下。

cpp 复制代码
#include <stdio.h>
#define ERR_EXIT(m) \
do \
{ \
    perror(m); \
    exit(EXIT_FAILURE); \
} while(0)

然后就可以用这个ERR_EXIT打印出错信息并且让程序退出。

在server里构建管道时只要实例化对象,传路径和文件名就行。

cpp 复制代码
//server.cc文件
#include "comm.hpp"

int main()
{
    NamedPipe fifo(".", "fifo"); //当前路径下创建名字为fifo的fifo文件

    return 0;   
}

FileOper类

我们再用一个FileOper类,里面提供了对fifo文件的操作接口,类成员变量和前面一样,这里还需要多加一个存文件描述符的变量,然后先把fd初始化为-1。

cpp 复制代码
class FileOper
{
public:
    FileOper(const string& path, const string& name)
        :_path(path), _name(name), _fd(-1)
    {
        _fifo_name = _path + "/" + _name;
    }

    ~FileOper()
    {}

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

对fifo文件的操作接就包括读方式打开文件,写方式打开文件,读数据,写数据,就是前面一样的实现。

cpp 复制代码
class FileOper
{
public:
    FileOper(const string& path, const string& name)
        :_path(path), _name(name), _fd(-1)
    {
        _fifo_name = _path + "/" + _name;
    }

    void OpenForRead()
    {
        _fd = open(_fifo_name.c_str(), O_RDONLY);
        if(_fd < 0) 
        {
            ERR_EXIT("open");
        }
        cout << "fifo打开成功" << endl;
    }

    void Read()
    {
        char buffer[1024];
        while(1)
        {
            int n = read(_fd, buffer, sizeof(buffer)-1);
            if(n > 0)
            {
                buffer[n] = 0;
                cout << "从client读取到的内容为:" << buffer << endl;
            }
            else if(n == 0)
            {
                cout << "读取完成" << endl;
                break;
            }
            else 
            {
                cout << "读取失败" << endl;
                break;
            }
        }
    }

    void OpenForWrite()
    {
        _fd = open(_fifo_name.c_str(), O_WRONLY); //写方式打开
        if(_fd < 0)
        {
            ERR_EXIT("open");
        }
        cout << "client打开fifo成功" << endl;
    }

    void Write()
    {
        string message;
        int num = 1;
        while(true)
        {
            cout << "请输入:" << endl;
            getline(cin, message);
            message += (", message num:" + to_string(num++) + ", pid:" + to_string(getpid()));
            write(_fd, message.c_str(), message.size());
        }
    }

    void Close()
    {
        if(_fd > 0) close(_fd);
    }

    ~FileOper()
    {}
private:
   string _path;
   string _name;
   string _fifo_name;
   int _fd;
};

此时server.cc文件和client.cc文件就只要调用类的接口就行了。

cpp 复制代码
//server.cc文件
#include "comm.hpp"

int main()
{
    NamedPipe fifo(".", "fifo"); //当前路径下创建名字为fifo的fifo文件
    
    //文件操作
    FileOper read_file(".", "fifo");
    read_file.OpenForRead();
    read_file.Read();
    read_file.Close();
    return 0;
}
cpp 复制代码
//client.cc文件
#include "comm.hpp"

int main()
{
    FileOper write_file(".", "fifo");
    write_file.OpenForWrite();
    write_file.Write();
    write_file.Close();
    return 0;
}

此时client就只有文件的操作,管道的所有操作都在server进程里。

本次分享就到这里了,我们下篇见~

相关推荐
IT 小阿姨(数据库)5 小时前
PgSQL监控死元组和自动清理状态的SQL语句执行报错ERROR: division by zero原因分析和解决方法
linux·运维·数据库·sql·postgresql·centos
THMAIL5 小时前
量化股票从贫穷到财务自由之路 - 零基础搭建Python量化环境:Anaconda、Jupyter实战指南
linux·人工智能·python·深度学习·机器学习·金融
曾经的三心草6 小时前
Python2-工具安装使用-anaconda-jupyter-PyCharm-Matplotlib
android·java·服务器
逍遥浪子~6 小时前
docker实践(一)
运维·docker·容器
让子弹飞026 小时前
36.2Linux单总线驱动DS18B20实验(详细讲解代码)_csdn
linux·ubuntu·驱动的分离和分层
Yana.nice6 小时前
yum list 和 repoquery的区别
linux
AI云原生6 小时前
如何使用Docker快速运行Firefox并实现远程访问本地火狐浏览器的教程
运维·docker·云原生·容器·serverless·firefox·kubeless
今生相伴9916 小时前
ELFK:企业级日志管理的完整解决方案——从入门到精通
运维·elk·elasticsearch
码出钞能力7 小时前
更换libc.so导致linux变砖,通过LD_PRELOAD挽救
linux·服务器