注:本文分析基于3.10.0-693.el7内核版本,即CentOS 7.4

1、背景

在《BDI writeback脏页回写》中我们了解了BDI的一些基本结构以及初始化和一些触发路径,现在我们要更深入了解下脏页的writeback,一个是脏页什么时候会被处理,另一个是多久会触发脏页的writeback。

2、脏页形成路径

脏页由写操作引入,毕竟读操作不会引起数据的不一致性。不管应用层以什么方式将数据变脏,最终都会调用__mark_inode_dirty将inode标记dirty。而调用__mark_inode_dirty主要有以下几种情况,

2.1 I_DIRTY_PAGES

该情况下,当前page被标记为dirty,但是该page对应的buffer_head并不一定都是dirty状态,因此又可分为自上而下和自下而上。所谓自上而下对应的是,如果整个page都是dirty状态,那么这个page对应的所有buffer_head都要值为dirty;而自下而上对应的就是,如果page里的某个buffer_head标记为dirty,那这个page也要标记为dirty。

自上而下一般通过set_page_dirty操作,

set_page_dirty-> ##标记整个page为dirty__set_page_dirty_buffers-> ##把page对应的buffer_head都标记为dirty__set_page_dirty-> ##标记page为dirty__mark_inode_dirty ##标记inode为dirty

对于自下而上一般通过mark_buffer_dirty操作,

mark_buffer_dirty-> ##标记单个buffer为dirty__set_page_dirty-> ##标记page为dirty__mark_inode_dirty ##标记inode为dirty

2.2 I_DIRTY

这种情况是最常见的,比如我们通过write系统调用写文件时,最后写入page cache后,就形成了脏页,

generic_write_end->mark_inode_dirty->__mark_inode_dirty

2.3 I_DIRTY_SYNC

这种情况一般就是更新文件的各个时间,比如access time、modify time和change time,这需要更新文件的metadata。

update_time->mark_inode_dirty_sync->__mark_inode_dirty

3、脏页的处理

3.1 脏页上链,唤醒writeback进程

__mark_inode_dirty主要做了以下几件事,

  • 对I_DIRTY_SYNC、I_DIRTY_DATASYNC的情况,向jbd2日志模块进行transaction提交
  • 把脏inode挂到BDI设备的b_dirty队列上
  • 判断该inode是否是该BDI第一个dirty inode,并确定是否需要唤醒writeback进程
void __mark_inode_dirty(struct inode *inode, int flags)
{struct super_block *sb = inode->i_sb;struct backing_dev_info *bdi = NULL;/** Don't do this for I_DIRTY_PAGES - that doesn't actually* dirty the inode itself*///如果dirty inode本身有修改,需要向jbd2日志模块transaction进行提交if (flags & (I_DIRTY_SYNC | I_DIRTY_DATASYNC)) {trace_writeback_dirty_inode_start(inode, flags);if (sb->s_op->dirty_inode)sb->s_op->dirty_inode(inode, flags);trace_writeback_dirty_inode(inode, flags);}...//是否开启IO调试信息,用于记录IO回写的具体信息//比如进程号,inode号,文件名和磁盘设备名if (unlikely(block_dump))block_dump___mark_inode_dirty(inode);spin_lock(&inode->i_lock);if ((inode->i_state & flags) != flags) {//确定inode确实是dirty状态const int was_dirty = inode->i_state & I_DIRTY;...if (!was_dirty) {bool wakeup_bdi = false;bdi = inode_to_bdi(inode);spin_unlock(&inode->i_lock);spin_lock(&bdi->wb.list_lock);if (bdi_cap_writeback_dirty(bdi)) {WARN(!test_bit(BDI_registered, &bdi->state),"bdi-%s not registered\n", bdi->name);//判断bdi的b_dirty、b_io、b_more_io队列中是否有脏inode//没有说明这是该BDI设备的第一个脏inode,需要唤醒writeback进程if (!wb_has_dirty_io(&bdi->wb))wakeup_bdi = true;}//记录inode dirty的时间,用于后面计算dirty inode是否该进行回写inode->dirtied_when = jiffies;//把dirty  inode挂到b_dirty队列上list_move(&inode->i_wb_list, &bdi->wb.b_dirty);spin_unlock(&bdi->wb.list_lock);if (wakeup_bdi)//唤醒脏页周期回写进程bdi_wakeup_thread_delayed(bdi);return;}}
out_unlock_inode:spin_unlock(&inode->i_lock);}void bdi_wakeup_thread_delayed(struct backing_dev_info *bdi)
{unsigned long timeout;//定时器周期为dirty_writeback_interval,//即/proc/sys/vm/dirty_writeback_centisecs,默认为5stimeout = msecs_to_jiffies(dirty_writeback_interval * 10);spin_lock_bh(&bdi->wb_lock);if (test_bit(BDI_registered, &bdi->state))//为工作队列设置定时器queue_delayed_work(bdi_wq, &bdi->wb.dwork, timeout);spin_unlock_bh(&bdi->wb_lock);
}

