进程间通信-命名管道

        先前已经了解了匿名管道,但是这是适用于有血缘关系的进程间,如果无血缘关系的进程要实现通信,此时需要有另一种通信方案-命名管道。为什么命名管道可以用于无血缘关系的进程间通信,什么是命名管道,为什么说它是有名字的,后面我们会一一了解。

一 函数介绍

        参数1就是管道文件名,参数2则是管道文件的权限,返回0表示创建成功,返回-1表示创建失败。这个管道文件看似在磁盘之所以要用mkfifo打开一个管道文件,而不是直接open打开一个普通的文件,就是为了不让数据刷新到磁盘上,管道文件还是在内存,所以大小为0,因为大小是inode里的属性,而inode是用来描述磁盘文件的属性,但是磁盘并没有为文件开辟空间,所以大小为零。

        此时echo 是写端,为什么会阻塞住,读端没打开,有意思的是匿名管道那里我曾说过如果读端关闭,写端打开后去写会直接出异常终止进程,但那是打开写端调用write函数后才被终止,而我们这里的命名管道是卡在open环节了,这里要等读写双方均打开后才会往后运行,所以只是阻塞而不是出异常。

        还有就是我先前在缓冲区博客中缓冲区介绍-CSDN博客曾提及,两个进程用不同的方式打开一个文件,此时系统会创建两个struct file结构体,但是只会有一个缓冲区,而且文件的属性等应该是只有一份的,为什么缓冲区只有一份呢?不会发生错乱吗,应该是会的,例如我还没读完,然后进程切换了,另一个进程就过来写,可能就会覆盖住要读的数据,但是linux为什么不限制呢,因为linux要给上层自由,要让我们自己来控制读写,自己处理缓冲区冲突问题。

二 实现通信

        先前匿名管道通信我们知道通信是要让不同的进程看到同一份资源,那命名管道又如何保证不同进程打开的是同一个文件呢,就是用路径+文件名,这个具有唯一性,显然这就是管道的名字,所以称该管道文件为命名管道,而非匿名管道,而且打开同一个文件就能看到这个文件唯一的缓冲区,也就是看到同一份资源。

        分client.cc和server.cc,log.hpp三个文件,其中log.hpp存头文件和创建管道mkfifo的相关参数,例如管道文件的文件名,给管道文件的初始化权限,这样两个进程都包含这个头文件就可以拿到管道名,后续打开这个文件时就看到了同一份资源,而server.cc则负责创建管道,管理管道文件,client.cc是客户端代码,不用创建管道文件,直接以写方式打开,然后发送指令就可以了。

1 创建管道

        这里是用类来封装创建管道,当Init 类创建对象,就会自动调用构造函数,然后把管道创建出来,如果要删除管道就要用unlink函数,显然可以放到析构函数处。

class Init
{
public:
    Init()
    {
        int ret = mkfifo(FILENAME, MODE);
        if (ret == -1)
        {
            perror("mkfifo");
            exit(MKFIFO_ERR);
        }
    }
    ~Init()
    {
        unlink(FILENAME);
    }
};

错误分享:注意下面两个进程都是以读方式打开这个管道。

server.cc
int main()
{
    // 1 创建管道
    Init it;
    int fd = open(FILENAME, O_APPEND | O_WRONLY, MODE); 
    
    //int fd = open(FILENAME, O_RDONLY);

    return 0;
}

client.cc
int main()
{
     //1 打开管道
    int fd = open(FILENAME, O_APPEND | O_WRONLY, MODE);    
    
    return 0;
}

        有时候我发现如果我./client先运行,client起的进程不会在open被卡住,但是./server先运行两个可执行文件都会在open卡住,./server先运行会导致两个进程一起被卡住我勉强理解,因为双方当时都用写打开,命名管道没有读端,就阻塞住了,而且我再次测试的时候server.cc用读方式打开,此时不会阻塞住也证实要打开管道的读写端才能往后执行,至于普通文件在open处都不会被阻塞,随便你怎么打开。可是为什么./client运行起来又没事呢,其实这是因为./client运行时没创建管道,所以open直接出错返回了,但是./server先运行的时候是先创建的管道,再打开,此时管道没有读端就一直卡着。

