内核为块设备提供了两种通用的缓存方案。

  • 页缓存(page cache)
  • 块缓存(buffer cache)

页缓存的结构

在页缓存中搜索一页所花费的时间必须最小化,以确保缓存失效的代价尽可能低廉,因为在缓存失效时,进行搜索的计算时间实际上被浪费了。因而,页缓存设计的一个关键的方面就是,对缓存的页进行高效的组织。

管理和查找缓存的页

对此用途而言,树数据结构是非常流行的,Linux也采用了这种结构来管理页缓存中包含的页,称为基数树(radix tree)

基数树也是不平衡的,换句话说,在树的不同分支之间,可能有任意数目的高度差。树本身由两种不同的数据结构组成,还需要另一种数据结构来表示叶,其中包含了有用的数据。因为页缓存组织的是内存页,因而基数树的叶子是 page 结构的实例,该事实并不会影响到树的实现。

树的根由一个简单的数据结构表示,其中包含了树的高度(所包含结点的最大层次数目)和一个指针,指向组成树的的第一个结点的数据结构。

结点本质上是数组。

树的各结点通过一个唯一的键来访问,键是一个整数。

树结点的增删涉及的工作量都很少,因此缓存管理操作所涉及的时间开销可以降低到最低限度。

树的结点具备两种搜索标记(search tag)。二者用于指定给定页当前是否是脏的,或该页是否正在向底层块设备回写。重要的是,标记不仅对叶结点设置,还一直向上设置到根结点。这使得内核可以判断,在某个范围内是否有一页或多页设置了某个标记位。

回写修改的数据

  • 几个专门的内核守护进程在后台运行,称为 pdflush ,它们将周期性激活,而不考虑页缓存中当前的情况。这些守护进程扫描缓存中的页,将超出一定时间没有与底层块设备同步的页写回。
  • pdflush 的第二种运作模式是:如果缓存中修改的数据项数目在短期内显著增加,则由内核激活 pdflush 。
  • 提供了相关的系统调用,可由用户或应用程序通知内核写回所有未同步的数据。最著名的是sync 调用,因为还有一个同名的用户空间工具,是基于该调用的。

为管理可以按整页处理和缓存的各种不同对象,内核使用了“地址空间”抽象,将内层中的页与特定的块设备(或任何其他系统单元,或系统单元的一部分)关联起来。

最初,我们只对一个方面感兴趣。每个地址空间都有一个“宿主”,作为其数据来源。大多数情况下,宿主都是表示一个文件的inode。

因为所有现存的inode都关联到其超级块,内核只需要扫描所有超级块的链表,并跟随相关的inode,即可获得被缓存页的列表。

通常,修改文件或其他按页缓存的对象时,只会修改页的一部分,而非全部。这在数据同步时引起了一个问题。将整页写回到块设备是没有意义的,因为内存中该页的大部分数据仍然与块设备是同步的。为节省时间,内核在写操作期间,将缓存中的每一页划分为较小的单位,称为缓冲区。在同步数据时,内核可以将回写操作限制于那些实际发生了修改的较小的单位上。因而,页缓存的思想没有受到危害。

块缓存的结构

与内存页相比,块不仅比较小(大多数情况下),而且长度是可变的,依赖于使用的块设备。

随着日渐倾向于使用基于页操作实现的通用文件存取方法,块缓存作为中枢系统缓存的重要性已经逐渐失去,主要的缓存任务现在由页缓存承担。

另外,基于块的I/O的标准数据结构,现在已经不再是缓冲区,而是第6章讨论的 struct bio 。

块缓存在结构上由两个部分组成:

  • 缓冲头(buffer head)包含了与缓冲区状态相关的所有管理数据,包括块号、块长度、访问计数器等,将在下文讨论。这些数据不是直接存储在缓冲头之后,而是存储在物理内存的一个独立区域中,由缓冲头结构中一个对应的指针表示。
  • 有用数据保存在专门分配的页中,这些页也可能同时存在于页缓存中。这进一步细分了页缓存

当然,有些应用程序在访问块设备时,使用的是块而不是页,读取文件系统的超级块,就是一个实例。一个独立的块缓存用于加速此类访问。该块缓存的运作独立于页缓存,而不是在其上建立的。为此,缓冲头数据结构(对块缓存和页缓存是相同的)群集在一个长度恒定的数组中,各个数组项按LRU(least recently used,最近最少使用)方式管理。在一个数组项用过之后,将其置于索引位置0,其他数组项相应下移。这意味着最常使用的数组项位于数组的开头,而不常用的数组项将被后推,如果很长时间不用,则会“掉出”数组。

因为数组的长度,或者说LRU列表中的项数,是一个固定值,在内核运行期间不改变,内核无须运行独立的线程来将缓存长度修整为合理值。相反,内核只需要在一项“掉出”数组时,将相关的缓冲区从缓存删除,以释放内存,用于其他目的。

地址空间

  • 内存中的页分配到每个地址空间。这些页的内容可以由用户进程或内核本身使用各式各样的方法操作。
  • 后备存储器指定了填充地址空间中页的数据的来源。地址空间关联到处理器的虚拟地址空间,是由处理器在虚拟内存中管理的一个区域到源设备(使用块设备)上对应位置之间的一个映射。

数据结构

<linux/fs.h>
struct address_space {
    struct inode        *host;      /* owner: inode, block_device */
    struct radix_tree_root  page_tree;  /* radix tree of all pages */
    rwlock_t        tree_lock;  /* and rwlock protecting it */
    unsigned int        i_mmap_writable;/* count VM_SHARED mappings */
    struct prio_tree_root   i_mmap;     /* tree of private and shared mappings */
    struct list_head    i_mmap_nonlinear;/*list VM_NONLINEAR mappings */
    spinlock_t      i_mmap_lock;    /* protect tree, count, list */
    unsigned int        truncate_count; /* Cover race condition with truncate */
    unsigned long       nrpages;    /* number of total pages */
    pgoff_t         writeback_index;/* writeback starts here */
    const struct address_space_operations *a_ops;   /* methods */
    unsigned long       flags;      /* error bits/gfp mask */
    struct backing_dev_info *backing_dev_info; /* device readahead, etc */
    spinlock_t      private_lock;   /* for use by the address_space */
    struct list_head    private_list;   /* ditto */
    struct address_space    *assoc_mapping; /* ditto */
} __attribute__((aligned(sizeof(long))));
  • 与地址空间所管理的区域之间的关联。是通过两个字段建立的。inode指向了后备存储器,一个基树的根列出了地址空间中所有的物理内存页。
  • 缓存页的总数保存在 nrpages 计数器变量中。
  • address_space_operations 是一个指向结构的指针,该结构包含了一组函数指针,指向用于处理地址空间的特定操作。
  • i_mmap 是一棵树的根结点,该树包含了与该inode相关的所有普通内存映射。该树的任务在于,支持查找包含了给定区间中至少一页的所有内存区域,而辅助宏 vma_prio_tree_foreach就 用于该目的。所有页都可以在树中找到,而且树的结构很容易操作,就足够了。(优先查找树(priority search tree)用于建立文件中的一个区域与该区域映射到的所有虚拟地址空间之间的关联。)
  • i_mmap_writeable 统计了所有用 VM_SHARED 属性创建的映射,它们可以由几个用户同时共享。 i_mmap_nonlinear 用于建立一个链表,包括所有包含在非线性映射中的页
  • backing_dev_info 是一个指针,指向另一个结构,其中包含了与地址空间相关的后备存储器的有关信息。