由上可以看出,对于单个BDI设备,每隔5s会唤醒writeback进程回写脏页。

但是我们也看到此时dirty inode只挂到BDI设备的b_dirty队列,但是实际在回写的时候操作的是b_io队列,因此inode dirty之后并不是马上回写,还需要一段时间才会启动,这个时间就是由dirty_expire_centisecs控制。

3.2 唤醒回写进程开始回写

3.2.1 bdi_writeback_workfn

所有前提准备好后,writeback进程被唤醒,此时就走到bdi_writeback_workfn上,这是在BDI初始化时注册的回写处理函数。

基本流程为:

  • 先处理BDI上所有的回写work
  • 处理完后重置定时器,进程睡眠,等待下次唤醒回写
/** Handle writeback of dirty data for the device backed by this bdi. Also* reschedules periodically and does kupdated style flushing.*/
void bdi_writeback_workfn(struct work_struct *work)
{struct bdi_writeback *wb = container_of(to_delayed_work(work),struct bdi_writeback, dwork);struct backing_dev_info *bdi = wb->bdi;long pages_written;set_worker_desc("flush-%s", dev_name(bdi->dev));current->flags |= PF_SWAPWRITE;//如果当前不是一个救援工作队列,或者当前bdi设备已注册,这是一般路径if (likely(!current_is_workqueue_rescuer() ||!test_bit(BDI_registered, &bdi->state))) {do {//从bdi的work_list取出队列里的任务,执行脏页回写pages_written = wb_do_writeback(wb);trace_writeback_pages_written(pages_written);} while (!list_empty(&bdi->work_list));} else {//如果当前workqueue不能获得足够的worker进行处理,//只提交一个work并限制写入1024个pagespages_written = writeback_inodes_wb(&bdi->wb, 1024, WB_REASON_FORKER_THREAD);trace_writeback_pages_written(pages_written);}//如果上面处理完到现在这段间隔又有了work,再次立马启动回写进程if (!list_empty(&bdi->work_list))mod_delayed_work(bdi_wq, &wb->dwork, 0);//如果所有bdi设备上挂的dirty inode回写完,那么就重置定制器,//再过dirty_writeback_interval,即5s后再唤醒回写进程else if (wb_has_dirty_io(wb) && dirty_writeback_interval)bdi_wakeup_thread_delayed(bdi);current->flags &= ~PF_SWAPWRITE;
}

3.2.2 wb_do_writeback

主要是处理已提交的回写work,同时检查并提交周期回写work和background回写work。

基本流程为:

  • 遍历处理该BDI上所有的回写work
  • 检查周期回写进程是否到期,到期则提交回写work
  • 检查background dirty page是否达到阈值,超过则提交回写work
/** Retrieve work items and do the writeback they describe*/
static long wb_do_writeback(struct bdi_writeback *wb)
{struct backing_dev_info *bdi = wb->bdi;struct wb_writeback_work *work;long wrote = 0;//设置bdi状态为正在回写set_bit(BDI_writeback_running, &wb->bdi->state);//遍历BDI里面的work,回写脏页while ((work = get_next_work_item(bdi)) != NULL) {trace_writeback_exec(bdi, work);//回写脏页wrote += wb_writeback(wb, work);if (work->done)//如果是同步写,调用complete函数通知等待的线程,此次写操作已完成complete(work->done);elsekfree(work);}/** Check for periodic writeback, kupdated() style*///如果dirty_writeback_centisecs时间到了,提交一个周期回写的workwrote += wb_check_old_data_flush(wb);//如果dirty page超过dirty_background_ratio/dirty_background_bytes,//提交一个background回写workwrote += wb_check_background_flush(wb);clear_bit(BDI_writeback_running, &wb->bdi->state);return wrote;
}

