在Mongodb中,其使用了操作系统底层提供的内存映射机制,即MMAP。MMAP可以把磁盘文件的一部分或全部内容直接映射到内存,这样文件中的信息位置就会在内存中有对应的地址空间,这时对文件的读写可以直接用指针来做,而不需要read/write函数了。同时操作系统会将数据刷新保存到磁盘上。如下图:
      

鉴于linux,window系统为mmap所提供的API大同小异(见下图)。这里仅以mongodb对window系统的mmap调用机制为例,来说明一下其具体的实现方式,以及在mongodb启动时,客户端提交查询和插入操作请求时mongodb的mmap执行流程。
   

上面类图中:
    MongoFile:定义了mongo文件对象常用操作,包括创建,关闭,设置名称,flushAll,获取MongoFile文件总尺寸等。
    MMF: 一个类型定义,其声明:typedef MemoryMappedFile MMF;   
    MongoMMF:为了便于journaling/durability操作,对MemoryMappedFile进行了一些封装(特别是对private views )
       
    下面着重看一下windows提供的mmap的常用API:
    MapViewOfFile(): 把文件数据映射到进程的地址空间
    CreateFileMapping() : 创建一个新的文件映射内核对象
    FlushViewOfFile(): 强制系统将内存中修改过的数据重新写入磁盘映像,从而可以确保所有的数据更新能及时保存到磁盘
    CloseHandle(): 关闭文件映射对象和文件对象
    MapViewOfFileEx(): 将文件映射到指定的进程地址空间

参数说明:

MapViewOfFile(
    __in HANDLE hFileMappingObject,  /*hFileMappingObject是共享文件对象*/
    __in DWORD dwDesiredAccess, /*dwDesiredAccess是文件共享属性*/
    __in DWORD dwFileOffsetHigh, /*dwFileOffsetHigh是文件共享区的偏移地址*/
    __in DWORD dwFileOffsetLow, /*dwFileOffsetLow是文件共享区的偏移地址*/
    __in SIZE_T dwNumberOfBytesToMap /*dwNumberOfBytesToMap是共享数据长度*/
    );
//winbase.h
    CreateFileMappingW(
    __in      HANDLE hFile,   /*hFile是创建共享文件的句柄*/
    __in_opt LPSECURITY_ATTRIBUTES lpFileMappingAttributes, /*lpFileMappingAttributes是文件共享的属性*/
    __in      DWORD flProtect,  /*flProtect是当文件映射时读写文件的属性*/
    __in      DWORD dwMaximumSizeHigh, /*是文件共享的大小高位字节*/
    __in      DWORD dwMaximumSizeLow, /*是文件共享的大小低位字节*/
    __in_opt LPCWSTR lpName /*lpName是共享文件对象名称*/
    );
    #ifdef UNICODE
    #define CreateFileMapping  CreateFileMappingW
    #else
    #define CreateFileMapping  CreateFileMappingA
    #endif // !UNICODE
    FlushViewOfFile(
    __in LPCVOID lpBaseAddress, /*内存映射文件中的视图的一个字节的地址*/
    __in SIZE_T dwNumberOfBytesToFlush /*想要刷新的字节数*/
    );
    MapViewOfFileEx(
    __in HANDLE hFileMappingObject,  /*共享文件对象*/
    __in DWORD dwDesiredAccess, /*文件共享属性*/
    __in DWORD dwFileOffsetHigh, /*文件共享区的偏移地址*/
    __in DWORD dwFileOffsetLow, /*文件共享区的偏移地址*/
    __in SIZE_T dwNumberOfBytesToMap /*共享数据长度*/
    __in_opt LPVOID lpBaseAddress /*指定映射文件映射对象的地址。如这个地址处没有足够的内存空间,
                                    那么对MapViewOfFileEx的调用会失效*/
    );

下面我们看一下mongodb如何使用上述API,来实现windows环境下对mongofile进行mmap操作的.

 //mmap_win.cpp
     mutex mapViewMutex("mapView");//声明mapView的互斥体(mutex)对象
    ourbitset writable;

/** unmapping 通知,以便清空 writable bits */
    void MemoryMappedFile::clearWritableBits(void *p) {
        for( unsigned i = ((size_t)p)/ChunkSize; i <= (((size_t)p)+len)/ChunkSize; i++ ) {
            writable.clear(i);
            assert( !writable.get(i) );
        }
    }

