Linux 进程通信——基于建造者模式的信号量

一.mmap文件映射

1.mmap介绍

基本作用:mmap系统调用可以将文件或设备的内容映射到进程地址空间中,可以省去read和write操作造成的IO开销。可以说,mmap是另一种共享内存,它不但可以优化文件操作,也可以用来实现共享内存。

cpp 复制代码
NAME
       mmap, munmap - map or unmap files or devices into memory

SYNOPSIS
       #include <sys/mman.h>

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

返回值:成功返回虚拟地址的起始地址,失败返回一个强转的-1.

addr:用户可以指定映射到哪个地址,如果使用默认则由操作系统分配。

length:起始地址+长度,并且这个长度应该为4kb的整数倍

prot:制定了映射区域的内存保护属性,它也是以标志位进行传参,若要传入多个参数可以使用按位或操作。

◦ PROT_READ :映射区域可读。

◦ PROT_WRITE :映射区域可写。

◦ PROT_EXEC :映射区域可执⾏

flags:映射类型

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

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

◦ 其他选项(如 MAP_ANONYMOUS 、 MAP_ANONYMOUS_SHARED 等)可能也存在于某些系统上,⽤于创建不与⽂件关联的匿名映射。

fd:映射一个被打开的文件

offset:从文件的某个位置开始映射(一般是从开始位置,但也可以自己制定),和length搭配使用删除映射:munmap。

mmap还可以用于实现共享内存,允许不同进程间共享数据。

2.mmap实操

我们写一个demo代码:文件名Write。基本的框架如下:打开文件,调整文件大小(文件初始用0填充),进行mmap文件映射,关闭映射,关闭文件。

cpp 复制代码
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <cstring>
#define SIZE 4096
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " filemame" << std::endl;
        return 1;
    }
    std::string filename = argv[1];
    //1.打开目标文件
    int fd = ::open(filename.c_str(), O_CREAT | O_RDWR, 0666);
    if (fd < 0)
    {
        std::cerr << "open error" << std::endl;
        return 2;
    } 
    // 2.手动调整文件大小,文件内容初始用0填充
    ::ftruncate(fd, SIZE);
    //3.文件映射操作
    char *mmap_addr = (char *)::mmap(nullptr, SIZE, PROT_READ | PROT_WRITE,
                                     MAP_SHARED, fd, 0);
    if (mmap_addr == MAP_FAILED)
    {
        perror("mmap");
        return 3;
    } // 4.操作⽂件,暂定
    
    // 5.取消映射
    ::munmap(mmap_addr, SIZE);
    // 6.关闭⽂件
    ::close(fd);
    return 0;
}

就例如我们向这个mmap空间中进行文件操作,由于mmap的选项我们可以将修改的内容反映到底层文件中(MAP_SHARED)。

然后我们写文件操作的逻辑(循环写入字母表)。

cpp 复制代码
for (int i = 0; i < SIZE; i++)
    {
        mmap_addr[i] = 'a' + i % 26;
    }

然后我们创建读取端:Read。Read整体的框架与Write没有什么区别,除了Read端不是创建文件并映射,而是找到映射的空间。调整文件大小中,我们可以创建一个stat的结构体,将文件的属性拷贝到这个结构体中,然后再mmap传length时使用这个属性中的文件大小。

cpp 复制代码
// 获取⽂件真实⼤⼩
    struct stat st;
    ::fstat(fd, &st);
    char *mmap_addr = (char *)::mmap(nullptr, st.st_size, PROT_READ,
                                     MAP_SHARED, fd, 0);
    if (mmap_addr == MAP_FAILED)
    {
        perror("mmap");
        return 3;
    }

完整的Read:

cpp 复制代码
#include <iostream>#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <cstring>
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " filemame" << std::endl;
        return 1;
    }
    std::string filename = argv[1];
    // 注意: 要成功进⾏写⼊映射,这⾥打开⽂件的模式必须是: O_RDWR
    int fd = ::open(filename.c_str(), O_RDONLY);
    if (fd < 0)
    {
        std::cerr << "open error" << std::endl;
        return 2;
    } // 获取⽂件真实⼤⼩
    struct stat st;
    ::fstat(fd, &st);
    char *mmap_addr = (char *)::mmap(nullptr, st.st_size, PROT_READ,
                                     MAP_SHARED, fd, 0);
    if (mmap_addr == MAP_FAILED)
    {
        perror("mmap");
        return 3;
    }
    std::cout << mmap_addr << std::endl;
    // 取消映射
    ::munmap(mmap_addr, st.st_size);
    // 关闭⽂件
    ::close(fd);
    return 0;
}

现象如下:

3.mmap简单实现malloc函数

mmap是malloc的底层系统调用之一。在使用mmap实现malloc时,我们使用私有映射的方式,从此mmap的映射就与文件无关,而仅仅是开辟一块进程地址空间。接下来我们开始实践。

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>
#include <bits/mman-linux.h>
// 使⽤mmap分配内存
void *my_malloc(size_t size)
{
    void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE,
                     MAP_PRIVATE |MAP_ANONYMOUS, -1, 0);
    if (ptr == MAP_FAILED)
    {
        perror("mmap");
        exit(EXIT_FAILURE);
    }
    return ptr;
} // 使⽤munmap释放内存
void my_free(void *ptr, size_t size)
{
    if (munmap(ptr, size) == -1)
    {
        perror("munmap");
        exit(EXIT_FAILURE);
    }
}