3.2.3 wb_writeback

主要是处理各个路径下提交的回写work,

基本流程为:

  • 优先处理非周期回写和background回写work
  • 对于background回写work还需要再次检查是否超过对应阈值,期间其他work有可能有回写部分脏页
  • 对于周期回写work设置older_than_this为当前往回拨dirty_expire_interval,便于及时处理dirty inode
  • 把过期的dirty node挂到b_io队列
  • 回写b_io队列上的dirty inode,调用对应文件系统的writepages函数写脏页
  • 回写b_more_io队列上的dirty inode
static long wb_writeback(struct bdi_writeback *wb,struct wb_writeback_work *work)
{unsigned long wb_start = jiffies;long nr_pages = work->nr_pages;unsigned long oldest_jif;struct inode *inode;long progress;oldest_jif = jiffies;//注意这个oldest_jif,对于周期回写时间会往前拨work->older_than_this = &oldest_jif;spin_lock(&wb->list_lock);for (;;) {/** Stop writeback when nr_pages has been consumed*/if (work->nr_pages <= 0)break;//优先处理显式回写work,比如手动sync,内存不足触发的回写//background和周期回写优先级较低,而且他们有可能一直处理if ((work->for_background || work->for_kupdate) &&!list_empty(&wb->bdi->work_list))break;/** For background writeout, stop when we are below the* background dirty threshold*///判断是否需要进行background回写//依据dirty_background_ratio和dirty_background_bytes计算//我们下次再讲if (work->for_background && !over_bground_thresh(wb->bdi))break;if (work->for_kupdate) {//如果是周期回写的work,把时间往回拨dirty_expire_interval,即30s//用于在queue_io时,只处理早于该时间dirty的inode,也就是inode dirty之后,//并不是马上回写,还需要等待dirty_expire_interval,才会开始回写oldest_jif = jiffies - msecs_to_jiffies(dirty_expire_interval * 10);} else if (work->for_background)oldest_jif = jiffies;trace_writeback_start(wb->bdi, work);//如果b_io上没有dirty inode,扫描一遍b_dirty队列if (list_empty(&wb->b_io))//把过期的dirty node挂到b_io队列queue_io(wb, work);//开始具体处理脏页,调用对应文件系统的writepages函数写脏页if (work->sb)progress = writeback_sb_inodes(work->sb, wb, work);elseprogress = __writeback_inodes_wb(wb, work);trace_writeback_written(wb->bdi, work);wb_update_bandwidth(wb, wb_start);/** Did we write something? Try for more** Dirty inodes are moved to b_io for writeback in batches.* The completion of the current batch does not necessarily* mean the overall work is done. So we keep looping as long* as made some progress on cleaning pages or inodes.*/if (progress)continue;/** No more inodes for IO, bail*///如果b_more_io都没有dirty inode需要处理,说明处理完成了if (list_empty(&wb->b_more_io))break;//b_more_io链表存放的一般是回写过程中暂时无法处理的inode//当b_io上都处理完时,再来处理b_more_io队列if (!list_empty(&wb->b_more_io))  {trace_writeback_wait(wb->bdi, work);inode = wb_inode(wb->b_more_io.prev);spin_lock(&inode->i_lock);spin_unlock(&wb->list_lock);/* This function drops i_lock... */inode_sleep_on_writeback(inode);spin_lock(&wb->list_lock);}}spin_unlock(&wb->list_lock);return nr_pages - work->nr_pages;
}

3.2.4 queue_io和move_expired_inodes

主要流程:

  • 遍历b_dirty队列,找出过期的inode挂到tmp队列上
  • 如果b_dirty队列上的inode同属于一个分区,直接将tmp队列join到b_io队列
  • 如果属于不同分区,按分区将inode添加到b_io队列