2 开始读写

当我们有了读写端,就可以开始读写来简单实现通信了。

server进程负责读client进程发送的信息。

#include "log.hpp"
int main()
{
    // 1 创建管道
    Init it;

    // 2 打开管道
    int fd = open(FILENAME, O_RDONLY);
    
    while (true)
    {
        char buffer[1024] = {0};
        int n = read(fd, buffer, sizeof(buffer));
        if(n > 0)
        {
            buffer[n]=0;
            cout<<"server已经读到:"<<buffer<<endl;
        }
        else写端关闭,此时read返回0,break读端结束读取
        {
            break;
        }
    }
    return 0;
}

client负责向管道内发送消息。

#include"log.hpp"
int main()
{
     //1 打开管道
    int fd = open(FILENAME, O_APPEND | O_WRONLY, MODE);
 
    //2 发送指令
    string s;
    cout<<"client准备写入:"<<s<<endl;
    while(getline(cin,s))
    {
        cout<<"client已经写入:"<<s<<endl;
        write(fd,s.c_str(),s.size());
    }
    close(fd);
    return 0;
}

三 日志代码实现

        log.hpp不仅让两个进程可以看到同一个文件名,还可以往这里添加一些日志代码,用于出错时输出提示信息。日志一般包括时间,事件等级,内容等,具体格式肯定是每个公司不一样的。不同的事件等级一般对应不同的处理方法,例如有normal(常规消息),warning(报警消息),error(出错消息),fatal(致命的),不处理就无法继续运行,debug(正常的调试信息),而warning和error都是运行出现了问题,可能需要立即处理,我们结合代码来理解。

1 logmessage介绍

        日志函数,参数为,包含等级,输出格式和可变参数列表,c的可变参数使用和c++有点不同。

2 可变参数使用

c语言中的可变参数使用如下

int sum(int n,...)
{
    va_list s;
    va_start(s,n);
    int sum = 0;
    while(n)
    {
        sum += va_arg(s,int);
        n--;
    }
    va_end(s);
    return sum;
}

        va_list s 就是定义一个变量s,s就是char*类型的,不过类型做了层层封装,看不出来,va_start就是&(n)+1然后赋值给s,此时s指向了可变参数的起始位置因为sum函数变量是要压栈的,而函数压栈是自右向左压栈,栈又是高地址到低地址生长的,所以sum函数中的n变量是最低地址,+1刚好指向可变模板参数部分,怪不得函数参数不能直接用可变参数...接收全部参数,必须要像sum函数一样有一个int n,就是因为设计者要我们有个具体的变量来取地址找到可变参数的起始位置。

        此后使用就是s根据va_start传的类型,依次往高地址找压栈的变量,由于va_start只能指定一种类型,所以参数必须是统一类型的,不然底层也不知道要跳过几个字节,所以为什么printf有个格式字符串呢,就是为了知道可变参数时每个参数占字节数。显然va_arg就是宏,因为int是个类型,而语法规定我们不可给函数传个类型。

logmessage函数细节

        当知道了可变参数的使用,后面就是真正介绍函数内部实现。

void logmessage(const char* level, const char *format, ...)
{
    char leftbuffer[SIZE];

    时间获取
    time_t t = time(nullptr);
    struct tm * ltime = localtime(&t);

    //默认部分 包含事件等级和时间
    snprintf(leftbuffer,sizeof(leftbuffer),"%s [%d %d %d %d:%d]",level,
    ltime->tm_year+1900,ltime->tm_mon+1,ltime->tm_mday,ltime->tm_hour,ltime->tm_min);
    
    //可变部分
    char rightbuffer[SIZE];
    va_list s;
    va_start(s,format);
    vsnprintf(rightbuffer,sizeof(rightbuffer),format,s);
    
    合并输出两个部分
    printf("%s %s\n",leftbuffer,rightbuffer);
}

