微信MMKV源码分析(二) | mmap映射

2,490 阅读3分钟

系列文章:

微信MMKV源码阅读随笔

微信MMKV源码分析(一) | 整体流程

加载文件

void MMKV::loadFromFile() {
    // 匿名内存的加载,本章不深入分析
    if (m_isAshmem) {
        loadFromAshmem();
        return;
    }

    m_metaInfo.read(m_metaFile.getMemory());

    /* O_RDWR:读、写打开
     * O_CREAT:若此文件不存在则创建它。使用此选择项时,需同时说明第三个参数mode,用其说明该新文件的存取许可权位。
     * S_IRWXU:模式标志:由用户读,写,执行。
     */
    m_fd = open(m_path.c_str(), O_RDWR | O_CREAT, S_IRWXU);
    if (m_fd < 0) {
        MMKVError("fail to open:%s, %s", m_path.c_str(), strerror(errno));
    } else {
        m_size = 0;
        struct stat st = {0};
        // 读取文件的大小
        if (fstat(m_fd, &st) != -1) {
            m_size = static_cast<size_t>(st.st_size);
        }
        // 对齐操作,mmap的使用要求
        // round up to (n * pagesize)
        if (m_size < DEFAULT_MMAP_SIZE || (m_size % DEFAULT_MMAP_SIZE != 0)) {
            size_t oldSize = m_size;
            m_size = ((m_size / DEFAULT_MMAP_SIZE) + 1) * DEFAULT_MMAP_SIZE;
            if (ftruncate(m_fd, m_size) != 0) {
                MMKVError("fail to truncate [%s] to size %zu, %s", m_mmapID.c_str(), m_size,
                          strerror(errno));
                m_size = static_cast<size_t>(st.st_size);
            }
            zeroFillFile(m_fd, oldSize, m_size - oldSize);
        }
        // MMKV的核心之一,使用mmap函数的MAP_SHARED来实现文件和内存形成映射,只要修改内存的数据,这个函数会自动的帮我们写到文件里,非常好用。
        m_ptr = (char *) mmap(nullptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
        if (m_ptr == MAP_FAILED) {
            MMKVError("fail to mmap [%s], %s", m_mmapID.c_str(), strerror(errno));
        } else {
            // 读取现在文件里数据的长度
            memcpy(&m_actualSize, m_ptr, Fixed32Size);
            MMKVInfo("loading [%s] with %zu size in total, file size is %zu", m_mmapID.c_str(),
                     m_actualSize, m_size);
            bool loaded = false;
            if (m_actualSize > 0) {
                if (m_actualSize < m_size && m_actualSize + Fixed32Size <= m_size) {
                    // 检查数据的有效性,MMKV的WIKI上说道微信每天都几十万次校验不通过的情况,恐怖如斯
                    if (checkFileCRCValid()) {
                        MMKVInfo("loading [%s] with crc %u sequence %u", m_mmapID.c_str(),
                                 m_metaInfo.m_crcDigest, m_metaInfo.m_sequence);
                        // 读取数据到内存里
                        MMBuffer inputBuffer(m_ptr + Fixed32Size, m_actualSize, MMBufferNoCopy);
                        // 如果是加密的话,得先解密
                        if (m_crypter) {
                            decryptBuffer(*m_crypter, inputBuffer);
                        }
                        // 将内存的数据反序列化到Map里,m_dic是个Map
                        m_dic = MiniPBCoder::decodeMap(inputBuffer);
                        m_output = new CodedOutputData(m_ptr + Fixed32Size + m_actualSize,
                                                       m_size - Fixed32Size - m_actualSize);
                        loaded = true;
                    }
                }
            }
            if (!loaded) {
                SCOPEDLOCK(m_exclusiveProcessLock);

                if (m_actualSize > 0) {
                    writeAcutalSize(0);
                }
                m_output = new CodedOutputData(m_ptr + Fixed32Size, m_size - Fixed32Size);
                recaculateCRCDigest();
            }
            MMKVInfo("loaded [%s] with %zu values", m_mmapID.c_str(), m_dic.size());
        }
    }

    if (!isFileValid()) {
        MMKVWarning("[%s] file not valid", m_mmapID.c_str());
    }

    m_needLoadFromFile = false;
}

参数解释

mmap函数是MMKV的干货之一了,如果没有这个函数的存在,或许就没有今天的MMKV了,下面说下这个函数的参数和使用方法。

mmap (一种内存映射文件的方法)

mmap将一个文件或者其它对象映射进内存。文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会清零。mmap在用户空间映射调用系统中作用很大。

头文件 <sys/mman.h>

函数原型

void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);

  • start:映射区的开始地址。设置null即可。
  • length:映射区的长度。传入文件对齐后的大小m_size。
  • prot:期望的内存保护标志,不能与文件的打开模式冲突。设置可读可写。
  • flags:指定映射对象的类型,映射选项和映射页是否可以共享。设置MAP_SHARED表示可进程共享,MMKV之所以可以实现跨进程使用,这里是关键。
  • fd:有效的文件描述词。用上面所打开的m_fd。
  • off_toffset:被映射对象内容的起点。从头开始,比较好理解。

内存重组

在跨进程读写的时候,进程A修改了一个数据,进程B去读的时候,就会校验内存的数据和文件的数据,一旦不相同,就说明有了跨进程的操作,这个时候就需要内存重组,清空原有数据,重新读最新的文件映射到内存中。

void MMKV::checkLoadData() {
    // 检查是否已经加载过数据
    if (m_needLoadFromFile) {
        SCOPEDLOCK(m_sharedProcessLock);

        m_needLoadFromFile = false;
        loadFromFile();
        return;
    }
    if (!m_isInterProcess) {
        return;
    }

    // TODO: atomic lock m_metaFile?
    MMKVMetaInfo metaInfo;
    // 读取文件的状态
    metaInfo.read(m_metaFile.getMemory());
    // 对比文件和内存的读写操作次数,次数不一样,说明跨进程操作了,清空下原数据,再加载新数据
    if (m_metaInfo.m_sequence != metaInfo.m_sequence) {
        MMKVInfo("[%s] oldSeq %u, newSeq %u", m_mmapID.c_str(), m_metaInfo.m_sequence,
                 metaInfo.m_sequence);
        SCOPEDLOCK(m_sharedProcessLock);

        clearMemoryState();
        loadFromFile();
    }
    // 比较下crc校验码
    else if (m_metaInfo.m_crcDigest != metaInfo.m_crcDigest) {
        MMKVDebug("[%s] oldCrc %u, newCrc %u", m_mmapID.c_str(), m_metaInfo.m_crcDigest,
                  metaInfo.m_crcDigest);
        SCOPEDLOCK(m_sharedProcessLock);

        size_t fileSize = 0;
        if (m_isAshmem) {
            fileSize = m_size;
        } else {
            struct stat st = {0};
            if (fstat(m_fd, &st) != -1) {
                fileSize = (size_t) st.st_size;
            }
        }
        if (m_size != fileSize) {
            MMKVInfo("file size has changed [%s] from %zu to %zu", m_mmapID.c_str(), m_size,
                     fileSize);
            clearMemoryState();
            loadFromFile();
        } else {
            partialLoadFromFile();
        }
    }
}

内存重组涉及到跨进程操作,跨进程的原理在后续文章再详细讲解。

关于CRC32校验,看《CRC32加密算法原理》

如果本文对您有用的话,记得点一个赞哦!