后备存储器是指与地址空间相关的外部设备,用作地址空间中信息的来源。它通常是块设备:

<backing-dev.h>
struct backing_dev_info {
    unsigned long ra_pages; /* max readahead in PAGE_CACHE_SIZE units *///预读的最大数目
    unsigned long state;    /* Always use atomic bitops on this *///状态
    unsigned int capabilities; /* Device capabilities *///BDI_CAP_NO_WRITEBACK ,那么不需要数据同步;否则,需要进行同步。
...
}
  • private_list 用于将包含文件系统元数据(通常是间接块)的 buffer_head 实例彼此连接起来。assoc_mapping 是一个指向相关的地址空间的指针。
  • flags 中的标志集主要用于保存映射页所来自的GFP内存区的有关信息。它也可以保存异步输入输出期间发生的错误信息,在异步I/O期间错误无法之间传递给调用者。 AS_EIO 代表一般性的I/O错误, AS_ENOSPC 表示没有足够的空间来完成一个异步写操作。

页树

内核使用了基数树来管理与一个地址空间相关的所有页。

radix_tree_root 结构是每个基数树的的根结点:

<linux/radix-tree.h>
struct radix_tree_root {
    unsigned int        height;
    gfp_t           gfp_mask;
    struct radix_tree_node  *rnode;
};
  • height 指定了树的高度,即根结点之下结点的层次数目。根据该信息和每个结点的项数,内核可以快速计算给定树中数据项的最大数目。
  • gfp_mask 指定了从哪个内存域分配内存。
  • rnode 是一个指针,指向树的第一个结点。

实现

基数树的结点基本上由以下数据结构表示:

<lib/radix-tree.c>
#ifdef __KERNEL__
#define RADIX_TREE_MAP_SHIFT    (CONFIG_BASE_SMALL ? 4 : 6)
#else
#define RADIX_TREE_MAP_SHIFT    3   /* For more stressful testing */
#endif

#define RADIX_TREE_MAP_SIZE (1UL << RADIX_TREE_MAP_SHIFT)
#define RADIX_TREE_MAP_MASK (RADIX_TREE_MAP_SIZE-1)

#define RADIX_TREE_TAG_LONGS    \
    ((RADIX_TREE_MAP_SIZE + BITS_PER_LONG - 1) / BITS_PER_LONG)

struct radix_tree_node {
    unsigned int    height;     /* Height from the bottom */
    unsigned int    count;
    struct rcu_head rcu_head;
    void        *slots[RADIX_TREE_MAP_SIZE];
    unsigned long   tags[RADIX_TREE_MAX_TAGS][RADIX_TREE_TAG_LONGS];
};
  • slots 是一个 void 指针的数组,根据结点所在的层次,指向数据或其他结点。
  • count 保存了该结点中已经使用的数组项的数目。

每个树结点都可以进一步指向64个结点(或叶子)。

标记

基数树的每个结点都包含了额外的标记信息,用于指定结点中的每个页是否具有标记中指定的属性。

当前支持如下两种标记。

  • PAGECACHE_TAG_DIRTY 指定页是否是脏的。
  • PAGECACHE_TAG_WRITEBACK 表示该页当前正在回写。

标记信息保存在一个二维数组中( tags ),它是 radix_tree_node 的一部分。数组的第一维区分不同的标记,而第二维包含了足够数量的 unsigned long ,使得对该结点中可能组织的每个页,都能分配到一个比特位。

radix_tree_tag_set 用于对一个特定的页设置一个标志:

<lib/radix-tree.c>
void *radix_tree_tag_set(struct radix_tree_root *root,
            unsigned long index, unsigned int tag)

内核在位串中操作对应的位置,并将该比特位设置为1。在完成后,将自上而下扫描树,更新所有结点中的信息。

为查找所有具备特定标记的页,内核仍然必须扫描整个树,但该操作现在可以被加速,首先可以过滤出至少有一页设置了该标志的所有子树。另外,这个操作还可以进一步加速,内核实际上无须逐比特位检查,只需要检查存储该标记的 unsigned long 中,是否有某个不为0即可。

访问基数树结点

内核还提供了以下函数来处理基数树(都实现在 lib/radix_tree.c 中):

<linux/radix-tree.h>
int radix_tree_insert(struct radix_tree_root *, unsigned long, void *);
void *radix_tree_lookup(struct radix_tree_root *, unsigned long);
void *radix_tree_delete(struct radix_tree_root *, unsigned long);
int radix_tree_tag_get(struct radix_tree_root *root,unsigned long index, unsigned int tag);
void *radix_tree_tag_clear(struct radix_tree_root *root,unsigned long index, unsigned int tag);
  • radix_tree_insert 向基数树添加一个新的数据项,由一个 void* 指针表示。如果树当前的容量过小,则会自动扩展。
  • radix_tree_lookup 根据键来查找基数树的数据项,键是一个整数,以参数的形式传递给该函数。返回值是一个 void 指针,必须转换为适当的目标数据类型。
  • radix_tree_delete 根据键值,删除对应的数据项。如果删除成功,则返回指向被删除对象的指针.
  • radix_tree_tag_get 检查指定的基数树结点上是否设置了某个标记。如果设置了标记,则函数返回1,否则返回0。
  • radix_tree_tag_clear 清除指定的基数树数据项上的标记。对该结点的修改,在树中会向上传播,即如果某个结点下一层的所有子结点结点都没有指定的标记了,那么该结点也需要清除此标记,依此类推。在成功的情况下,将返回被标记数据项的地址。

