LMDB-基础结构与Mmap思想

2月9日学习手册

ACID记录

  • Atomicity(原子性):一个事务(transaction)中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。即,事务不可分割、不可约简。
  • Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束触发器级联回滚等。
  • Isolation(隔离性):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括未提交读(Read uncommitted)、提交读(read committed)、可重复读(repeatable read)和串行化(Serializable)。
  • Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

整体架构

image.png

LMDB(Lighting Memory DB)是在BerkeleyDB的基础上进行改编具有高效、紧凑、健壮性的数据库。它文件结构简单,一个文件夹,里面一个数据文件,一个锁文件。数据随意复制,随意传输。它的访问简单,不需要运行单独的数据库管理进程,只要在访问数据的代码里引用LMDB库,访问时给文件路径即可。

lmdb的基本做法是使用mmap文件映射,不管这个文件存储实在内存上还是在持久存储上。lmdb的所有读取操作都是通过mmap将要访问的文件只读的映射到虚拟内存中,直接访问相应的地址.因为使用了read-only的mmap,同样避免了程序错误将存储结构写坏的风险。并且IO的调度由操作系统的页调度机制完成。

而写操作,则是通过write系统调用进行的,这主要是为了利用操作系统的文件系统一致性,避免在被访问的地址上进行同步。

设计上使用了mmap和COW技术,因此整体架构比较简单,没有其他数据库的缓存管理、日志管理、外存管理等组件。

mmap文件映射是基础,lmdb通过只读文件映射(默认)避免了因为应用程序bug导致数据库被破坏的情形。其上的一些基础结构比如Locktables,MVCC,COW等都是实现事务控制的基础,通过这些理论基础,lmdb实现了完整的ACI属性,D通过mmap实现。最后,系统对外提供了关于B+Tree的操作方式,利用cursor游标进行。可以进行增删改查。

B+Tree的所有变动方式与其他的实现类似,不过lmdb的实现基础是append only B+Tree

下面简单的介绍一下该数据库的基本情况:

1 数据库在运行时需要创建一个环境mdb_env_open(),我们需要传入目录路径,并在目录下产生一个锁文件以及存储文件。

2 当环境创建完成后,我们便需要创建mdb_txn_begin()来创建事物,而该事物在同一个时间只能有一个线程来执行。

3 我们可以调用mdb_dbi_open()来打开已有的数据库。

4 使用mdb_get() 与mdb_put()操作键值对,而该KV通常会被表示为MDB_val结构,该结构有两个域,包括mv_size以及mv_data

typedef struct MDB_val {
    size_t       mv_size;   /**< size of the data item */
    void        *mv_data;   /**< address of the data item */
} MDB_val;

由于LMDB使用0拷贝技术,且直接将数据攻disk映射到Memory中。

5 为了提升操作的执行效率,LMDB使用了游标机制,其能存储、获取或删除多个值。

6 LMDB使用了POSIX文件锁,当一个进程多次调用打开函数打开同一个文件的时候会产生问题。所以,LMDB对所有线程共享打开环境。

7 LMDB必须使用mdb_txn_commit()将事务提交,否则所有的操作均被丢弃。对于读操作来说,所有的游标不会被自动释放而在读写事务中所有游标被自动释放且无法复用。

LMDB允许多个读操作同时进行而只允许一次写。一旦一个读写事务开始时,其他的操作将会被阻塞直到第一个进程完成。

8 mdb_get()与 mdb_put() 对于一个key对于多个value的情况,只会返回第一个value。

当需要一个key多值时,那么需要对MDB_DUPSORT进行设置

9 如果用户频繁开始或者结束读操作,那么LMDB会选择reset而不是清除重建。

10 对于读操作,结束后必须调用函数关闭游标并清除;

LMDB源码中涉及的数据结构

LMDB的C版本源码只有四个非常关键的文件

image.png

首先我们需要学习一下LMDB的环境结构体MDB_env。

