第十一部分(下):进程通信

目录

1、消息队列

1.1、原理

1.2、接口

1.3、接口使用

1.4、责任链模式

2、信号量

2.1、原理

2.2、接口

2.3、建造者模式

[3、System V 内核数据结构](#3、System V 内核数据结构)

4、文件映射

4.1、接口

4.2、使用

4.3、malloc实现


1、消息队列

1.1、原理

消息队列提供了从一个进程向另外一个进程发送一块数据的方法。每个数据块都被认为是有⼀个类型,接收者进程接收的数据块可以有不同的类型值。

消息队列也是需要被管理的,是通过先描述再组织的方式进行管理的。

注:除了管道是单向通信的,System V都是可以双向通信的,只不过为了避免一些可能存在的问题,一般都采用单向通信。另外,消息队列的生命周期也是随内核的。

我们可以使用如下命令来查看消息队列:

bash 复制代码
ipcs -q

因为并没有创建消息队列,所以这里为一个消息队列也没有。

可以使用如下命令删除消息队列:

bash 复制代码
ipcrm -q msqid

1.2、接口

使用msgget函数创建一个消息队列:

cpp 复制代码
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
 
int msgget(key_t key, int msgflg);

key:来自ftok函数,使用方法和之前共享内存那里讲的是一样的。

msgflg:由权限(与创建文件时的mode类似)和一些其他的选项构成,例如下面的两个选项

IPC_CREAT:如果申请的消息队列不存在,则创建;如果存在,则获取并返回。

IPC_CREAT | IPC_EXCL:如果申请的消息队列不存在,就创建;如果存在,则出错返回。(这两个选项一使用,可以确保我们申请的消息队列是新的)。注:IPC_EXCL:是不能单独使用的。

返回值:成功返回⼀个非负整数,即该消息队列的标识码;失败返回-1。

使用msgctl函数控制消息队列:

cpp 复制代码
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
 
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

其中,msqid指msgget函数的返回值,即消息队列的标识码。

cmd表示将要采取的动作,可以为IPC_STAT:用于获取当前消息队列的属性值(用该动作时,我们需要使用shmid_ds类型创建一个变量传进第三个形参中)。IPC_RMID:删除消息队列(用该动作时,第三个参数一般填NULL)。

其中buf指向下面的结构:

返回值:成功返回0;失败返回-1。

使用msgsnd函数发送数据块:

cpp 复制代码
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
 
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

msqid:由msgget函数返回的消息队列标识码。

msgp:指向要发送数据块的起始地址。这个要发送的数据块的结构如下:

其中mtype是消息类型,mtext是消息内容(那个mtext数组的大小不一定非得是1,可以定义它的大小)。

msgsz:要的发送的数据块的大小,但是这个大小并不包括数据块中的消息类型的长度。

msgflg:一般写0即可。

返回值:成功返回0;失败返回-1。

使用msgrcv函数接收数据块:

cpp 复制代码
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
 
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,
                      int msgflg);

msgid:由msgget函数返回的消息队列标识码。

msgp:是⼀个指针,指针指向准备接收的消息(这个指针指向的就是msgbuf结构)。

msgsz:是msgp指向的消息长度, 这个长度不含保存消息类型的长度。

msgtype:指要读的类型。msgtype=0 读队列第⼀条信息(不管类型);msgtype>0 读队列第⼀条类型等于msgtype的消息;msgtype<0 读取队列中类型小于等于 |msgtyp| 的所有消息里,类型值最小的那一条。

msgflg:一般写0即可。

返回值:成功返回实际放到接收缓冲区中的字符个数,失败返回-1。

1.3、接口使用

Client.cc

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

int main()
{
    Client client;
    while(true)
    {
        //只让client发送消息
        std::string input;
        std::cout << "Please input message: ";
        std::getline(std::cin, input);
        client.Send(MSG_TYPE_CLIENT, input);
        if(input == "exit")
        {
            break;
        }
    }
    return 0;
}

Server.cc

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

int main()
{
    std::string text;
    Server server;

    while(true)
    {
        // 如果消息队列为空,阻塞等待
        server.Recv(MSG_TYPE_CLIENT, text);
        std::cout << "Received: " << text << std::endl;
        if(text == "exit")
        {
            break;
        }
    }

    return 0;
}

MsgQueue.hpp

cpp 复制代码
#ifndef MSGQUEUE_HPP
#define MSGQUEUE_HPP

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#include <iostream>
#include <string>
#include <cstring>

#define PATHNAME "/home/wang/vscode/QueueMessage"
#define PROJID 0x123

const int default_fd = -1;
const int default_size = 1024;

#define GET_MSGQUEUE (IPC_CREAT)
#define CREATE_MSGQUEUE (IPC_CREAT | IPC_EXCL | 0666)

class MsgQueue
{
    struct msgbuf
    {
        long mtype;
        char mtext[default_size];
    };
public:
    MsgQueue()
        :_msgfd(default_fd)
    {}