我们可以尝试使用这个malloc

cpp 复制代码
#include "Malloc.c"
int main()
{
    size_t size = 1024; // 分配1KB内存
    char *ptr = (char *)my_malloc(size);
    // 使⽤分配的内存(这⾥只是简单地打印指针值)
    printf("Allocated memory at address: %p\n", ptr);
    // ... 在这⾥可以使⽤ptr指向的内存 ...
    memset(ptr, 'A', size);
    for (int i = 0; i < size; i++)
    {
        printf("%c ", ptr[i]);
        sleep(1);
    }
    my_free(ptr, size);
    return 0;
}

可以用gdb查看ptr地址情况,以及用指令info proc mmaping查看当前进程映射情况

再继续run,在执行上面的地址就会新增一个映射,以及虚拟地址空间的起止位置,以及有效位置的开始(这里为0)

可以看到这个调试信息中,有objfile

当前进程也需要被映射到空间中,而我们写的my_malloc的 objfile是空的,因为我们上面制定了匿名映射。

进程地址空间,到底是怎么跟文件关联起来的?

我们看内核中进程地址空间某字段的数据结构vm_struct

发现其中有一个指针。

当进行文件映射时,一方面系统帮我们创建vm_area_struct,并链入mmap_struct中,虚拟地址就有了;而vm_file指针指向打开的文件,那么文件的属性,内容都可以被拿到,因此虚拟地址的空间成为可能,其余的工作就是填充页表;匿名映射,这个指针会被置为空,跟文件无关,vm_area_struct的start,end映射到地址空间就是单纯开辟了一段空间,就类似于我们的共享内存原理。

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

关于信号量基本接口的操作我们上一章已有详细介绍,这里我们不再赘述。接下来我们将基于建造者模式创建并使用信号量。

1.建造者模式

建造者模式(Builder Pattern)是一种创建型设计模式,用于将复杂对象的构建 与它的表示分离。对于信号量我们知道,它的创建和初始化过程略微复杂,因此建造者模式就十分适用于信号量的构建和使用过程。

大致框架如下:

  1. 产品类(Product):表示被构建的复杂对象,包含多个部件。

  2. 抽象建造者(Builder):指定创建一个产品各个部件的抽象接口。

  3. 具体建造者(ConcreteBuilder):实现Builder接口,构造和装配各个部件,并提供返回产品的接口。

  4. 指挥者(Director):构建一个使用Builder接口的对象,它负责控制构建过程。

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

// 产品类
class Product {
public:
    void setPartA(const std::string& partA) {
        partA_ = partA;
    }

    void setPartB(const std::string& partB) {
        partB_ = partB;
    }

    void show() const {
        std::cout << "Product has PartA: " << partA_ << ", PartB: " << partB_ << std::endl;
    }

private:
    std::string partA_;
    std::string partB_;
};

// 抽象建造者
class Builder {
public:
    virtual ~Builder() = default;
    virtual void buildPartA() = 0;
    virtual void buildPartB() = 0;
    virtual Product getResult() = 0;
};

// 具体建造者
class ConcreteBuilder : public Builder {
public:
    ConcreteBuilder() {
        product_ = Product();
    }

    void buildPartA() override {
        product_.setPartA("PartA built by ConcreteBuilder");
    }

    void buildPartB() override {
        product_.setPartB("PartB built by ConcreteBuilder");
    }

    Product getResult() override {
        return product_;
    }

private:
    Product product_;
};

// 指挥者
class Director {
public:
    Director(Builder* builder) : builder_(builder) {}

    void construct() {
        builder_->buildPartA();
        builder_->buildPartB();
    }

private:
    Builder* builder_;
};

// 使用示例
int main() {
    ConcreteBuilder builder;
    Director director(&builder);
    director.construct();
    Product product = builder.getResult();
    product.show();

    return 0;
}

接着我们一步步来设计信号量的建造者模式方法。

2.产品类Semphaphore

产品类是一个信号量集,我们可以指定信号量集中的某几个信号量进行操作。由于是建造者模式的最终产品,这个类只负责产品的主要职能------进行PV操作。由上层创建出来的信号量集标识semid来初始化构造函数即可。