static void queue_io(struct bdi_writeback *wb, struct wb_writeback_work *work)
{int moved;assert_spin_locked(&wb->list_lock);list_splice_init(&wb->b_more_io, &wb->b_io);//把b_dirty上过期的dirty inode移到b_io上,这部分需要回写了moved = move_expired_inodes(&wb->b_dirty, &wb->b_io, work);trace_writeback_queue_io(wb, work, moved);
}/** Move expired (dirtied before work->older_than_this) dirty inodes from* @delaying_queue to @dispatch_queue.*/
static int move_expired_inodes(struct list_head *delaying_queue,struct list_head *dispatch_queue,struct wb_writeback_work *work)
{LIST_HEAD(tmp);struct list_head *pos, *node;struct super_block *sb = NULL;struct inode *inode;int do_sb_sort = 0;int moved = 0;//遍历b_dirty队列,找出过期的inode挂到tmp队列上while (!list_empty(delaying_queue)) {inode = wb_inode(delaying_queue->prev);//如果当前inode变dirty的时间晚于work上的过期时间,//也就是没过期,不处理if (work->older_than_this &&inode_dirtied_after(inode, *work->older_than_this))break;//过期inode先挂到tmp队列list_move(&inode->i_wb_list, &tmp);moved++;if (sb_is_blkdev_sb(inode->i_sb))continue;//判断这些inode是否是属于同一个超级块,也就是是否是同一个分区if (sb && sb != inode->i_sb)//inode属于不同分区,需要分类do_sb_sort = 1;sb = inode->i_sb;}/* just one sb in list, splice to dispatch_queue and we're done *///如果所有inode都属于同一块分区,就不需要sortif (!do_sb_sort) {//直接把tmp队列join到b_io队列就好了list_splice(&tmp, dispatch_queue);goto out;}/* Move inodes from one superblock together *///如果inode属于不同分区,将tmp中属于同一块分区的inode挂到一起//通过遍历,每次将一个分区的inode放一起,达到分类排序的效果while (!list_empty(&tmp)) {sb = wb_inode(tmp.prev)->i_sb;list_for_each_prev_safe(pos, node, &tmp) {inode = wb_inode(pos);if (inode->i_sb == sb)//将inode挂到b_io队列list_move(&inode->i_wb_list, dispatch_queue);}}
out:return moved;
}

3.2.5 wb_check_old_data_flush

主要流程:

  • 确定是否启用周期回写进程
  • 根据dirty_writeback_centisecs定义的值判断周期回写是否超期
  • 超期后重置时间,并提交周期回写work
static long wb_check_old_data_flush(struct bdi_writeback *wb)
{unsigned long expired;long nr_pages;//dirty_writeback_centisecs为0,则不启动周期回写进程if (!dirty_writeback_interval)return 0;//计算超期时间,间隔为dirty_writeback_centisecsexpired = wb->last_old_flush +msecs_to_jiffies(dirty_writeback_interval * 10);//判断是否超期if (time_before(jiffies, expired))return 0;//重置时间wb->last_old_flush = jiffies;//获取脏页数量nr_pages = get_nr_dirty_pages();//如果有脏页,提交周期回写workif (nr_pages) {struct wb_writeback_work work = {.nr_pages    = nr_pages,.sync_mode  = WB_SYNC_NONE,.for_kupdate    = 1,.range_cyclic  = 1,.reason        = WB_REASON_PERIODIC,};return wb_writeback(wb, &work);}return 0;
}

4、小结

  • dirty_writeback_centisecs
    控制周期回写进程的唤醒时间,默认值为500,单位是厘秒,实际内核中是*10使用,即5s,也就是每隔5秒唤醒脏页回写进程,降低这个值可以把尖峰的写操作削平成多次写操作。

  • dirty_expire_centisecs
    控制dirty inode实际回写的等待时间,默认值是3000,即30s,只有当超过这个值后,内核回写进程才会将dirty数据回写到磁盘