MemoryMappedFile::MemoryMappedFile()
        : _flushMutex(new mutex("flushMutex")) {
        fd = 0;
        maphandle = 0;
        len = 0;
        created();
    }
    //关闭文件MemoryMappedFile
    void MemoryMappedFile::close() {
        for( vector<void*>::iterator i = views.begin(); i != views.end(); i++ ) {
            clearWritableBits(*i);
            UnmapViewOfFile(*i);
        }
        views.clear();
        if ( maphandle )
            CloseHandle(maphandle);//关闭文件映射对象和文件对象
        maphandle = 0;
        if ( fd )
            CloseHandle(fd);//关闭文件映射对象和文件对象
        fd = 0;
    }

unsigned long long mapped = 0;
    //创建只读map
    void* MemoryMappedFile::createReadOnlyMap() {
        assert( maphandle );
        scoped_lock lk(mapViewMutex);
        void *p = MapViewOfFile(maphandle, FILE_MAP_READ, /*f ofs hi*/0, /*f ofs lo*/ 0, /*dwNumberOfBytesToMap 0 means to eof*/0);
        if ( p == 0 ) {
            DWORD e = GetLastError();
            log() << "FILE_MAP_READ MapViewOfFile failed " << filename() << " " << errnoWithDescription(e) << endl;
        }
        else {
            views.push_back(p);
        }
        return p;
    }

//创建指定名称和大小的MapViewOfFile
    void* MemoryMappedFile::map(const char *filenameIn, unsigned long long &length, int options) {
        assert( fd == 0 && len == 0 ); // 仅能打开一次
        setFilename(filenameIn);
        /* big hack here: Babble uses db names with colons.  doesn't seem to work on windows.  temporary perhaps. */
        char filename[256];
        strncpy(filename, filenameIn, 255);
        filename[255] = 0;
        {
            size_t len = strlen( filename );
            for ( size_t i=len-1; i>=0; i-- ) {
                if ( filename[i] == '/' ||
                        filename[i] == '\\' )
                    break;

if ( filename[i] == ':' )
                    filename[i] = '_';
            }
        }

updateLength( filename, length );//如果指定文件已存在,则用已存在的文件长度更新length值
        {
            DWORD createOptions = FILE_ATTRIBUTE_NORMAL;
            if ( options & SEQUENTIAL )
                createOptions |= FILE_FLAG_SEQUENTIAL_SCAN;//针对连续访问对文件缓冲进行优化选项
            DWORD rw = GENERIC_READ | GENERIC_WRITE;//普通读/写
            fd = CreateFile(//创建相关文件
                     toNativeString(filename).c_str(),//创建的文件名称
                     rw, // desired access
                     FILE_SHARE_WRITE | FILE_SHARE_READ, // share mode
                     NULL, // security
                     OPEN_ALWAYS, // create disposition
                     createOptions , // flags
                     NULL); // hTempl
            if ( fd == INVALID_HANDLE_VALUE ) {
                DWORD e = GetLastError();
                log() << "Create/OpenFile failed " << filename << " errno:" << e << endl;
                return 0;
            }
        }

mapped += length;
        {

//采用“读写文件数据”方式的页面保护属性

           DWORD flProtect = PAGE_READWRITE;

//创建一个文件映射内核对象并告诉系统文件的尺寸以及访问文件的方式
            maphandle = CreateFileMapping(fd, NULL, flProtect,
                                          length >> 32 /*maxsizehigh*/,
                                          (unsigned) length /*maxsizelow*/,
                                          NULL/*lpName*/);
            if ( maphandle == NULL ) {

// 先获取操作信息, 因为下面的log()要删除lasterror信息
                DWORD e = GetLastError();
                log() << "CreateFileMapping failed " << filename << ' ' << errnoWithDescription(e) << endl;
                close();
                return 0;
            }
        }

void *view = 0;
        {
            scoped_lock lk(mapViewMutex);
            DWORD access = (options&READONLY)? FILE_MAP_READ : FILE_MAP_ALL_ACCESS;

//把文件数据映射到进程的地址空间
            view = MapViewOfFile(maphandle, access, /*f ofs hi*/0, /*f ofs lo*/ 0, /*dwNumberOfBytesToMap 0 means to eof*/0);
        }
        if ( view == 0 ) {
            DWORD e = GetLastError();
            log() << "MapViewOfFile failed " << filename << " " << errnoWithDescription(e) << endl;
            close();
        }
        else {
            views.push_back(view);
        }
        len = length;

return view;
    }

