进程间通信IPC(3)system V标准下基于责任链模式的消息队列,基于建造者模式的信号量

本章目标

1,基于责任链模式的消息队列

2.基于建造者模式的信号量

1,基于责任链模式的消息队列

1.消息队列实现原理

消息队列同样是属于system V标准下的一致进程间通信,但是于共享内存不同的的是,它并不是一整块的内存块,在内存当中是通过一个队列进行维护一些特定大小的数据块,通过类型进行区分二者究竟是谁发送的消息.

在我们后面的学习,我们会知道,共享内存是典型的数据流,而消息队列这种就是数据报

1.同样的一个队列就一定要有一个队列头,为了让A,B两个进程能够找到他们对应的队列就一样要有key

2.这个消息队列是由内核进行管理,它的声明周期同样属于内核

3.因为是队列的形式,它的每个数据块有固定的大小,这个每一个数据块的大小我们可以自己定义,但是单个数据块不能够超过MSGMAX这个宏的大小的字节,同时这个消息队列的长度是有限制的不能够超过MSGMNI,这个长度,总字节数不能够超过MSGMNB

2.systemV消息队列的系统调用

因为同属于同一个标准下,接口设计,返回值,参数都是类似的,这点包括后面的信号量

1.msgget

参数

• key:某个消息队列的名字,键值

• msgflg:由九个权限标志构成,它们的⽤⽤法和创建⽂件时使⽤的mode模式标志是样的,这点可以参考共享内存,我们在这里使用的同样是IPC_CREAT 和IPC_EXCL 这两个宏去创建和获取消息队列

返回值

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

2.msgctl

参数

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

•cmd :将要采取的动作(有三个可取值),分别如下:

•buf :属性缓冲区

返回值

• 成功返回0;失败返回-1

对于msgctl,我们可以获取这个消息队列的信息,同样可以删除一个消息队列再后面的代码实现通信的时候,我们只要用 IPC_RMID,这个宏,而第一个,我们放到后面的IPC资源管理介绍

3,msgsnd

参数

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

• msgp:是⼀个指针,指针指向准备发送的消息

• msgsz:是msgp指向的消息⻓度,这个⻓度不含保存消息类型的那个longint⻓整型

• msgflg:控制着当前消息队列满或到达系统上限时将要发⽣⽣的事情,0即可

(msgflg=IPC_NOWAIT 表⽰⽰队列满不等待,返回EAGAIN 错误 )。

c 复制代码
struct msgbuf {
long mtype;       
char mtext[1];    
};
/* message type, must be > 0 */
/* message data */
// 
以⼀个long int⻓整数开始,接收者函数将利⽤⽤这个⻓整数确定消息的类型
 

发送消息大小的正文大小并不是1,我们可以自定义,而第一个表示一个消息结点的类型,

例如A进程发给B进程的类型是A,B可以通过介绍A类型的信息完成通信,同样的,B同样可以给A进程发消息,消息队列可以实现一个半双工类型的通信

4.msgrcv

参数

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

•msgp :是⼀个指针,指针指向准备接收的消息

•msgsz :是msgp 指向的消息⻓度,这个⻓度不含保存消息类型的那个longint⻓整型

•msgtype :它可以实现接收消息的类型,也可以模拟优先级的简单形式进⾏接收

•msgflg :控制着队列中没有相应类型的消息可供接收时将要发⽣⽣的事

返回值

• 成功返回实际放到接收缓冲区⾥⾥去的字符个数,失败返回-1

msgflg标志位

bash 复制代码
msgtype=0返回队列第⼀条信息
msgtype>0返回队列第⼀条类型等于msgtype的消息 
msgtype<0返回队列第⼀条类型⼩于等于msgtype绝对值的消息,并且是满⾜⾜条件的消息类型最⼩的消息
msgflg=IPC_NOWAIT,队列没有可读消息不等待,返回ENOMSG错误。
msgflg=MSG_NOERROR,消息⼤⼩超过msgsz时被截断
msgtype>0且msgflg=MSG_EXCEPT,接收类型不等于msgtype的第⼀条消息
  

我们下面实现的代码只是client和server的那样的单工通信,这里只需要给0即可.其他的需求我们暂时用不到

3.基于system V标准下的消息的队列实现的client和server通信

cpp 复制代码
#ifndef MSGQUEUE_HPP
#define MSGQUEUE_HPP

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

#define CREAT_MSG (IPC_CREAT | IPC_EXCL | 0666)
#define GET_MSG (IPC_CREAT)

