当前位置: 首页 > news >正文

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

一.mmap文件映射

1.mmap介绍

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

NAMEmmap, munmap - map or unmap files or devices into memorySYNOPSIS#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文件映射,关闭映射,关闭文件。

#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)。

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

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

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

// 获取⽂件真实⼤⼩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:

#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_RDWRint 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的映射就与文件无关,而仅仅是开辟一块进程地址空间。接下来我们开始实践。

#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

#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接口的对象,它负责控制构建过程。

#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来初始化构造函数即可。

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_opsem_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。

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

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

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

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

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.使用建造者模式的信号量实现同步

#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

NAMEsemctl - System V semaphore control operationsSYNOPSIS#include <sys/types.h>#include <sys/ipc.h>#include <sys/sem.h>int semctl(int semid, int semnum, int cmd, ...);

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

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接口允许我们同时对多个信号量进行操作,如果要操作多个信号量,需要一个结构体数组和信号量个数。

NAMEsemop, semtimedop - System V semaphore operationsSYNOPSIS#include <sys/types.h>#include <sys/ipc.h>#include <sys/sem.h>int semop(int semid, struct sembuf *sops, size_t nsops);

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

struct sembuf, containing the following members:unsigned short sem_num;  /* semaphore number */short          sem_op;   /* semaphore operation */short          sem_flg;  /* operation flags */

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

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_opsem_buf.sem_flg = SEM_UNDO; // 不关心int n = semop(_semid, &sem_buf, 1);if (n < 0){std::cerr << "semop PV failed" << std::endl;}}

http://www.dtcms.com/a/469200.html

相关文章:

  • 在Mac上安装CocoaPods问题处理
  • 深入 Spring 条件化配置底层:从硬编码到通用注解的实现原理
  • SpringBoot之配置文件
  • Linux中kmalloc内存分配函数的实现
  • 【Spring Security】Spring Security 概念
  • 杂记 12
  • 织梦程序如何搭建网站洛阳凯锦腾网业有限公司
  • Socket网络编程(2)-command_server
  • vscode 连接远程服务器同步方法
  • 传统数据安全措施与云计算数据安全的区别
  • Linux下如何在vim里使用异步编译和运行?
  • Python高效实现Excel转PDF:无Office依赖的轻量化方案
  • 做网站PPPOE网络可以吗一个好网站设计
  • 混淆矩阵在金融领域白话解说
  • 深耕金融调研领域,用科学调研破解银行服务困境(市场调研)
  • 未备案网站处理系统写作墨问题 网站
  • 【Linux】手搓日志(附源码)
  • Excel 下拉选项设置 级联式
  • pycharm自动化测试初始化
  • nacos3.0.4升级到3.1.0
  • linux入门5.5(高可用)
  • JAVA·数组的定义与使用
  • Transformer 面试题及详细答案120道(81-90)-- 性能与评估
  • 可以做软件的网站有哪些功能中国新闻社待遇
  • 【鉴权架构】SpringBoot + Sa-Token + MyBatis + MySQL + Redis 实现用户鉴权、角色管理、权限管理
  • 三星S25Ultra/S24安卓16系统Oneui8成功获取完美root权限+LSP框架
  • ffmpeg 播放视频 暂停
  • 老题新解|大整数的因子
  • Eureka的自我保护机制
  • 探索颜色科学:从物理现象到数字再现