mmap 在进程的虚拟地址空间开辟一块区域,这块区域映射文件在磁盘上的物理地址,是将内存地址空间映射到磁盘地址空间的一种方法
读/写操作访问虚拟地址空间这一段映射地址,通过查询页表发现这一段地址并不在物理页面上(因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中),因此引发缺页异常,内核发起请求调页过程。调页过程先在交换缓存空间(swap cache)中寻找需要访问的内存页,如果没有则调用 nopage 函数把所缺的页从磁盘装入到主存中。
之后如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程;修改过的脏页面并不会立即更新回文件中,而是有一段时间的延迟,可以调用 msync() 来强制同步, 这样所写的内容就能立即保存到文件里了
它的优点有:
MMKV 使用 MemoryFile 包装 mmap 相关逻辑
使用 mmap 需要注意的一个关键点是,mmap 映射区域大小必须是物理页大小(page_size)的整倍数(32位系统中通常是4k字节)。原因是内存的最小粒度是页,而进程虚拟地址空间和内存的映射也是以页为单位。为了匹配内存的操作,mmap 从磁盘到虚拟地址空间的映射也必须是页。
class MemoryFile {
File m_diskFile; // 磁盘上的文件
void *m_ptr; // 映射到文件物理地址的区域(在内存地址空间里),它的起始地址
size_t m_size; // 内存空间区域的大小
}
using MMKVFileHandle_t = HANDLE;
using MMKVPath_t = std::wstring;
class File {
MMKVPath_t m_path; // 文件路径
MMKVFileHandle_t m_fd; // 打开的文件描述符
}
// 通过系统调用 open 打开文件拿到文件描述符 fd,并用系统调用 fstat 拿到文件大小,然后 mmap 这整个文件获得映射区域的内存地址
using MMKVPath_t = std::wstring;
MemoryFile::MemoryFile(MMKVPath_t path) : m_diskFile(std::move(path), OpenFlag::ReadWrite | OpenFlag::Create), m_ptr(nullptr), m_size(0) {
reloadFromFile();
}
void MemoryFile::reloadFromFile() {
if (!m_diskFile.open()) {
MMKVError("fail to open:%s, %s", m_diskFile.m_path.c_str(), strerror(errno));
} else {
FileLock fileLock(m_diskFile.m_fd);
InterProcessLock lock(&fileLock, ExclusiveLockType);
SCOPED_LOCK(&lock);
mmkv::getFileSize(m_diskFile.m_fd, m_size);
if (m_size < DEFAULT_MMAP_SIZE || (m_size % DEFAULT_MMAP_SIZE != 0)) { // 确保文件大小是内存页大小的整数倍
size_t roundSize = ((m_size / DEFAULT_MMAP_SIZE) + 1) * DEFAULT_MMAP_SIZE;
truncate(roundSize);
} else {
auto ret = mmap();
if (!ret) {
doCleanMemoryCache(true);
}
}
}
}
bool File::open() {
if (isFileValid()) {
return true;
}
m_fd = ::open(m_path.c_str(), OpenFlag2NativeFlag(m_flag), S_IRWXU);
if (!isFileValid()) {
MMKVError("fail to open [%s], %d(%s)", m_path.c_str(), errno, strerror(errno));
return false;
}
MMKVInfo("open fd[%p], %s", m_fd, m_path.c_str());
return true;
}
bool getFileSize(int fd, size_t &size) {
struct stat st = {};
if (fstat(fd, &st) != -1) {
size = (size_t) st.st_size;
return true;
}
return false;
}
bool MemoryFile::mmap() {
m_ptr = (char *) ::mmap(m_ptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_diskFile.m_fd, 0);
if (m_ptr == MAP_FAILED) {
MMKVError("fail to mmap [%s], %s", m_diskFile.m_path.c_str(), strerror(errno));
m_ptr = nullptr;
return false;
}
return true;
}
在 深入 SharedPreferences:架构、缺点和优化 研究过 SharedPreferences 本质上是内存中的 HashMap 和磁盘上的 XML 文件,Java HashMap 提供了 CURD Api,持久化时序列化为 XML 格式,本质上是以字符串存储
但在 MMKV 里面对的是一整块内存区域,怎么对这块区域进行 CURD 操作呢?怎么实现 Key-Value Mapping 呢?
在内存空间里,MMKV 使用 std::unordered_map 这一数据结构实现 Key-Value Mapping,而 Key-Value 对的内容则是以一种很紧凑的格式存储在 mmap 开辟的内存区域
[(key-size)(key-data)(value-size)(value-data)][(key-size)(key-data)(value-size)(value-data)]...
因为 mmap 内存区域是对文件物理地址的映射,所以持久化在磁盘上的格式也是上面这种紧凑格式