class WindowsFlushable : public MemoryMappedFile::Flushable {
    public:
        WindowsFlushable( void * view , HANDLE fd , string filename , boost::shared_ptr<mutex> flushMutex )
            : _view(view) , _fd(fd) , _filename(filename) , _flushMutex(flushMutex)
        {}

void flush() {
            if (!_view || !_fd)
                return;

scoped_lock lk(*_flushMutex);
            // 强制系统将内存中修改过的数据重新写入磁盘映像,从而可以确保所有的数据更新能及时保存到磁盘。
            bool success = FlushViewOfFile(_view, 0 /*0表示全部mapping*/);
            if (!success) {
                int err = GetLastError();
                out() << "FlushViewOfFile failed " << err << " file: " << _filename << endl;
            }

success = FlushFileBuffers(_fd);//刷新内部文件缓冲区的数据刷到磁盘上
            if (!success) {
                int err = GetLastError();
                out() << "FlushFileBuffers failed " << err << " file: " << _filename << endl;
            }
        }

void * _view;
        HANDLE _fd;
        string _filename;
        boost::shared_ptr<mutex> _flushMutex;
    };
    //是否进行异步的flush操作(该操作会将修改过的数据部分或全部重新写入磁盘映像)
    void MemoryMappedFile::flush(bool sync) {
     uassert(13056, "Async flushing not supported on windows", sync);//windows系统不支持异步flush
        if( !views.empty() ) {
            WindowsFlushable f( views[0] , fd , filename() , _flushMutex);
            f.flush();
        }
    }
   //预先刷数据操作,该方法确保这个对象是可以执行flush()操作,以便在调用该方法之后执行flush操作.
   //参见mmap.cpp flushAll操作
   MemoryMappedFile::Flushable * MemoryMappedFile::prepareFlush() {
        return new WindowsFlushable( views.empty() ? 0 : views[0] , fd , filename() , _flushMutex );
    }
    void MemoryMappedFile::_lock() {}
    void MemoryMappedFile::_unlock() {}

上面的代码比较简单,大家看一下注释就可以了,下面看一下mmf对于上面的MemoryMappedFile类实现是如何封装的,因为mmf会在journaling/durability这类场景下使用PrivateMap():

   //mongommf.cpp文件
    //构造PrivateMap
    void* MemoryMappedFile::createPrivateMap() {
        assert( maphandle );
        scoped_lock lk(mapViewMutex);
        //void *p = mapaligned(maphandle, len);
        void *p = MapViewOfFile(maphandle, FILE_MAP_READ, 0, 0, 0);
        if ( p == 0 ) {
            DWORD e = GetLastError();
            log() << "createPrivateMap failed " << filename() << " " << errnoWithDescription(e) << endl;
        }
        else {
            clearWritableBits(p);
            views.push_back(p);
        }
        return p;
    }
    //重新映射PrivateView
    void* MemoryMappedFile::remapPrivateView(void *oldPrivateAddr) {
        dbMutex.assertWriteLocked(); // short window where we are unmapped so must be exclusive

// mapViewMutex确保在重新映射时获得相同的地址
        scoped_lock lk(mapViewMutex);
        //清空 writable bits
        clearWritableBits(oldPrivateAddr);
        //从进程的地址空间(oldPrivateAddr)撤消文件数据的映像
        if( !UnmapViewOfFile(oldPrivateAddr) ) {
            DWORD e = GetLastError();
            log() << "UnMapViewOfFile failed " << filename() << ' ' << errnoWithDescription(e) << endl;
            assert(false);
        }

// 将文件映射到指定的进程地址空间
        void *p = MapViewOfFileEx(maphandle, FILE_MAP_READ, 0, 0,
                                  /*dwNumberOfBytesToMap 0 means to eof*/0 /*len*/,
                                  oldPrivateAddr);
        
        if ( p == 0 ) {
            DWORD e = GetLastError();
            log() << "MapViewOfFileEx failed " << filename() << " " << errnoWithDescription(e) << endl;
            assert(p);
        }
        assert(p == oldPrivateAddr);
        return p;
    }
