文件缓存page cache

 address_space结构

  1.  host:指向当前 address_space 对象所属的文件 inode 对象
  2. page_tree:用于存储当前文件的 页缓存
  3. rb_boot i_mmap存储着共享该文件页的所有进程的VMA
struct 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 字段中。

1.page cache文件缓存

        linux中几乎所有的文件I/O操作都会依赖于page cache,只有当O_DIRECT标志置位才跳过page cache使用buffer cache.也就是说linux系统的文件I/O往往只会和page cache进行交互,并不会直接和系统内存交互。

文件缓存page cache_第1张图片

2. 文件页反向映射

        若某个物理页是文件页,则该页的结构描述符struct page的mapping成员指向一个address_space结构体。

        了解page cache的都知道address_space结构中的page_tree成员指向的基数树用于维护并存储该文件特定区域的文件缓存页。而在文件页的反向映射中address_space结构体的i_mmap成员指向的的是一个rbtree,该rbtree存储着共享该文件页的所有进程的VMA。所以文件页的反向映射流程如图所示。

文件缓存page cache_第2张图片

 

        文件页的结构描述符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))

问题1.page index和vma的pgoff什么关系?

  1. 在文件中的偏移量为struct page->index
  2. 某一进程中映射的的虚拟地址记为viraddress,通过逆向映射计算出该文件页在该进程中对应的虚拟地址段结构描述符记为VMA。则
viraddress = VMA->vm_start + ((pgoff - vma->vm_pgoff) << PAGE_SHIFT)

文件缓存page cache_第3张图片

 

3.带文件缓存的读写

读文件操作

用户可以通过调用 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. 通过调用 find_get_page 函数查找要读取的文件偏移量所对应的页缓存是否存在如果存在就把页缓存中的数据拷贝到应用程序的内存中。
  2. 否则调用 page_cache_alloc_cold 函数申请一个空闲的内存页作为新的页缓存,并且通过调用 add_to_page_cache_lru 函数把新申请的页缓存添加到文件页缓存和 LRU 队列中(后面会介绍)。
  3. 通过调用 readpage 接口从文件中读取数据到页缓存中,并且把页缓存的数据拷贝到应用程序的内存中。

        一个页缓存中的页如果被修改,那么会被标记成脏页。脏页需要写回到磁盘中的文件块。有两种方式可以把脏页写回磁盘:

(1)手动调用sync()或者fsync()系统调用把脏页写回

(2)pdflush进程会定时把脏页写回到磁盘

同时注意,脏页不能被置换出内存,如果脏页正在被写回,那么会被设置写回标记,这时候该页就被上锁,其他写请求被阻塞直到锁释放。
 

4.小结

        带page cache的常规文件操作,因为在读文件时需要1)将磁盘文件数据拷贝到页缓存,而页缓存页处于内核空间内,不能直接被用户进程进行寻址,所以2)还需要将页缓存中的数据再次拷贝到用户空间对应的内存空间中,这样经过两次数据拷贝,用户进程才完成了对磁盘文件的读取任务。

        为了提高用户进程对磁盘数据处理效率,对于大型磁盘文件处理许多用户进程通过mmap函数来操纵文件,mmap直接将用户空间主存中的物理页作为文件页来与磁盘文件数据页进行映射,而放弃使用页缓存机制(mmap操纵磁盘数据只需进行一次数据拷贝操作)。

文件缓存page cache_第4张图片

 

参考:

一文看懂 | 什么是页缓存(Page Cache)

[内核内存] page cache

[内核内存]反向映射

你可能感兴趣的:(linux内核内存管理,大数据)