抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

mmap 可以把磁盘文件的一部分直接映射到内存,这样文件中的位置直接就有对应的内存地址,对文件的读写可以直接用指针而不需要 read/write 函数。


1. 虚拟内存

https://welkinx.com/2020/02/07/mmap/

1.1 Linux 增加交换空间

未开启交换空间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sudo su
# 查看内存使用情况
free -h

# 创建 swap 文件
# 单位 bs 为 G,大小 count 为 20,创建 20G 交换空间
dd if=/dev/zero of=/swapfile bs=1G count=20

# 激活 swap 文件
chmod 600 swapfile
mkswap swapfile

# 开启 swap
swapon swapfile

free -h
# 查看已开启的交换空间
swapon --show

已开启交换空间,重新修改 swap 大小:

1
2
3
4
5
6
7
8
9
10
11
12
# 查看内存使用情况
free -h

# 关闭指定 swap
swapoff /swapfile

# 重新分配
fallocate -l 30G /swapfile

chmod 600 swapfile
mkswap swapfile
swapon swapfile

1.2 创建虚拟内存实例

1.2.1 创建虚拟内存、在虚拟内存中读入大文件数据并存储

注意 placement new 表达式的用法,其中 new(place) 中 place 为指针,指向预分配的内存地址。

点击查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
#include <iostream>
#include <fstream>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <cstring>
#include <vector>

/**
* TODO
* 用 mmap 实现虚拟大内存,并构建一个接口调用;
* 在这块虚拟大内存中读入数据集并保存,用于建图。
*/


/**
* @name: create_virtual_mem
* @msg: 创建虚拟内存,并返回指向该内存地址的指针
* @param {off_t&} mem_size: 虚拟内存大小
* @param {int&} fd: 文件描述符,main 函数中指定
* @return {void*}: 虚拟内存指针
*/
void* create_virtual_mem(off_t &mem_size, int &fd)
{
// 使用 /dev/zero 作为匿名内存映射的数据源
fd = open("/dev/zero", O_RDWR);
void* v_mem = mmap(nullptr, mem_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, fd, 0);
// 映射失败
if (v_mem == MAP_FAILED)
{
std::cout << "Create virtual memory failed. Reason: " << strerror(errno) << std::endl;
exit(EXIT_FAILURE);
}

// 文件和映射统一关闭

// 返回虚拟内存指针
return v_mem;
}


/**
* @name: release_mem
* @msg: 释放虚拟内存以及内存映射
* @param {void*} v_mem: 指向虚拟内存的指针
* @param {off_t&} mem_size: 虚拟内存大小
* @param {int&} fd: 文件描述符,用于关闭虚拟内存打开的 /dev/zero
* @param {u_char* &} file_map: 读取大文件时的内存映射的指针
* @param {off_t &} file_size: 被映射的大文件的长度(字节)
* @return {void}
*/
void release_mem(void* v_mem, off_t &mem_size, int &fd, u_char* &file_map, off_t &file_size)
{
munmap(v_mem, mem_size);
close(fd);
// 关闭 read data 映射
munmap(file_map, file_size);
// 关闭文件
close(fd);
}