    void Create(int flag)
    {
        key_t key = ftok(PATHNAME, PROJID);
        if(key == -1)
        {
            std::cerr << "ftok error" << std::endl;
            exit(1);
        }
        std::cout << "key: " << std::hex << key << std::endl; // 以十六进制打印

        // IPC_CREAT:创建消息队列,如果不存在则创建,如果存在则获取它。
        // IPC_CREAT|IPC_EXCL(IPC_EXCL不单独使用):若消息队列存在则出错返回,不存在则创建。总是创建新的消息队列
        _msgfd = msgget(key, flag);
        if(_msgfd == -1)
        {
            std::cerr << "msgget error" << std::endl;
            exit(2);
        }
        std::cout << "msgqueue created: " << _msgfd << std::endl;
    }

    // 发送消息
    void Send(int type, const std::string& text)
    {
        struct msgbuf msg;
        memset(&msg, 0, sizeof(msg)); // 把msg给清空
        msg.mtype = type;
        memcpy(msg.mtext, text.c_str(), text.size());

        int n = msgsnd(_msgfd, &msg, sizeof(msg.mtext), 0);
        if(n == -1)
        {
            std::cerr << "msgsnd error" << std::endl;
            return;
        }
    }
    
    // 接收消息,参数设计为输出型参数
    void Recv(int type, std::string& text)
    {
        struct msgbuf msg;

        int n = msgrcv(_msgfd, &msg, sizeof(msg.mtext), type, 0);
        if(n == -1)
        {
            std::cerr << "msgrcv error" << std::endl;
            return;
        }
        msg.mtext[n] = '\0';
        text = msg.mtext;
    }

    //获取消息队列的属性
    void GetAttr()
    {
        struct msqid_ds outbuffer;
        int n = msgctl(_msgfd, IPC_STAT, &outbuffer);
        if(n == -1)
        {
            std::cerr << "msgctl error" << std::endl;
            return;
        }
        std::cout << "outbuffer.msg_perm.__key: " << std::hex << outbuffer.msg_perm.__key << std::endl;
    }

    void Destory()
    {
        int n = msgctl(_msgfd, IPC_RMID, 0);
        if(n == -1)
        {
            std::cerr << "msgctl error" <<std::endl;
            return;
        }
        std::cout << "msgqueue destoryed" << std::endl; 
    }
    ~MsgQueue()
    {}

private:
    int _msgfd;
};

// 定义消息类型
#define MSG_TYPE_CLIENT 1
#define MSG_TYPE_SERVER 2

class Server : public MsgQueue
{
public:
    Server()
    {
        MsgQueue::Create(CREATE_MSGQUEUE);
        std::cout << "server create msgqueue done" << std::endl;
        MsgQueue::GetAttr();
    }
    ~Server()
    {
        MsgQueue::Destory();
    }
};

class Client : public MsgQueue
{
public:
    Client()
    {
        MsgQueue::Create(GET_MSGQUEUE);
        std::cout << "client get msgqueue done" << std::endl;
    }
    ~Client()
    {}
};

#endif

1.4、责任链模式

比如需要完成以下需求:

1、client发送给server的输入内容,拼接上时间,进程pid信息

2、server收到的内容持久化保存到文件中

3、文件的内容如果过大,要进行切片保存并在指定的目录下打包保存。

责任链模式是⼀种行为设计模式,它允许将请求沿着处理者链进行传递。每个处理者都对请求进行检查,以决定是否处理它。如果处理者能够处理该请求,它就处理它;否则,它将请求传递给链中的下一个处理者。责任链模式有点类似与生产线,如下图所示:

注:设计模式有很多种,但是可以粗略的分为两类,一类是构建类设计模式,例如单例模式,另一类是行为类设计模式,例如责任链模式。

ChainOfResponsibility.hpp

cpp 复制代码
#ifndef CHAINOFRESPONSIBILITY_HPP
#define CHAINOFRESPONSIBILITY_HPP

#include <iostream>
#include <memory>
#include <string>
#include <sstream>
#include <ctime>
#include <filesystem>
#include <fstream>

#include <unistd.h>
#include <sys/wait.h>

//责任链基类
class HandlerText
{
public:
    virtual void Excute(const std::string& text) = 0;

    void SetNext(std::shared_ptr<HandlerText> next) // 设置下一个责任链节点
    {
        _next = next;
    }

    void Enable() // 启动该责任链节点
    {
        _enable = true;
    }

    void Disable() // 关闭该责任链节点
    {
        _enable = false;
    }

    virtual ~HandlerText()
    {}
protected:
    std::shared_ptr<HandlerText> _next; // 下一个责任链节点
    bool _enable = true; // 是否启用该节点 
};

//对文本进行格式化处理
class HandlerTextFormat : public HandlerText
{
public:
    void Excute(const std::string& text) override
    {
        std::string format_result = text;
        if(_enable) // 该节点被开启,对文本进行格式化处理
        {
            std::stringstream ss;
            ss << time(nullptr) << "-" << getpid() << "-" << text << "\n"; // 时间戳-进程pid-文本信息
            format_result = ss.str();
            std::cout << "1、格式化消息: " << text << " 结果: " << format_result <<  std::endl;
        }

        if(_next)
        {
            _next->Excute(format_result);
        }
        else
        {
            std::cout << "到达责任链处理结尾,完成处理" << std::endl;
        }
    }
};