struct MDB_env {
    HANDLE      me_fd;      /**< The main data file */
    HANDLE      me_lfd;     /**< The lock file */
    HANDLE      me_mfd;     /**< For writing and syncing the meta pages */
    /** Failed to update the meta page. Probably an I/O error. */
#define MDB_FATAL_ERROR 0x80000000U
    /** Some fields are initialized. */
#define MDB_ENV_ACTIVE  0x20000000U
    /** me_txkey is set */
#define MDB_ENV_TXKEY   0x10000000U
    /** fdatasync is unreliable */
#define MDB_FSYNCONLY   0x08000000U

    uint32_t    me_flags;       /**< @ref mdb_env */
    unsigned int    me_psize;   /**< DB page size, inited from me_os_psize */
    unsigned int    me_os_psize;    /**< OS page size, from #GET_PAGESIZE */
    unsigned int    me_maxreaders;  /**< size of the reader table */
    /** Max #MDB_txninfo.%mti_numreaders of interest to #mdb_env_close() */
    volatile int    me_close_readers;
    MDB_dbi     me_numdbs;      /**< number of DBs opened */
    MDB_dbi     me_maxdbs;      /**< size of the DB table */
    MDB_PID_T   me_pid;     /**< process ID of this env */
    char        *me_path;       /**< path to the DB files */
    char        *me_map;        /**< the memory map of the data file */
    MDB_meta    *me_metas[NUM_METAS];   /**< pointers to the two meta pages */
    // 元数据列表,lmdb使用两个页面作为meta页面,因此其大小为2. meta页面的一个主要作用是
    // 用于保存B+Tree的root_page指针。其内部采用COW技术
    /*
      root page指针可能会被修改,因此使用两个不同的页面进行切换保存最新页面,
      类似于double-buffer设计。由此可知,虽然lmdb支持一个文件中多个B+Tree,
      由于meta页面的限制,其个数是有限的。
    */
    void        *me_pbuf;       /**< scratch area for DUPSORT put() */
    MDB_txninfo *me_txns;       /**< the memory map of the lock file or NULL */
    MDB_txn     *me_txn;        /**< current write transaction */
    /*
      me_txn,me_txns:目前环境中使用的事务列表,一个env对象归属于一个进程,一个进程
      可能有多个线程使用同一个env,每个线程可以开启一个事务,因此在一
      个进程级的env对象需要维护txn列表以了解目前多少个线程及事务在进行工作。
    */

    MDB_txn     *me_txn0;       /**< prealloc'd write transaction */
    size_t      me_mapsize;     /**< size of the data memory map */
    off_t       me_size;        /**< current file size */
    pgno_t      me_maxpg;       /**< me_mapsize / me_psize */
    MDB_dbx     *me_dbxs;       /**< array of static DB info */
    uint16_t    *me_dbflags;    /**< array of flags from MDB_db.md_flags */
    unsigned int    *me_dbiseqs;    /**< array of dbi sequence numbers */
    pthread_key_t   me_txkey;   /**< thread-key for readers */
    txnid_t     me_pgoldest;    /**< ID of oldest reader last time we looked */
    MDB_pgstate me_pgstate;     /**< state of old pages from freeDB */
#   define      me_pglast   me_pgstate.mf_pglast
#   define      me_pghead   me_pgstate.mf_pghead
    MDB_page    *me_dpages;     /**< list of malloc'd blocks for re-use */
    /** IDL of pages that became unused in a write txn */
    MDB_IDL     me_free_pgs;
    /** ID2L of pages written during a write txn. Length MDB_IDL_UM_SIZE. */
    // 可用页面,可用页面用于控制MVCC导致的文件大小膨胀,
    // 可用页面是指已经没有事务使用但是已经被修改,根据MVCC原理,其已经是旧版本的页面。


    /*
        对于需要查阅历史数据的数据库来说,比如说需要恢复到任意时刻的要求,
        所有的旧版本应该被保存,而对于只需要保持最新一致数据的数据库系统比
        如lmdb来说,这些页面是可以重用的,页面重用就可以有效避免物理文件的
        无限增大。free_pgs为当前写事务导致的可重用页面列表。
    */

