address_space结构
host
:指向当前 address_space
对象所属的文件 inode
对象page_tree
:用于存储当前文件的 页缓存
rb_boot i_mmap
存储着共享该文件页的所有进程的VMAstruct address_space {
struct inode *host; /* owner: inode, block_device */
struct radix_tree_root page_tree; /* radix tree of all pages */
struct rb_root i_mmap; /* tree of private and shared mappings */
}
从 address_space
对象的定义可以看出,文件的 页缓存
使用了 radix树
来存储。
radix树
:又名基数树,它使用键值(key-value)对的形式来保存数据,并且可以通过键快速查找到其对应的值。内核以文件读写操作中的数据偏移量
作为键,以数据偏移量所在的页缓存
作为值,存储在address_space
结构的page_tree
字段中。
linux中几乎所有的文件I/O操作都会依赖于page cache,只有当O_DIRECT标志置位才跳过page cache使用buffer cache.也就是说linux系统的文件I/O往往只会和page cache进行交互,并不会直接和系统内存交互。
若某个物理页是文件页,则该页的结构描述符struct page的mapping成员指向一个address_space结构体。
了解page cache的都知道address_space结构中的page_tree成员指向的基数树用于维护并存储该文件特定区域的文件缓存页。而在文件页的反向映射中address_space结构体的i_mmap成员指向的的是一个rbtree,该rbtree存储着共享该文件页的所有进程的VMA。所以文件页的反向映射流程如图所示。
文件页的结构描述符struct page的mapping成员指向adress_space,struct address_space的i_mmap成员指向一个rbtree的树根,因为一个共享文件页会被映射到多个进程的VMA中,因此所有的这些VMA都会被插入到上述rbtree树中。
最后只需将struct page的index成员和红黑树中每个节点存储的VMA数据相结合,os就能获取到所有映射了该文件页的进程pid和文件页在对应进程中的虚拟地址。
到此文件页反向映射流程全部打通。
static int rmap_walk_file(struct page *page, struct rmap_walk_control *rwc)
{
///page->index << (PAGE_CACHE_SHIFT - PAGE_SHIFT);
pgoff = page_to_pgoff(page);
mutex_lock(&mapping->i_mmap_mutex);
vma_interval_tree_foreach(vma, &mapping->i_mmap, pgoff, pgoff) {
unsigned long address = vma_address(page, vma);
ret = rwc->rmap_one(page, vma, address, rwc->arg);
cond_resched();
}
}
static inline unsigned long
__vma_address(struct page *page, struct vm_area_struct *vma)
{
pgoff_t pgoff = page_to_pgoff(page);
return vma->vm_start + ((pgoff - vma->vm_pgoff) << PAGE_SHIFT);
}
#define vma_interval_tree_foreach(vma, root, start, last) \
for (vma = vma_interval_tree_iter_first(root, start, last); \
vma; vma = vma_interval_tree_iter_next(vma, start, last))
viraddress = VMA->vm_start + ((pgoff - vma->vm_pgoff) << PAGE_SHIFT)
用户可以通过调用 read
系统调用来读取文件中的数据,其调用链如下:
read()
└→ sys_read()
└→ vfs_read()
└→ do_sync_read()
└→ generic_file_aio_read()
└→ do_generic_file_read()
└→ do_generic_mapping_read()
从上面的调用链可以看出,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:
...
}
do_generic_mapping_read 函数的实现比较复杂,经过精简后,上面代码只留下最重要的逻辑,可以归纳为以下几个步骤:
一个页缓存中的页如果被修改,那么会被标记成脏页。脏页需要写回到磁盘中的文件块。有两种方式可以把脏页写回磁盘:
(1)手动调用sync()或者fsync()系统调用把脏页写回
(2)pdflush进程会定时把脏页写回到磁盘
同时注意,脏页不能被置换出内存,如果脏页正在被写回,那么会被设置写回标记,这时候该页就被上锁,其他写请求被阻塞直到锁释放。
带page cache的常规文件操作,因为在读文件时需要1)将磁盘文件数据拷贝到页缓存,而页缓存页处于内核空间内,不能直接被用户进程进行寻址,所以2)还需要将页缓存中的数据再次拷贝到用户空间对应的内存空间中,这样经过两次数据拷贝,用户进程才完成了对磁盘文件的读取任务。
为了提高用户进程对磁盘数据处理效率,对于大型磁盘文件处理许多用户进程通过mmap函数来操纵文件,mmap直接将用户空间主存中的物理页作为文件页来与磁盘文件数据页进行映射,而放弃使用页缓存机制(mmap操纵磁盘数据只需进行一次数据拷贝操作)。
参考:
一文看懂 | 什么是页缓存(Page Cache)
[内核内存] page cache
[内核内存]反向映射