// 文件的基本信息
std::string defaultfilepath = "./tmp/"; // 路径
std::string defaultfilename = "test.log"; // 文件名

//对文本进行保存
class HandlerTextSaveFile : public HandlerText
{
public:
    HandlerTextSaveFile(const std::string& filepath = defaultfilepath, const std::string& filename = defaultfilename)
    :_filepath(filepath), _filename(filename)
    {
        // 形成默认的目录名
        if(std::filesystem::exists(_filepath)) return;

        try
        {
            std::filesystem::create_directories(_filepath); // 创建目录
        }
        catch(std::filesystem::filesystem_error const& e)
        {
            std::cerr << e.what() << '\n';
        }
    }

    void Excute(const std::string& text) override
    {
        if(_enable) // 该节点被开启,对文本进行保存
        {
            std::string file = _filepath + _filename;
            std::ofstream ofs(file, std::ios::app);
            if(!ofs.is_open())
            {
                std::cerr << "open file error: " << file << std::endl;
                return; 
            }
            ofs << text;
            ofs.close();
            std::cout << "2、保存消息: " << text << " 到文件: " << file << std::endl;
        }

        if(_next)
        {
            _next->Excute(text);
        }
        else
        {
            std::cout << "到达责任链处理结尾,完成处理" << std::endl;
        }
    }
private:
    std::string _filepath;
    std::string _filename;
};

// 最大行数
const int defaultmaxline = 5;

//对文件内容的长度进行检查,如果长度过长,则对文件内容进行打包备份
class HandlerTextBackup : public HandlerText
{
public:
    HandlerTextBackup(const std::string& filepath = defaultfilepath, const std::string& filename = defaultfilename, \
        const int& maxline = defaultmaxline):_filepath(filepath), _filename(filename), _maxline(maxline)
    {}

    void Excute(const std::string& text) override
    {
        if(_enable) // 该节点是否被开启
        {
            std::cout << "3、检查文件是否超范围" << std::endl;
            std::string file = _filepath + _filename;
            if(IsOutOfRange(file))
            {
                // 超了范围进行切片备份
                std::cout << "文件超范围,进行备份" << std::endl; 
                BackUp(file);
            }
        }

        if(_next)
        {
            _next->Excute(text);
        }
        else
        {
            std::cout << "到达责任链处理结尾,完成处理" << std::endl;
        }
    }

private:
    bool IsOutOfRange(const std::string& file) // 判断是否超出范围
    {
        std::ifstream ifs(file);
        if(!ifs.is_open())
        {
            std::cerr << "open file error: " << file << std::endl;
            return false;
        }

        int lines = 0;
        std::string line;
        while(std::getline(ifs, line))
        {
            lines++;
        }

        ifs.close();
        return lines > _maxline;
    }

    void BackUp(const std::string& file)  // 进行备份
    {
        std::string suffix = std::to_string(time(nullptr));
        std::string backup_file = file + '.' + suffix; // _filepath + _filename + '.' + suffix

        std::string src_file = _filename + '.' + suffix; // _filename + '.' + suffix
        std::string tar_file = src_file + ".tgz"; // _filename + '.' + suffix + ".tgz"

        pid_t pid = fork();
        if(pid == 0) // 子进程
        {
            // 先对文件进行重命名,在Linux中对文件进行重命名是原子性的,这是因为rename()系统调用是原子性的
            std::filesystem::rename(file, backup_file);
            std::cout << "4、备份文件: " << file << " 到: " << backup_file << std::endl;

            // 对备份文件进行打包,打包成.tgz
            std::filesystem::current_path(_filepath); // 切换进程工作目录
            execlp("tar", "tar", "-czf", tar_file.c_str(), src_file.c_str(), nullptr);
            exit(1); // 如果替换失败,返回1
        }

        int status;
        pid_t rid = waitpid(pid, &status, 0);
        if(rid > 0)
        {
            if(WIFEXITED(status) && WEXITSTATUS(status) == 0)
            {
                std::filesystem::remove(backup_file); // 删除原本的备份文件,只留下打包的文件
                std::cout << "删除备份文件: " << backup_file << std::endl;
            }
        }
    }

private:
    std::string _filepath;
    std::string _filename;
    int _maxline;
};

// 后续可以扩展责任链中其他子类

// 责任链的入口类 //涉及到智能指针,暂时停止
class HandlerEntry
{
public:
    HandlerEntry()
    {
        // 构造责任链节点
        _format = std::make_shared<HandlerTextFormat>();
        _save = std::make_shared<HandlerTextSaveFile>();
        _backup = std::make_shared<HandlerTextBackup>();

        // 设置责任链节点处理顺序
        _format->SetNext(_save);
        _save->SetNext(_backup);
    }