#endif
    //重新映射PrivateView
    void MongoMMF::remapThePrivateView() {
        assert( cmdLine.dur );

// todo 1.9 : it turns out we require that we always remap to the same address.
        // so the remove / add isn't necessary and can be removed
        privateViews.remove(_view_private);
        _view_private = remapPrivateView(_view_private);
        privateViews.add(_view_private, this);
    }
    ......

//打开指定的文件并执行mmap操作
    bool MongoMMF::open(string fname, bool sequentialHint) {
        setPath(fname);
        _view_write = mapWithOptions(fname.c_str(), sequentialHint ? SEQUENTIAL : 0);
        return finishOpening();
    }
    //创建指定名称的文件并执行mmap操作
    bool MongoMMF::create(string fname, unsigned long long& len, bool sequentialHint) {
        setPath(fname);
        _view_write = map(fname.c_str(), len, sequentialHint ? SEQUENTIAL : 0);
        return finishOpening();
    }
    //创建PrivateMap并加载到privateViews集合中
    bool MongoMMF::finishOpening() {
        if( _view_write ) {
            if( cmdLine.dur ) {
                _view_private = createPrivateMap();
                if( _view_private == 0 ) {
                    massert( 13636 , "createPrivateMap failed (look in log for error)" , false );
                }
                privateViews.add(_view_private, this); // note that testIntent builds use this, even though it points to view_write then...
            }
            else {
                _view_private = _view_write;
            }
            return true;
        }
        return false;
    }
    ......
    //从privateViews集合中移除当前 _view_private,并关闭文件映射对象和文件对象
    void MongoMMF::close() {
        {
            if( cmdLine.dur && _view_write/*actually was opened*/ ) {
                if( debug )
                    log() << "closingFileNotication:" << filename() << endl;
                dur::closingFileNotification();
            }
            privateViews.remove(_view_private);
        }
        _view_write = _view_private = 0;
        MemoryMappedFile::close();//关闭文件映射对象和文件对象
    }

mongodb完成了上面的工具类的声明定义之后,就会在前台使用这些类了,下面通过插入数据操作(之前主要流程我已在这篇文章中有所描述)过程中,对上面类的使用来进行阐述.