    MDB_ID2L    me_dirty_list;
    /** Max number of freelist items that can fit in a single overflow page */
    // 脏页列表,是写事务已经修改过的但没有提交到物理文件中的所有页面列表。
    int         me_maxfree_1pg;
    /** Max size of a node on a page */
    unsigned int    me_nodemax;
#if !(MDB_MAXKEYSIZE)
    unsigned int    me_maxkey;  /**< max size of a key */
#endif
    int     me_live_reader;     /**< have liveness lock in reader table */
#ifdef _WIN32
    int     me_pidquery;        /**< Used in OpenProcess */
#endif
#ifdef MDB_USE_POSIX_MUTEX  /* Posix mutexes reside in shared mem */
#   define      me_rmutex   me_txns->mti_rmutex /**< Shared reader lock */
#   define      me_wmutex   me_txns->mti_wmutex /**< Shared writer lock */
#else
    mdb_mutex_t me_rmutex;
    mdb_mutex_t me_wmutex;
    /*
        锁表互斥所,lmdb可以支持多线程、多进程。多进程之间的同步访问通过系统级的互斥来达到。
        其mutex本身存在于系统的共享内存当中而非进程本身的内存,因此在进行读写页面时,首先访
        问锁表看看对应的资源是否有别的进程、线程在进行,有的话需要根据事务规则要求进行排队等待。
    */
#endif
    void        *me_userctx;     /**< User-settable context */
    // 用户数据,用户上下文数据,主要用于进行key比较时进行辅助。
    MDB_assert_func *me_assert_func; /**< Callback for assertion failures */
};

下面是MDB_envinfo结构,其中存储的是LMDB环境的信息:

/** @brief Information about the environment */
typedef struct MDB_envinfo {
    void    *me_mapaddr;            /**< Address of map, if fixed */
    size_t  me_mapsize;             /**< Size of the data memory map */
    size_t  me_last_pgno;           /**< ID of the last used page */
    size_t  me_last_txnid;          /**< ID of the last committed transaction */
    unsigned int me_maxreaders;     /**< max reader slots in the environment */
    unsigned int me_numreaders;     /**< max reader slots used in the environment */
} MDB_envinfo;

下面我们看一下数据库的Meta页情况:

其中主要关注MDB_db mm_dbs[CORE_DBS];以及uint32_t mm_version;

typedef struct MDB_meta {
        /** Stamp identifying this as an LMDB file. It must be set
         *  to #MDB_MAGIC. */
    uint32_t    mm_magic;
        /** Version number of this file. Must be set to #MDB_DATA_VERSION. */
    uint32_t    mm_version;
    // mm_version: 当前lock文件的version,是实现MVCC的重要成员,必须设置为MDB_DATA_VERSION.
    void        *mm_address;        /**< address for fixed mapping */
    size_t      mm_mapsize;         /**< size of mmap region */
    MDB_db      mm_dbs[CORE_DBS];   /**< first is free space, 2nd is main db */
    // mm_dbs: 数据库B+Tree根,同时保存两个,0为目前使用的可替代的root page指针,1为当前使用的主数据库。
    /** The size of pages used in this DB */
#define mm_psize    mm_dbs[FREE_DBI].md_pad
    /** Any persistent environment flags. @ref mdb_env */
#define mm_flags    mm_dbs[FREE_DBI].md_flags
    /** Last used page in the datafile.
     *  Actually the file may be shorter if the freeDB lists the final pages.
     */
    pgno_t      mm_last_pg;
    volatile txnid_t    mm_txnid;   /**< txnid that committed this page */
} MDB_meta;

下面是MDB_page函数:

/** Common header for all page types. The page type depends on #mp_flags.
 *
 * #P_BRANCH and #P_LEAF pages have unsorted '#MDB_node's at the end, with
 * sorted #mp_ptrs[] entries referring to them. Exception: #P_LEAF2 pages
 * omit mp_ptrs and pack sorted #MDB_DUPFIXED values after the page header.
 *
 * #P_OVERFLOW records occupy one or more contiguous pages where only the
 * first has a page header. They hold the real data of #F_BIGDATA nodes.
 *
 * #P_SUBP sub-pages are small leaf "pages" with duplicate data.
 * A node with flag #F_DUPDATA but not #F_SUBDATA contains a sub-page.
 * (Duplicate data can also go in sub-databases, which use normal pages.)
 *
 * #P_META pages contain #MDB_meta, the start point of an LMDB snapshot.
 *
 * Each non-metapage up to #MDB_meta.%mm_last_pg is reachable exactly once
 * in the snapshot: Either used by a database or listed in a freeDB record.
 */