    void EnableHandler(bool isformat, bool issave, bool isbackup)
    {
        isformat ? _format->Enable() : _format->Disable();
        issave ? _save->Enable() : _save->Disable();
        isbackup ? _backup->Enable() : _backup->Disable();
    }

    void Run(const std::string& text)
    {
        _format->Excute(text);
    }

    ~HandlerEntry()
    {}
private:
    std::shared_ptr<HandlerText> _format;
    std::shared_ptr<HandlerText> _save;
    std::shared_ptr<HandlerText> _backup;
};

#endif

Client.cc

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

int main()
{
    Client client;
    while(true)
    {
        //只让client发送消息
        std::string input;
        std::cout << "Please input message: ";
        std::getline(std::cin, input);
        client.Send(MSG_TYPE_CLIENT, input);
        if(input == "exit")
        {
            break;
        }
    }
    return 0;
}

Msgqueue.hpp

cpp 复制代码
#ifndef MSGQUEUE_HPP
#define MSGQUEUE_HPP
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#include <iostream>
#include <string>
#include <cstring>

#define PATHNAME "/home/ubuntu/vscode/QueueMessage"
#define PROJID 0x123

const int default_fd = -1;
const int default_size = 1024;

#define GET_MSGQUEUE (IPC_CREAT)
#define CREATE_MSGQUEUE (IPC_CREAT | IPC_EXCL | 0666)

class MsgQueue
{
    // 有类型数据块
    struct msgbuf
    {
        long mtype;
        char mtext[default_size];
    };

public:
    MsgQueue()
        :_msgfd(default_fd)
    {}

    void Create(int flag)
    {
        key_t key = ftok(PATHNAME, PROJID);
        if(key == -1)
        {
            std::cerr << "ftok error: " << strerror(errno) << std::endl;
            exit(1);
        }
        std::cout << "key: " << std::hex << key << std::endl; // 以十六进制打印

        // IPC_CREAT:创建消息队列,如果不存在则创建,如果存在则获取它。
        // IPC_CREAT|IPC_EXCL(IPC_EXCL不单独使用):若消息队列存在则出错返回,不存在则创建。总是创建新的消息队列
        _msgfd = msgget(key, flag);
        if(_msgfd == -1)
        {
            std::cerr << "msgget error" << std::endl;
            exit(2);
        }
        std::cout << "msgqueue created: " << _msgfd << std::endl;
    }

    // 发送消息
    void Send(int type, const std::string& text)
    {
        struct msgbuf msg;
        memset(&msg, 0, sizeof(msg)); // 把msg给清空
        msg.mtype = type;
        memcpy(msg.mtext, text.c_str(), text.size());

        int n = msgsnd(_msgfd, &msg, sizeof(msg.mtext), 0);
        if(n == -1)
        {
            std::cerr << "msgsnd error" << std::endl;
            return;
        }
    }
    
    // 接收消息,参数设计为输出型参数
    void Recv(int type, std::string& text)
    {
        struct msgbuf msg;

        int n = msgrcv(_msgfd, &msg, sizeof(msg.mtext), type, 0);
        if(n == -1)
        {
            std::cerr << "msgrcv error" << std::endl;
            return;
        }
        msg.mtext[n] = '\0';
        text = msg.mtext;
    }

    //获取消息队列的属性
    void GetAttr()
    {
        struct msqid_ds outbuffer;
        int n = msgctl(_msgfd, IPC_STAT, &outbuffer);
        if(n == -1)
        {
            std::cerr << "msgctl error" << std::endl;
            return;
        }
        std::cout << "outbuffer.msg_perm.__key: " << std::hex << outbuffer.msg_perm.__key << std::endl;
    }

    void Destory()
    {
        int n = msgctl(_msgfd, IPC_RMID, 0);
        if(n == -1)
        {
            std::cerr << "msgctl error" <<std::endl;
            return;
        }
        std::cout << "msgqueue destoryed" << std::endl; 
    }
    ~MsgQueue()
    {}

private:
    int _msgfd;
};

// 定义消息类型
#define MSG_TYPE_CLIENT 1
#define MSG_TYPE_SERVER 2

class Server : public MsgQueue
{
public:
    Server()
    {
        MsgQueue::Create(CREATE_MSGQUEUE);
        std::cout << "server create msgqueue done" << std::endl;
        MsgQueue::GetAttr();
    }
    ~Server()
    {
        MsgQueue::Destory();
    }
};

class Client : public MsgQueue
{
public:
    Client()
    {
        MsgQueue::Create(GET_MSGQUEUE);
        std::cout << "client get msgqueue done" << std::endl;
    }
    ~Client()
    {}
};

#endif

Server.cc

cpp 复制代码
#include "MsgQueue.hpp"
#include "ChainOfResponsibility.hpp"

int main()
{
    std::string text;
    Server server;
    HandlerEntry he;
    he.EnableHandler(true, true, true);

    while(true)
    {
        // 如果消息队列为空,阻塞等待
        server.Recv(MSG_TYPE_CLIENT, text);
        std::cout << "Received: " << text << std::endl;
        if(text == "exit")
        {
            break;
        }
        // 加工处理数据,采用责任链模式
        he.Run(text);
    }
    return 0;
}