地址空间操作

地址空间将后备存储器与内存区关联起来。在二者之间传输数据,不仅需要数据结构,还需要相应的函数。

在讨论 struct address_space 时已经说明,每个地址空间都包含了一个指向 address_space_operations 实例的指针,该实例保存了所述函数指针的列表:

<linux/fs.h>
struct address_space_operations {
    int (*writepage)(struct page *page, struct writeback_control *wbc);
    int (*readpage)(struct file *, struct page *);
    void (*sync_page)(struct page *);

    /* Write back some dirty pages from this mapping. */
    int (*writepages)(struct address_space *, struct writeback_control *);

    /* Set a page dirty.  Return true if this dirtied it */
    int (*set_page_dirty)(struct page *page);

    int (*readpages)(struct file *filp, struct address_space *mapping,
            struct list_head *pages, unsigned nr_pages);

    /*
     * ext3 requires that a successful prepare_write() call be followed
     * by a commit_write() call - they must be balanced
     */
    int (*prepare_write)(struct file *, struct page *, unsigned, unsigned);
    int (*commit_write)(struct file *, struct page *, unsigned, unsigned);

    int (*write_begin)(struct file *, struct address_space *mapping,
                loff_t pos, unsigned len, unsigned flags,
                struct page **pagep, void **fsdata);
    int (*write_end)(struct file *, struct address_space *mapping,
                loff_t pos, unsigned len, unsigned copied,
                struct page *page, void *fsdata);

    /* Unfortunately this kludge is needed for FIBMAP. Don't use it */
    sector_t (*bmap)(struct address_space *, sector_t);
    void (*invalidatepage) (struct page *, unsigned long);
    int (*releasepage) (struct page *, gfp_t);
    ssize_t (*direct_IO)(int, struct kiocb *, const struct iovec *iov,
            loff_t offset, unsigned long nr_segs);
    struct page* (*get_xip_page)(struct address_space *, sector_t,
            int);
    /* migrate the contents of a page to the specified target */
    int (*migratepage) (struct address_space *,
            struct page *, struct page *);
    int (*launder_page) (struct page *);
};
  • writepage 和 writepages 将地址空间的一页或多页写回到底层块设备。这是通过向块层发出一个相应的请求来完成的
  • readpage 和 readpages 从后备存储器将一页或多个连续的页读入页帧。
  • sync_page 对尚未回写到后备存储器的数据进行同步。不同于 writepage ,该函数在块层的层次上运作,试图将仍然保存在缓冲区中的待决写操作写入到块层。与此相反, writepage 在地址空间的层次上运作,只是将数据转发到块层,而不关注块层中的缓冲问题。内核提供了标准函数 block_sync_page ,该函数获得所述页所属的地址空间映射,并“拔出”块设备队列,开始I/O。
  • set_page_dirty 容许地址空间提供一个特定的方法,将一页标记为脏
  • prepare_write 和 commit_write 执行由 write 系统调用触发的写操作
  • write_begin 和 write_end 是 prepare_write 和 commit_write 的代替物
  • bmap 将地址空间内的逻辑块偏移量映射为物理块号。
  • releasepage 用于日志文件系统中,准备释放页
  • 如果一页将要从地址空间移除,而通过 PG_Private 标志可判断有缓冲区与之相关,则调用invalidatepage 。
  • direct_IO 用于实现直接的读写访问。这绕过了块层的缓冲机制,允许应用程序非常直接地与块设备进行通信。
  • get_xip_page 用于就地执行(execute-in-place)机制,该机制可用于启动可执行代码,而无须将其先加载到页缓存。这对有些场合是有用的,例如,基于内存的文件系统如RAM磁盘,或在内存较少的小型系统上,CPU可直接寻址ROM区域包含的文件系统。
  • 在内核想要重新定位一页时会使用 migrate_page ,即将一页的内容移动到另外一页。由于页通常都带有私有数据,只是将两页对应的物理页帧的裸数据进行复制是不够的。举例来说,支持内存热插拔就需要对页进行移动。
  • launder_page 在释放页之前,提供了回写脏页的最后的机会。

address_space_operations 结构中的函数和内核提供的通用辅助函数使用的参数不同,因而需要一些简短的包装器函数对参数进行转换。

页缓存的实现

页缓存的实现基于基数树。尽管该缓存属于内核中性能要求最苛刻的部分之一,而且广泛用于内核的所有子系统,但其实现简单得惊人。能做到这一点,精心设计的数据结构是一个必要前提。

分配页

page_cache_alloc 用于为一个即将加入页缓存的新页分配数据结构。与后缀为_ cold 的变体工作方式相同,但试图获取一个冷页(对CPU高速缓存而言):

<linux/pagemap.h>
struct page *page_cache_alloc(struct address_space *x)
struct page *page_cache_alloc_cold(struct address_space *x)

最初,不会访问基数树,因为工作委托给 alloc_pages ,该函数从伙伴系统(在第3章描述)获取一个页帧。但需要地址空间参数,确定该页所来自的内存域。

add_to_page_cache将页添加到页缓存中:

int add_to_page_cache(struct page *page, struct address_space *mapping,
        pgoff_t offset, gfp_t gfp_mask)
{
    int error = radix_tree_preload(gfp_mask & ~__GFP_HIGHMEM);

    if (error == 0) {
        write_lock_irq(&mapping->tree_lock);
        error = radix_tree_insert(&mapping->page_tree, offset, page);//将页插入所属地址空间的基数树
        if (!error) {
            page_cache_get(page);
            SetPageLocked(page);
            page->mapping = mapping;//
            page->index = offset;//
            mapping->nrpages++;//
            __inc_zone_page_state(page, NR_FILE_PAGES);
        }
        write_unlock_irq(&mapping->tree_lock);
        radix_tree_preload_end();
    }
    return error;
}

内核还提供了另一个可选的函数 add_to_page_cache_lru ,其原型是相同的。该函数首先调用add_to_page_cache 向地址空间相关的页缓存添加一页,然后使用 lru_cache_add 函数将该页添加到系统的LRU缓存。

查找页

<mm/filemap.c>
struct page * find_get_page(struct address_space *mapping, pgoff_t offset)
{
    struct page *page;

