Mongodb源码分析--内存文件映射(MMAP)

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

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

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

上面类图中:

  1. MongoFile:定义了mongo文件对象常用操作,包括创建,关闭,设置名称,flushAll,获取MongoFile文件总尺寸等。
  2. MMF: 一个类型定义,其声明:typedef MemoryMappedFile MMF;    
  3. MongoMMF:为了便于journaling/durability操作,对MemoryMappedFile进行了一些封装(特别是对private views )

下面着重看一下windows提供的mmap的常用API:

  1. MapViewOfFile(): 把文件数据映射到进程的地址空间
  2. CreateFileMapping() : 创建一个新的文件映射内核对象 
  3. FlushViewOfFile(): 强制系统将内存中修改过的数据重新写入磁盘映像,从而可以确保所有的数据更新能及时保存到磁盘
  4. CloseHandle(): 关闭文件映射对象和文件对象
  5. MapViewOfFileEx(): 将文件映射到指定的进程地址空间

参数说明:   

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

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

  1. //mmap_win.cpp  
  2.     mutex mapViewMutex("mapView");//声明mapView的互斥体(mutex)对象  
  3.    ourbitset writable;  
  4.  
  5.    /** unmapping 通知,以便清空 writable bits */ 
  6.    void MemoryMappedFile::clearWritableBits(void *p) {  
  7.        for( unsigned i = ((size_t)p)/ChunkSize; i <= (((size_t)p)+len)/ChunkSize; i++ ) {  
  8.            writable.clear(i);  
  9.            assert( !writable.get(i) );  
  10.        }  
  11.    }  
  12.  
  13.    MemoryMappedFile::MemoryMappedFile()  
  14.        : _flushMutex(new mutex("flushMutex")) {  
  15.        fd = 0;  
  16.        maphandle = 0;  
  17.        len = 0;  
  18.        created();  
  19.    }  
  20.    //关闭文件MemoryMappedFile  
  21.    void MemoryMappedFile::close() {  
  22.        for( vector<void*>::iterator i = views.begin(); i != views.end(); i++ ) {  
  23.            clearWritableBits(*i);  
  24.            UnmapViewOfFile(*i);  
  25.        }  
  26.        views.clear();  
  27.        if ( maphandle )  
  28.            CloseHandle(maphandle);//关闭文件映射对象和文件对象  
  29.        maphandle = 0;  
  30.        if ( fd )  
  31.            CloseHandle(fd);//关闭文件映射对象和文件对象  
  32.        fd = 0;  
  33.    }  
  34.  
  35.    unsigned long long mapped = 0;  
  36.    //创建只读map  
  37.    void* MemoryMappedFile::createReadOnlyMap() {  
  38.        assert( maphandle );  
  39.        scoped_lock lk(mapViewMutex);  
  40.        void *p = MapViewOfFile(maphandle, FILE_MAP_READ, /*f ofs hi*/0, /*f ofs lo*/ 0, /*dwNumberOfBytesToMap 0 means to eof*/0);  
  41.        if ( p == 0 ) {  
  42.            DWORD e = GetLastError();  
  43.            log() << "FILE_MAP_READ MapViewOfFile failed " << filename() << " " << errnoWithDescription(e) << endl;  
  44.        }  
  45.        else {  
  46.            views.push_back(p);  
  47.        }  
  48.        return p;  
  49.    }  
  50.  
  51.    //创建指定名称和大小的MapViewOfFile  
  52.    void* MemoryMappedFile::map(const char *filenameIn, unsigned long long &length, int options) {  
  53.        assert( fd == 0 && len == 0 ); // 仅能打开一次  
  54.        setFilename(filenameIn);  
  55.        /* big hack here: Babble uses db names with colons.  doesn't seem to work on windows.  temporary perhaps. */ 
  56.        char filename[256];  
  57.        strncpy(filename, filenameIn, 255);  
  58.        filename[255] = 0;  
  59.        {  
  60.            size_t len = strlen( filename );  
  61.            for ( size_t i=len-1; i>=0; i-- ) {  
  62.                if ( filename[i] == '/' ||  
  63.                        filename[i] == '\\' )  
  64.                    break;  
  65.  
  66.                if ( filename[i] == ':' )  
  67.                    filename[i] = '_';  
  68.            }  
  69.        }  
  70.  
  71.        updateLength( filename, length );//如果指定文件已存在,则用已存在的文件长度更新length值  
  72.        {  
  73.            DWORD createOptions = FILE_ATTRIBUTE_NORMAL;  
  74.            if ( options & SEQUENTIAL )  
  75.                createOptions |= FILE_FLAG_SEQUENTIAL_SCAN;//针对连续访问对文件缓冲进行优化选项  
  76.            DWORD rw = GENERIC_READ | GENERIC_WRITE;//普通读/写  
  77.            fd = CreateFile(//创建相关文件  
  78.                     toNativeString(filename).c_str(),//创建的文件名称  
  79.                     rw, // desired access  
  80.                     FILE_SHARE_WRITE | FILE_SHARE_READ, // share mode  
  81.                     NULL, // security  
  82.                     OPEN_ALWAYS, // create disposition  
  83.                     createOptions , // flags  
  84.                     NULL); // hTempl  
  85.            if ( fd == INVALID_HANDLE_VALUE ) {  
  86.                DWORD e = GetLastError();  
  87.                log() << "Create/OpenFile failed " << filename << " errno:" << e << endl;  
  88.                return 0;  
  89.            }  
  90.        }  
  91.  
  92.        mapped += length;  
  93.        {  
  94.  
  95.           //采用“读写文件数据”方式的页面保护属性             
  96.  
  97.           DWORD flProtect = PAGE_READWRITE;  
  98.  
  99.            //创建一个文件映射内核对象并告诉系统文件的尺寸以及访问文件的方式  
  100.            maphandle = CreateFileMapping(fd, NULL, flProtect,  
  101.                                          length >> 32 /*maxsizehigh*/,  
  102.                                          (unsigned) length /*maxsizelow*/,  
  103.                                          NULL/*lpName*/);  
  104.            if ( maphandle == NULL ) {  
  105.  
  106.               // 先获取操作信息, 因为下面的log()要删除lasterror信息  
  107.                DWORD e = GetLastError();  
  108.                log() << "CreateFileMapping failed " << filename << ' ' << errnoWithDescription(e) << endl;  
  109.                close();  
  110.                return 0;  
  111.            }  
  112.        }  
  113.  
  114.        void *view = 0;  
  115.        {  
  116.            scoped_lock lk(mapViewMutex);  
  117.            DWORD access = (options&READONLY)? FILE_MAP_READ : FILE_MAP_ALL_ACCESS;  
  118.  
  119.            //把文件数据映射到进程的地址空间  
  120.            view = MapViewOfFile(maphandle, access, /*f ofs hi*/0, /*f ofs lo*/ 0, /*dwNumberOfBytesToMap 0 means to eof*/0);  
  121.        }  
  122.        if ( view == 0 ) {  
  123.            DWORD e = GetLastError();  
  124.            log() << "MapViewOfFile failed " << filename << " " << errnoWithDescription(e) << endl;  
  125.            close();  
  126.        }  
  127.        else {  
  128.            views.push_back(view);  
  129.        }  
  130.        len = length;  
  131.  
  132.        return view;  
  133.    }  
  134.  
  135.    class WindowsFlushable : public MemoryMappedFile::Flushable {  
  136.    public:  
  137.        WindowsFlushable( void * view , HANDLE fd , string filename , boost::shared_ptr<mutex> flushMutex )  
  138.            : _view(view) , _fd(fd) , _filename(filename) , _flushMutex(flushMutex)  
  139.        {}  
  140.  
  141.        void flush() {  
  142.            if (!_view || !_fd)  
  143.                return;  
  144.  
  145.            scoped_lock lk(*_flushMutex);  
  146.            // 强制系统将内存中修改过的数据重新写入磁盘映像,从而可以确保所有的数据更新能及时保存到磁盘。  
  147.            bool success = FlushViewOfFile(_view, 0 /*0表示全部mapping*/);  
  148.            if (!success) {  
  149.                int err = GetLastError();  
  150.                out() << "FlushViewOfFile failed " << err << " file: " << _filename << endl;  
  151.            }  
  152.  
  153.            success = FlushFileBuffers(_fd);//刷新内部文件缓冲区的数据刷到磁盘上  
  154.            if (!success) {  
  155.                int err = GetLastError();  
  156.                out() << "FlushFileBuffers failed " << err << " file: " << _filename << endl;  
  157.            }  
  158.        }  
  159.  
  160.        void * _view;  
  161.        HANDLE _fd;  
  162.        string _filename;  
  163.        boost::shared_ptr<mutex> _flushMutex;  
  164.    };  
  165.    //是否进行异步的flush操作(该操作会将修改过的数据部分或全部重新写入磁盘映像)  
  166.    void MemoryMappedFile::flush(bool sync) {  
  167.     uassert(13056, "Async flushing not supported on windows", sync);//windows系统不支持异步flush  
  168.        if( !views.empty() ) {  
  169.            WindowsFlushable f( views[0] , fd , filename() , _flushMutex);  
  170.            f.flush();  
  171.        }  
  172.    }  
  173.   //预先刷数据操作,该方法确保这个对象是可以执行flush()操作,以便在调用该方法之后执行flush操作.  
  174.   //参见mmap.cpp flushAll操作  
  175.   MemoryMappedFile::Flushable * MemoryMappedFile::prepareFlush() {  
  176.        return new WindowsFlushable( views.empty() ? 0 : views[0] , fd , filename() , _flushMutex );  
  177.    }  
  178.    void MemoryMappedFile::_lock() {}  
  179.    void MemoryMappedFile::_unlock() {} 

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

  1.    //mongommf.cpp文件  
  2.     //构造PrivateMap  
  3.     void* MemoryMappedFile::createPrivateMap() {  
  4.         assert( maphandle );  
  5.         scoped_lock lk(mapViewMutex);  
  6.         //void *p = mapaligned(maphandle, len);  
  7.         void *p = MapViewOfFile(maphandle, FILE_MAP_READ, 0, 0, 0);  
  8.         if ( p == 0 ) {  
  9.             DWORD e = GetLastError();  
  10.             log() << "createPrivateMap failed " << filename() << " " << errnoWithDescription(e) << endl;  
  11.         }  
  12.         else {  
  13.             clearWritableBits(p);  
  14.             views.push_back(p);  
  15.         }  
  16.         return p;  
  17.     }  
  18.     //重新映射PrivateView  
  19.     void* MemoryMappedFile::remapPrivateView(void *oldPrivateAddr) {  
  20.         dbMutex.assertWriteLocked(); // short window where we are unmapped so must be exclusive  
  21.  
  22.         // mapViewMutex确保在重新映射时获得相同的地址  
  23.         scoped_lock lk(mapViewMutex);  
  24.         //清空 writable bits  
  25.         clearWritableBits(oldPrivateAddr);  
  26.         //从进程的地址空间(oldPrivateAddr)撤消文件数据的映像  
  27.         if( !UnmapViewOfFile(oldPrivateAddr) ) {  
  28.             DWORD e = GetLastError();  
  29.             log() << "UnMapViewOfFile failed " << filename() << ' ' << errnoWithDescription(e) << endl;  
  30.             assert(false);  
  31.         }  
  32.  
  33.         // 将文件映射到指定的进程地址空间  
  34.         void *p = MapViewOfFileEx(maphandle, FILE_MAP_READ, 0, 0,  
  35.                                   /*dwNumberOfBytesToMap 0 means to eof*//*len*/,  
  36.                                   oldPrivateAddr);  
  37.           
  38.         if ( p == 0 ) {  
  39.             DWORD e = GetLastError();  
  40.             log() << "MapViewOfFileEx failed " << filename() << " " << errnoWithDescription(e) << endl;  
  41.             assert(p);  
  42.         }  
  43.         assert(p == oldPrivateAddr);  
  44.         return p;  
  45.     }  
  46. #endif  
  47.     //重新映射PrivateView  
  48.     void MongoMMF::remapThePrivateView() {  
  49.         assert( cmdLine.dur );  
  50.  
  51.         // todo 1.9 : it turns out we require that we always remap to the same address.  
  52.         // so the remove / add isn't necessary and can be removed  
  53.         privateViews.remove(_view_private);  
  54.         _view_private = remapPrivateView(_view_private);  
  55.         privateViews.add(_view_private, this);  
  56.     }  
  57.     ......  
  58.  
  59.     //打开指定的文件并执行mmap操作  
  60.     bool MongoMMF::open(string fname, bool sequentialHint) {  
  61.         setPath(fname);  
  62.         _view_write = mapWithOptions(fname.c_str(), sequentialHint ? SEQUENTIAL : 0);  
  63.         return finishOpening();  
  64.     }  
  65.     //创建指定名称的文件并执行mmap操作  
  66.     bool MongoMMF::create(string fname, unsigned long long& len, bool sequentialHint) {  
  67.         setPath(fname);  
  68.         _view_write = map(fname.c_str(), len, sequentialHint ? SEQUENTIAL : 0);  
  69.         return finishOpening();  
  70.     }  
  71.     //创建PrivateMap并加载到privateViews集合中  
  72.     bool MongoMMF::finishOpening() {  
  73.         if( _view_write ) {  
  74.             if( cmdLine.dur ) {  
  75.                 _view_private = createPrivateMap();  
  76.                 if( _view_private == 0 ) {  
  77.                     massert( 13636 , "createPrivateMap failed (look in log for error)" , false );  
  78.                 }  
  79.                 privateViews.add(_view_private, this); // note that testIntent builds use this, even though it points to view_write then...  
  80.             }  
  81.             else {  
  82.                 _view_private = _view_write;  
  83.             }  
  84.             return true;  
  85.         }  
  86.         return false;  
  87.     }  
  88.     ......  
  89.     //从privateViews集合中移除当前 _view_private,并关闭文件映射对象和文件对象  
  90.     void MongoMMF::close() {  
  91.         {  
  92.             if( cmdLine.dur && _view_write/*actually was opened*/ ) {  
  93.                 if( debug )  
  94.                     log() << "closingFileNotication:" << filename() << endl;  
  95.                 dur::closingFileNotification();  
  96.             }  
  97.             privateViews.remove(_view_private);  
  98.         }  
  99.         _view_write = _view_private = 0;  
  100.         MemoryMappedFile::close();//关闭文件映射对象和文件对象  
  101.     } 

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

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

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

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

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

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

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

  1. //database.cpp  
  2.    MongoDataFile* Database::addAFile( int sizeNeeded, bool preallocateNextFile ) {  
  3.        int n = (int) files.size();  
  4.        MongoDataFile *ret = getFile( n, sizeNeeded );//调用下面的getFile方法  
  5.        .....  
  6.    }  
  7.  
  8.    //database.cpp    
  9.    MongoDataFile* Database::getFile( int n, int sizeNeeded , bool preallocateOnly) {  
  10.        ......  
  11.        namespaceIndex.init();  
  12.        .....  
  13.    }  
  14.      
  15.    //namespace.cpp    
  16.    void NamespaceIndex::init() {  
  17.        ......  
  18.        unsigned long long len = 0;  
  19.        boost::filesystem::path nsPath = path();  
  20.        string pathString = nsPath.string();  
  21.        void *p = 0;  
  22.        if( MMF::exists(nsPath) ) {//使用本文前面提到的MMF类,判断数据库文件是否存在  
  23.            if( f.open(pathString, true) ) {//打开指定的文件并执行mmap操作  
  24.                len = f.length();  
  25.                if ( len % (1024*1024) != 0 ) {  
  26.                    log() << "bad .ns file: " << pathString << endl;  
  27.                    uassert( 10079 ,  "bad .ns file length, cannot open database", len % (1024*1024) == 0 );  
  28.                }  
  29.                p = f.getView();//返回mapview  
  30.            }  
  31.        }  
  32.        else {//不存在  
  33.            // use lenForNewNsFiles, we are making a new database  
  34.            massert( 10343, "bad lenForNewNsFiles", lenForNewNsFiles >= 1024*1024 );  
  35.            maybeMkdir();//创建相应目录(如不存在)  
  36.            unsigned long long l = lenForNewNsFiles;  
  37.            if( f.create(pathString, l, true) ) {//创建指定名称的文件并执行mmap操作  
  38.                getDur().createdFile(pathString, l); // always a new file  
  39.                len = l;  
  40.                assert( len == lenForNewNsFiles );  
  41.                p = f.getView();//返回mapview  
  42.            }  
  43.        }  
  44.        ......  
  45.    } 

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

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

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