获取时间,localtime函数,传的参数是用time函数的返回值。

然后localtime会返回一个结构体指针,这个指针内部成员如下。

        我们直接访问成员打印年月日就好了,就是年份要加上1900,因为那一年计算机刚刚诞生,显然这个时间戳也是从这个时候开始计数。

        当日志时间和日志等级都有了后,我们就可以考虑输出日志信息了,日志格式:默认部分+可变部分,默认部分就是日志时间和等级,显然日志时间直接打印即可。日志等级也可以传整型,就是要用下面的函数转字符串,不然你打印个1,2,3,4,谁知道对应什么等级。

        可变部分就是日志内容了,可变内容是用可变参数传入的,打印格式在format中,刚好vsnprintf函数就是用于处理可变参数,而且都无需我们解析可变参数了。

最后大致使用如下图。

4 日志往文件输出

        上面代码里最后是直接printf出来的,如果我们可以选择将日志信息往文件打印或者往屏幕打印,此时这个日志输出就更加灵活,且符合实际了。首先我们把之前了解的logmessage函数封装到类内,这样封装的比较美观,不然代码多了,一堆函数这怎么找,直接弄到一个类内多好。

#define FILENAME "myfifo"
#define Logname "log.txt"
enum PMethod
{
    Screen = 1,//输出到屏幕
    OneFile ,//输出到一个文件上
    ClassFile//分类输出到多个文件中
};
enum ErrorLevel
{
    Info = 1,
    Warning,
    Fatal,
    Debug
};
class Log
{
public:
    Log(int method = Screen)//用该成员变量记录输出目标地点,默认是到屏幕
    :printmethod(method)
    {
        ;
    }
    string leveltostring(int level)
    {
        switch (level)
        {
            case Info:
                return "Info";
            case Warning:
                return "Warning";
            case Fatal:
                return "Fatal";
            case Debug:
                return "Debug";
            default: 
                return "None";    
        }
    }
    //日志信息
    void logmessage(int level, const char *format, ...)
    {
        char leftbuffer[SIZE];
        time_t t = time(nullptr);
        struct tm * ltime = localtime(&t);
        //默认部分 事件等级和时间
        snprintf(leftbuffer,sizeof(leftbuffer),"%s [%d %d %d %d:%d]",leveltostring(level).c_str(),
        ltime->tm_year+1900,ltime->tm_mon+1,ltime->tm_mday,ltime->tm_hour,ltime->tm_min);
    
        //可变部分
        char rightbuffer[SIZE];
        va_list s;
        va_start(s,format);
        vsnprintf(rightbuffer,sizeof(rightbuffer),format,s);
        char Logbuffer[SIZE*2];
        snprintf(Logbuffer,sizeof(Logbuffer),"%s %s",leftbuffer,rightbuffer);
        LogPrint(level,Logbuffer);
    }

    调用LogPrint函数,输出buffer信息。

    void LogPrint(int level , string lbuffer)
    {
        switch(printmethod)    由printmethod选择输出到文件还是输出到屏幕
        {
            case Screen://输出到屏幕
               cout<<lbuffer<<endl;
                break;
            case OneFile: //输出到一个文件上
                PrintOnefile(Logname,lbuffer);
                break;
            case ClassFile://将错误信息按等级分流到不同的文件
                PrintClassFile(level,lbuffer);
                break;
        }
    }
    