    read_lock_irq(&mapping->tree_lock);
    page = radix_tree_lookup(&mapping->page_tree, offset);//在地址空间基数树中查找该页
    if (page)
        page_cache_get(page);//引用计数加一
    read_unlock_irq(&mapping->tree_lock);
    return page;
}

但在很多情况下,页是属于文件的。遗憾的是,文件中的位置是按字节偏移量指定的,而非页缓存中的偏移量。如何将文件偏移量转换为页缓存偏移量呢?

当前,页缓存的粒度是单个页,即页缓存基数树的页结点是一个页。但未来的内核可能增加该缓存的粒度,因而假定缓存的粒度为单页是不可靠的。相反,内核提供了 PAGE_CACHE_SHIFT 宏。页缓存结点的对象长度,可通过2 PAGE_CACHE_SHIFT 计算。

那么,在文件的字节偏移量和页缓存偏移量之间的转换就变得比较简单,将文件偏移量右移PAGE_CACHE_SHIFT 位即可:

index = ppos >> PAGE_CACHE_SHIFT;

ppos 是文件的字节偏移量,而 index 则是页缓存中对应的偏移量。

为方便使用,内核提供了两个辅助函数:

<pagemap.h>
struct page * find_or_create_page(struct address_space *mapping,pgoff_t index, gfp_t gfp_mask);//find_or_create_page 的功能可根据其名称判断,它在页缓存中查找一页,如果没有则分配一个新页。然后通过调用 add_to_page_cache_lru 插入到页缓存和LRU链表中。
struct page * find_lock_page(struct address_space *mapping,pgoff_t index);//find_lock_page 的工作与 find_get_page 类似,但会锁定该页。

还可以查找多个页。对应的辅助函数原型如下:

<pagemap.h>
unsigned find_get_pages(struct address_space *mapping, pgoff_t start,unsigned int nr_pages, struct page **pages);
unsigned find_get_pages_contig(struct address_space *mapping, pgoff_t start,unsigned int nr_pages, struct page **pages);
unsigned find_get_pages_tag(struct address_space *mapping, pgoff_t *index,int tag, unsigned int nr_pages, struct page **pages);
  • find_get_pages 从页缓存偏移量 start 开始,返回映射中最多 nr_pages 页。指向这些页的指针放置在数组 pages 中。该函数不保证返回的页是连续的,不存在的页会形成空洞。该函数的返回值是找到的页的数目。
  • find_get_pages_contig 的工作方式类似于 find_get_pages ,但所选的页保证是连续的。在遇到第一个空洞时,该函数会停止查找,并将找到的页填充到 pages 数组中。
  • find_get_pages_tag 的运作方式类似于 find_pages ,但它只选择设置了特定标记的页。此外,在函数返回后, index 参数中将包含一个页缓存的索引,指向 pages 数组中最后一页的下一页。

在页上等待

内核经常需要在页上等待,直至其状态改变为某些预期值。例如,数据同步的实现有时候需要确保对某页的回写操作已经结束,而内存页中的内容与底层块设备的数据是相同的。处于回写过程中的页会设置 PG_writeback 标志位。

内核提供了 wait_on_page_writeback 函数,用于等待页的该标志位清除:

<linux/pagemap.h>
static inline void wait_on_page_writeback(struct page *page)
{
    if (PageWriteback(page))
        wait_on_page_bit(page, PG_writeback);
}

wait_on_page_bit 安装一个等待队列,进程可以在其上睡眠,直至 PG_writeback 标志位从页的标志中清除。

同样地,也可能有等待页解锁的需求。 wait_on_page_locked 负责处理这种情况。

对整页的操作

在重新设计块层的过程中,内核版本2.5开发期间引入了BIO,以替换缓冲区,来处理与块设备的数据传输。内核添加了4个新的函数,来支持读写一页或多页:

<mpage.h>
int mpage_readpages(struct address_space *mapping, struct list_head *pages,unsigned nr_pages, get_block_t get_block);
int mpage_readpage(struct page *page, get_block_t get_block);
int mpage_writepages(struct address_space *mapping,struct writeback_control *wbc, get_block_t get_block);
int mpage_writepage(struct page *page, get_block_t *get_block,struct writeback_control *wbc);

由于这4个函数的实现有很多共同之处(其目标都是构建一个适当的BIO实例,用于对块层进行传输),接下来以其中一个为例进行讨论,即 mpage_readpages 。

<fs/mpage.c>
int
mpage_readpages(struct address_space *mapping, struct list_head *pages,
                unsigned nr_pages, get_block_t get_block)
{
    struct bio *bio = NULL;
    unsigned page_idx;
    sector_t last_block_in_bio = 0;
    struct buffer_head map_bh;
    unsigned long first_logical_block = 0;

    clear_buffer_mapped(&map_bh);
    for (page_idx = 0; page_idx < nr_pages; page_idx++) {
        struct page *page = list_entry(pages->prev, struct page, lru);

        prefetchw(&page->flags);
        list_del(&page->lru);
        if (!add_to_page_cache_lru(page, mapping,
                    page->index, GFP_KERNEL)) {//添加到页缓存和内核的LRU链表。
            bio = do_mpage_readpage(bio, page,
                    nr_pages - page_idx,
                    &last_block_in_bio, &map_bh,
                    &first_logical_block,
                    get_block);//创建BIO并提交处理
        }
        page_cache_release(page);
    }
    BUG_ON(!list_empty(pages));
    if (bio)//如果在循环结束时, do_mpage_readpage 留下一个未处理的BIO请求,则提交该请求
        mpage_bio_submit(READ, bio);
    return 0;
}

页缓存预读

预读不能由页缓存独立解决,还需要VFS和内存管理层的支持。

预读是从3个地方控制的:

  • do_generic_mapping_read ,这是一个通用的读取例程,其中,大多数依赖内核的标准例程来读取数据的文件系统都结束于某些位置。
  • 缺页异常处理程序 filemap_fault ,它负责为内存映射读取缺页。
  • __generic_file_splice_read ,调用该例程是为支持 splice 系统调用,该系统调用使得可以直接在内核空间中在两个文件描述符之间传输数据,而无须涉及用户空间。

为简单起见,下文只考虑 do_generic_mapping_read 。