#define PATHNAME "/tmp"
#define KEYNUM 0x1314520

#define MAX_SIZE 1024


#define Client 1
#define Server 2


struct msgbuf1
{
    long mtype;    /* message type, must be > 0 */
    char mtext[MAX_SIZE]; /* message data */
};

enum class status
{
    key_not_create = 1,
    msg_not_create
};

class Msgqueue
{
    key_t Getkey()
    {
        key_t tmp = ftok(PATHNAME, KEYNUM);
        if (tmp < 0)
        {
            std::cerr << "key not create" << std::endl;
            exit(static_cast<int>(status::key_not_create));
        }
        return tmp;
    }

public:
    Msgqueue() : _key(-1), _msg_id(-1)
    {
    }
    void CreateMsgqueue(int flag)
    {
        // 1.获取key
        _key = Getkey();
        // 2.创建消息队列
        _msg_id = msgget(_key, flag);
        if (_msg_id < 0)
        {
            std::cerr << "_msg not creat" << std::endl;
            exit(static_cast<int>(status::msg_not_create));
        }
    }
    bool Msgqueue_Send(const std::string& text,long flag)
    {
        struct msgbuf1 buffer;
        memset(&buffer,0,sizeof buffer);
        buffer.mtype = flag;
        memcpy(buffer.mtext,text.c_str(),text.size());
        int n = msgsnd(_msg_id,&buffer,MAX_SIZE,0);
        if(n<0)
        return false;
        return true;
    }
    bool Msgqueue_Recv(std::string* text,long flag)
    {
        struct msgbuf1 buffer;
        memset(&buffer,0,sizeof buffer);
        int n =msgrcv(_msg_id,&buffer,MAX_SIZE,flag,0);
        if(n<0)
        return false;
        buffer.mtext[n] = 0;
        *text = buffer.mtext;
        return true;
        
    }
    void Delete_Msgqueue()
    {
        // 该函数只给client端使用,我们让client 去读数据,让它管理资源
        int n = msgctl(_msg_id, IPC_RMID, nullptr);
        (void)n;
        std::cout << "delete finish" << std::endl;
    }
    virtual ~Msgqueue()
    {
    }

private:
    key_t _key;
    int _msg_id;
};

class Client_Msgqueue : public Msgqueue
{
public:
    Client_Msgqueue() {
        this->CreateMsgqueue(CREAT_MSG);
        std::cout<<"creat finish client"<<std::endl;
    }
    ~Client_Msgqueue() {
        this->Delete_Msgqueue();
    }

private:
};
class Server_Msgqueue : public Msgqueue
{
public:
    Server_Msgqueue() {
        this->CreateMsgqueue(GET_MSG);
        std::cout<<" get finsh server"<<std::endl;
    }
    ~Server_Msgqueue() {}

private:
};

#endif // MSGQUEUE.HPP

具体代码实现

4.责任链模式处理额外的需求

责任链模式是行为型设计模式的核心代表之一,核心设计思路是将多个请求处理对象按业务规则连成一条链式结构,当请求发起时,会从链的首节点开始依次传递,若某个节点能处理该请求则直接处理并终止传递,若不能则将请求转发给下一个节点,直到请求被处理或抵达链尾(可设置兜底节点处理未匹配请求)。

简单来说,责任链模式让请求的发起者和请求的处理者完全解耦:发起者无需知道请求最终由谁处理、链路有多少个节点、节点的执行顺序,只需将请求投入链中即可;而每个处理者只需关注自身职责范围内的处理逻辑,无需关心链上其他节点的实现,符合设计模式的单一职责原则和开闭原则。

责任链模式的 3 个核心组成 一套标准的责任链模式,由 3 个核心角色构成,所有实现均围绕这 3 个角色展开,结构清晰且易扩展:

抽象处理者(Handler):定义处理请求的统一接口 / 抽象类,包含两个核心要素 ------

处理请求的抽象方法、指向下一个处理者的引用,用于规范所有具体处理者的行为,是链路能串联的基础。

具体处理者(ConcreteHandler):实现 / 继承抽象处理者,重写请求处理方法,核心逻辑是先判断,再处理 /

转发:先检查当前请求是否属于自身处理范围,若是则直接处理;若否则调用下一个处理者的方法,将请求转发。

客户端(Client):负责创建并组装具体处理者,按业务规则设置节点的先后顺序,形成完整的责任链,同时作为请求的发起方,将请求传入链的首节点,触发链路流转。