    void PrintOnefile(const char *filename,string& lbuffer)
    {
        lbuffer+='\n';
        int fd = open(filename, O_CREAT|O_APPEND|O_WRONLY,0666);
        if(fd < 0)
            return;
         write(fd,lbuffer.c_str(),lbuffer.size());   
    }
    void PrintClassFile(int level,string& lbuffer)
    {
        string filename = Logname;
        filename += ".";
      
    //log.txt.Info log.txt.Warning log.txt.Fatal

        filename += leveltostring(level);//这样文件名就会以事故等级的方式区分开来
        PrintOnefile(filename.c_str(),lbuffer);
       然后直接复用先前的函数
    }
   
private:
    int printmethod;
};

四 进程池实现

        先前是用了匿名管道来实现进程池,现在学了命名管道,就要试试用命名管道来给进程池做一些添加。这里有server.cc模拟服务端进程,client.cc模拟客户端进程,当客户端发指令时,服务端会创建子进程去执行,但是由于来一个指令,才创建子进程,影响响应时间,所以设计出了进程池,服务端提前创建子进程,有任务来就给子进程发指令去执行,而父子进程间的通信我们用的是匿名管道,用命名管道反而有点麻烦。

1 server.cc

#include "log.hpp"
void slaver()
{
    //创建日志
    Log log;
    cout<<"子进程准备读取"<<endl;
     while (true)
    {
        char buffer[1024] = {0};
        int n = read(0, buffer, sizeof(buffer));
        if(n > 0)
        {
            cout<<"子进程server已经读到:"<<buffer<<" 立刻执行"<<endl;
        }
        else
        {
            log.logmessage(Debug,"子进程读取结束 退出信息:%s 退出码:%d",strerror(errno),errno);
            break;
        }
    }
}
void InitChannels(vector<channel>& channels)
{
    vector<int> ProcessPid;
    for(int i = 0; i < PRONUM; i++)
    {
        创建匿名管道
        
        int fd[2]={0};
        int ret = pipe(fd);
        int id = fork();
        if(id < 0)
        {
            perror("fork");
            exit(FORK_ERR);
        }
        else if(id == 0)  子进程读
        {
            for(auto e : ProcessPid)  关闭残留写端,在匿名管道曾详细解释过
                close(e);
            close(fd[1]);
            dup2(fd[0],0);
            slaver();
            exit(0);
        }

        父进程负责写

        close(fd[0]);
        string proname = "process->";
        proname += to_string(i);
        channels.push_back(channel(fd[1],id,proname));
        ProcessPid.push_back(fd[1]);
    }
}

void Ctrlprocess(const vector<channel>& channels)
{
    Init it;  创建命名管道
    Log log;
    打开命名管道
    int fd = open(FILENAME,O_CREAT|O_APPEND|O_RDONLY,0666);
    int which = 0;
    从客户端读取信息

     while (true)
    {
        char buffer[1024] = {0};
        int n = read(fd, buffer, sizeof(buffer));
        if(n > 0)
        {
            cout<<"父进程server已经读到:"<<buffer<<" 正在让子进程处理"<<endl;
            //轮转挑选子进程
            int fd = channels[which++]._fd;
            write(fd,buffer,sizeof(buffer));
            which%=channels.size();
        }
        else
        {
            log.logmessage(Debug,"父进程读取结束 退出信息:%s 退出码:%d",strerror(errno),errno);
            break;
        }
    }
}
void QuitProcess(const vector<channel>& channels)回收子进程
{
    for(auto e : channels)
    {
        close(e._fd);
        int status = 0;
        waitpid(e._pid,&status,0);
    }
}
int main()
{

    // 1 初始化channels
    vector<channel> channels;
    InitChannels(channels);
 
    //2 控制子进程
    Ctrlprocess(channels);

    //3 回收子进程
    QuitProcess(channels);
    return 0;
}

2 client.cc

#include"log.hpp"
int main()
{
     //1 打开管道
    int fd = open(FILENAME, O_APPEND | O_WRONLY, MODE);
    //2 发送指令
    string s;
    cout<<"client准备写入:"<<s<<endl;
    while(getline(cin,s))
    {
        cout<<"client已经写入:"<<s<<endl;
        write(fd,s.c_str(),s.size());
    }
    close(fd);
    return 0;
}