假定进程已经打开了一个文件,想要读取第一页。该页尚未读入页缓存。由于通常的所有者不会只读取一页,而是顺序读取多页,内核采用 page_cache_sync_readahead 读取一行中的8页,这个数字只是举例来说,实际上不见得如此。第一页对 do_generic_mapping_read 来说是立即可用的。 而在实际需要之前就被选择读入页缓存的页,则称为处于预读窗口中。

进程现在继续读取接下来的各页,与我们的预期相同。在访问第6页时(请注意,在进程发出读请求之前,该页已经读入页缓存), do_generic_mapping_read 注意到,该页在同步读取处理过程中设置了 PG_Readahead 标志位。 这触发了一个异步操作,在后台读取若干页。由于页缓存中还有两页可用,不必匆忙读取,所以不需要一个同步操作。但在后台进行的I/O操作,将确保在进程进一步读取文件时,相关页已经读入缓存。如果内核不采用这种方案,预读只能在进程遇到一个缺页异常后开始。虽然所需的页(以及另一些预读的页)可以同步读入页缓存,但这将引入延迟,显然不是我们期待的情形。

现在将进一步重复这种做法。由于 page_cache_async_read (负责发出异步读请求)又将预读窗口中的一页标记为 PG_Readahead ,在进程遇到该页时,将再次开始异步预读,依此类推。

对 do_generic_readahead 就讲到这里。 filemap_fault 的处理方式,与 do_generic_readahead的区别有两个方面:仅当设置了顺序读取提示的情况下,才会进行异步自适应的预读。如果没有设置预读提示,那么 do_page_cache_readahead 只进行一次预读,而不设置 PG_Readahead ,也不会更新文件的预读状态跟踪信息。

内核会记录每个文件上一次的设置。下列数据结构将关联到每个 file 实例:

<linux/fs.h>
struct file_ra_state {
    pgoff_t start;          /* where readahead started */
    unsigned int size;      /* # of readahead pages */
    unsigned int async_size;    /* do asynchronous readahead when
                       there are only # of pages ahead */

    unsigned int ra_pages;      /* Maximum readahead window */
    int mmap_miss;          /* Cache miss stat for mmap accesses */
    loff_t prev_pos;        /* Cache last read() position */
};
  • start 表示页缓存中开始预读的位置
  • size 给出了预读窗口的长度
  • async_size 表示剩余预读页的最小值。
  • ra_pages 表示预读窗口的最大长度,
  • prev_pos 表示前一次读取时,最后访问的位置。(这个偏移量是文件中的字节偏移量)

ondemand_readahead 例程负责实现预读策略,即判断读入多少当前并不需要的页。 page_cache_sync_readahead 和 page_cache_async_readahead 都依赖于该函数。在确定预读窗口的长度之后,调用 ra_submit ,将技术性问题委托给 __do_page_cache_readahead 完成。在这里,页是在页缓存中分配的,而后由块层填充。

get_init_ra_size 为一个文件确定最初的预读窗口长度。

get_next_ra_size 为后来的读取计算窗口长度,即此时已经有一个先前的预读窗口存在。

块缓存的实现

块缓存不仅仅用作页缓存的附加功能,对以块而不是页进行处理的对象来说,块缓存是一个独立的缓存。

数据结构

两种类型的块缓存,即独立的块缓存和用作页缓存附加功能的块缓存,二者的数据结构是相同的。

块缓存主要的数据元素是缓冲头:

<linux/buffer_head.h>
struct buffer_head {
    unsigned long b_state;      /* buffer state bitmap (see above) */
    struct buffer_head *b_this_page;/* circular list of page's buffers */
    struct page *b_page;        /* the page this bh is mapped to */

    sector_t b_blocknr;     /* start block number */
    size_t b_size;          /* size of mapping */
    char *b_data;           /* pointer to data within the page */

    struct block_device *b_bdev;
    bh_end_io_t *b_end_io;      /* I/O completion */
    void *b_private;        /* reserved for b_end_io */
    struct list_head b_assoc_buffers; /* associated with another mapping */
    struct address_space *b_assoc_map;  /* mapping this buffer is
                           associated with */
    atomic_t b_count;       /* users using this buffer_head */
}; 

缓冲头的当前状态保存在 b_state 成员中,可接受下列值:

  • 如果缓冲区当前的数据与后备存储器匹配,则状态为 BH_Uptodate 。
  • 如果缓冲区中的数据已经修改,不再与后备存储器匹配,则状态标记为 BH_Dirty 。
  • BH_Lock 表示缓冲区被锁定,以便进行进一步的访问。缓冲区在I/O操作期间会显式锁定,以防几个线程并发处理缓冲区,导致彼此干扰。
  • BH_Mapped 意味着存在一个缓冲区内容到二级存储设备的映射,所有起源于文件系统或直接访问块设备的缓冲区,都是这样。
  • BH_New 标记新创建的缓冲区。
  • b_count 实现了通常的访问计数器,以防内核释放仍然处于使用中的缓冲头。
  • b_page 保存一个指向 page 实例的指针,它表示在块缓存基于页缓存实现的情况下,当前缓冲头相关的 page 实例。如果块缓存是独立于页缓存的,则 b_page 为NULL指针。
  • 会使用几个缓冲区,将一页的内容划分为几个较小的单位。所有隶属于这些单位的缓冲头都保存在一个环形单链表上,链表元素为 b_this_page。
  • b_blocknr 保存了底层块设备上对应的块号, b_size 指定了块长度。 b_bdev 是一个指向块设备的 block_device 实例的指针。
  • 指向内存中数据的指针保存在 b_data
  • b_end_io 指向一个例程,在涉及该缓冲区的一个I/O操作完成时,由内核自动调用
  • b_private 是一个指针,预留给 b_end_io 使用。

操作

alloc_buffer_head生成一个新缓冲头,而 free_buffer_head 销毁一个现存的缓冲头。二者都定义在 fs/buffer.c 中。这两个函数只使用了内存管理的函数,还涉及一些统计工作。

页缓存和块缓存的交互

struct page的private 成员还可以用作其他用途,根据页的具体用途,可能与缓冲头完全无关。但其主要的用途是关联缓冲区和页。这样的话, private 指向将页划分为更小单位的第一个缓冲头。各个缓冲头通过 b_this_page 连接为一个环形链表。在该链表中,每个缓冲头的 b_this_page 成员指向下一个缓冲头,而最后一个缓冲头的 b_this_page 成员指向第一个缓冲头。这使得内核从 page 结构开始,可以轻易地扫描与页关联的所有 buffer_head 实例。