上面是责任链模式的概念,我们可以通过责任链模式对我们接收的文本实现下面四个需求

1.能够将文本格式化,带上pid和时间戳

2.能够将内容保存到文件

3.能够将过长的文件进行切片

4.能够实现将分出来的文件进行打包

具体实现效果如上

因为代码过长,我在这里直接给出我的仓库链接
基于责任链模式的消息队列代码实现

2.基于建造者模式的信号量

2.1信号量概念补充

system V标准下的除了共享内存,还有消息队列,还有一种就是信号量.

而在说信号量之前,我要补充几个概念

我们进程间想要实现IPC就需要让,不同进程看到同一份资源,而同一块资源(file,内存块,队列),如果同时有不同的进程同时进行写入就可能出现数据不一致问题

而这种数据不一致问题,我们叫做并发问题

1.由多个执行流同时看到资源,我们叫做共享资源

2.被保护的共享资源,我们叫做临界资源

3.任何时间只有一个执行流访问共享资源,我们叫做互斥

4.访问临界资源是进程访问,是cpu执行临界资源所在的代码,这个代码块,我们叫做临界区

5.要么完成,要么没有完成,这种01关系,我们叫做原子性

6.执行流按照一定顺序去访问临界资源我们叫做同步

2.2信号量的原理

信号量于上面两种共享内存和消息队列不同,它是一种手段,它并没有和上面那种明确的实体,它本质是计数器.它可以保护像显示器文件,或者共享内存这种没有任何保护措施的共享资源.

我们举个例子

电影院买票,只要我们买到票,我们就对这个座位有了拥有权.而买票的本质是对资源的预定.如果把总票数比作共享资源,信号量就是对共享资源进行保护,因为信号量本质就是共享资源.我买一张票信号量的总数--申请资源,而退一张票就++释放资源.买票的加减的行为是为了保证信号量的安全

但是对于++ 和--这两个操作实际上在cpu当中并不是原子的.

我们拿一个举例++操作a和b两个数首先要先传入到cpu当中的上下文数据,而计算结果会先保存到cpu的通用寄存器当中,而cpu会再合适的实际重新写会到内存当中.

这么一数就至少3个操作了.

而在这个过程中可能可能当前进程的时间片到了,而其他进程进来了同样进行++这个操作,这样就造成数据不一致的问题了,就不能完成保证信号量安全这个事情了

而cpu的指令集当中给我们提供了这种只用一行就能完成这个操作的指令.

这种操作我们称为PV原语,P原语(--)释放资源,V(++)申请资源

对于进程来说成功申请到信号量,就运行,没有就阻塞.

这个过程是由内核完成的,不需要我们操心.由特定的算法和机制去支持.

再假设一种情况,如果这个信号量的初始值为1,同一时间只能够有一个进程来执行,这种操作我们叫做二元信号量,二元信号量与我们后面介绍的锁的机制特别类似.但是二元信号量并无所有权的限制假如A进程P一下,b进程V一下,就会导致统一时间有两个进程进入临界区了.而锁更强调所有权.谁的锁谁开.同一个进程加锁只能由这个进程解锁.

二元信号量能够更加自由实现互斥,

互斥锁是更加严格的互斥

这两个加上共享内存可以实现简单的同步,但是具体的策略仍然需要调整

而计数器我们上面讲的第一种信号量是多元信号量可以实现互斥+同步,可以实现我们后面说的生产消费者模型.这个操作我们后面再介绍

2.3信号量的接口介绍

信号量与前面几种IPC的接口实现同样类似.但是对于它的创建和初始化它是分开的,同时它有这一些致命的问题.我们依此介绍

1.semget

参数介绍

• key:信号量集的键值,同消息队列和共享内存

• nsems:信号量集中信号量的个数

• semflg:同消息队列和共享内存

注意,system V标准下,我们semget拿到的是一个信号量集,里面拿到的是这个信号量的个数,这个函数并没有对信号量的初始值进行设置.

2.semctl

这个函数我们在我们信号量里我们不止用它删除信号量了.我们还要用它对信号量进行初始化

第一个参数是semid,这个是由semget给我们返回的由系统给我们分配的id值

第二个是我们要对这个信号量当中哪个值进行操作.如果像批量化初始化,和删除真个信号量集就直接传0即可.而对于信号量集中的单个信号量就需要指明了.第一个信号量是下标0

第三个参数我们一般只用两个SETVAL,用来初始化信号量的初始值.以及IPC_RMID,这个用来删除整个信号量集.

