在磁盘进行标准IO操作时,操作系统内核会先把数据写入到PageCache,读取数据时会直接从Cache中读取,由此提升读写效率,提高系统磁盘的I/O吞吐量(减少了IO的次数)。
- 应用程序发起读请求,由系统调用
read()
函数,用户态切换为内核态 - 文件系统通过目录项、页缓存树(Radix tree),查询Page Cache,如果存在则直接读取(避免了对物理磁盘I/O操作)
- Page Cache不存在产生缺页中断,CPU向DMA发出控制指令, DMA 控制器将数据从硬盘拷贝到内核空间的缓冲区(read buffer)
- DMA 磁盘控制器向 CPU 发出数据读完的信号,由 CPU 负责将数据从内核缓冲区拷贝到用户缓冲区
- 用户进程由内核态切换回用户态,获得文件数据
- 应用程序发起写请求,由系统调用
write()
函数,用户态切换为内核态 - 文件系统通过目录项、页缓存树,查询 Page Cache是否存在,如果不存在则需要创建
- Page Cache 存在后,CPU将数据从用户缓冲区拷贝到内核缓冲区,Page Cache 变为脏页,写流程返回
- 用户主动触发刷盘或者达到特定条件后内核触发刷盘,唤醒 pdflush 线程,pdflush 将内核缓冲区的数据刷入磁盘
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;
}
~ 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 Cache会存在内存中的数据与磁盘中的数据不一致的问题,我们说保证文件(文件=数据+元数据)一致性其实包含了两个方面:数据一致+元数据一致。
当前 Linux 下以两种方式实现文件一致性:
- Write Through(写穿):向用户层提供特定接口,应用程序可主动调用接口来保证文件一致性;一旦写入不丢失,但以牺牲系统 I/O 吞吐量作为代价。
- Write back(写回):系统中存在定期任务(表现形式为内核线程),周期性地同步文件系统中文件脏数据块,这是默认的 Linux 一致性方案;效率较高,但宕机的情况下有丢失数据的风险。
回写时机:
- 应用程序主动调用回写接口,如下表所示:
| 方法 | 含义 |
| ------ | ------ |
| fsync(intfd) | 将文件的脏数据和脏元数据全部刷新至磁盘 |
| fdatasync(int fd) | 将文件的脏数据刷新至磁盘,同时对必要的元数据(接下来读写文件至关重要的信息,如文件大小等)刷新至磁盘 |
| sync() | 将系统中所有的脏的文件数据及元数据刷新至磁盘 | - 管理线程周期性地唤醒设备上的回写线程进行回写
- 某些应用程序/内核任务发现内存不足时事先进行脏页面回写,回收部分缓存页面