page 和 buffer_head 结构之间的关联是如何建立的呢?内核为此提供了 create_empty_buffers 和 link_dev_buffers 函数,二者都实现在 fs/buffer.c 中。后者用来将一组现存的缓冲头关联到一页,而 create_empty_buffers 创建一组全新的缓冲区,以便与页进行关联。

<fs/buffer.c>
void create_empty_buffers(struct page *page,
            unsigned long blocksize, unsigned long b_state)
{
    struct buffer_head *bh, *head, *tail;

    head = alloc_page_buffers(page, blocksize, 1);
    bh = head;
    do {//遍历所有缓冲头,设置其状态,并建立一个环形链表
        bh->b_state |= b_state;
        tail = bh;
        bh = bh->b_this_page;
    } while (bh);
    tail->b_this_page = head;

    spin_lock(&page->mapping->private_lock);
    if (PageUptodate(page) || PageDirty(page)) {//缓冲头的状态依赖于页的状态
        bh = head;
        do {
            if (PageDirty(page))
                set_buffer_dirty(bh);
            if (PageUptodate(page))
                set_buffer_uptodate(bh);
            bh = bh->b_this_page;
        } while (bh != head);
    }
    attach_page_buffers(page, head);//将缓冲区关联到页:
                                    //设置页标志的 PG_private 标志位,通知内核其他部分, page 实例的 private 成员正在使用中。
                                    //将页的 private 成员设置为一个指向环形链表中第一个缓冲头的指针。
    spin_unlock(&page->mapping->private_lock);
}

内核提供了page_has_buffers(page)来检查页是否与缓冲区关联。

交互

如果对内核的其他部分无益,那么在页和缓冲区之间建立关联就没起作用。如上所述,一些与块设备之间的传输操作,传输单位的长度依赖于底层设备的块长度,而内核的许多部分更喜欢按页的粒度来执行I/O操作,因为这使得其他事情更容易处理,特别是内存管理方面。在这种场景下,缓冲区充当了双方的中介。

在缓冲区中读取整页

首先考察内核在从块设备读取整页时采用的方法,以 block_read_full_page 为例。

<fs/buffer.c>
int block_read_full_page(struct page *page, get_block_t *get_block)
{
    struct inode *inode = page->mapping->host;
    sector_t iblock, lblock;
    struct buffer_head *bh, *head, *arr[MAX_BUF_PER_PAGE];
    unsigned int blocksize;
    int nr, i;
    int fully_mapped = 1;

    BUG_ON(!PageLocked(page));
    blocksize = 1 << inode->i_blkbits;
    if (!page_has_buffers(page))//检测页是否有相关联的缓冲区
        create_empty_buffers(page, blocksize, 0);//没有则创建
    head = page_buffers(page);//获得缓冲区头

    iblock = (sector_t)page->index << (PAGE_CACHE_SHIFT - inode->i_blkbits);
    lblock = (i_size_read(inode)+blocksize-1) >> inode->i_blkbits;
    bh = head;
    nr = 0;
    i = 0;

    do {
        if (buffer_uptodate(bh))//新的
            continue;

        if (!buffer_mapped(bh)) {//没有映射
            int err = 0;

            fully_mapped = 0;
            if (iblock < lblock) {
                WARN_ON(bh->b_size != blocksize);
                err = get_block(inode, iblock, bh, 0);//获取块在块设备上的位置,本质上设置设置头 b_bdev 和 b_blocknr 字段
                if (err)
                    SetPageError(page);
            }
            if (!buffer_mapped(bh)) {
                zero_user_page(page, i * blocksize, blocksize,
                        KM_USER0);
                if (!err)
                    set_buffer_uptodate(bh);
                continue;
            }
            /*
             * get_block() might have updated the buffer
             * synchronously
             */
            if (buffer_uptodate(bh))
                continue;
        }
        arr[nr++] = bh;//缓冲区内容不是最新的
    } while (i++, iblock++, (bh = bh->b_this_page) != head);

    if (fully_mapped)
        SetPageMappedToDisk(page);

    if (!nr) {
        /*
         * All buffers are uptodate - we can set the page uptodate
         * as well. But not if get_block() returned an error.
         */
        if (!PageError(page))//如果关联的所有缓冲区都是最新的
            SetPageUptodate(page);//则设置整页的状态
        unlock_page(page);
        return 0;
    }

    /* Stage two: lock the buffers */
    for (i = 0; i < nr; i++) {//锁定所需要读取的缓冲区
        bh = arr[i];
        lock_buffer(bh);
        mark_buffer_async_read(bh);//将buffer_head的b_end_io设置为end_buffer_async_read,该函数将在数据传输结束时自动调用。
    }

    /*
     * Stage 3: start the IO.  Check for uptodateness
     * inside the buffer lock in case another process reading
     * the underlying blockdev brought it uptodate (the sct fix).
     */
    for (i = 0; i < nr; i++) {//此时 submit_bh 将所有需要读取的缓冲区转交给块层,在其中开始读操作。在读操作结束时,将调用保存在 b_end_io 中的函数。它将遍历页的所有缓冲区,检查其状态,并将整页的状态设置为最新,假定所有缓冲区的状态都已经是最新的
        bh = arr[i];
        if (buffer_uptodate(bh))
            end_buffer_async_read(bh, 1);
        else
            submit_bh(READ, bh);
    }
    return 0;
}
将整页写入到缓冲区

除了读操作之外,页的写操作也可以划分为更小的单位。只有页中实际修改的内容需要回写,而不用回写整页的内容。

__block_write_full_page 函数中回写脏页涉及的缓冲区:

<fs/buffer.c>
static int __block_write_full_page(struct inode *inode, struct page *page,
            get_block_t *get_block, struct writeback_control *wbc)
{
    int err;
    sector_t block;
    sector_t last_block;
    struct buffer_head *bh, *head;
    const unsigned blocksize = 1 << inode->i_blkbits;
    int nr_underway = 0;

    BUG_ON(!PageLocked(page));

    last_block = (i_size_read(inode) - 1) >> inode->i_blkbits;

    if (!page_has_buffers(page)) {//确认页是否有与之关联的缓冲区,没有则创建
        create_empty_buffers(page, blocksize,
                    (1 << BH_Dirty)|(1 << BH_Uptodate));
    }

    /*
     * Be very careful.  We have no exclusion from __set_page_dirty_buffers
     * here, and the (potentially unmapped) buffers may become dirty at
     * any time.  If a buffer becomes dirty here after we've inspected it
     * then we just miss that fact, and the page stays dirty.
     *
     * Buffers outside i_size may be dirtied by __set_page_dirty_buffers;
     * handle that here by just cleaning them.
     */

    block = (sector_t)page->index << (PAGE_CACHE_SHIFT - inode->i_blkbits);
    head = page_buffers(page);
    bh = head;

    /*
     * Get all the dirty buffers mapped to disk addresses and
     * handle any aliases from the underlying blockdev's mapping.
     */
    do {//
        if (block > last_block) {
            /*
             * mapped buffers outside i_size will occur, because
             * this page can be outside i_size when there is a
             * truncate in progress.
             */
            /*
             * The buffer was zeroed by block_write_full_page()
             */
            clear_buffer_dirty(bh);
            set_buffer_uptodate(bh);
        } else if (!buffer_mapped(bh) && buffer_dirty(bh)) {//第一次遍历的目的是,对所有未映射的脏缓冲区,在缓冲区和块设备之间建立映射
            WARN_ON(bh->b_size != blocksize);
            err = get_block(inode, block, bh, 1);
            if (err)
                goto recover;
            if (buffer_new(bh)) {
                /* blockdev mappings never come here */
                clear_buffer_new(bh);
                unmap_underlying_metadata(bh->b_bdev,
                            bh->b_blocknr);
            }
        }
        bh = bh->b_this_page;
        block++;
    } while (bh != head);

    do {
        if (!buffer_mapped(bh))
            continue;
        /*
         * If it's a fully non-blocking write attempt and we cannot
         * lock the buffer then redirty the page.  Note that this can
         * potentially cause a busy-wait loop from pdflush and kswapd
         * activity, but those code paths have their own higher-level
         * throttling.
         */
        if (wbc->sync_mode != WB_SYNC_NONE || !wbc->nonblocking) {
            lock_buffer(bh);
        } else if (test_set_buffer_locked(bh)) {
            redirty_page_for_writepage(wbc, page);
            continue;
        }
        if (test_clear_buffer_dirty(bh)) {//在第二次遍历中,将滤出所有的脏缓冲区。这可以通过 test_clear_buffer_dirty 检查,如果设置了脏标志,则会在调用该函数时清除,因为缓冲区的内容将立即回写。
            mark_buffer_async_write(bh);//设置 BH_Async_Write 状态位,并将 end_buffer_async_write 指定为BIO完成处理程序
        } else {
            unlock_buffer(bh);
        }
    } while ((bh = bh->b_this_page) != head);

    /*
     * The page and its buffers are protected by PageWriteback(), so we can
     * drop the bh refcounts early.
     */
    BUG_ON(PageWriteback(page));
    set_page_writeback(page);//set_page_writeback 对整页设置 PG_writeback 标志。

    do {
        struct buffer_head *next = bh->b_this_page;
        if (buffer_async_write(bh)) {//在第三次也就是最后一次遍历中,调用 submit_bh 将前一次遍历中标记为 BH_Async_Write的所有缓冲区转交给块层执行实际的写操作,该函数向块层提交了一个对应的请求
            submit_bh(WRITE, bh);//在针对某个缓冲区的写操作结束时,将自动调用 end_buffer_async_write ,检查页的所有其他缓冲区上的写操作是否也已经结束。倘若如此,则唤醒在与该页相关的队列上睡眠、等待此事件的所有进程。
            nr_underway++;
        }
        bh = next;
    } while (bh != head);
    unlock_page(page);

    err = 0;
done:
    if (nr_underway == 0) {
        /*
         * The page was marked dirty, but the buffers were
         * clean.  Someone wrote them back by hand with
         * ll_rw_block/submit_bh.  A rare case.
         */
        end_page_writeback(page);

        /*
         * The page and buffer_heads can be released at any time from
         * here on.
         */
    }
    return err;

recover:
    /*
     * ENOSPC, or some other error.  We may already have added some
     * blocks to the file, so we need to write these out to avoid
     * exposing stale data.
     * The page is currently locked and not marked for writeback
     */
    bh = head;
    /* Recovery: lock and submit the mapped buffers */
    do {
        if (buffer_mapped(bh) && buffer_dirty(bh)) {
            lock_buffer(bh);
            mark_buffer_async_write(bh);
        } else {
            /*
             * The buffer may have been set dirty during
             * attachment to a dirty page.
             */
            clear_buffer_dirty(bh);
        }
    } while ((bh = bh->b_this_page) != head);
    SetPageError(page);
    BUG_ON(PageWriteback(page));
    mapping_set_error(page->mapping, err);
    set_page_writeback(page);
    do {
        struct buffer_head *next = bh->b_this_page;
        if (buffer_async_write(bh)) {
            clear_buffer_dirty(bh);
            submit_bh(WRITE, bh);
            nr_underway++;
        }
        bh = next;
    } while (bh != head);
    unlock_page(page);
    goto done;
}

独立的缓冲区

实现

<fs/buffer.c>
struct bh_lru {
    struct buffer_head *bhs[BH_LRU_SIZE];
};

static DEFINE_PER_CPU(struct bh_lru, bh_lrus) = {{ NULL }};
  • bhs 是一个缓冲头指针的数组,用作实现LRU算法的基础
  • 内核使用 DEFINE_PER_CPU ,为系统的每个CPU都建立一个实例,改进对CPU高速缓存的利用率。

该缓存通过内核提供的两个公开的函数来进行管理和使用: lookup_bh_lru 检查所需数据项是否在缓存中,而 bh_lru_install 将新的缓冲头添加到缓存中。

接口函数

普通的内核代码通常不会接触到 bh_lookup_lru 或 bh_lru_install ,因为二者被封装起来。内核提供了通用例程来访问各个块,它们自动涵盖了块缓存,使得没必要与块缓存进行显式交互。这些例程包括 __getblk 和 __bread ,实现在 fs/buffer.c 中。

数据块可通过所在块设备的 block_device 实例、扇区编号( sector_t 类型)和块长度唯一标识。

不同点与两个函数的目标有关。 __bread 保证返回一个包含最新数据的缓冲区。这导致在必要的情况下,需要读取底层块设备。

