在看内核文件系统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枚举变量里:

  1. enum pageflags {
  2. PG_locked,      /* Page is locked. Don't touch. */
  3. PG_error,
  4. PG_referenced,
  5. PG_uptodate,
  6. PG_dirty,
  7. PG_lru,
  8. PG_active,
  9. PG_slab,
  10. PG_owner_priv_1,    /* Owner use. If pagecache, fs may use*/
  11. PG_arch_1,
  12. PG_reserved,
  13. PG_private,     /* If pagecache, has fs-private data */
  14. PG_private_2,       /* If pagecache, has fs aux data */
  15. PG_writeback,       /* Page is under writeback */
  16. .........
  17. PG_reclaim,     /* To be reclaimed asap */
  18. }

在该文件中定了很多设置page状态的宏定义

比如清理page的” PageUptodate”状态的宏定义 ClearPageUptodate如下:

  1. CLEARPAGEFLAG(Uptodate, uptodate)
  2. #define CLEARPAGEFLAG(uname, lname)                 \
  3. static inline void ClearPage##uname(struct page *page)          \
  4. { clear_bit(PG_##lname, &page->flags); }

再比如清理page状态的writeback状态的test_clear_page_writeback宏定义

  1. TESTSCFLAG(Writeback, writeback)
  2. #define TESTSCFLAG(uname, lname)                    \
  3. TESTSETFLAG(uname, lname) TESTCLEARFLAG(uname, lname)
  4. #define TESTCLEARFLAG(uname, lname)                 \
  5. static inline int TestClearPage##uname(struct page *page)       \
  6. { 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源码如下:

  1. static void __set_page_dirty(struct page *page,
  2. struct address_space *mapping, int warn)
  3. {
  4. unsigned long flags;
  5. spin_lock_irqsave(&mapping->tree_lock, flags);
  6. if (page->mapping) {    /* Race with truncate? */
  7. WARN_ON_ONCE(warn && !PageUptodate(page));
  8. //增加脏页NR_FILE_DIRTY、BDI_DIRTIED
  9. account_page_dirtied(page, mapping);
  10. //增加radix tree的PAGECACHE_TAG_DIRTY脏页统计
  11. radix_tree_tag_set(&mapping->page_tree,
  12. page_index(page), PAGECACHE_TAG_DIRTY);
  13. }
  14. spin_unlock_irqrestore(&mapping->tree_lock, flags);
  15. //标记page所属文件的inode脏
  16. __mark_inode_dirty(mapping->host, I_DIRTY_PAGES);
  17. }
  18. void account_page_dirtied(struct page *page, struct address_space *mapping)
  19. {
  20. trace_writeback_dirty_page(page, mapping);
  21. if (mapping_cap_account_dirty(mapping)) {
  22. //增加脏页NR_FILE_DIRTY
  23. __inc_zone_page_state(page, NR_FILE_DIRTY);
  24. __inc_zone_page_state(page, NR_DIRTIED);
  25. //BDI_RECLAIMABLE加1
  26. __inc_bdi_stat(mapping->backing_dev_info, BDI_RECLAIMABLE);
  27. //BDI_DIRTIED加1
  28. __inc_bdi_stat(mapping->backing_dev_info, BDI_DIRTIED);
  29. task_io_account_write(PAGE_CACHE_SIZE);
  30. current->nr_dirtied++;
  31. this_cpu_inc(bdp_ratelimits);
  32. }
  33. }

启动文件数据传输前执行,清理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函数源码如下:

  1. void end_page_writeback(struct page *page)
  2. {
  3. //如果该page被设置了"Reclaim"标记位,
  4. if (TestClearPageReclaim(page))
  5. rotate_reclaimable_page(page);
  6. //清除掉page writeback标记
  7. if (!test_clear_page_writeback(page))
  8. BUG();
  9. smp_mb__after_clear_bit();
  10. //唤醒在该page的PG_writeback等待队列休眠的进程
  11. wake_up_page(page, PG_writeback);
  12. }

这里整理一下page的 Dirty状态、writeback状态变迁过程

  1. 进程write操作把最新的文件数据写入page文件页,page成脏页,则page被标记Dirty,并且脏页数加
  2. 要把page脏页刷入磁盘文件系统了,则清理page脏页标记,并且脏页数减1
  3. 要把page脏页刷入磁盘文件系统了,page被标记writeback
  4. 进程执行submit_bio把page发送把脏页刷入磁盘的命令
  5. 进程在page的writeback等待队列休眠
  6. 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函数源码

  1. /*内存回收完成后,被标记"reclaimable"的page的数据刷入了磁盘,执行rotate_reclaimable_page->end_page_writeback把该page移动到inactive lru链表尾,下轮内存回收就会释放该page到伙伴系统*/
  2. void rotate_reclaimable_page(struct page *page)
  3. {
  4. //page没有上PG_locked,page不是脏页,page要有acive标记,page没有设置不可回收标记,page要在lru链表
  5. if (!PageLocked(page) && !PageDirty(page) && !PageActive(page) &&
  6. !PageUnevictable(page) && PageLRU(page)) {
  7. struct pagevec *pvec;
  8. unsigned long flags;
  9. //page->count ++
  10. page_cache_get(page);
  11. local_irq_save(flags);
  12. //取出本地cpu lru缓存pagevec
  13. pvec = &__get_cpu_var(lru_rotate_pvecs);
  14. //先尝试把page添加到本地cpu lru缓存pagevec,如果添加后lru缓存pagevec满了,则把lru缓存pagevec中的所有page移动到inactive lru链表
  15. if (!pagevec_add(pvec, page))
  16. pagevec_move_tail(pvec);
  17. local_irq_restore(flags);
  18. }

}

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,看下源码:

  1. static ssize_t generic_perform_write(struct file *file,
  2. struct iov_iter *i, loff_t pos)
  3. {
  4. ext4_write_begin->lock_page(page);
  5. //把write系统调用传入的最新文件数据从用户空间buf复制到page文件页
  6. copied = iov_iter_copy_from_user_atomic(page, i, offset, bytes);
  7. mark_page_accessed(page);//这里标记page最近被访问
  8. ext4_write_end->unlock_page(page);
  9. balance_dirty_pages_ratelimited(mapping);//脏页平衡
  10. }

mark_page_accessed如下:

  1. void mark_page_accessed(struct page *page)
  2. {
  3. //page是inactive的、page有"Referenced"标记、page可回收、page在 lru链表
  4. if (!PageActive(page) && !PageUnevictable(page) &&
  5. PageReferenced(page) && PageLRU(page)) {
  6. //把page从inactive lru链表移动到active lru链表
  7. activate_page(page);
  8. //清理page的"Referenced"标记
  9. ClearPageReferenced(page);
  10. } else if (!PageReferenced(page)) {//page之前没有"Referenced"标记
  11. SetPageReferenced(page);//设置page的"Referenced"标记
  12. }
  13. }

显然,随着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,源码如下:

  1. static ssize_t generic_perform_write(struct file *file,
  2. struct iov_iter *i, loff_t pos)
  3. {
  4. ext4_write_begin->lock_page(page);
  5. //把write系统调用传入的最新文件数据从用户空间buf复制到page文件页
  6. copied = iov_iter_copy_from_user_atomic(page, i, offset, bytes);
  7. mark_page_accessed(page);//这里标记page最近被访问
  8. ext4_write_end->unlock_page(page);
  9. balance_dirty_pages_ratelimited(mapping);//脏页平衡
  10. }

这应该可以说明,在把write系统调用传入的最新文件数据从用户空间buf复制到page文件页过程,是lock_page的。此时其他进程若想访问page文件页的数据,lock_page将会失败,只能等前边完成向page文件页复制最新的文件。本小节涉及的其他函数源码整理如下:

  1. static inline void __set_page_locked(struct page *page)
  2. {
  3. __set_bit(PG_locked, &page->flags);
  4. }
  5. static inline void __clear_page_locked(struct page *page)
  6. {
  7. __clear_bit(PG_locked, &page->flags);
  8. }
  9. //清除page的PG_locked标记位,并唤醒在page PG_locked等待队列的休眠的进程
  10. void unlock_page(struct page *page)
  11. {
  12. VM_BUG_ON(!PageLocked(page));
  13. clear_bit_unlock(PG_locked, &page->flags);//清除page PG_locked标记
  14. smp_mb__after_clear_bit();
  15. wake_up_page(page, PG_locked);//唤醒在page PG_locked等待队列的休眠的进程
  16. }
  17. static inline void lock_page(struct page *page)
  18. {
  19. might_sleep();
  20. if (!trylock_page(page))
  21. __lock_page(page);
  22. }
  23. //尝试对page加锁,如果page之前已经其他进程被加锁则加锁失败返回0,否则当前进程对page加锁成功并返回1
  24. static inline int trylock_page(struct page *page)
  25. {
  26. return (likely(!test_and_set_bit_lock(PG_locked, &page->flags)));
  27. }

PageDirty、PageWriteback、PageReclaim、PageReferenced、PageUptodate等page的各个状态源码讲解相关推荐

  1. mysql page header_MySQL系列:innodb源码分析之page结构解析

    page header是page的头信息,占用38个字节,分别存储以下信息: FIL_PAGE_SPACE            4字节                        page所属的表 ...

  2. WebKit 内核源码分析 (三) Page

    浏览器的请求一般是以页面请求为单位,当用户通过网址栏输入一个url,浏览器就开始一个页面请求.而一个页面请求可能包含有一到多个页面子帧,以及图片.CSS和插件等派生子资源.Page类就是用来对应这样的 ...

  3. page_to_pfn 、virt_to_page、 virt_to_phys、page、页帧pfn、内核虚拟地址、物理内存地址linux内核源码详解

    首先说说内核态虚拟地址和物理内存地址转换关系 #define PAGE_OFFSET     UL(0xffffffc000000000) /* PHYS_OFFSET - the physical ...

  4. Spring源码研究之how is returned hello string converted to jsp page

    Created by Wang, Jerry, last modified on Aug 22, 2016

  5. 附加:PageHelper分页插件的:Page和PageInfo的区别;

    说明: (1)为什么写本篇博客?: ● 在[Spring Boot电商项目29:商品分类模块八:后台的[分类列表(平铺)]接口:]中,实现分页功能时,使用到了PageHelper分页插件: ● 但是, ...

  6. zzfrom水木-Linux环境学习和开发心得(作者:lunker)

    转自水木lunker,非常好的文章,在此鸣谢之. 本人水平有限,如果有错误和遗漏,或者有更好的建议,请大家认真的拍. 强烈建议: 文中涉及的图书最好入手一个英文版的,如果实在阅读有困难,可以在电脑中准 ...

  7. Android So简单加固

    Android下的dex加固技术从最初爱加密实现的dex整体内存加载,到目前各大厂商都在研究的代码虚拟化技术,整体技术已经很成熟了.但是因为Android系统开源的特性,这种加固方法的安全性并不高,只 ...

  8. zeroclipboard浏览器复制插件使用记录

    一个简单例子: <html><body><button id="copy-button" data-clipboard-text="Copy ...

  9. ASP.NET 2.0(C#) - Themes(主题)

    原文链接:http://www.netfocus.cn/article1648.html 作者:webabcd 介绍 使用 ASP.NET 2.0 的"主题"功能,可以将样式和布局 ...

最新文章

  1. 转 Android开发学习笔记:浅谈WebView
  2. 垃圾回收器算法之引用计数器法
  3. 什么是二维数组?二维遍历?Java二维数组制作图片迷宫 使用如鹏游戏引擎制作窗口界面 附带压缩包下载,解压后双击start.bat启动...
  4. 基于ABP落地领域驱动设计-05.实体创建和更新最佳实践
  5. GitHub删除已有文件和提交文件的方法
  6. java查询多条_Mybatis查询多条记录并返回List集合的方法
  7. python 表白程序代码_python抖音表白程序源代码
  8. 用html编写你好,02 - HTML5第一个项目:HelloWorld!(收藏)
  9. angular学习的一些Mark
  10. 【CCCC】L2-031 深入虎穴 (25分),,求多叉树最深的节点编号,大水题!!!
  11. 数据库事务特征、数据库隔离级别,以及各级别数据库加锁情况(含实操)--read uncommitted篇...
  12. Java程序连接数据库
  13. 《The Django Book 2.0》中文版笔记
  14. 一款秒杀wireshark和burpsuite的国产网络分析神器,你还没用过?
  15. 修改版本名称及手机型号
  16. Ubuntu 缩减磁盘空间
  17. 茜在人名可以读xi吗_茜读xi还是qian?
  18. Ubuntu16.04下安装MATLAB 2016b!完美运行
  19. Ansible秘钥认证
  20. 关于yml文件图标为粉红色转成绿色的问题

热门文章

  1. r语言 figure margins too large
  2. 教师教学公开课创意黑板学习PPT模板
  3. 脱毛仪出口欧盟和美国 CE FCC UKCA认证是什么
  4. 单片机 定时器/计数器
  5. 怎么用电脑把qsv格式转换成mp4格式
  6. java基础巩固-宇宙第一AiYWM:为了维持生计,架构知识+分布式微服务+高并发高可用高性能知识序幕就此拉开(三:注册中心、补充CAP定理、BASE 理论)~整起
  7. win10无法自动修复此计算机,Win10提示自动修复无法修复你的电脑的解决方法 win10自动修复无法修复你的电脑怎么办 - 云骑士一键重装系统...
  8. 虚函数绕过 GS保护 学习
  9. 停车场车辆定位管理系统
  10. 新材料细分龙头被严重低估 环球新材国际(6616.HK)具备哪些高成长基因