首先需要说明的是,如果是首次在本地运行mongod,则不会在指定的数据库目录(dbpath 参数)下生成数据库文件,但如果有数据插入时,则会生成相应文件,这里可以理解为生成文件的过程就是mmap的创建过程。
    
    之前的文章中提到过,当客户端要插入记录时,则系统会根据客户端的操作枚举信息来调用相应的操作,这里它会执行instance.cpp文件中的receivedInsert方法,并进而调用 pdfile.cpp 文件的 insert()函数,而在该方法下有如下一段代码:

  DiskLoc DataFileMgr::insert(const char *ns, const void *obuf, int len, bool god, const BSONElement &writeId, bool mayAddIndex) {
  ......
        NamespaceDetails *d = nsdetails(ns);//获取ns的详细信息
        if ( d == 0 ) {
            addNewNamespaceToCatalog(ns);//向system catalog添加新的名空间,它会再次调用当前insert()方法
          
            // 创建第一个数据库文件,方法位于database.cpp
            cc().database()->allocExtent(ns, Extent::initialSize(len), false);
  ......
  }

上面的allocExtent方法用于分配Extent要求的磁盘空间,其中Extent用于记录多个record记录信息,而record就是数据库中的一条记录。这里可以将Extent看成是一个数据集合,但与我们通常所理解的"数据表"(datatable)有所差异,因为在同一个namespace下可以有一个或多个extent(可以不连续),extent之间是一个双向链表结构,其通过cursor进行向前(forward)或反转(reverse)的访问。有关这些内容,参见我之前写的这篇文章。

言归正传,在上面的allocExtent方法中,会执行pdfile.cpp中的如下方法:

   //pdfile.cpp
    Extent* MongoDataFile::createExtent(const char *ns, int approxSize, bool newCapped, int loops) {
        .....
        int ExtentSize = approxSize <= header()->unusedLength ? approxSize : header()->unusedLength;
        DiskLoc loc;
        if ( ExtentSize < Extent::minSize() ) {//判断当前ExtentSize的大小
            ......
            //addAFile方法位于 database.cpp
            return cc().database()->addAFile( 0, true )->createExtent(ns, approxSize, newCapped, loops+1);
        .....

}

最后在addAFile方法中,我们会看下如下代码段:

 //database.cpp
    MongoDataFile* Database::addAFile( int sizeNeeded, bool preallocateNextFile ) {
        int n = (int) files.size();
        MongoDataFile *ret = getFile( n, sizeNeeded );//调用下面的getFile方法
        .....
    }

//database.cpp  
    MongoDataFile* Database::getFile( int n, int sizeNeeded , bool preallocateOnly) {
        ......
        namespaceIndex.init();
        .....
    }
    
    //namespace.cpp  
    void NamespaceIndex::init() {
        ......
        unsigned long long len = 0;
        boost::filesystem::path nsPath = path();
        string pathString = nsPath.string();
        void *p = 0;
        if( MMF::exists(nsPath) ) {//使用本文前面提到的MMF类,判断数据库文件是否存在
            if( f.open(pathString, true) ) {//打开指定的文件并执行mmap操作
                len = f.length();
                if ( len % (1024*1024) != 0 ) {
                    log() << "bad .ns file: " << pathString << endl;
                    uassert( 10079 ,  "bad .ns file length, cannot open database", len % (1024*1024) == 0 );
                }
                p = f.getView();//返回mapview
            }
        }
        else {//不存在
            // use lenForNewNsFiles, we are making a new database
            massert( 10343, "bad lenForNewNsFiles", lenForNewNsFiles >= 1024*1024 );
            maybeMkdir();//创建相应目录(如不存在)
            unsigned long long l = lenForNewNsFiles;
            if( f.create(pathString, l, true) ) {//创建指定名称的文件并执行mmap操作
                getDur().createdFile(pathString, l); // always a new file
                len = l;
                assert( len == lenForNewNsFiles );
                p = f.getView();//返回mapview
            }
        }
        ......
    }

下面用一张时序图来大体回顾一下这一流程:
  

在创建了该数据库文件及相应mmap操作之后,下面再重新启动mongod时,系统会通过构造client类的上下文对象 (context)方法来最终调用namespaceIndex.init()方法,其时序图如下,大家可以通过调试源码来难证这一流程:
  

好了,今天的内容到这里就告一段落。

参考链接:

http://www.cnblogs.com/daizhj/archive/2011/03/30/1999699.html

http://en.wikipedia.org/wiki/Mmap
             http://linux.about.com/library/cmd/blcmdl2_mmap.htm
             http://msdn.microsoft.com/en-us/library/aa366761.aspx
             http://hi.baidu.com/%B2%A4%B2%CB%B1%F9%B1%F9/blog/item/f6e6fb2561c0136a35a80f70.html

http://www.10gen.com/video/mongosv2010/storageengine

原文链接:http://www.cnblogs.com/daizhj/archive/2011/04/25/mongos_mmap_source_code.html

作者: daizhj, 代震军   
    微博: http://t.sina.com.cn/daizhj
    Tags: mongodb,c++,mmap

转载于:https://www.cnblogs.com/daizhj/archive/2011/04/25/mongos_mmap_source_code.html

Mongodb源码分析--内存文件映射(MMAP)相关推荐

  1. v15.03 鸿蒙内核源码分析(内存映射) | 映射真是个好东西 | 百篇博客分析HarmonyOS源码

    子曰:"德不孤,必有邻." <论语>:里仁篇 百篇博客系列篇.本篇为: v15.xx 鸿蒙内核源码分析(内存映射篇) | 映射真是个好东西 内存管理相关篇为: v11. ...

  2. linux内存源码分析 - 内存压缩(同步关系)

    本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 概述 最近在看内存回收,内存回收在进行同步的一些情况非常复杂,然后就想,不会内存压缩的页面迁移过程中的同步关系也 ...

  3. Mongodb源码分析--Replication之主从模式--Master

    mongodb中提供了复制(Replication)机制,通过该机制可以帮助我们很容易实现读写分离方案,并支持灾难恢复(服务器断电)等意外情况下的数据安全. 在老版本(1.6)中,Mongo提供了两种 ...

  4. nginx源码分析—内存池结构ngx_pool_t及内存管理

    本博客( http://blog.csdn.net/livelylittlefish)贴出作者(阿波)相关研究.学习内容所做的笔记,欢迎广大朋友指正! Content 0.序 1.内存池结构 1.1 ...

  5. Nginx源码分析-内存池

    本文转自淘宝平台http://www.tbdata.org/archives/1390,不是为了夺他人之美,只是觉得写得很好,怕淘宝万一删掉就找不到了,放在这里保存一下.大家可以直接链接过去,他们那个 ...

  6. v11.03 鸿蒙内核源码分析(内存分配) | 内存有哪些分配方式 | 百篇博客分析HarmonyOS源码

    子曰:"君子周而不比,小人比而不周."<论语>:为政篇 百篇博客系列篇.本篇为: v11.xx 鸿蒙内核源码分析(内存分配篇) | 内存有哪些分配方式 内存管理相关篇为 ...

  7. Memcached源码分析 - 内存存储机制Slabs(5)

    Memcached源码分析 - 网络模型(1) Memcached源码分析 - 命令解析(2) Memcached源码分析 - 数据存储(3) Memcached源码分析 - 增删改查操作(4) Me ...

  8. Tornado源码分析 --- 静态文件处理模块

    每个web框架都会有对静态文件的处理支持,下面对于Tornado的静态文件的处理模块的源码进行分析,以加强自己对静态文件处理的理解. 先从Tornado的主要模块 web.py 入手,可以看到在App ...

  9. Python3.5源码分析-内存管理

    Python3源码分析 本文环境python3.5.2. 参考书籍<<Python源码剖析>> python官网 Python3的内存管理概述 python提供了对内存的垃圾收 ...

  10. Android UIL图片加载缓存源码分析-内存缓存

    本篇文章我们来分析一下著名图片加载库Android-Universal-Image-Loader的图片缓存源码. 源码环境 版本:V1.9.5 GitHub链接地址:https://github.co ...

最新文章

  1. Java枚举类使用方式
  2. python 搭配 及目录结构
  3. 岗位内推 | 深睿医疗实验室招聘机器学习实习生
  4. ubuntu20.04安裝迅雷
  5. Machine Learning System Design的一道题
  6. UEFI 引导与 BIOS 引导
  7. 令人超赞的ButterKnife8.8.1—快捷、方面、好用!你还在等什么
  8. dsp2812软件周期耗时测试,基于DSP2812设计的简易数字频率计
  9. 应用程序无响应的原因
  10. java监听鼠标双击_java鼠标双击事件 java鼠标双击监听
  11. aliyun的产品都是有哪些,主要是做什么的呢?
  12. 微星GS65 英雄联盟崩溃
  13. vue使用vue-ueditor-wrap集成秀米
  14. 【Collaborative Perception - 2】V2X-ViT(ECCV2022)
  15. FWT(快速沃尔什变换)零基础详解qaq(ACM/OI)
  16. 崩坏3九游服务器稳定吗,为什么崩坏三萌新入坑推荐选择官服而非渠道服,盘点几个必要的理由...
  17. (一)路径规划算法---Astar与C++可视化在RVIZ的二维栅格地图
  18. 计算机开启休眠模式,win7怎么开启休眠模式
  19. Word转Latex:基于Python的半自动化排版
  20. 关于携程多玩对接-订单对接

热门文章

  1. spring 整合 JUnit(spring 内单元测试类调用带注入的报空指针异常NullPointException)
  2. Java面试题超详细讲解系列之四【Jvm篇】
  3. 34. Differentiate between inheritance of interface and inheritance ofimplementations
  4. android 自定义园动画,如何在Android中绘制一个带有动画的圆
  5. zookeeper 进入客户端_探究—Zookeeper的启动过程
  6. python判断图片类型_python模块之imghdr检测图片类型
  7. .net session 有效时间_【面试题】|干货!.NET C# 简答题Part 07
  8. arcgis 将栅格值提取到点_ArcGIS教程:值提取至点 (空间分析)
  9. python钓鱼网站_Python+MySQL获取PhishTank的钓鱼网站列表作业笔记
  10. java实现用户分组,java实现分组算法,根据每组多少人来进行分组