/**
* @name: mmap_read_bvecs
* @msg: 通过 mmap 内存映射读取 *.bvecs 文件
* @param {string} &filename: 文件名
* @param {void*} &v_mem: 虚拟内存指针
* @param {u_char* &} file_map: 读取大文件时的内存映射的指针
* @param {off_t &} file_size: 被映射的大文件的长度(字节)
* @param {std::vector<std::vector<float>>* &} data: 存储数据
* @return {void}
*/
void mmap_read_bvecs(std::string &filename, void* v_mem, u_char* &file_map, off_t &file_size,
std::vector<std::vector<float>>* &data)
{
// 获取文件描述符
int fd = open(filename.c_str(), O_RDWR);
// 打开失败
if (fd == -1)
{
perror(("Open \"" + filename + "\" failed. Reason").c_str());
exit(EXIT_FAILURE);
}
// 获取文件信息
struct stat fs;
// 获取失败
if (fstat(fd, &fs) == -1) {
perror("Get file information filed. Reason");
close(fd);
exit(EXIT_FAILURE);
}

// 文件大小
file_size = fs.st_size;
std::cout << "File size: " << file_size << std::endl;

// read data 映射,获得指针
file_map = (u_char*)mmap(nullptr, file_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// 映射失败
if (file_map == MAP_FAILED)
{
perror("Memory mapping failed. Reason");
close(fd);
exit(EXIT_FAILURE);
}

// 访问数据
long long byte_count = 0LL;
// vector number
long long num = 0;
std::cout << "Reading..." << std::endl;
while(byte_count < file_size)
{
// vector dimension
int dim;
u_char* q = file_map + byte_count;
dim = *(int*)q;
q += sizeof(int);
// 物理内存内保存单个数据
std::vector<float> vec(dim);
for (int i = 0; i < dim; i++)
{
vec[i] = (float)*(q + i);
}
// 存储到 data 中
data->push_back(std::move(vec));

byte_count += sizeof(int) + dim * sizeof(u_char);
num += 1;
}

// 文件和映射需要统一关闭
}


int main()
{
std::string filename = "./dataset/bigann_learn.bvecs";

// 创建虚拟内存
int fd_v_mem;
off_t mem_size = 1024 * 1024 * 1024 * 20LL;
auto v_mem = create_virtual_mem(mem_size, fd_v_mem);

// 读入数据
u_char *file_map = nullptr;
off_t file_size = 0LL;
// 在虚拟内存中开辟空间存储
// 注意 placement new 表达式的用法
std::vector<std::vector<float>>* data = new(v_mem) std::vector<std::vector<float>>();
mmap_read_bvecs(filename, v_mem, file_map, file_size, data);

// output
auto vec_num = data->size();
auto vec_dim = (*data)[0].size();
std::cout << "vector number: " << vec_num << std::endl;
std::cout << "vector dimension: " << vec_dim << std::endl;
// for (size_t i = 0; i < vec_num; ++i)
// {
// std::cout << "[";
// for (size_t j = 0; j < vec_dim; ++j)
// {
// std::cout << (*data)[i][j] << ((j == vec_dim - 1) ? "" : ", ");
// }
// std::cout << "]\n";
// }

// 关闭映射和虚拟内存
release_mem(v_mem, mem_size, fd_v_mem, file_map, file_size);

return 0;
}

2. mmap

https://www.cnblogs.com/huxiao-tee/p/4660352.html

2.1 mmap 简介

mmap 可以把磁盘文件的一部分直接映射到内存,这样文件中的位置直接就有对应的内存地址,对文件的读写可以直接用指针而不需要 read/write 函数。

此函数的作用是创建一个新的虚拟内存区域,并将指定的对象映射到此区域。

内存映射(Memory Mapping)将文件内容映射到进程的虚拟地址空间。在这种机制下,文件可以被视为内存的一部分,从而允许程序直接对这部分内存进行读写操作,而无需传统的文件 I/O 调用,从而减少系统的用户态、内核态切换,减少切换开销。通过这种方式,文件内容可以通过指针直接访问,就像访问普通的内存数组一样,这极大地提高了文件操作的效率和直观性。

映射时,操作系统将文件的一部分或全部内容映射到虚拟内存地址空间。这些虚拟地址与物理内存地址相关联,但并不是所有数据立即加载到物理内存中。

当访问映射的地址时,如果对应数据不在物理内存中,操作系统会自动从磁盘加载所需的数据页到物理内存中(这称为“页错误”处理)。

2.2 mmap 特点

2.2.1 数据持久化

通过 mmap 映射的数据通常来自文件系统中的文件。这意味着数据是持久化的,即使程序终止,文件中的数据依然存在。当你通过映射的内存区域修改数据时,这些更改最终会反映到磁盘上的文件中。

2.2.2 大文件读写

mmap 特别适合于需要频繁读写大文件的场景,因为它可以减少磁盘 I/O 操作的次数。它也允许文件的一部分被映射到内存中,这对于处理大型文件尤为有用。

2.2.3 性能和效率

映射文件到内存可以提高文件访问的效率,尤其是对于随机访问或频繁读写的场景。系统可以利用虚拟内存管理和页缓存机制来优化访问。

2.2.4 同步和一致性

使用 mmap 时,必须考虑到文件内容的同步问题。例如,使用 msync 调用来确保内存中的更改被同步到磁盘文件中。

2.2.5 页缓存

使用 mmap 映射文件到内存时,操作系统利用页缓存(提升文件读写效率。在内存上,与文件中的数据块进行绑定。文件被划分为多个页大小的数据块)来优化对这些文件数据的访问。页缓存是操作系统的一部分,用于存储从磁盘读取的数据页。 访问 mmap 映射的文件时,并不是每次读取都会直接触及磁盘。如果所需数据已经在页缓存中(由于之前的读取操作),则直接从缓存中获取数据,而不需要磁盘 I/O。

2.3 mmap 函数介绍

1
2
3
4
void *mmap(void *addr, size_t len, int prot,
int flags, int filedes, __off_t offset);

int munmap(void *addr, size_t len);

2.3.1 参数

  • addr: 内存上的映射地址,给内核一个提示(建议),从(内存上)什么地址开始映射。建立映射后,真正的映射首地址通过返回值得到。如果 addr 为空,则内核自己选择合适的地址。

  • len: 需要映射的那部分文件的长度(多少个字节),代表将文件中多大的部分映射到内存。

  • prot: 4 个取值
    • PROT_EXEC: 映射部分可执行,如动态库
    • PROT_READ: 映射部分可读
    • PROT_WRITE: 映射部分可写
    • PROT_NONE: 映射部分不可访问
  • flags: 2 个取值
    • MAP_SHARED: 多个进程对相同映射文件共享
    • MAP_PRIVATE: 多个进程对相同映射文件不共享
  • filedes: 文件描述符。文件描述符是内核为了高效管理已经被打开的文件的索引,是一个非负整数,用于指代被打开的文件。文件描述符可以通过系统函数 open()create() 获取,也可以从父进程继承,用于进行文件操作或网络通信。

  • offset: 从文件的什么位置开始映射,必须是页(内存上)大小的整数倍。

2.3.2 返回值

返回值为 void* 指针,因此可以进行各种类型转换,如 int*char* 等。C++ 中不能直接 int* p = mmap(),需要对 mmap() 进行类型转换,如 int* p = (int*)mmap()

2.3.3 需要的头文件

1
2
3
4
5
6
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>

2.4 mmap 实例

2.4.1 简单对文件进行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <cstring>

int main()
{
std::string filename = "./dataset/test.txt";
int len = 12;
// 获取文件描述符
int fd = open(filename.c_str(), O_RDWR);
// 内存映射
auto* p = (char*)mmap(NULL, len, PROT_WRITE | PROT_READ, MAP_SHARED, fd, 0);
// 直接对内存进行修改
for (int i = 0; i < len; i++)
{
p[i] = 'a' + i;
}
// 关闭内存映射
munmap(p, len);
return 0;
}

2.4.2 读取 bvecs 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
std::vector<std::vector<float>> mmap_read_bvecs(std::string &filename)
{
// 获取文件描述符
int fd = open(filename.c_str(), O_RDWR);
// 打开失败
if (fd == -1)
{
perror(("Open \"" + filename + "\" failed. Reason").c_str());
exit(EXIT_FAILURE);
}
// 获取文件信息
struct stat fs;
// 获取失败
if (fstat(fd, &fs) == -1) {
perror("Get file information filed. Reason");
close(fd);
exit(EXIT_FAILURE);
}

// 文件大小
off_t file_size = fs.st_size;
std::cout << "File size: " << file_size << std::endl;

// 内存映射,获得指针
u_char* p = (u_char*)mmap(NULL, file_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// 映射失败
if (p == MAP_FAILED)
{
perror("Memory mapping failed. Reason");
close(fd);
exit(EXIT_FAILURE);
}

// 访问数据
long long byte_count = 0LL;
// 获取 vector number
int num = 0;
while(byte_count < file_size)
{
// vector dimension
int dim;
u_char *q = p + byte_count;
dim = *(int*)q;
q += sizeof(int);
// output vector
// std::cout << "[";
for (int i = 0; i < dim; i++)
{
std::cout << (float)*(q + i) << ((i == dim - 1) ? "]\n" : ", ");
}

byte_count += sizeof(int) + dim * sizeof(u_char);
num += 1;
}

// output vector number
std::cout << "vector num: " << num << std::endl;
// 关闭映射
munmap(p, file_size);
// 关闭文件
close(fd);

// return data;
return {};
}

3. 虚拟内存分配例子

3.1 mmap_read_bvecs

mmap_read_bvecs 函数单独写在一个 cpp 文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#include <iostream>
#include <fstream>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <cstring>
#include <mimalloc.h>
#include <vector>

/**
* @name: mmap_read_bvecs
* @msg: 通过 mmap 内存映射读取 *.bvecs 文件
* @param {string} &filename: 文件名
* @param {void*} &v_mem: 虚拟内存指针
* @param {u_char* &} file_map: 读取大文件时的内存映射的指针
* @param {off_t &} file_size: 被映射的大文件的长度(字节)
* @param {std::vector<std::vector<float>>* &} data: 存储数据
* @return {void}
*/
void mmap_read_bvecs(std::string &filename,
u_char* &file_map, off_t &file_size,
std::vector<std::vector<float>>* &data)
{
// 获取文件描述符
int fd = open(filename.c_str(), O_RDWR);
// 打开失败
if (fd == -1)
{
perror(("Open \"" + filename + "\" failed. Reason").c_str());
exit(EXIT_FAILURE);
}
// 获取文件信息
struct stat fs;
// 获取失败
if (fstat(fd, &fs) == -1) {
perror("Get file information filed. Reason");
close(fd);
exit(EXIT_FAILURE);
}

// 文件大小
file_size = fs.st_size;
std::cout << "File size: " << file_size << std::endl;

// read data 映射,获得指针
file_map = (u_char*)mmap(nullptr, file_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// 映射失败
if (file_map == MAP_FAILED)
{
perror("Memory mapping failed. Reason");
close(fd);
exit(EXIT_FAILURE);
}

// 访问数据
long long byte_count = 0LL;
// vector number
long long num = 0;
std::cout << "Reading..." << std::endl;
while(byte_count < file_size)
{
// vector dimension
int dim;
u_char* q = file_map + byte_count;
dim = *(int*)q;
q += sizeof(int);
// 物理内存内保存单个数据
std::vector<float> vec(dim);
for (int i = 0; i < dim; i++)
{
vec[i] = (float)*(q + i);
}
// 存储到 data 中
data->push_back(std::move(vec));

byte_count += sizeof(int) + dim * sizeof(u_char);
num += 1;
}

// 文件和映射需要统一关闭
close(fd);
}


/**
* @name: release_mmap
* @msg: 释放内存映射
* @param {u_char* &} file_map: 读取大文件时的内存映射的指针
* @param {off_t &} file_size: 被映射的大文件的长度(字节)
* @return {void}
*/
void release_mmap(u_char* &file_map, off_t &file_size)
{
// 关闭 read data 映射
munmap(file_map, file_size);
}

3.2 virtual_mem_test

直接调用 Linux API,手动创建虚拟内存后,在虚拟内存中开辟空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
int main()
{
std::string filename = "./dataset/bigann_learn.bvecs";

// 创建虚拟内存
int fd_v_mem;
off_t mem_size = 1024 * 1024 * 1024 * 65LL;
auto v_mem = create_virtual_mem(mem_size, fd_v_mem);

// 读入数据
u_char *file_map = nullptr;
off_t file_size = 0LL;
// 在虚拟内存中开辟空间存储
// 注意 placement new 表达式的用法
std::vector<std::vector<float>>* data = new (v_mem) std::vector<std::vector<float>>();
mmap_read_bvecs(filename, file_map, file_size, data);

// output
auto vec_num = data->size();
auto vec_dim = (*data)[0].size();
std::cout << "vector number: " << vec_num << std::endl;
std::cout << "vector dimension: " << vec_dim << std::endl;
// for (size_t i = 0; i < vec_num; ++i)
// {
// std::cout << "[";
// for (size_t j = 0; j < vec_dim; ++j)
// {
// std::cout << (*data)[i][j] << ((j == vec_dim - 1) ? "" : ", ");
// }
// std::cout << "]\n";
// }

// 关闭映射和虚拟内存
release_mem(v_mem, mem_size, fd_v_mem, file_map, file_size);

return 0;
}

3.3 boost_interprocess_test

使用 Boost::interprocess 库,共享内存。在磁盘上创建一块空间用于文件映射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <boost/interprocess/managed_mapped_file.hpp>
#include "mmap_read_bvecs.cpp"

namespace bip = boost::interprocess;

int main()
{
// 创建共享内存
std::size_t mem_size = 1024 * 1024 * 1024 * 100LL;
bip::managed_mapped_file file(bip::open_or_create, "./shared_memory.bin", mem_size);

// 在共享内存中分配一个大内存块
std::size_t alloc_size = 1024 * 1024 * 1024 * 80LL;
void* mem_ptr = file.get_segment_manager()->allocate(alloc_size);
if (mem_ptr == nullptr)
{
perror("Memory allocation failed. Reason");
exit(EXIT_FAILURE);
}

// read
std::string filename = "./dataset/bigann_learn.bvecs";
off_t file_size = 0LL;
u_char *file_map = nullptr;
std::vector<std::vector<float>>* data = new (mem_ptr) std::vector<std::vector<float>>();
mmap_read_bvecs(filename, file_map, file_size, data);

// output
auto vec_num = data->size();
auto vec_dim = (*data)[0].size();
std::cout << "vector number: " << vec_num << std::endl;
std::cout << "vector dimension: " << vec_dim << std::endl;

// 释放分配的大内存块
file.get_segment_manager()->deallocate(mem_ptr);
release_mmap(file_map, file_size);

return 0;
}

3.4 mimalloc_test

使用 mimalloc 内存分配器分配虚拟内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include "mmap_read_bvecs.cpp"

int main()
{
std::string filename = "./dataset/bigann_learn.bvecs";

// 分配未对齐内存
size_t mem_size = 1024 * 1024 * 1024 * 65LL;
auto v_mem = mi_malloc(mem_size);

// 读入数据
u_char *file_map = nullptr;
off_t file_size = 0LL;
// 在虚拟内存中开辟空间存储
std::vector<std::vector<float>>* data = new (v_mem) std::vector<std::vector<float>>();
mmap_read_bvecs(filename, file_map, file_size, data);

// output
auto vec_num = data->size();
auto vec_dim = (*data)[0].size();
std::cout << "vector number: " << vec_num << std::endl;
std::cout << "vector dimension: " << vec_dim << std::endl;

// 释放内存
mi_free(v_mem);
mi_stats_print(NULL);
// 手动触发一次垃圾回收
mi_collect(true);

return 0;
}