一.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)是一种创建型设计模式,用于将复杂对象的构建 与它的表示分离。对于信号量我们知道,它的创建和初始化过程略微复杂,因此建造者模式就十分适用于信号量的构建和使用过程。
大致框架如下:
-
产品类(Product):表示被构建的复杂对象,包含多个部件。
-
抽象建造者(Builder):指定创建一个产品各个部件的抽象接口。
-
具体建造者(ConcreteBuilder):实现Builder接口,构造和装配各个部件,并提供返回产品的接口。
-
指挥者(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;
}
}