2、信号量

2.1、原理

信号量本质上就是一把计数器,用来描述资源数量的多少。

1、申请计数器成功,就表示我具有访问资源的权限了。

2、申请了计数器资源,申请成功时并没有访问资源,这是对资源的一种预定机制。

3、计数器可有效的保证进入共享资源的执行流的数量。

4、所以每一个执行流,想访问共享资源中的一部分的时候,不是直接访问,而是先申请计数器资源。

如果一个计数器的值只能为0或者1,那么该计数器又叫二元信号量 ,本质就是一个。资源为1的本质就是将临界资源不要分成很多块了,而是当成一个整体使用与释放。

申请信号量,本质是对计数器进行--,称为P操作 。释放资源,释放信号量,本质是对计数器进行++,称为V操作 。我们把信号量的申请和释放称为PV操作 ,该操作是原子的,简单来说原子的是指要么不做,要做就要做完,是两态的,没有"正在做"这种概念。

信号量为什么是进程间通信的一种呢?通信不仅仅是通信数据,互相协同也是。要协同,信号量首先要被所有的通信进程看到。因此也是进程间通信的一种。

可以使用如下命令查看信号量:

bash 复制代码
ipcs -s

也可以使用如下命令删除信号量:

bash 复制代码
ipcrm -s semid

注:信号量生命周期也是随内核的。

2.2、接口

使用semget函数申请信号量:

cpp 复制代码
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
 
int semget(key_t key, int nsems, int semflg);

key:来自ftok函数,使用方法和之前共享内存以及消息队列那里讲的是一样的。

nsems:是指要申请的信号量集合中有几个信号量。

semflg:由权限(与创建文件时的mode类似)和一些其他的选项构成,例如下面的两个选项

IPC_CREAT:如果申请的信号量集合不存在,则创建;如果存在,则获取并返回。

IPC_CREAT | IPC_EXCL:如果申请的信号量集合不存在,就创建;如果存在,则出错返回。(这两个选项一使用,可以确保我们申请的信号量集合是新的)。注:IPC_EXCL:是不能单独使用的。

返回值:成功返回信号量集合的标识,失败返回-1。

使用semctl函数控制信号量(主要是初始化信号量值、查信号量值、删除信号量集):

cpp 复制代码
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
 
int semctl(int semid, int semnum, int cmd, ...);

semid:由semget返回的信号集标识码。

semnum:指要操作的单个信号量在集合中的编号,从零开始。如果是操作单个信号量,传对应编号即可;如果是操作整个集合,如删除,该参数会被忽略,传0即可。

cmd:是指进行什么样的操作,其中SETVAL(设初始值)、GETVAL(查当前值)、IPC_RMID(删除信号量集),详细介绍如下:

1、如果cmd传的值为SETVAL,那么需要定义如下结构,并初始化val,进行传参。此时函数成功返回零,失败返回-1。

其中buf是用来设置或者获取信号量的属性的(如果cmd为IPC_STAT或IPC_SET),array是用来批量操作信号量集合中的信号量的(如果cmd为GETALL或SETALL)。struct semid_ds结构如下:

2、如果cmd传的值为GETVAL,表示查看当前信号量的值,此时可变参数部分可以不传参数或者传NULL。函数成功返回信号量的当前值,失败返回-1。

3、如果cmd传的值为IPC_RMID,表示删除该信号量集合,此时可变参数部分可以不传参数或者传NULL。函数成功返回0,失败返回-1。

使用semop函数操作信号量:

cpp 复制代码
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
 
int semop(int semid, struct sembuf *sops, unsigned nsops);

semid:由semget返回的信号集标识码。

sops:指向如下的结构

其中,sem_num指要对哪一个信号量进行操作,编号从0开始;只有一个信号量,那这个参数就填0。sem_op一般为1(表示实现一个V操作)或者-1(表示实现一个P操作)。sem_flg一般填SEM_UNDO即可。

nsops:指sops结构体数组中的元素个数。

返回值:函数成功返回0,失败返回-1。

2.3、建造者模式

建造者模式是一种构建类设计模式,其核心思想就是将一个复杂对象的构建过程交给建造者来完成。一个标准的建造者模式包含4个核心角色(实际使用中可灵活简化):

1、产品:要构建的复杂对象。

2、抽象建造者:定义接口。

3、具体建造者:实现抽象建造者的接口。

4、指挥者:调用具体建造者的方法,按固定流程构建。

代码如下:

Sem.hpp

cpp 复制代码
#ifndef SEM_HPP
#define SEM_HPP

#include <iostream>
#include <memory>
#include <string>
#include <vector>

#include <sys/sem.h>
#include <sys/ipc.h>

#define GET_SEM (IPC_CREAT)
#define BUILD_SEM (IPC_CREAT | IPC_EXCL)

const std::string SEM_PATH = "/tmp";
const int SEM_PROJ_ID = 0x5555;

const int defaultnum = 1;

