linux pagecache回收过程page状态详解
首先各个page的状态定义在include/linux/page-flags.h 文件的pageflags枚举变量里:
enum pageflags {
PG_locked, /* Page is locked. Don't touch. */
PG_error,
PG_referenced,
PG_uptodate,
PG_dirty,
PG_lru,
PG_active,
PG_slab,
PG_owner_priv_1, /* Owner use. If pagecache, fs may use*/
PG_arch_1,
PG_reserved,
PG_private, /* If pagecache, has fs-private data */
PG_private_2, /* If pagecache, has fs aux data */
PG_writeback, /* Page is under writeback */
.........
PG_reclaim, /* To be reclaimed asap */
}
在该文件中定了很多设置page状态的宏定义
比如清理page的” PageUptodate”状态的宏定义 ClearPageUptodate如下:
CLEARPAGEFLAG(Uptodate, uptodate)
#define CLEARPAGEFLAG(uname, lname) \
static inline void ClearPage##uname(struct page *page) \
{ clear_bit(PG_##lname, &page->flags); }
再比如清理page状态的writeback状态的test_clear_page_writeback宏定义
TESTSCFLAG(Writeback, writeback)
#define TESTSCFLAG(uname, lname) \
TESTSETFLAG(uname, lname) TESTCLEARFLAG(uname, lname)
#define TESTCLEARFLAG(uname, lname) \
static inline int TestClearPage##uname(struct page *page) \
{ return test_and_clear_bit(PG_##lname, &page->flags); }
设置和清理page的” Dirty”、”writeback”、” Reclaim”、” Referenced”、” Uptodate”、”lock”状态的宏定义及函数整理如下:
page状态
设置page状态
清理page状态
PG_dirty
TestSetPageDirty(page)或set_page_dirty(page)
TestClearPageDirty(page)
PG_reclaim
SetPageReclaim(page)
TestClearPageReclaim(page)
PG_writeback
set_page_writeback(page)
test_set_page_writeback(page)
PG_referenced
SetPageReferenced(page)
mark_page_accessed(page)
PG_locked
__set_page_locked(page) 或trylock_page(page)或lock_page
__clear_page_locked(page)或unlock_page(page)
PG_uptodate
SetPageUptodate(page)
ClearPageUptodate(page
如果你要是找不到这些宏定义,去include/linux/page-flags.h文件查找应该就可以找到。下边就说说设置和清理page的” Dirty”、”writeback”、” Reclaim”、” Referenced”、” Uptodate”、”lock”状态的内核过程,基于ext4文件系统,内核源码3.10.96,详细源码注释见GitHub - dongzhiyan-stack/kernel-code-comment: 3.10.96 内核源代码注释。
1 page的” Dirty”状态 设置和清理过程
以sync wirte 过程为例,标记page脏页 和 脏页数加1,源码流程如下:
vfs_write->do_sync_write->ext4_file_write->generic_file_aio_write->generic_file_buffered_write->generic_perform_write->ext4_write_end->block_write_end->__block_commit_write->mark_buffer_dirty->TestSetPageDirty(page) 和 __set_page_dirty->account_page_dirtied->__inc_zone_page_state(page, NR_FILE_DIRTY)
__set_page_dirty源码如下:
static void __set_page_dirty(struct page *page,
struct address_space *mapping, int warn)
{
unsigned long flags;
spin_lock_irqsave(&mapping->tree_lock, flags);
if (page->mapping) { /* Race with truncate? */
WARN_ON_ONCE(warn && !PageUptodate(page));
//增加脏页NR_FILE_DIRTY、BDI_DIRTIED
account_page_dirtied(page, mapping);
//增加radix tree的PAGECACHE_TAG_DIRTY脏页统计
radix_tree_tag_set(&mapping->page_tree,
page_index(page), PAGECACHE_TAG_DIRTY);
}
spin_unlock_irqrestore(&mapping->tree_lock, flags);
//标记page所属文件的inode脏
__mark_inode_dirty(mapping->host, I_DIRTY_PAGES);
}
void account_page_dirtied(struct page *page, struct address_space *mapping)
{
trace_writeback_dirty_page(page, mapping);
if (mapping_cap_account_dirty(mapping)) {
//增加脏页NR_FILE_DIRTY
__inc_zone_page_state(page, NR_FILE_DIRTY);
__inc_zone_page_state(page, NR_DIRTIED);
//BDI_RECLAIMABLE加1
__inc_bdi_stat(mapping->backing_dev_info, BDI_RECLAIMABLE);
//BDI_DIRTIED加1
__inc_bdi_stat(mapping->backing_dev_info, BDI_DIRTIED);
task_io_account_write(PAGE_CACHE_SIZE);
current->nr_dirtied++;
this_cpu_inc(bdp_ratelimits);
}
}
启动文件数据传输前执行,清理page脏页标记,脏页数减1,源码流程如下:
vfs_write->do_sync_write->ext4_file_write->generic_file_aio_write->generic_write_sync->vfs_fsync_range->ext4_sync_file->filemap_write_and_wait_range->__filemap_fdatawrite_range->do_writepages->generic_writepages->write_cache_pages->clear_page_dirty_for_io(page)->TestClearPageDirty(page)和dec_zone_page_state(page, NR_FILE_DIRTY)
2 page的” writeback”状态 设置和清理过程
启动文件数据传输前执行,设置page的” writeback”状态,源码流程如下:
vfs_write->do_sync_write->ext4_file_write->generic_file_aio_write->generic_write_sync->vfs_fsync_range->ext4_sync_file->filemap_write_and_wait_range->__filemap_fdatawrite_range->do_writepages->generic_writepages->write_cache_pages->__writepage->ext4_writepage->ext4_bio_write_page->set_page_writeback(page)
接着是执行启动page脏页数据落盘,源码流程如下:
vfs_write->do_sync_write->ext4_file_write->generic_file_aio_write->generic_write_sync->vfs_fsync_range->ext4_sync_file->filemap_write_and_wait_range->__filemap_fdatawrite_range->do_writepages->generic_writepages->write_cache_pages->__writepage->ext4_writepage->ext4_bio_write_page->ext4_io_submit->submit_bio
高版本内核流程是
vfs_write->do_sync_write->ext4_file_write->generic_file_aio_write->generic_write_sync->vfs_fsync_range->ext4_sync_file->filemap_write_and_wait_range->__filemap_fdatawrite_range->do_writepages->ext4_writepages->ext4_io_submit->submit_bio
之后进程在page的writeback等待队列休眠,等page数据传输完成被唤醒,源码流程如下:
vfs_write->do_sync_write->ext4_file_write->generic_file_aio_write->generic_write_sync->vfs_fsync_range->ext4_sync_file->filemap_write_and_wait_range->filemap_fdatawait_range->wait_on_page_writeback
等page脏页数据落盘完成,产生硬中断和软中断,软中断里然后清理page的writeback标记,唤醒在该page等待队列休眠的进程,源码流程如下:
blk_done_softirq->scsi_softirq_done->scsi_finish_command->scsi_io_completion->scsi_end_request->blk_update_request->bio_endio->ext4_end_bio->ext4_finish_bio->end_page_writeback->test_clear_page_writeback(page)
end_page_writeback函数源码如下:
void end_page_writeback(struct page *page)
{
//如果该page被设置了"Reclaim"标记位,
if (TestClearPageReclaim(page))
rotate_reclaimable_page(page);
//清除掉page writeback标记
if (!test_clear_page_writeback(page))
BUG();
smp_mb__after_clear_bit();
//唤醒在该page的PG_writeback等待队列休眠的进程
wake_up_page(page, PG_writeback);
}
这里整理一下page的 Dirty状态、writeback状态变迁过程
进程write操作把最新的文件数据写入page文件页,page成脏页,则page被标记Dirty,并且脏页数加
要把page脏页刷入磁盘文件系统了,则清理page脏页标记,并且脏页数减1
要把page脏页刷入磁盘文件系统了,page被标记writeback
进程执行submit_bio把page脏页刷入磁盘
进程在page的writeback等待队列休眠
page脏页刷入磁盘完成,产生硬中断和软中断,软中断里然后清理page的writeback标记,唤醒在该page等待队列休眠的进程
3 page的” Reclaim”状态 设置和清理过程
内存回收,设置page的Reclaim状态,然后把page页数据刷回磁盘,源码流程如下:
shrink_page_list->pageout->SetPageReclaim(page)和ext4_writepage()
等page脏页数据落盘完成,产生硬中断和软中断,软中断里清理page的Reclaim状态,然后把page添加到inactive lru list尾部,下次就先回收这个page。源码流程如下:
blk_done_softirq->scsi_softirq_done->scsi_finish_command->scsi_io_completion->scsi_end_request->blk_update_request->bio_endio->ext4_end_bio->ext4_finish_bio->end_page_writeback->TestClearPageReclaim(page)和rotate_reclaimable_page(page)
看下rotate_reclaimable_page函数源码
/*内存回收完成后,被标记"reclaimable"的page的数据刷入了磁盘,执行rotate_reclaimable_page->end_page_writeback把该page移动到inactive lru链表尾,下轮内存回收就会释放该page到伙伴系统*/
void rotate_reclaimable_page(struct page *page)
{
//page没有上PG_locked,page不是脏页,page要有acive标记,page没有设置不可回收标记,page要在lru链表
if (!PageLocked(page) && !PageDirty(page) && !PageActive(page) &&
!PageUnevictable(page) && PageLRU(page)) {
struct pagevec *pvec;
unsigned long flags;
//page->count ++
page_cache_get(page);
local_irq_save(flags);
//取出本地cpu lru缓存pagevec
pvec = &__get_cpu_var(lru_rotate_pvecs);
//先尝试把page添加到本地cpu lru缓存pagevec,如果添加后lru缓存pagevec满了,则把lru缓存pagevec中的所有page移动到inactive lru链表
if (!pagevec_add(pvec, page))
pagevec_move_tail(pvec);
local_irq_restore(flags);
}
}
4 page的” Referenced”状态 设置和清理过程
当读写page文件时,其实就是访问page,最后都执行mark_page_accessed(),整理的几个流程如下:
vfs_write->do_sync_write->ext4_file_write->generic_file_aio_write->generic_file_buffered_write->generic_perform_write->mark_page_accessed
do_generic_file_read->mark_page_accessed
ext4_readdir->ext4_bread->ext4_getblk->sb_getblk->__getblk->__find_get_block->touch_buffer->mark_page_accessed
还有写文件过程vfs_write->do_sync_write->ext4_file_write->generic_file_aio_write->generic_file_buffered_write->generic_perform_write,看下源码:
static ssize_t generic_perform_write(struct file *file,
struct iov_iter *i, loff_t pos)
{
ext4_write_begin->lock_page(page);
//把write系统调用传入的最新文件数据从用户空间buf复制到page文件页
copied = iov_iter_copy_from_user_atomic(page, i, offset, bytes);
mark_page_accessed(page);//这里标记page最近被访问
ext4_write_end->unlock_page(page);
balance_dirty_pages_ratelimited(mapping);//脏页平衡
}
mark_page_accessed如下:
void mark_page_accessed(struct page *page)
{
//page是inactive的、page有"Referenced"标记、page可回收、page在 lru链表
if (!PageActive(page) && !PageUnevictable(page) &&
PageReferenced(page) && PageLRU(page)) {
//把page从inactive lru链表移动到active lru链表
activate_page(page);
//清理page的"Referenced"标记
ClearPageReferenced(page);
} else if (!PageReferenced(page)) {//page之前没有"Referenced"标记
SetPageReferenced(page);//设置page的"Referenced"标记
}
}
显然,随着page随着被访问的次数增加,page的referenced状态就会发生改变,并且page也会在inactive/active lru链表之间迁移,主要有如下3步:
1 page在inactive lru链表且page无Referenced标记,则设置page的Referenced标记。
2 page在inactive lru链表且page有Referenced标记,则把page移动到active lru链表,并清理掉Referenced标记
3 page在active lru链表且无referenced标记,则把仅仅标记该page的Referenced标记
Referenced标记表示该page被访问了,上边这3步表示了page的3个状态的顺序变迁。一个page在inactive lru链表并且长时间未被访问,第一次有进程访问该page,则只是把page标记Referenced。第2次进程再访问该page,则把该page移动到active lru链表,但清理掉Referenced标记。第3次再有进程访问该page,则标记该page Referenced。如下是转移过程:page在inactive lru(unreferenced)----->page在inactive lru(referenced) ----->page在active lru(unreferenced) ----->page在active lru(referenced)
5 page的” Uptodate”状态 设置和清理
当我们第一次读取文件,需要从磁盘读取各个文件页数据到page文件页,先执行do_generic_file_read->……->ext4_readpages->mpage_readpages->mpage_bio_submit-> submit_bio ,之后就等待磁盘文件数据读取到page文件页指向的内存。文件数据传输完成执行 blk_update_request->bio_endio->mpage_end_io->SetPageUptodate就会设置page的“PageUptodate”状态。那什么时候清理page的“PageUptodate”呢?正常情况并不会清理!
清理page的Uptodate状态执行ClearPageUptodate,但是查看write写文件过程的内核代码时,并没有发现执行ClearPageUptodate呀。我最初是这样想的:进程1在写文件page1页面时,很快把该page的数据刷入磁盘,此时应该要清理掉page的PageUptodate状态吧?然后,进程2读取文件时,要重新从磁盘读取该文件的page1文件页对应的数据,因为page1的PageUptodate状态被清理掉了,需从磁盘重新读取该page的文件数据。等读完数据到page1的页面指向的内存,会执行SetPageUptodate设置page的PageUptodate状态。最后,进程2判断出page1已经是PageUptodate状态,顺利读到page1页面的最新数据并返回。这个理解错误的!
因为进程1写文件page1页面时,是先把用户空间的数据保存到page1对应的内存,然后才会把该page的数据刷入磁盘。等进程2读取page1页面的数据时,page1页面指向的内存已经是最新的文件数据,没有必要再从磁盘读取呀!
所以我认为,page的PageUptodate状态只有在第一次从磁盘读取文件数据到文件页面page时才有效。或者说,只有在读取的那一片磁盘文件数据没有映射的文件页page时才有效:按照读取的文件地址范围,建立磁盘文件与对应文件页page的映射,然后等待本次读取的文件数据到该page指向的内存,page被标记PageUptodate状态,把该page的数据读取到read系统调用传入的buf,就顺利读读到了文件数据。
接着,page的PageUptodate状态应该一直存在,除非page被释放吧。那怎么做到读写该page数据同步呢?要向文件的该page文件页写数据,复制最新的文件数据,lock_page就行了吧,复制完unlock_page。然后read系统调用读取文件时就可以直接读取该page最新的数据了。lock_page的目的是为了防止在修改page文件页数据时,禁止其他进程此时读写该page文件页的数据吧。我的理解对不对呢?下一节讲解
6 page的” lock”状态 设置和清理
对page得 lock操作是执行lock_page(page) 或trylock_page(page) 或 __set_page_locked(page) 宏或函数,对page的unlock操作是执行__clear_page_locked(page) 和 unlock_page(page)宏或函数。读取文件触发文件页page预读时,page文件页内存还不是最新的文件数据,需对page加PG_locked锁。源码流程如下:
do_generic_file_read->page_cache_sync_readahead/page_cache_async_readahead->ondemand_readahead->__do_page_cache_readahead->read_pages->ext4_readpages->mpage_readpages->mpage_readpages->add_to_page_cache_lru->add_to_page_cache->__set_page_locked
之后进程 trylock_page(page) 获取page PG_locked锁,获取失败则在page的PG_locked等待队列休眠。等把文件最新数据读取到该page文件页内存,产生硬中断中断、软中断,软中断回调函数执行blk_update_request->bio_endio->mpage_end_io,里边执行SetPageUptodate(page)设置page的"PageUptodate"状态,还执行unlock_page(page)清理page的PG_locked锁,然后唤醒在page的PG_locked等待队列休眠的进程。在page的PG_locked等待队列休眠被唤醒后,if (PageUptodate(page)) 成立,则先 unlock_page(page) 释放page PG_locked锁,然后把page文件页数据读取read系统调用传入的buf。这个过程在讲解文件预读那篇文章详细解释过,可以看下。
再列举一个过程,在文件write过程,需要先把用户空间传入的最新文件数据写入page文件页过程也会lock_page。看些函数流程,vfs_write->do_sync_write->ext4_file_write->generic_file_aio_write->generic_file_buffered_write->generic_perform_write,源码如下:
static ssize_t generic_perform_write(struct file *file,
struct iov_iter *i, loff_t pos)
{
ext4_write_begin->lock_page(page);
//把write系统调用传入的最新文件数据从用户空间buf复制到page文件页
copied = iov_iter_copy_from_user_atomic(page, i, offset, bytes);
mark_page_accessed(page);//这里标记page最近被访问
ext4_write_end->unlock_page(page);
balance_dirty_pages_ratelimited(mapping);//脏页平衡
}
这应该可以说明,在把write系统调用传入的最新文件数据从用户空间buf复制到page文件页过程,是lock_page的。此时其他进程若想访问page文件页的数据,lock_page将会失败,只能等前边完成向page文件页复制最新的文件。本小节涉及的其他函数源码整理如下:
static inline void __set_page_locked(struct page *page)
{
__set_bit(PG_locked, &page->flags);
}
static inline void __clear_page_locked(struct page *page)
{
__clear_bit(PG_locked, &page->flags);
}
//清除page的PG_locked标记位,并唤醒在page PG_locked等待队列的休眠的进程
void unlock_page(struct page *page)
{
VM_BUG_ON(!PageLocked(page));
clear_bit_unlock(PG_locked, &page->flags);//清除page PG_locked标记
smp_mb__after_clear_bit();
wake_up_page(page, PG_locked);//唤醒在page PG_locked等待队列的休眠的进程
}
static inline void lock_page(struct page *page)
{
might_sleep();
if (!trylock_page(page))
__lock_page(page);
}
//尝试对page加锁,如果page之前已经被其他进程加锁则加锁失败返回0,否则当前进程对page加锁成功并返回1
static inline int trylock_page(struct page *page)
{
return (likely(!test_and_set_bit_lock(PG_locked, &page->flags)));
}
原文地址:https://blog.csdn.net/weixin_43778179/article/details/145283595
免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!