参考链接:

  1. http://www.cnblogs.com/daizhj/archive/2011/03/30/1999699.html
  2. http://en.wikipedia.org/wiki/Mmap
  3. http://linux.about.com/library/cmd/blcmdl2_mmap.htm
  4. http://msdn.microsoft.com/en-us/library/aa366761.aspx
  5. http://hi.baidu.com/%B2%A4%B2%CB%B1%F9%B1%F9/blog/item/f6e6fb2561c0136a35a80f70.html            

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

【编辑推荐】

  1. 走进MongoDB的世界 展开MongoDB的学习之旅
  2. 浅析Mongodb源码之游标Cursor
  3. 野心勃勃的NoSQL新贵 MongoDB应用实战
  4. MongoDB与CouchDB全方位对比
  5. MongoDB1.8发布,分布式文档数据库

 

责任编辑:艾婧 来源: 博客园
相关推荐

2023-03-01 10:37:51

2021-11-11 05:00:02

JavaMmap内存

2014-07-28 11:20:20

mmap虚拟映射Linux

2021-12-03 16:20:26

鸿蒙HarmonyOS应用

2011-05-26 10:05:48

MongoDB

2009-07-24 10:00:38

.NET 4.0内存映

2011-05-26 16:18:51

Mongodb

2011-08-16 09:34:34

Nginx

2011-04-29 13:40:37

MongoDBCommand

2012-06-20 14:16:36

Java内存映射

2021-12-03 16:22:05

鸿蒙HarmonyOS应用

2009-08-13 14:21:04

.NET内存映射文件

2020-10-09 07:13:11

Linux系统编程mmap

2015-11-16 11:22:05

Java对象内存分配

2021-04-27 13:56:49

内存.映射地址

2023-08-03 07:30:01

JavaNIO库

2021-12-02 15:08:23

鸿蒙HarmonyOS应用

2012-05-10 09:44:39

MongoDBJava

2021-04-23 20:59:02

ThreadLocal内存

2016-01-13 09:19:27

点赞
收藏

51CTO技术栈公众号