Skip to content

Latest commit

 

History

History
146 lines (133 loc) · 6.94 KB

5.Page-Cache.md

File metadata and controls

146 lines (133 loc) · 6.94 KB

Page Cache(内核读写缓存)

在磁盘进行标准IO操作时,操作系统内核会先把数据写入到PageCache,读取数据时会直接从Cache中读取,由此提升读写效率,提高系统磁盘的I/O吞吐量(减少了IO的次数)。

文件读流程

  1. 应用程序发起读请求,由系统调用read()函数,用户态切换为内核态
  2. 文件系统通过目录项、页缓存树(Radix tree),查询Page Cache,如果存在则直接读取(避免了对物理磁盘I/O操作)
  3. Page Cache不存在产生缺页中断,CPU向DMA发出控制指令, DMA 控制器将数据从硬盘拷贝到内核空间的缓冲区(read buffer)
  4. DMA 磁盘控制器向 CPU 发出数据读完的信号,由 CPU 负责将数据从内核缓冲区拷贝到用户缓冲区
  5. 用户进程由内核态切换回用户态,获得文件数据

文件写流程

  1. 应用程序发起写请求,由系统调用write()函数,用户态切换为内核态
  2. 文件系统通过目录项、页缓存树,查询 Page Cache是否存在,如果不存在则需要创建
  3. Page Cache 存在后,CPU将数据从用户缓冲区拷贝到内核缓冲区,Page Cache 变为脏页,写流程返回
  4. 用户主动触发刷盘或者达到特定条件后内核触发刷盘,唤醒 pdflush 线程,pdflush 将内核缓冲区的数据刷入磁盘

Linux内核实现页缓存机制

struct file {
    ...
    struct address_space *f_mapping;
};

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 */
    ...
};

读文件调用链:

read()

sys_read()

vfs_read()

do_sync_read()

generic_file_aio_read()

do_generic_file_read()

do_generic_mapping_read()

void
do_generic_mapping_read(struct address_space *mapping,
        struct file_ra_state *_ra,
        struct file *filp,
        loff_t *ppos,
        read_descriptor_t *desc,
        read_actor_t actor) 
{
    struct inode *inode = mapping->host;
    unsigned long index;
    struct page *cached_page;
    ...
    cached_page = NULL;
    index = *ppos >> PAGE_CACHE_SHIFT;
    ...
    for (;;) {
        struct page *page;
        ...
        find_page:
        // 1. 查找文件偏移量所在的页缓存是否存在
        page = find_get_page(mapping, index);
        if (!page) {
            ...
            // 2. 如果页缓存不存在, 那么跳到 no_cached_page 进行处理
            goto no_cached_page; 
        }
        ...
        page_ok:
        ...
        // 3. 如果页缓存存在, 那么把页缓存的数据拷贝到用户应用程序的内存中
        ret = actor(desc, page, offset, nr);
        ...
        if (ret == nr && desc->count)
            continue;
        goto out;
        ...
        readpage:
        // 4. 从文件读取数据到页缓存中
        error = mapping->a_ops->readpage(filp, page);
        ...
        goto page_ok;
        ...
        no_cached_page:
        if (!cached_page) {
            // 5. 申请一个内存页作为页缓存
            cached_page = page_cache_alloc_cold(mapping);
            ...
        }
        // 6. 把新申请的页缓存添加到文件页缓存中
        error = add_to_page_cache_lru(cached_page, mapping, index, GFP_KERNEL);
        ...
        page = cached_page;
        cached_page = NULL;
        goto readpage;
    }
out:
    ...
}
int add_to_page_cache_lru(struct page *page, struct address_space *mapping,
                            pgoff_t offset, gfp_t gfp_mask)
{
    // 1. 把页缓存添加到文件页缓存中
    int ret = add_to_page_cache(page, mapping, offset, gfp_mask);
    if (ret == 0) 
        // 2. 把页缓存添加到 LRU 队列中
        lru_cache_add(page); 
   return ret;
}

Page Cache 与 Buffer Cache

~ free -m
total       used       free     shared    buffers     cached
Mem:        128956      96440      32515          0       5368      39900
-/+ buffers/cache:      51172      77784
Swap:        16002          0      16001

执行 free 命令后可以看到,cached 列表示当前的页缓存(Page Cache)占用量,buffers 列表示当前的块缓存(buffer cache)占用量。用一句话来解释:Page Cache 用于缓存文件的页数据,buffer cache 用于缓存块设备(如磁盘)的块数据。页是逻辑上的概念,因此 Page Cache 是与文件系统同级的;块是物理上的概念,因此 buffer cache 是与块设备驱动程序同级的。
在 Linux 2.4 版本的内核之前,Page Cache 与 buffer cache 是完全分离的。但是,块设备大多是磁盘,磁盘上的数据又大多通过文件系统来组织,这种设计导致很多数据被缓存了两次,浪费内存。所以在 2.4 版本内核之后,两块缓存近似融合在了一起:如果一个文件的页加载到了 Page Cache,那么同时 buffer cache 只需要维护块指向页的指针就可以了。只有那些没有文件表示的块,或者绕过了文件系统直接操作(如dd命令)的块,才会真正放到 buffer cache 里。

Page Cache 中的每个文件都是一棵基数树(radix tree,本质上是多叉搜索树),树的每个节点都是一个页。根据文件内的偏移量就可以快速定位到所在的页。如下图所示:

Page Cahce 与 文件持久化一致性

Page Cache会存在内存中的数据与磁盘中的数据不一致的问题,我们说保证文件(文件=数据+元数据)一致性其实包含了两个方面:数据一致+元数据一致。

当前 Linux 下以两种方式实现文件一致性:

  1. Write Through(写穿):向用户层提供特定接口,应用程序可主动调用接口来保证文件一致性;一旦写入不丢失,但以牺牲系统 I/O 吞吐量作为代价。
  2. Write back(写回):系统中存在定期任务(表现形式为内核线程),周期性地同步文件系统中文件脏数据块,这是默认的 Linux 一致性方案;效率较高,但宕机的情况下有丢失数据的风险。

回写时机:

  1. 应用程序主动调用回写接口,如下表所示:
    | 方法 | 含义 |
    | ------ | ------ |
    | fsync(intfd) | 将文件的脏数据和脏元数据全部刷新至磁盘 |
    | fdatasync(int fd) | 将文件的脏数据刷新至磁盘,同时对必要的元数据(接下来读写文件至关重要的信息,如文件大小等)刷新至磁盘 |
    | sync() | 将系统中所有的脏的文件数据及元数据刷新至磁盘 |
  2. 管理线程周期性地唤醒设备上的回写线程进行回写
  3. 某些应用程序/内核任务发现内存不足时事先进行脏页面回写,回收部分缓存页面