std::string intToHex(int num) // 十进制转十六进制
{
    char hex[64];
    snprintf(hex, sizeof(hex), "0x%x", num);
    return std::string(hex);
}

// 产品类
class Semaphore
{
private:
    void PV(int who, int data) // PV操作
    {
        struct sembuf sem_buf; // struct sembuf由系统提供

        sem_buf.sem_num = who; // 信号量的编号是从0开始的
        sem_buf.sem_op = data;
        sem_buf.sem_flg = SEM_UNDO;
        int n = semop(_semid, &sem_buf, 1);
        if (n < 0)
        {
            std::cerr << "semop P error" << std::endl;
        }
    }

public:
    Semaphore(int semid) : _semid(semid) {}

    int Id() const
    {
        return _semid;
    }

    void P(int who) // P操作
    {
        PV(who, -1);
    }

    void V(int who) // V操作
    {
        PV(who, 1);
    }

    ~Semaphore()
    {
        if (_semid >= 0)
        {
            int n = semctl(_semid, 0, IPC_RMID);
            if (n < 0)
            {
                std::cerr << "semctl IPC_RMID failed" << std::endl;
            }
            std::cout << "Semaphore: " << _semid << " removed" << std::endl;
        }
    }
private:
    int _semid;
};

// 建造者接口
class SemaphoreBuilder
{
public:
    virtual ~SemaphoreBuilder() = default;
    virtual void BuildKey() = 0;
    virtual void SetPermission(int perm) = 0;
    virtual void SetSemNum(int num) = 0;
    virtual void SetInitVal(std::vector<int> initVal) = 0;
    virtual void Build(int flag) = 0;
    virtual void InitSem() = 0;
    virtual std::shared_ptr<Semaphore> GetSem() = 0;
};

// 具体的建造者类
class ConcreteSemaphoreBuilder : public SemaphoreBuilder
{
public:
    ConcreteSemaphoreBuilder() {}

    virtual void BuildKey() override // 获取key
    {
        // 获取key值
        _key = ftok(SEM_PATH.c_str(), SEM_PROJ_ID);
        if (_key < 0)
        {
            std::cerr << "ftok error" << std::endl;
            exit(1);
        }
        std::cout << "get a key: " << intToHex(_key) << std::endl;
    }

    virtual void SetPermission(int perm) override // 设置权限
    {
        _perm = perm;
    }

    virtual void SetSemNum(int num) override // 设置信号量集中信号量数量
    {
        _num = num;
    }

    virtual void SetInitVal(std::vector<int> initVal) override // 设置信号量集中信号量的初始值
    {
        _initVal = initVal;
    }

    virtual void Build(int flag) override // 构建信号量
    {
        // 创建信号量集合
        int semid = semget(_key, _num, flag | _perm);
        if (semid < 0)
        {
            std::cerr << "semget failed" << std::endl;
            exit(2);
        }
        std::cout << "get a semid: " << semid << std::endl;

        _sem = std::make_shared<Semaphore>(semid);
    }

    virtual void InitSem() override // 初始化信号量的值
    {
        if (_num > 0 && _initVal.size() == _num)
        {
            // 初始化信号量集合
            for (int i = 0; i < _num; i++)
            {
                if (!init(_sem->Id(), i, _initVal[i]))
                {
                    std::cerr << "Init failed" << std::endl;
                    exit(3);
                }
            }
        }
    }

    virtual std::shared_ptr<Semaphore> GetSem() override
    {
        return _sem;
    }

private:
    bool init(int semid, int num, int val)
    {
        union semun
        {
            int val;
            struct semid_ds *buf;
            unsigned short *array;
            struct seminfo *__buf;
        } un;

        un.val = val;
        int n = semctl(semid, num, SETVAL, un);
        if (n < 0)
        {
            std::cerr << "semctl SETVAL failed" << std::endl;
            return false;
        }
        return true;
    }

private:
    key_t _key;
    int _perm;                       // 权限
    int _num;                        // 数量
    std::vector<int> _initVal;       // 初始化值
    std::shared_ptr<Semaphore> _sem; // 创建的具体产品
};

// 指挥者类
class Director
{
public:
    void Construct(std::shared_ptr<SemaphoreBuilder> builder, int flag, int perm = 0666, int num = defaultnum,
                   std::vector<int> initVal = {1})
    {
        builder->BuildKey();
        builder->SetPermission(perm);
        builder->SetSemNum(num);
        builder->SetInitVal(initVal);
        
        builder->Build(flag);
        if (flag == BUILD_SEM)
        {
            builder->InitSem();
        }
    }
};

#endif

Writer.cc

cpp 复制代码
#include "Sem.hpp"
#include <unistd.h>
#include <ctime>

