mremap 不会扩展过去一页的大小
mremap will not expand past size of one page
我正在创建一个模板 class,其中包含一个行为类似于 std::vector
的动态数组,但底层数组存储在共享内存中,以便它可以在进程之间共享。目前,没有内置同步,只是努力让内存映射调整大小按预期工作,代码是一个最小的例子来显示错误的来源
我遇到的问题是,一旦我调整内存大小超过一页大小,如果我尝试访问第二页,我会收到 SIGBUS
错误。如果我使用 mmap
分配更大的页面大小,例如我已经尝试了最多 1 MB,它会很好地分配该数量,但如果我尝试将该映射调整得更大,我将再次获得 SIGBUS
如果我尝试访问超过映射的原始边界,则会出错
template<typename T>
class DistributedVector {
private:
T* create_shared_memory(size_t size) {
int protection = PROT_READ | PROT_WRITE;
int visibility = MAP_SHARED;
m_shm_fd = memfd_create("shm", 0);
ftruncate(m_shm_fd, size);
void* result = mmap(nullptr, size * sizeof(T), protection, visibility, m_shm_fd, 0);
if (result == MAP_FAILED){
std::cerr << "Mapping failed!\n";
}
file_size = lseek(m_shm_fd, 0, SEEK_END);
return (T*)result;
}
void resize_shared_memory(T* addr, size_t size, size_t new_size){
auto temp = create_shared_memory(new_size);
if (temp == MAP_FAILED) {
std::cerr << "Mapping failed!\n";
}
memcpy(temp, m_begin, size*sizeof(T));
munmap(m_begin, size);
m_begin = temp;
m_end = m_begin + size;
m_end_cap = m_begin + new_size;
}
public:
DistributedVector() {
m_begin = create_shared_memory(INITIAL_VEC_CAPACITY);
m_end_cap = m_begin + INITIAL_VEC_CAPACITY;
m_end = m_begin;
}
size_t size() {
return m_end - m_begin;
}
size_t capacity() {
return m_end_cap - m_begin;
}
T* data(){
return m_begin;
}
T at(size_t index){
if (index < (m_end - m_begin)) {
return *(m_begin+index);
} else {
return -1;
}
}
T push_back(T ele){
if (m_end == m_end_cap){
resize_shared_memory(m_begin,capacity(), 2*capacity());
}
*m_end = ele;
m_end++;
return *(m_end-1);
}
private:
T* m_begin;
T* m_end;
T* m_end_cap;
int m_shm_fd;
size_t file_size;
};
运行本次测试:
std::vector<int> v;
dtl::DistributedVector<int> d;
assert(v.size() == d.size());
for(int i = 0; i < 20000000; ++i){
v.push_back(i);
d.push_back(i);
auto v_i = v.at(i);
auto d_i = d.at(i);
assert(v_i == d_i);
if (i % 100 == 0)
std::cout << v_i << ":" << d_i << '\n';
}
std::cout << '\n';
assert(v.size() == d.size());
在文档中没有看到任何关于 mremap
页面大小限制的内容,如果我用 mmap
手动分配一个新的内存部分,复制数据,然后 munmap
旧页面,它工作正常。 mremap
中是否有我忽略的内容?
编辑:更新代码以使用 memfd_create
、ftruncate
,并通过 mmap
新内存区域和 munmap
旧区域调整大小,以及现在的代码按预期工作
我认为不可能像您期望的那样完成这项工作。
假设您有两个进程共享初始内存映射。
当决定重新映射以加倍容量时,如果 mremap()
必须移动 (MREMAP_MAYMOVE
) 地址,因为之后没有足够的连续虚拟地址 space 可用,则该过程这决定了这个调整大小操作为这个更大的映射获取新的虚拟地址。
但是,将更大的映射移动到新地址的相同问题也可能出现在其他进程中;没有什么能保证它的一侧有足够的连续虚拟地址 space。
另一个进程怎么会知道它的旧映射变得无效并且应该使用新映射呢?
我不知道为什么这不会立即失败(MAP_FAILED
),但是如果你想让你的共享容器在许多进程中增长,你肯定需要一个应用协议(具有适当的同步)来通知每个进程在这些过程中,必须考虑一些新的映射。
请注意,问题与 mmap()
/copy/munmap()
.
相同
顺便说一下,push_back()
末尾的 return *m_end;
是错误的,因为它是 past-the-end(甚至超过了调整大小之前的内存区域)。
虽然我没有直接回答为什么您的代码无法重新映射内存(可能是 MAP_ANONYMOUS
+ MAP_SHARED
+ remap
不太有效),但您有您的代码中存在更大的问题,当您修复此问题时,您也会修复您的 mremap
。
最大的问题是mremap
将执行内存区域的按位复制,留下旧数据。对于模板化向量,这是不可接受的。除非你的 T 既可复制又可破坏,否则你必须做法向量所做的事情(从不使用 realloc
)。
您需要分配一个全新的适当大小的共享内存段,将元素从前一个段复制(或移动,如果不可抛出可移动)到另一个段,而不是破坏旧段中的元素。
说了这么多,你为什么要自己做向量还不是很明显class。为什么不使用带共享内存分配器的标准向量?
mremap
无法调整共享和匿名映射的大小。
每个虚拟地址区域都有一个与之关联的可选文件对象,用于获取新页面和写回旧的修改页面。由于匿名映射没有这样的文件,所以当你缩小它时没有地方可以存储页面,当你展开它时也没有地方可以得到更多的页面。当线程通过映射访问超出范围的文件时,内核生成 SIGBUS
- 当文件在映射后收缩时发生。
理论上内核可以做一些事情来让它工作,但到目前为止它还没有,它仍然会导致这种映射的行为方式以及跟踪该映射的使用部分的复杂性问题。
您需要提供一个文件来支持您的映射。要调整它的大小,您必须先调整文件大小,然后重新映射映射。如果您不想在磁盘上保留文件,请使用 memfd_create
在内存中创建匿名文件 - 这样您还可以获得一个 fd
可以通过 Unix 套接字发送到另一个进程。
我正在创建一个模板 class,其中包含一个行为类似于 std::vector
的动态数组,但底层数组存储在共享内存中,以便它可以在进程之间共享。目前,没有内置同步,只是努力让内存映射调整大小按预期工作,代码是一个最小的例子来显示错误的来源
我遇到的问题是,一旦我调整内存大小超过一页大小,如果我尝试访问第二页,我会收到 SIGBUS
错误。如果我使用 mmap
分配更大的页面大小,例如我已经尝试了最多 1 MB,它会很好地分配该数量,但如果我尝试将该映射调整得更大,我将再次获得 SIGBUS
如果我尝试访问超过映射的原始边界,则会出错
template<typename T>
class DistributedVector {
private:
T* create_shared_memory(size_t size) {
int protection = PROT_READ | PROT_WRITE;
int visibility = MAP_SHARED;
m_shm_fd = memfd_create("shm", 0);
ftruncate(m_shm_fd, size);
void* result = mmap(nullptr, size * sizeof(T), protection, visibility, m_shm_fd, 0);
if (result == MAP_FAILED){
std::cerr << "Mapping failed!\n";
}
file_size = lseek(m_shm_fd, 0, SEEK_END);
return (T*)result;
}
void resize_shared_memory(T* addr, size_t size, size_t new_size){
auto temp = create_shared_memory(new_size);
if (temp == MAP_FAILED) {
std::cerr << "Mapping failed!\n";
}
memcpy(temp, m_begin, size*sizeof(T));
munmap(m_begin, size);
m_begin = temp;
m_end = m_begin + size;
m_end_cap = m_begin + new_size;
}
public:
DistributedVector() {
m_begin = create_shared_memory(INITIAL_VEC_CAPACITY);
m_end_cap = m_begin + INITIAL_VEC_CAPACITY;
m_end = m_begin;
}
size_t size() {
return m_end - m_begin;
}
size_t capacity() {
return m_end_cap - m_begin;
}
T* data(){
return m_begin;
}
T at(size_t index){
if (index < (m_end - m_begin)) {
return *(m_begin+index);
} else {
return -1;
}
}
T push_back(T ele){
if (m_end == m_end_cap){
resize_shared_memory(m_begin,capacity(), 2*capacity());
}
*m_end = ele;
m_end++;
return *(m_end-1);
}
private:
T* m_begin;
T* m_end;
T* m_end_cap;
int m_shm_fd;
size_t file_size;
};
运行本次测试:
std::vector<int> v;
dtl::DistributedVector<int> d;
assert(v.size() == d.size());
for(int i = 0; i < 20000000; ++i){
v.push_back(i);
d.push_back(i);
auto v_i = v.at(i);
auto d_i = d.at(i);
assert(v_i == d_i);
if (i % 100 == 0)
std::cout << v_i << ":" << d_i << '\n';
}
std::cout << '\n';
assert(v.size() == d.size());
在文档中没有看到任何关于 mremap
页面大小限制的内容,如果我用 mmap
手动分配一个新的内存部分,复制数据,然后 munmap
旧页面,它工作正常。 mremap
中是否有我忽略的内容?
编辑:更新代码以使用 memfd_create
、ftruncate
,并通过 mmap
新内存区域和 munmap
旧区域调整大小,以及现在的代码按预期工作
我认为不可能像您期望的那样完成这项工作。
假设您有两个进程共享初始内存映射。
当决定重新映射以加倍容量时,如果 mremap()
必须移动 (MREMAP_MAYMOVE
) 地址,因为之后没有足够的连续虚拟地址 space 可用,则该过程这决定了这个调整大小操作为这个更大的映射获取新的虚拟地址。
但是,将更大的映射移动到新地址的相同问题也可能出现在其他进程中;没有什么能保证它的一侧有足够的连续虚拟地址 space。
另一个进程怎么会知道它的旧映射变得无效并且应该使用新映射呢?
我不知道为什么这不会立即失败(MAP_FAILED
),但是如果你想让你的共享容器在许多进程中增长,你肯定需要一个应用协议(具有适当的同步)来通知每个进程在这些过程中,必须考虑一些新的映射。
请注意,问题与 mmap()
/copy/munmap()
.
顺便说一下,push_back()
末尾的 return *m_end;
是错误的,因为它是 past-the-end(甚至超过了调整大小之前的内存区域)。
虽然我没有直接回答为什么您的代码无法重新映射内存(可能是 MAP_ANONYMOUS
+ MAP_SHARED
+ remap
不太有效),但您有您的代码中存在更大的问题,当您修复此问题时,您也会修复您的 mremap
。
最大的问题是mremap
将执行内存区域的按位复制,留下旧数据。对于模板化向量,这是不可接受的。除非你的 T 既可复制又可破坏,否则你必须做法向量所做的事情(从不使用 realloc
)。
您需要分配一个全新的适当大小的共享内存段,将元素从前一个段复制(或移动,如果不可抛出可移动)到另一个段,而不是破坏旧段中的元素。
说了这么多,你为什么要自己做向量还不是很明显class。为什么不使用带共享内存分配器的标准向量?
mremap
无法调整共享和匿名映射的大小。
每个虚拟地址区域都有一个与之关联的可选文件对象,用于获取新页面和写回旧的修改页面。由于匿名映射没有这样的文件,所以当你缩小它时没有地方可以存储页面,当你展开它时也没有地方可以得到更多的页面。当线程通过映射访问超出范围的文件时,内核生成 SIGBUS
- 当文件在映射后收缩时发生。
理论上内核可以做一些事情来让它工作,但到目前为止它还没有,它仍然会导致这种映射的行为方式以及跟踪该映射的使用部分的复杂性问题。
您需要提供一个文件来支持您的映射。要调整它的大小,您必须先调整文件大小,然后重新映射映射。如果您不想在磁盘上保留文件,请使用 memfd_create
在内存中创建匿名文件 - 这样您还可以获得一个 fd
可以通过 Unix 套接字发送到另一个进程。