在看内核文件系统read/write、pagecache、内存回收相关代码时,多多少少应该看过if(PageDirty(page))、if(PageWriteback(page))、if(PageReclaim(page))、if (PageReferenced(page))、if (PageUptodate(page))、trylock_page这样内核代码,依次判断page是否有” Dirty”、”writeback”、” Reclaim”、” Referenced”、” Uptodate”、”lock”状态。曾经迷茫过,page的这些状态是怎么设置的?又是怎么清理掉的?这些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,详细源码注释见https://github.com/dongzhiyan-stack/kernel-code-comment。
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)));
- }