调用 __getblk 总是返回一个非NULL指针(即一个缓冲头)。 如果所要缓冲区的数据已经在内存中,则返回数据,但不保证数据的状态。与 __bread 相比,数据可能不是最新的。而另一种可能性是,缓冲区对应的块尚未读入内存。在这种情况下, __getblk 确保分配数据所需的内存空间,并将缓冲头插入到LRU缓存。

在文件系统中的使用

在何种情况下,有必要按块读取?内核中必须用这种读取方式的场景不多,但都很重要。特别是,文件系统在读取超级块或管理块时利用了上述的例程。

内核定义了两个函数,以简化文件系统处理单个块的工作:

<buffer_head.h>
static inline struct buffer_head *
sb_bread(struct super_block *sb, sector_t block)
{
    return __bread(sb->s_bdev, block, sb->s_blocksize);
}
static inline struct buffer_head *
sb_getblk(struct super_block *sb, sector_t block)
{
    return __getblk(sb->s_bdev, block, sb->s_blocksize);
}

Linux内核入门到放弃-页缓存和块缓存-《深入Linux内核架构》笔记的更多相关文章

  1. Linux内核入门到放弃-进程管理和调度-《深入Linux内核架构》笔记

    进程优先级 硬实时进程 软实时进程 普通进程 O(1)调度.完全公平调度器 抢占式多任务处理(preemptive multitasking):各个进程都分配到一定的时间段可以执行.时间段到期后,内核 ...

  2. Linux从入门到放弃、零基础入门Linux(第四篇):在虚拟机vmware中安装centos7.7

    如果是新手,建议安装带图形化界面的centos,这里以安装centos7.7的64位为例 一.下载系统镜像 镜像文件下载链接https://wiki.centos.org/Download 阿里云官网 ...

  3. Linux从入门到放弃、零基础入门Linux(第三篇):在虚拟机vmware中安装linux(二)超详细手把手教你安装centos6分步图解

    一.继续在vmware中安装centos6.9 本次安装是进行最小化安装,即没有图形化界面的安装,如果是新手,建议安装带图形化界面的centos, 具体参考Linux从入门到放弃.零基础入门Linux ...

  4. Linux内核入门到放弃-页面回收和页交换-《深入Linux内核架构》笔记

    概述 可换出页 只有少量几种页可以换出到交换区,对其他页来说,换出到块设备上与之对应的后备存储器即可,如下所述. 类别为 MAP_ANONYMOUS 的页,没有关联到文件,例如,这可能是进程的栈或是使 ...

  5. Linux内核入门到放弃-网络-《深入Linux内核架构》笔记

    网络命名空间 struct net { atomic_t count; /* To decided when the network * namespace should be freed. */ a ...

  6. Linux内核入门到放弃-内存管理-《深入Linux内核架构》笔记

    概述 内存管理的实现涵盖了许多领域: 内存中的物理内存页管理 分配大块内存的伙伴系统 分配较小内存块的slab.slub和slob分配器 分配非连续内存块的vmalloc机制 进程的地址空间 在IA- ...

  7. Linux内核入门到放弃-设备驱动程序-《深入Linux内核架构》笔记

    I/O体系结构 总线系统 PCI(Peripheral Component Interconnect) ISA(Industrial Standard Architecture) SBus IEEE1 ...

  8. Linux内核入门到放弃-进程虚拟内存-《深入Linux内核架构》笔记

    进程地址空间的布局 <mm_types.h> <mm_types.h> struct mm_struct { ... unsigned long (*get_unmapped_ ...

  9. Linux从入门到放弃

    Ch.0 几点Linux常识 Linux严格区分大小写,不像windows中命令是不区分大小写的 Linux中所有内容以文件形式保存,包括硬件 Linux不靠扩展名区分文件类型,所有扩展名只是为了方便 ...

随机推荐

  1. 设计模式总结篇系列:建造者模式(Builder)

    关于建造者模式网上有很多文章,也有些不同的理解.在此结合网上其他文章对建造者模式进行总结. 总体说来,建造者模式适合于一个具有较多的零件(属性)的产品(对象)的创建过程.根据产品创建过程中零件的构造是 ...

  2. 装饰器模式 Decorator 结构型 设计模式 (十)

    引子           现实世界的装饰器模式 大家应该都吃过手抓饼,本文装饰器模式以手抓饼为模型展开简介 "老板,来一个手抓饼,  加个培根,  加个鸡蛋,多少钱?" 这句话会不 ...

  3. Mybatis环境配置学习

    Mybatis的使用环境配置步骤主要分为以下三步 1.导入jar包 2.创建mybatis的全局配置文件,并编写 3.创建mapper的配置文件 一.导入jar包 --- (踩坑:这一步中的导入mys ...

  4. SpringBoot系列——Spring-Data-JPA

    前言 jpa是ORM映射框架,更多详情,请戳:apring-data-jpa官网:http://spring.io/projects/spring-data-jpa,以及一篇优秀的博客:https:/ ...

  5. 详解什么是平衡二叉树(AVL)(修订补充版)

    详解什么是平衡二叉树(AVL)(修订补充版) 前言 Wiki:在计算机科学中,AVL树是最早被发明的自平衡二叉查找树.在AVL树中,任一节点对应的两棵子树的最大高度差为1,因此它也被称为高度平衡树.查 ...

  6. 使用表类型(Table Type-SqlServer)实现百万级别的数据一次性毫秒级别插入

    使用表类型(Table Type)实现百万级别的数据一次性插入 思路 1 创建表类型(TaBleType)         2 创建添加存储过程         3 使用C#语言构建一个DataTab ...

  7. 17、字符串转换整数 (atoi)

    17.字符串转换整数 (atoi) 请你来实现一个 atoi 函数,使其能将字符串转换成整数. 首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止. 当我们寻找到的第一个非 ...

  8. 2018/12/21:Date类

    1.Date类 getDate()返回一个月的某一天 1-31 getDay()返回一周的某一天 getFullyear()返回四位数的年份 getMonth()返回月份 比实际情况小 1 0代表1月 ...

  9. netfilter及iptables基本概念

    网络访问控制 网络访问控制可以简单理解为防火墙,常用的网络访问控制有:哪些IP可以访问服务器, 可以使用哪些协议,哪些接口,是否需要对数据包进行修改等. netfilter netfilter是通过i ...

  10. 中间件(3)NoSQL

    NoSQL最常见的解释是non-relational,或者not only SQL,从字段意思上就可以看出,它是指非关系型数据库的统称. NoSQL诞生的背景 随着大型网站分布式架构的发展,使用传统关 ...