我们还可以看出这个函数是可变参数,我们一般初始化还需要传一个联合

c 复制代码
   union semun {
               int              val;    /* Value for SETVAL */
               struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
               unsigned short  *array;  /* Array for GETALL, SETALL */
               struct seminfo  *__buf;  /* Buffer for IPC_INFO
                                           (Linux-specific) */
           };

第一个就是单个信号量的初始值是什么,我们一会只用这一个,这个联合在标准库当中并没有定义需要我们自己传,自己定义.这是是非常坑的点

在这里就有一个system V 信号量的重要问题.它的创建和初始化是分离的,因此不是原子的.

假设有两个进程同样执行

这两条语句

semget

semctl(设置值)

a进程执行完,semget可是因为时间片到了切出去了.没来及设置初始值.

b进来了,因为信号量已经创建,他要通过semop来进行使用.但是这个值我们本质上是脏数据.可能是0,会阻塞,也可能是随机值.这就引发了问题.当a进程回来了之后.b进程可能因为这个问题就已经挂了/

3.semop

这个函数就是用来实现PV原语的

•semid: 是该信号量的标识码,也就是semget 函数的返回值

•sops: 指向⼀个结构sembuf 的指针

•nsops: sops 对应的信号量的个数,也就是可以同时对多个信号量进⾏PV操作

对于这个sembuf,它的值如下

第一个操作sem_num你要对信号量集当中的哪个信号量进行操作/

第二个op PV原语 的操作 -1表示P原语--,1表示V原语++操作

第三个我们给0就行,它是当程序异常的时候信号量要进行回滚的

当我们要进行批量操作的时候要传一个这个sembuf类型的数组.

这个结构体在标准库当中可能未定义.有的时候有有的时候没有

2.4基于system V标准下的信号量

我们使⽤信号量,简化信号量使⽤,测试使⽤⼆元信号量进⾏显⽰器交替打印

cpp 复制代码
#pragma once
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string>
#include <vector>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <memory>

#define GET_SEM IPC_CREAT
#define CREAT_SEM (IPC_CREAT | IPC_EXCL | 0666)

// 产品只管使用,PV原语
class Semaphore
{
    void PV(int op)
    {
        struct sembuf sem;
        sem.sem_num = 0;
        sem.sem_op = op;
        sem.sem_flg = SEM_UNDO;
        // 第三个参数是要操作信号量的个数
        int n = semop(_sem_id, &sem, 1);
        (void)n;
    }

public:
    Semaphore(int sem_id, int flag) : _sem_id(sem_id), _flag(flag)
    {
    }
    void P()
    {
        //PV(-1);
        struct sembuf sem;
        sem.sem_num = 0;
        sem.sem_op = -1;
        sem.sem_flg = SEM_UNDO;
        // 第三个参数是要操作信号量的个数
        int n = semop(_sem_id, &sem, 1);
        (void)n;
    }
    void V()
    {
       // PV(1);
       struct sembuf sem;
        sem.sem_num = 0;
        sem.sem_op = 1;
        sem.sem_flg = SEM_UNDO;
        // 第三个参数是要操作信号量的个数
        int n = semop(_sem_id, &sem, 1);
        (void)n;
    }
    ~Semaphore()
    {
        // 在这里处理整体的信号量
        if (_flag == GET_SEM)
            return;
       int n = semctl(_sem_id, 0, IPC_RMID);
       (void)n; 
       std::cout << "destruct " << std::endl;
    }

private:
    int _sem_id;
    int _flag;
};
#define SEMNUM 1
#define PATHNAME "/tmp"
#define KEY_NUM 1314
// v1 版本简单的建造者只实现用信号量模拟锁的功能
// 二元信号量,信号量集的个数只有一个
class SemaphoreBuilder
{
    void get_key()
    {
        key_t tmp = ftok(PATHNAME, KEY_NUM);
        if (tmp < 0)
        {
            std::cerr << "key not create" << std::endl;
            exit(1);
        }
        _key = tmp;
    }
    void init(int sem_id)
    {
        union semun
        {
            int val;               /* Value for SETVAL */
            struct semid_ds *buf;  /* Buffer for IPC_STAT, IPC_SET */
            unsigned short *array; /* Array for GETALL, SETALL */
            struct seminfo *__buf; /* Buffer for IPC_INFO
                                      (Linux-specific) */
        } un;
        un.val = _val;
        // 信号量集中只有一个信号量下标传0
        int ret = semctl(sem_id, 0, SETVAL, un);
        std::cout<<"初始化"<<std::endl;
        if (ret < 0)
        {
            std::cerr << "sem not set" << std::endl;
            exit(3);
        }
    }

public:
    SemaphoreBuilder &Setval(int val)
    {
        // 设置信号量的初始值
        _val = val;
        return *this;
    }
    SemaphoreBuilder() : _key(-1), _val(-1)
    {
    }
    std::shared_ptr<Semaphore> build(int flag)
    {
        if (_val < 0)
        {
            std::cerr << "you must set sem_val" << std::endl;
            return nullptr;
        }
        // 1. 获取键值
        get_key();
        // 2. 创建信号量集
        int sem_id = semget(_key, 1, flag);
        if (sem_id < 0)
        {
            std::cerr << "sem not creat" << std::endl;
            exit(2);
        }
        // 根据flag 选择初始化
        if (flag == CREAT_SEM)
        {
            init(sem_id);
        }
        return std::make_shared<Semaphore>(sem_id, flag);
    }