typedef struct MDB_page {
#define mp_pgno mp_p.p_pgno
#define mp_next mp_p.p_next
    union {
        pgno_t      p_pgno; /**< page number */
        struct MDB_page *p_next; /**< for in-memory list of freed pages */
    } mp_p;
    uint16_t    mp_pad;         /**< key size if this is a LEAF2 page */
/** @defgroup mdb_page  Page Flags
 *  @ingroup internal
 *  Flags for the page headers.
 *  @{
 */
#define P_BRANCH     0x01       /**< branch page */
#define P_LEAF       0x02       /**< leaf page */
#define P_OVERFLOW   0x04       /**< overflow page */
#define P_META       0x08       /**< meta page */
#define P_DIRTY      0x10       /**< dirty page, also set for #P_SUBP pages */
#define P_LEAF2      0x20       /**< for #MDB_DUPFIXED records */
#define P_SUBP       0x40       /**< for #MDB_DUPSORT sub-pages */
#define P_LOOSE      0x4000     /**< page was dirtied then freed, can be reused */
#define P_KEEP       0x8000     /**< leave this page alone during spill */
/** @} */
    uint16_t    mp_flags;       /**< @ref mdb_page */
#define mp_lower    mp_pb.pb.pb_lower
#define mp_upper    mp_pb.pb.pb_upper
#define mp_pages    mp_pb.pb_pages
    union {
        struct {
            indx_t      pb_lower;       /**< lower bound of free space */
            indx_t      pb_upper;       /**< upper bound of free space */
        } pb;
        uint32_t    pb_pages;   /**< number of overflow pages */
    } mp_pb;
    indx_t      mp_ptrs[1];     /**< dynamic size */
} MDB_page;

page描述了不同页面的头。不管是树中的root、还是branch、leaf页面,都是用它描述。

对于overflow页面来说,只有第一页使用头进行描述,其后的连续页面不使用,仅仅使用指针。

下面看MDB_node结构:

typedef struct MDB_node {
    /** part of data size or pgno
     *  @{ */
#if BYTE_ORDER == LITTLE_ENDIAN
    unsigned short  mn_lo, mn_hi;
#else
    unsigned short  mn_hi, mn_lo;
#endif
    /** @} */
/** @defgroup mdb_node Node Flags
 *  @ingroup internal
 *  Flags for node headers.
 *  @{
 */
#define F_BIGDATA    0x01           /**< data put on overflow page */
#define F_SUBDATA    0x02           /**< data is a sub-database */
#define F_DUPDATA    0x04           /**< data has duplicates */

/** valid flags for #mdb_node_add() */
#define NODE_ADD_FLAGS  (F_DUPDATA|F_SUBDATA|MDB_RESERVE|MDB_APPEND)

/** @} */
    unsigned short  mn_flags;       /**< @ref mdb_node */
    unsigned short  mn_ksize;       /**< key size */
    char        mn_data[1];         /**< key and data are appended here */
} MDB_node;

KV数据对被此数据结构管理,mn_lo, mn_hi用来表示数据的大小。

    /** Information about a single database in the environment. */
typedef struct MDB_db {
    uint32_t    md_pad;     /**< also ksize for LEAF2 pages */
    uint16_t    md_flags;   /**< @ref mdb_dbi_open */
    uint16_t    md_depth;   /**< depth of this tree */
    pgno_t      md_branch_pages;    /**< number of internal pages */
    pgno_t      md_leaf_pages;      /**< number of leaf pages */
    pgno_t      md_overflow_pages;  /**< number of overflow pages */
    size_t      md_entries;     /**< number of data items */
    pgno_t      md_root;        /**< the root page of this tree */
} MDB_db;

MDB_db表示数据库中的一个b+树。

最后是游标结构:

struct MDB_cursor {
    /** Next cursor on this DB in this txn */
    MDB_cursor  *mc_next;
    /** Backup of the original cursor if this cursor is a shadow */
    MDB_cursor  *mc_backup;
    /** Context used for databases with #MDB_DUPSORT, otherwise NULL */
    struct MDB_xcursor  *mc_xcursor;
    /** The transaction that owns this cursor */
    MDB_txn     *mc_txn;
    /** The database handle this cursor operates on */
    MDB_dbi     mc_dbi;
    /** The database record for this cursor */
    MDB_db      *mc_db;
    /** The database auxiliary record for this cursor */
    MDB_dbx     *mc_dbx;
    /** The @ref mt_dbflag for this database */
    unsigned char   *mc_dbflag;
    unsigned short  mc_snum;    /**< number of pushed pages */
    unsigned short  mc_top;     /**< index of top page, normally mc_snum-1 */
/** @defgroup mdb_cursor    Cursor Flags
 *  @ingroup internal
 *  Cursor state flags.
 *  @{
 */
#define C_INITIALIZED   0x01    /**< cursor has been initialized and is valid */
#define C_EOF   0x02            /**< No more data */
#define C_SUB   0x04            /**< Cursor is a sub-cursor */
#define C_DEL   0x08            /**< last op was a cursor_del */
#define C_UNTRACK   0x40        /**< Un-track cursor when closing */
/** @} */
    unsigned int    mc_flags;   /**< @ref mdb_cursor */
    MDB_page    *mc_pg[CURSOR_STACK];   /**< stack of pushed pages */
    indx_t      mc_ki[CURSOR_STACK];    /**< stack of page indices */
};

该结构对于DB所有操作均适用,其保存了页指针与键的索引

mc_next: 同一个事务中关于同一个db的游标组成一个列表。next指向下一个游标

mc_top: 最上层页面id

mc_xcursor: 用于key可重复b+tree。

mc_pg: cursor打开的页面组成一个堆栈,最多32个,具体作用还有待探查。

mc_ki: 所有打开页面的索引

最后是事务结构:

    /** A database transaction.
     *  Every operation requires a transaction handle.
     */
struct MDB_txn {
    MDB_txn     *mt_parent;     /**< parent of a nested txn */
    /** Nested txn under this txn, set together with flag #MDB_TXN_HAS_CHILD */
    MDB_txn     *mt_child;
    pgno_t      mt_next_pgno;   /**< next unallocated page */
    /** The ID of this transaction. IDs are integers incrementing from 1.
     *  Only committed write transactions increment the ID. If a transaction
     *  aborts, the ID may be re-used by the next writer.
     */
    txnid_t     mt_txnid;
    MDB_env     *mt_env;        /**< the DB environment */
    /** The list of pages that became unused during this transaction.
     */
    MDB_IDL     mt_free_pgs;
    /** The list of loose pages that became unused and may be reused
     *  in this transaction, linked through #NEXT_LOOSE_PAGE(page).
     */
    MDB_page    *mt_loose_pgs;
    /** Number of loose pages (#mt_loose_pgs) */
    int         mt_loose_count;
    /** The sorted list of dirty pages we temporarily wrote to disk
     *  because the dirty list was full. page numbers in here are
     *  shifted left by 1, deleted slots have the LSB set.
     */
    MDB_IDL     mt_spill_pgs;
    union {
        /** For write txns: Modified pages. Sorted when not MDB_WRITEMAP. */
        MDB_ID2L    dirty_list;
        /** For read txns: This thread/txn's reader table slot, or NULL. */
        MDB_reader  *reader;
    } mt_u;
    /** Array of records for each DB known in the environment. */
    MDB_dbx     *mt_dbxs;
    /** Array of MDB_db records for each known DB */
    MDB_db      *mt_dbs;
    /** Array of sequence numbers for each DB handle */
    unsigned int    *mt_dbiseqs;
/** @defgroup mt_dbflag Transaction DB Flags
 *  @ingroup internal
 * @{
 */
#define DB_DIRTY    0x01        /**< DB was written in this txn */
#define DB_STALE    0x02        /**< Named-DB record is older than txnID */
#define DB_NEW      0x04        /**< Named-DB handle opened in this txn */
#define DB_VALID    0x08        /**< DB handle is valid, see also #MDB_VALID */
#define DB_USRVALID 0x10        /**< As #DB_VALID, but not set for #FREE_DBI */
#define DB_DUPDATA  0x20        /**< DB is #MDB_DUPSORT data */
/** @} */
    /** In write txns, array of cursors for each DB */
    MDB_cursor  **mt_cursors;
    /** Array of flags for each DB */
    unsigned char   *mt_dbflags;
    /** Number of DB records in use, or 0 when the txn is finished.
     *  This number only ever increments until the txn finishes; we
     *  don't decrement it when individual DB handles are closed.
     */
    MDB_dbi     mt_numdbs;

/** @defgroup mdb_txn   Transaction Flags
 *  @ingroup internal
 *  @{
 */
    /** #mdb_txn_begin() flags */
#define MDB_TXN_BEGIN_FLAGS MDB_RDONLY
#define MDB_TXN_RDONLY      MDB_RDONLY  /**< read-only transaction */
    /* internal txn flags */
#define MDB_TXN_WRITEMAP    MDB_WRITEMAP    /**< copy of #MDB_env flag in writers */
#define MDB_TXN_FINISHED    0x01        /**< txn is finished or never began */
#define MDB_TXN_ERROR       0x02        /**< txn is unusable after an error */
#define MDB_TXN_DIRTY       0x04        /**< must write, even if dirty list is empty */
#define MDB_TXN_SPILLS      0x08        /**< txn or a parent has spilled pages */
#define MDB_TXN_HAS_CHILD   0x10        /**< txn has an #MDB_txn.%mt_child */
    /** most operations on the txn are currently illegal */
#define MDB_TXN_BLOCKED     (MDB_TXN_FINISHED|MDB_TXN_ERROR|MDB_TXN_HAS_CHILD)
/** @} */
    unsigned int    mt_flags;       /**< @ref mdb_txn */
    /** #dirty_list room: Array size - \#dirty pages visible to this txn.
     *  Includes ancestor txns' dirty pages not hidden by other txns'
     *  dirty/spilled pages. Thus commit(nested txn) has room to merge
     *  dirty_list into mt_parent after freeing hidden mt_parent pages.
     */
    unsigned int    mt_dirty_room;
};