3 log.hpp

下面代码在上面日志代码实现中已经解释了。

#include <iostream>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include<time.h>
#include<errno.h>
#include <cstdarg>
#include<vector>
#include <sys/wait.h>
#define FILENAME "myfifo"
#define Logname "log.txt"
#define MODE 0666
#define SIZE 1024
#define INFO "Info"
#define WARNING "Warning"
#define  FATAL "Fatal"
#define  DEBUG "Debug"
#define PRONUM 2
using namespace std;
enum Fail
{
    MKFIFO_ERR = 1,
    OPEN_ERR,
    FORK_ERR
};

//错误等级
enum ErrorLevel
{
    Info = 1,
    Warning,
    Fatal,
    Debug
};
//接收输出的文件
enum PMethod
{
    Screen = 1,//输出到屏幕
    OneFile ,//输出到一个文件上
    ClassFile//分类输出到多个文件中
};
class channel
{
public:
    channel(int fd,int pid,string& proname)
    :_fd(fd)
    ,_pid(pid)
    ,_proname(proname)
    {
        ;
    }
    int _fd;//存管道fd
    int _pid;//存子进程pid
    string _proname;//进程名
};
// 实现自动创建和销毁管道文件
class Init
{
public:
    Init()
    {
        int ret = mkfifo(FILENAME, MODE);
        if (ret == -1)
        {
            perror("mkfifo");
            exit(MKFIFO_ERR);
        }
    }
    ~Init()
    {
        unlink(FILENAME);
    }
};
class Log
{
public:
    Log(int method = Screen)
    :printmethod(method)
    {
        ;
    }
    string leveltostring(int level)
    {
        switch (level)
        {
            case Info:
                return "Info";
            case Warning:
                return "Warning";
            case Fatal:
                return "Fatal";
            case Debug:
                return "Debug";
            default: 
                return "None";    
        }
    }
    //日志信息
    void logmessage(int level, const char *format, ...)
    {
        char leftbuffer[SIZE];
        time_t t = time(nullptr);
        struct tm * ltime = localtime(&t);
        //默认部分 事件等级和时间
        snprintf(leftbuffer,sizeof(leftbuffer),"%s [%d %d %d %d:%d]",leveltostring(level).c_str(),
        ltime->tm_year+1900,ltime->tm_mon+1,ltime->tm_mday,ltime->tm_hour,ltime->tm_min);

        //可变部分
        char rightbuffer[SIZE];
        va_list s;
        va_start(s,format);
        vsnprintf(rightbuffer,sizeof(rightbuffer),format,s);
        char Logbuffer[SIZE*2];
        snprintf(Logbuffer,sizeof(Logbuffer),"%s %s",leftbuffer,rightbuffer);
        LogPrint(level,Logbuffer);
    }
    void PrintOnefile( const char *filename,string& lbuffer)
    {
        lbuffer+='\n';
        int fd = open(filename, O_CREAT|O_APPEND|O_WRONLY,0666);
        if(fd < 0)
            return;
         write(fd,lbuffer.c_str(),lbuffer.size());   
    }
    void PrintClassFile(int level,string& lbuffer)
    {
        string filename = Logname;//将不同错误信息分流到对应的文件
        filename += ".";
        filename += leveltostring(level);
        PrintOnefile(filename.c_str(),lbuffer);
    }
    void LogPrint(int level,string lbuffer)
    {
        switch(printmethod)
        {
            case Screen://输出到屏幕
               cout<<lbuffer<<endl;
                break;
            case OneFile: //输出到一个文件上
                PrintOnefile(Logname,lbuffer);
                break;
            case ClassFile:
                PrintClassFile(level,lbuffer);
                break;
        }
    }
private:
    int printmethod;
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小何只露尖尖角

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值