    ~SemaphoreBuilder()
    {
    }

private:
    key_t _key;
    // size_t _Sem_Num;
    // int _Sem_flag;
    int _val;
};

2.5基于建造者模式的信号量

建造者模式(Builder Pattern)是创建型设计模式的核心之一,核心设计思路是:将复杂对象的构建过程与最终表示形式彻底分离,通过

"分步构建 + 统一组装" 的方式,让相同的构建流程能创建出不同表现形式的对象。 简单来说,建造者模式把 "复杂对象的创建" 拆成多个独立的

"建造步骤"(比如造汽车拆成装发动机、装底盘、装座椅),由专门的 "建造者" 负责一步步完成,再由 "指挥者"

把控整体流程,最终组装出完整对象。它解决的核心问题是:复杂对象初始化时参数多、顺序乱、可选配置多,导致构造方法臃肿、代码可读性差、易出错。

一套标准的建造者模式由 4 个核心角色构成,分工明确且易扩展:

产品(Product):需要构建的复杂对象,是最终产出物。比如 "汽车"(包含发动机、底盘、座椅、颜色等多个组成部分)、"电商订单"(包含商品信息、收货地址、支付方式等)。

抽象建造者(Builder):定义构建产品的统一步骤接口 / 抽象类,规范所有具体建造者的行为。比如定义buildEngine()(装发动机)、buildChassis()(装底盘)、getResult()(返回最终产品)等抽象方法,只规定 "要做什么",不规定 "怎么做"。

具体建造者(ConcreteBuilder):实现 / 继承抽象建造者,针对不同的产品变体,实现各步骤的具体构建逻辑。比如 "家用轿车建造者"(装 1.5T 低功耗发动机、舒适型座椅)、"跑车建造者"(装 3.0T 高性能发动机、运动型座椅),各自实现专属的构建细节。

指挥者(Director):负责控制建造流程的顺序,调用具体建造者的步骤方法,按固定流程完成对象构建(比如先装底盘→再装发动机→最后装座椅)。指挥者只关注 "流程",不关注 "具体怎么装",彻底解耦流程和实现。

我们根据上面的流程同样实现了一份代码,但是在这里同样只给我代码仓库链接.

因为篇幅原因
具体实现代码

相关推荐
柒儿吖6 小时前
DDlog 高性能异步日志库在 OpenHarmony 的 lycium 适配与分步测试
c++·c#·openharmony
yuanmenghao6 小时前
Linux 性能实战 | 第 17 篇:strace 系统调用分析与性能调优 [特殊字符]
linux·python·性能优化
民国二十三画生6 小时前
C++(兼容 C 语言) 的标准输入语法,用来读取一行文本
c语言·开发语言·c++
hweiyu006 小时前
Linux 命令:setfacl
linux·运维·服务器
柒儿吖6 小时前
基于 lycium 在 OpenHarmony 上交叉编译 utfcpp 完整实践
c++·c#·harmonyos
sTone873756 小时前
std::function/模板/裸函数指针选型指南
c++
wdfk_prog6 小时前
[Linux]学习笔记系列 -- [drivers]char
linux·笔记·学习
社会零时工6 小时前
Ubuntu安装的OpenCV如何更换版本
linux·opencv·ubuntu
无聊的小坏坏6 小时前
一文讲通:二分查找的边界处理
数据结构·c++·算法
m0_528749006 小时前
C语言错误处理宏两个比较重要的
java·linux·算法