LMDB中的Mmap

我们先从mdb_put()函数开始进行。

int mdb_put(MDB_txn *txn, MDB_dbi dbi, MDB_val *key, MDB_val *data, unsigned int flags);

该函数需要用户传入key与value,并传入模式。而这个模式包括:

MDB_NODUPDATA:只能在key没有被插入的情况下进行;
MDB_NOOVERWRITE:同上,但是最后会将指针指向已经存在的那个数据;
MDB_RESERVE:为给定的数据保留一个空间大小,并返回指针指向这个空间
MDB_APPEND:以追加的形式将数据放到DB,如果数据本身在插入时是有序的,那么这个会加速操作

在mdb.c文件中,我们能看到具体的函数实现细节:

int
mdb_put(MDB_txn *txn, MDB_dbi dbi,
    MDB_val *key, MDB_val *data, unsigned int flags)
{
    MDB_cursor mc;
    MDB_xcursor mx;
    int rc;

    if (!key || !data || !TXN_DBI_EXIST(txn, dbi, DB_USRVALID))
        return EINVAL;

    if (flags & ~(MDB_NOOVERWRITE|MDB_NODUPDATA|MDB_RESERVE|MDB_APPEND|MDB_APPENDDUP))
        return EINVAL;

    if (txn->mt_flags & (MDB_TXN_RDONLY|MDB_TXN_BLOCKED))
        return (txn->mt_flags & MDB_TXN_RDONLY) ? EACCES : MDB_BAD_TXN;

    mdb_cursor_init(&mc, txn, dbi, &mx);
    mc.mc_next = txn->mt_cursors[dbi];
    txn->mt_cursors[dbi] = &mc;
    rc = mdb_cursor_put(&mc, key, data, flags);
    txn->mt_cursors[dbi] = mc.mc_next;
    return rc;
}

之后进行mdb_cursor_put操作。该方法在lmdb.h中定义。

该函数有500行,我们选择重点进行分析。

1 开始均是错误检测,包括传入数据是否为空,flag是否是错的等等。

暂时放这里

在LMDB中的Mmap介绍