int main()
{
    std::shared_ptr<SemaphoreBuilder> builder = std::make_shared<ConcreteSemaphoreBuilder>(); // 建造者对象
    std::shared_ptr<Director> director = std::make_shared<Director>(); // 指挥者对象

    // 在指挥者的指示下,完成建造过程
    director->Construct(builder, BUILD_SEM, 0660, 3, {1, 2, 3});

    auto fsem = builder->GetSem();

    srand(time(0) ^ getpid());
    pid_t pid = fork();

    // 期望父子进程打印时,C或者F必须成对的出现
    if (pid == 0)
    {
        director->Construct(builder, GET_SEM);
        auto csem = builder->GetSem();
        while (true)
        {
            csem->P(0);

            printf("C");
            usleep(rand() % 95270);
            fflush(stdout);

            printf("C");
            usleep(rand() % 43990);
            fflush(stdout);

            csem->V(0);
        }
    }

    while (true)
    {
        fsem->P(0);

        printf("F");
        usleep(rand() % 95270);
        fflush(stdout);

        printf("F");
        usleep(rand() % 43990);
        fflush(stdout);

        fsem->V(0);
    }

    return 0;
}

3、System V 内核数据结构

如下图所示,当每创建一次共享内存时,在内核中创建对应的数据结构,然后使用数组管理起来;当每一次创建消息队列,也是在内核中创建对应的数据结构,然后再由数组进行管理起来;创建信号量也是如此。

其中这个数组的下标经过一定转化就是共享内存或者是消息队列和信号量的标识符(注意:这个标识符不是key)。

当需要访问对应的共享资源时,就可以通过上面的数组找到对应的struct ipc_perm,然后可以通过强制类型转换将该地址转换为对应的共享内存或者是消息队列和信号量的结构体即可。

4、文件映射

文件映射就是可以让一个进程,把打开的文件映射到自己的地址空间中,映射好后,将获得一个虚拟地址,从此对于文件内容的操作就不再需要使用系统调用来完成,直接使用虚拟地址就可以。如下图所示:

文件映射允许用户空间程序将文件或设备的内容直接映射到进程的虚拟地址空间中,从而可以高效的访问文件数据,而无需使用read或者write等系统调用进行数据的拷贝。此外mmap还可以用于实现共享内存(类似于之前的System V 共享内存),允许不同进程间共享数据。

注:文件映射的本质就是将内存中的内核级文件缓冲区映射到虚拟地址空间,通过虚拟地址空间来操作文件缓冲区。

4.1、接口

使用mmap函数进行文件映射:

cpp 复制代码
#include <sys/mman.h>

void *mmap(void addr[.length], size_t length, int prot, int flags,
                  int fd, off_t offset);

addr:⼀个提示地址,表示希望映射区域开始的地址。然而,这个地址可能会被内核忽略,特别是当我们没有足够的权限来请求特定的地址时。如果 addr 是 NULL ,则系统会自动选择⼀个合适的地址。一般使用NULL,让系统选一个合适的地址。

length:要映射到进程地址空间中的字节数。如果指定的 length 不是内存页的整数倍(内存页大小通常是4KB ,但可能因系统而异),系统可能会向上舍入到最近的内存页边界,例如请求的内存大小为3500字节,则按照向上舍入的原则,应分配4096字节的内存。

prot:指定了映射区域的权限,该权限不能超过打开文件时的权限,比如文件以读打开,那么就不能把映射区的权限设置为可写,可以是以下值的组合(使用按位或运算符 | ):

1、PROT_READ :映射区域可读。

2、PROT_WRITE :映射区域可写。

3、PROT_EXEC :映射区域可执行。

flags:指定了映射的类型,如下:

1、MAP_PRIVATE :创建⼀个私有映射,对映射区域的修改不会反映到底层文件中。

2、MAP_SHARED :创建⼀个共享映射,对映射区域的修改会反映到底层文件中(前提是文件是 以写方式打开的,并且文件系统支持这种操作)。

3、MAP_ANONYMOUS:用于创建不与文件关联的匿名映射。

fd:⼀个有效的文件描述符,指向要映射的文件或设备。对于匿名映射,这个参数是-1。

offset:文件中的起始偏移量,即映射区域的开始位置。 offset和length⼀起定义了映射区域在文件中的位置和大小。也就是从文件的哪里开始进行映射。另外,offset必须是内存页大小的整数倍。

返回值:函数成功返回虚拟地址的起始地址,失败返回MAP_FAILED(本质上就是(void *) -1)。

使用munmap函数解除文件映射:

cpp 复制代码
#include <sys/mman.h>

int munmap(void addr[.length], size_t length);

其中addr就是mmap函数的返回值,length参数和上面的mmap函数的参数含义是相同的。

返回值:函数成功返回0,失败返回-1。

除了上面的两个接口,还有两个常常要搭配上面接口使用的接口,如下:

使用ftruncate函数调整文件大小:

cpp 复制代码
#include <unistd.h>

int ftruncate(int fd, off_t length);

其中fd表示要调整的文件;length表示要调整的大小,如果文件当前大小 > length,超出部分会被直接删除(数据丢失);如果文件当前大小 < length,文件会被扩展,新增部分填充为 \0(空字节)。

返回值:函数成功返回0,失败返回-1。

注:使用该函数,需要文件必须以可写模式打开,否则必失败;