vm内核参数之内存脏页dirty_writeback_centisecs和dirty_expire_centisecs相关推荐

  1. vm内核参数优化设置

    http://www.cnblogs.com/wjoyxt/archive/2014/06/08/3777042.html (1)vm.overcommit_memory 执行grep -i comm ...

  2. 谨慎调整内核参数:vm.min_free_kbytes

    内核参数:内存相关 内存管理从三个层次管理内存,分别是node, zone ,page; 64位的x86物理机内存从高地址到低地址分为: Normal DMA32 DMA.随着地址降低. [root@ ...

  3. Linux内核参数解释

    第1章 内核参数说明 1.1 内核参数列表 kernel.acct acct功能用于系统记录进程信息,正常结束的进程都会在该文件尾添加对应的信息.异常结束是指重启或其它致命的系统问题,不能够记录永不停 ...

  4. linux内核参数汇总

    目录: 目录 linux内核参数配置 内核参数列表 内存参数列表 网络参数列表 linux内核参数配置 Linux在系统运行时修改内核参数(/proc/sys与/etc/sysctl.conf),而不 ...

  5. epub 深入linux内核架构_深入分析Linux内核源代码6-Linux 内存管理(2)

    每天十五分钟,熟读一个技术点,水滴石穿,一切只为渴望更优秀的你! ----零声学院 6.3 内存的分配和回收 在内存初始化完成以后,内存中就常驻有内核映像(内核代码和数据).以后,随着用 户程序的执行 ...

  6. Mysql - 脏页刷新机制

    什么是脏页? Mysql InnoDB表基本都是基于B+树索引进行存储的,而数据存储的最小单元就是数据页(物理存储结构:表 -> 表空间(和索引)-> 段 -> 区 -> 页 ...

  7. 《MySQL实战45讲》——学习笔记12 “InnoDB刷脏页的控制策略“

    本篇介绍MYSQL InnoDB的WAL机制带来的小问题--利用WAL技术,数据库将随机写转换成了顺序写,大大提升了数据库的性能,但也带来了内存脏页的问题: 脏页会被后台线程自动flush,也会由于数 ...

  8. 透明大页相关内核参数_Oracle关于内存参数及大页设置的相关概念和设置之hugepage概念和配置...

    一.概念: Oracle 数据库和 Linux 内存管理 系统中使用的内存越多,管理该内存所需的资源也就越多.对于 Linux 操作系统,通过 Linux kswapd 进程和页表内存结构(针对系统中 ...

  9. linux内存回收 内核参数

    ss -atu| awk '/^tcp/{++S[$2]} END {for(a in S) print a,S[a]}' ps up $(pid)   (RSS:实际内存大小,长驻内存) ps o ...

最新文章

  1. 当产品上线前出了 Bug | 每日趣闻
  2. 每日一皮​:昨天晚上下班回家,一民警迎面巡逻而来。 突然对我大喊:站住!...
  3. 博图wincc连接数据块_西门子博途WINCC 可通过创建画面模板提高编程效率
  4. redis 高可用
  5. Serverless 全能选手,再添一“金”
  6. 编译ffmpeg、SDL、x264开源库 for ubuntu
  7. 磁盘占用率100%——哪些程序可以禁用(详细版)【还讲到独立显卡、集成显卡、双显卡、固态硬盘卡机卡死卡顿解决】
  8. Test failed due to unrecognized service account for this product, please submit initial report for t
  9. 凌晨!科比因坠机离世, 球迷泪崩:凌晨4点,我的青春没了
  10. AUI 滚动视图使用
  11. matlab拟合多自变量函数,matlab曲线拟合公式中含有两个变量,x是自变量,y既是自变量又是因变量,求指导,y=f(x,y)...
  12. 安卓结构能转换成苹果借口吗_在外面开着手机WiFi有多危险你知道吗?
  13. 【分享】“飞书自建“在集简云平台集成应用的常见问题与解决方案
  14. 泽塔云在2022年Gartner中国区超融合市场竞争报告中被评为优秀厂商代表
  15. c2-00支持java_诺基亚双卡双待C2-00亮相
  16. 大数据精准投放平台_大数据精准营销+全媒体精准投放+信息流广告=真实有效得客户...
  17. 如何用无人机倾斜摄影采集影像,完成实景三维重建?
  18. linux u盘更新程序,嵌入式linux下插u盘自动更新的设计
  19. 高通骁龙845的android手机有哪些,2018年骁龙845手机有哪些?骁龙845手机怎么样?...
  20. 计算机组装与维修王利民版,计算机组装与维修.pdf

热门文章

  1. 硅谷:大火是这样烧起来的!
  2. 函数getopt(),及其参数optind
  3. 沈阳师范大学大一上册C语言PTA题目集以及答案(第三章 循环结构程序设计 编程题篇)
  4. GPLT练习集L1 25--32
  5. 嵌入式软件测试之初体验
  6. docker安装konga
  7. 闲人闲谈PS之十一——WBS-BOM的用法
  8. F5虚拟机(LTM Virtual Edition)安装教程
  9. 对MySQL进行新建、修改外键级联删除操作
  10. 男子开设28个黄色网站被捕,一年牟利70余万元