普通文件IO中所有的文件内容的读取(无论一开始是命中页缓存还是没有命中页缓存)最终都是直接来源于页缓存。当将数据通过缺页中断从磁盘复制到页缓存之后,还要将页缓冲的数据通过CPU复制到read调用提供的缓冲区中。这样,必须通过两次数据拷贝过程,才能完成用户进程对文件内容的获取任务。

内存映射就是把物理内存映射到进程的地址空间之内,这些应用程序就可以直接使用输入输出的地址空间.由此可以看出,使用内存映射文件处理存储于磁盘上的文件时,将不需要由应用程序对文件执行I/O操作,这意味着在对文件进行处理时将不必再为文件申请并分配缓存,所有的文件缓存操作均由系统直接管理,由于取消了将文件数据加载到内存、数据从内存到文件的回写以及释放内存块等步骤,使得内存映射文件在处理大数据量的文件时能起到相当重要的作用。

下图为普通文件的I/O访问:
由此可知,步骤1表示将数据拷贝到物理页cache中;步骤2表示将物理页cache中的page1复制一份到page2(用户只能访问用户空间),之后通过指针对数据访问。


image.png

而使用了MMap之后,可大致分为如下步骤:1 进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域(过程1)

2 调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系(过程3)

3 进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝

image.png

在写的过程中:

1.进程(用户态)将需要写入的数据直接copy到对应的mmap地址(内存copy)

2.若mmap地址未对应物理内存,则产生缺页异常,由内核处理

3.若已对应,则直接copy到对应的物理内存

4.由操作系统调用,将脏页回写到磁盘(通常是异步的)

因为物理内存是有限的,mmap在写入数据超过物理内存时,操作系统会进行页置换,根据淘汰算法,将需要淘汰的页置换成所需的新页,所以mmap对应的内存是可以被淘汰的(若内存页是"脏"的,则操作系统会先将数据回写磁盘再淘汰)。这样,就算mmap的数据远大于物理内存,操作系统也能很好地处理,不会产生功能上的问题。

  • 优点如下:

1、对文件的读取操作跨过了页缓存,减少了数据的拷贝次数,用内存读写取代I/O读写,提高了文件读取效率。

2、实现了用户空间和内核空间的高效交互方式。两空间的各自修改操作可以直接反映在映射的区域内,从而被对方空间及时捕捉。

3、提供进程间共享内存及相互通信的方式。不管是父子进程还是无亲缘关系的进程,都可以将自身用户空间映射到同一个文件或匿名映射到同一片区域。从而通过各自对映射区域的改动,达到进程间通信和进程间共享的目的。同时,如果进程A和进程B都映射了区域C,当A第一次读取C时通过缺页从磁盘复制文件页到内存中;但当B再读C的相同页面时,虽然也会产生缺页异常,但是不再需要从磁盘中复制文件过来,而可直接使用已经保存在内存中的文件数据。

4、可用于实现高效的大规模数据传输。内存空间不足,是制约大数据操作的一个方面,解决方案往往是借助硬盘空间协助操作,补充内存的不足。但是进一步会造成大量的文件I/O操作,极大影响效率。这个问题可以通过mmap映射很好的解决。换句话说,但凡是需要用磁盘空间代替内存的时候,mmap都可以发挥其功效。

  • 缺点如下:

1.文件如果很小,是小于4096字节的,比如10字节,由于内存的最小粒度是页,而进程虚拟地址空间和内存的映射也是以页为单位。虽然被映射的文件只有10字节,但是对应到进程虚拟地址区域的大小需要满足整页大小,因此mmap函数执行后,实际映射到虚拟内存区域的是4096个字节,11~4096的字节部分用零填充。因此如果连续mmap小文件,会浪费内存空间。

2 对变长文件不适合,文件无法完成拓展,因为mmap到内存的时候,你所能够操作的范围就确定了。

3.如果更新文件的操作很多,会触发大量的脏页回写及由此引发的随机IO上。所以在随机写很多的情况下,mmap方式在效率上不一定会比带缓冲区的一般写快。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,271评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,725评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,252评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,634评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,549评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,985评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,471评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,128评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,257评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,233评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,235评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,940评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,528评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,623评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,858评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,245评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,790评论 2 339

推荐阅读更多精彩内容