使用fstat函数获取文件属性:

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

int fstat(int fd, struct stat *statbuf);

其中fd表示要获取属性的文件;struct stat结构如下图所示:

其中比较常见的就是st_size,表示文件的总大小。

返回值:函数成功返回0,失败返回-1。

4.2、使用

例如:进行读取

cpp 复制代码
#include <iostream>

#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>

int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " filename" << std::endl;
        return 1;
    }

    // 1、打开文件
    int fd = ::open(argv[1], O_RDONLY);
    if(fd < 0)
    {
        std::cerr << "Failed to open file: " << argv[1] << std::endl;
        return 2;
    }
    
    // 2、获取文件大小
    struct stat st;
    if(::fstat(fd, &st) == -1)
    {
        std::cerr << "Failed to stat file: " << argv[1] << std::endl;
        ::close(fd);
        return 3;
    }

    // 3、文件映射
    char* shmaddr = (char*)::mmap(nullptr, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
    if(shmaddr == MAP_FAILED)
    {
        std::cerr << "Failed to mmap file: " << argv[1] << std::endl;
        ::close(fd);
        return 4;
    }

    // 4、读取数据
    std::cout << "File content: " << shmaddr << std::endl;

    // 5、解除映射
    if(::munmap(shmaddr, st.st_size) == -1)
    {
        std::cerr << "Failed to munmap file: " << argv[1] << std::endl;
        ::close(fd);
        return 5;
    }

    ::close(fd);

    return 0;
}

例如:进行写入

cpp 复制代码
#include <iostream>

#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>

#define PAGE_SIZE 4096

int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " filename" << std::endl;
        return 1;
    }

    // 1、打开文件
    int fd = ::open(argv[1], O_RDWR | O_CREAT | O_TRUNC, 0666);
    if(fd < 0)
    {
        std::cerr << "Failed to open file: " << argv[1] << std::endl;
        return 2;
    }

    // 2、调整文件大小,方便进行合法映射
    if(::ftruncate(fd, PAGE_SIZE) == -1)
    {
        std::cerr << "Failed to truncate file: " << argv[1] << std::endl;
        ::close(fd);
        return 3;
    }

    // 3、文件映射
    char* shmaddr = (char*)::mmap(nullptr, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if(shmaddr == MAP_FAILED)
    {
        std::cerr << "Failed to mmap file: " << argv[1] << std::endl;
        ::close(fd);
        return 4;
    }

    // 4、写入数据
    for(char c = 'a'; c <= 'z'; ++c)
    {
        shmaddr[c - 'a'] = c;
        sleep(1);
    }

    // 5、解除映射
    if(::munmap(shmaddr, PAGE_SIZE) == -1)
    {
        std::cerr << "Failed to munmap file: " << argv[1] << std::endl;
        ::close(fd);
        return 5;
    }

    ::close(fd);

    return 0;
}

4.3、malloc实现

极简的malloc函数实现,如下:

cpp 复制代码
#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>

#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>

void *my_malloc(size_t size)
{
    if (size > 0)
    {
        void *ptr = ::mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
        if (ptr == MAP_FAILED)
        {
            return nullptr;
        }
        return ptr;
    }

    return nullptr;
}

void my_free(void *start, size_t size)
{
    if (start && size > 0)
    {
        int ret = ::munmap(start, size);
        if (ret != 0)
        {
            std::cerr << "Failed to munmap memory" << std::endl;
        }
    }
}

int main()
{
    char* ptr = (char*)my_malloc(1024);
    if (ptr == nullptr)
    {
        std::cout << "malloc failed" << std::endl;
        return 1;
    }
    printf("Allocated memory at address: %p\n", ptr);

    memset(ptr, 'A', 1024);

    for (int i = 0; i < 1024; i++)
    {
        printf("%c ", ptr[i]);
        fflush(stdout);
        sleep(1);
    }

    my_free(ptr, 1024);
    return 0;
}
相关推荐
2401_863905441 小时前
haproxy
linux
皮皮哎哟2 小时前
Linux多线程通信:告别数据混乱
linux·互斥锁·进程间通信·信号量
三天不学习2 小时前
Linux inotify 机制详解,解决“用户实例限制”问题
linux·运维·c#
ZFB00012 小时前
【麒麟桌面系统】V10-SP1 2503 系统知识——插入U盘(移动硬盘)为只读状态
linux·运维·kylin
龙仔7252 小时前
在麒麟V10服务器安全加固,sshd防暴力破解加固,实现“密码错误3次封IP”的需求
服务器·tcp/ip·安全
unfeeling_2 小时前
Keepalived实验
linux·服务器·网络
山峰哥2 小时前
吃透 SQL 优化:告别慢查询,解锁数据库高性能
服务器·数据库·sql·oracle·性能优化·编辑器
Web极客码3 小时前
解决WordPress后台“外观”菜单消失
linux·服务器·wordpress
熬夜有啥好3 小时前
Linux软件编程——综合小练习
linux·算法·目录遍历·fgets·strcpy·linux内核与用户交互·strtok