cpp 复制代码
class Semaphore
{
private:
    void PV(int who, int data)
    {
        struct sembuf sem_buf;
        sem_buf.sem_num = who;        // 信号量编号,从0开始
        sem_buf.sem_op = data;      // S + sem_buf.sem_op
        sem_buf.sem_flg = SEM_UNDO; // 不关心
        int n = semop(_semid, &sem_buf, 1);
        if (n < 0)
        {
            std::cerr << "semop PV failed" << std::endl;
        }
    }
public:
    Semaphore(int semid) : _semid(semid)
    {
    }
    int Id() const
    {
        return _semid;
    }
    void P(int who)
    {
        PV(who, -1);
    }
    void V(int who)
    {
        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;
    // key_t _key; // 信号量集合的键值
    // int _perm;  // 权限
    // int _num;   // 信号量集合的个数
};

3.建造者接口类SemaphoreBuilder

在这个类中我们需要定义出要被实现的建造者接口,大致包含了创建信号量的各种参数,例如创造信号量集semget所需的BuildKey函数来初始化key,设置权限perm的SetPermission函数,设置信号量数量num的SetSemNum函数,设置存放信号量的容器,设置创建信号量集的函数Build,以及设置初始化信号量的函数InitSem。

cpp 复制代码
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;
};

4.具体建造者类ConcreteSemaphoreBuilder

这个类继承自上一个建造者接口类,专门具体实现每个接口。

cpp 复制代码
class ConcreteSemaphoreBuilder : public SemaphoreBuilder
{
public:
    ConcreteSemaphoreBuilder() {}
    virtual void BuildKey() override
    {
        // 1. 构建键值
        std::cout << "Building a semaphore" << std::endl;
        _key = ftok(SEM_PATH.c_str(), SEM_PROJ_ID);
        if (_key < 0)
        {
            std::cerr << "ftok failed" << std::endl;
            exit(1);
        }
        std::cout << "Got 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
    {
        // 2. 创建信号量集合
        int semid = semget(_key, _num, flag | _perm);
        if (semid < 0)
        {
            std::cerr << "semget failed" << std::endl;
            exit(2);
        }
        std::cout << "Got semaphore id: " << semid << std::endl;
        _sem = std::make_shared<Semaphore>(semid);
    }
    virtual void InitSem() override
    {
        if (_num > 0 && _initVal.size() == _num)
        {
            // 3. 初始化信号量集合
            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;               /* 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;
        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; // 我们要创建的具体产品
};

5.指挥者类Construct

这个类专门用来执行各个方法,传入具体的参数并创建对象,调用具体的方法,在将来使用时我们只需要创建一个具体建造者类和指挥者类,然后传入合适的参数即可。

cpp 复制代码
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();
        }
    }
};

6.使用建造者模式的信号量实现同步

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

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, 0600, 3, {1, 2, 3});

    // 完成了对象的创建的过程,获取对象
    auto fsem = builder->GetSem();

    // sleep(10);

    // SemaphoreBuilder sb;
    // auto fsem = sb.SetVar(1).build(BUILD_SEM, 1);
    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;
}

7.一些细节

1.初始化操作使用接口semctl

cpp 复制代码
NAME
       semctl - System V semaphore control operations

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

       int semctl(int semid, int semnum, int cmd, ...);

它的参数分别代表,哪个信号量集,那个信号量,做什么操作,以及一个可变参数。可变参数需要我们传入一个union联合体

cpp 复制代码
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;

2.semop接口允许我们同时对多个信号量进行操作,如果要操作多个信号量,需要一个结构体数组和信号量个数。

cpp 复制代码
NAME
       semop, semtimedop - System V semaphore operations

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

       int semop(int semid, struct sembuf *sops, size_t nsops);

其中结构体中的每个成员如下:

cpp 复制代码
struct sembuf, containing the following members:

           unsigned short sem_num;  /* semaphore number */
           short          sem_op;   /* semaphore operation */
           short          sem_flg;  /* operation flags */

于是在进行PV操作时就可以这样设置

cpp 复制代码
void PV(int who, int data)
    {
        struct sembuf sem_buf;
        sem_buf.sem_num = who;        // 信号量编号,从0开始
        sem_buf.sem_op = data;      // S + sem_buf.sem_op
        sem_buf.sem_flg = SEM_UNDO; // 不关心
        int n = semop(_semid, &sem_buf, 1);
        if (n < 0)
        {
            std::cerr << "semop PV failed" << std::endl;
        }
    }
相关推荐
阿巴~阿巴~2 小时前
Centos 7/8 安装 Redis
linux·服务器·数据库·redis·centos
怀旧,2 小时前
【Linux系统编程】2. Linux基本指令(上)
linux·运维·服务器
rongqing20192 小时前
Google 智能体设计模式:探索与发现
人工智能·设计模式
骥龙3 小时前
1.2、网络安全攻防实验室搭建指南:VMware + Kali Linux + Win10 全流程
linux·安全·web安全
迎風吹頭髮3 小时前
Linux内核架构浅谈9-Linux内核的开源生态:开发者协作与版本迭代机制
linux·运维·架构
Wang's Blog3 小时前
Linux小课堂: 文件系统结构与核心命令解析
linux·运维·服务器
2301_787328493 小时前
24.集群及高可用-Keepalived
linux·运维·云原生
难以触及的高度3 小时前
Linux-CentOS 7 上安装 MySQL 8.0.43(保姆级教程)
linux·mysql·centos
早晚会去希腊3 小时前
VScode怎么使用Jupyter并且设置内核
linux·运维·服务器