说起脏页回写,大部分文章介绍的是与脏页回写有关的参数,讲解内核源码的并不多,本文主要讲解脏页回写的内核源码,顺便会把这些脏页回写参数与内核源码结合起来。
文章正式开始前,先把这些与脏页回写有关的参数列一下:
1 dirty_writeback_centisecs:脏页回写周期,默认5s执行一次脏页回写。
2 dirty_expire_centisecs:控制脏页在内存中停留的时间,默认30s。就是说,当一个page被标记为脏页后,30s后就要被回写到磁盘。
3 dirty_background_bytes/dirty_background_ratio:控制脏页回写阀值,就是超脏页总数超过这个阀值,内核强制回写脏页直到脏页总数小于脏页阀值。dirty_background_ratio是控制脏页总数占总内存的百分比,dirty_background_bytes是脏页阀值具体字节数,这两个参数其中一个设置过另一个会被强制清0,默认dirty_background_ratio是10。
4 dirty_bytes/dirty_ratio:与dirty_background_bytes/dirty_background_ratio有点像,当脏页总数达到dirty_bytes/dirty_ratio设定的阀值,会调用脏页回写进程回写脏页,此时进程会因此而阻塞。
1 bdi脏页回写涉及的数据结构简介
每一个块设备在驱动初始化时会分配一个块设备专属的运行队列struct request_queue,request_queue有一个成员struct backing_dev_info backing_dev_info,这就是传说中的bdi数据结构,是脏页回写母体吧。这里需要说明一下,每个块设备都有一个唯一的struct backing_dev_info结构,像devicemapper这种虚拟的块设备也是如此。脏页回写进程是以块设备为准执行脏页回写入口函数,不管你是什么文件系统、什么文件。
struct backing_dev_info的成员struct bdi_writeback在实际脏页回写代码中出现频率较高,它的成员都挺关键的,这里先只介绍struct delayed_work dwork,这与脏页回写进程有关。每个块设备的脏页回写就是靠struct delayed_work dwork插入到bdi脏页回写队列,然后脏页回写进程再从这个队列取出dwork,顺藤摸瓜找到块设备的struct backing_dev_info,然后才能进行实际的脏页回写。
struct backing_dev_info、struct bdi_writeback、struct delayed_work dwork的初始化见mmc_init_queue->blk_init_queue->blk_init_queue_node->blk_alloc_queue_node->bdi_init->bdi_wb_init,这里以mmc驱动为例,其他类似。
好了,主要数据结构介绍完了。为了叙述方便,下文struct backing_dev_info、struct
bdi_writeback、struct delayed_work dwork分别以bdi、wb、dwork简称代替。
2 bdi脏页回写的发起
脏页回写与每个块设备的dwork紧密相关,先看下它的初始化:
static void bdi_wb_init(struct bdi_writeback *wb, struct backing_dev_info *bdi)
{
memset(wb, 0, sizeof(*wb));
wb->bdi = bdi;
wb->last_old_flush = jiffies;
INIT_LIST_HEAD(&wb->b_dirty);
INIT_LIST_HEAD(&wb->b_io);
INIT_LIST_HEAD(&wb->b_more_io);
spin_lock_init(&wb->list_lock);
/*初始化wb->dwork, bdi_writeback_workfn 函数指针赋值于
dwork->work->func,并初始化dwork->timer定时器函数delayed_work_timer_fn()*/
INIT_DELAYED_WORK(&wb->dwork, bdi_writeback_workfn);
}
脏页回写的dwork在bdi_wb_init()函数完成初始化,对应注册的函数是bdi_writeback_workfn(),这是脏页回写的入口函数。dwork->timer的内核定时器函数是delayed_work_timer_fn(),脏页回写默认5s执行一次就是靠这个内核定时器。在5s定时到后执行这个函数,把dwork插入到bdi脏页回写队列,脏页回写进程很快再从bdi脏页回写队列取出这个dwork,开始新一轮的脏页回写。
内核初始化时,会执行default_bdi_init()分配struct workqueue_struct *bdi_wq
,这是个work队列,也是前文所说的bdi脏页回写队列相关work。每个块设备的dwork是插入到bdi_wq的脏页回写队列,脏页回写进程运行后,执行线程函数worker_thread()->process_one_work()从bdi_wq脏页回写队列取出块设备的dwork,然后执行注册的函数bdi_writeback_workfn(),进行实际的脏页回写。这里边涉及到了内核workqueue模块,还是有点复杂的,有机会单独写一篇文章。bdi_writeback_workfn()函数源码精简后如下:
void bdi_writeback_workfn(struct work_struct *work)
{
//先通过struct work_struct *work 找到struct delayed_work,再通过struct
delayed_work container_of找到struct bdi_writeback
struct bdi_writeback *wb = container_of(to_delayed_work(work),
struct bdi_writeback, dwork);
struct backing_dev_info *bdi = wb->bdi;
//回写脏页,返回值回写的脏页总数
pages_written = wb_do_writeback(wb, 0);
if (!list_empty(&bdi->work_list))
//这应该是work_list链表有work,立即把dwork加入bdi_wq脏页回写工作队列,立即执行
mod_delayed_work(bdi_wq, &wb->dwork, 0);
else if (wb_has_dirty_io(wb) && dirty_writeback_interval)
/*否则,定时5s后,把dwork加入到bdi_wq脏页回写工作队列,之后脏页回写进程很快
取出该dwork,再次执行bdi_writeback_workfn()函数回写脏页*/
bdi_wakeup_thread_delayed(bdi);
}
具体回写脏页是在wb_do_writeback()函数,它的返回值是回写的脏页总数。接着,一般是执行bdi_wakeup_thread_delayed()函数,定时5s后,把bdi->wb.dwork(即脏页回写的dwork)加入到bdi_wq脏页回写工作队列。之后脏页回写进程很快取出该dwork,再次执行bdi_writeback_workfn()函数。然后根据dwork找到该块设备的wb,进行新一轮的脏页回写。bdi_wakeup_thread_delayed()源码如下。
void bdi_wakeup_thread_delayed(struct backing_dev_info *bdi)
{
unsigned long timeout;
//每5s回写一次脏页,dirty_writeback_interval默认5s
timeout = msecs_to_jiffies(dirty_writeback_interval * 10);
spin_lock_bh(&bdi->wb_lock);
if (test_bit(BDI_registered, &bdi->state))
/*定时5s后,把bdi->wb.dwork加入到bdi_wq脏页回写工作队列,之后脏页回写进程很快取出该
dwork,再次执行bdi_writeback_workfn()函数*/
queue_delayed_work(bdi_wq, &bdi->wb.dwork, timeout);
spin_unlock_bh(&bdi->wb_lock);
}
这里就出现了dirty_writeback_interval,它对应可设置的脏页回写参数dirty_writeback_centisecs,默认是5s。其实没啥神秘的,就是启动一个内核定时器,定时5s后,执行定时器函数delayed_work_timer_fn(),把bdi->wb.dwork插入到bdi_wq脏页回写工作队列,接着脏页回写进程从bdi_wq脏页回写工作队列取出该dwork,执行dwork注册的函数bdi_writeback_workfn(),进行脏页回写。
如果块设备一直有脏页,这种机制保证了每隔5s执行一次bdi_writeback_workfn()函数,直到把它的脏页刷干净。如果块设备没有脏页了,因为bdi_writeback_workfn()函数最后的wb_has_dirty_io(wb)不成立,就不会再执行bdi_wakeup_thread_delayed(bdi)了。自然,该块设备的脏页回写dwork就无法再插入到bdi_wq脏页回写工作队列,该块设备的脏页回写函数bdi_writeback_workfn()就无法再执行了。如果再产生脏页怎么办?此时会执行__mark_inode_dirty()函数,该函数中同样执行bdi_wakeup_thread_delayed()函数。源码如下:
void __mark_inode_dirty(struct inode *inode, int flags)
{//flags监控到有I_DIRTY和I_DIRTY_PAGES两种情况
struct super_block *sb = inode->i_sb;
struct backing_dev_info *bdi = NULL;
/*如果重复设置inode->i_state同一个状态则直接返回。即如果一个inode连续设置I_DIRTY或者
I_DIRTY_PAGES,在这里直接return*/
if ((inode->i_state & flags) == flags)
return;
if ((inode->i_state & flags) != flags) {
//inode之前是否已经设置脏标记,I_DIRTY: bit0、bit1、bit2 置1
const int was_dirty = inode->i_state & I_DIRTY;
inode->i_state |= flags;
//如果inode已经标记过脏,if不成立。
if (!was_dirty) {
bool wakeup_bdi = false;
//返回inode对应文件所在块设备的backing_dev_info结构
bdi = inode_to_bdi(inode);
if (bdi_cap_writeback_dirty(bdi)) {
WARN(!test_bit(BDI_registered, &bdi->state),
"bdi-%s not registered\n", bdi->name);
//如果块设备wb->b_io、wb->b_dirty、wb->b_more_io链表上已经有脏inode,则if不成立,
if (!wb_has_dirty_io(&bdi->wb))
wakeup_bdi = true;
}
//1:更新inode脏时间
inode->dirtied_when = jiffies;
//2:把dirty inode移动到bdi->wb.b_dirty链表
list_move(&inode->i_wb_list, &bdi->wb.b_dirty);
/*定时5s后,把bdi->wb.dwork加入到bdi_wq脏页回写工作队列,之后脏页回写进程很快
取出该dwork,执行bdi_writeback_workfn()回写脏页*/
if (wakeup_bdi)
bdi_wakeup_thread_delayed(bdi);
return;
}
}
}
当我们写文件时,就会执行到__mark_inode_dirty()函数,把该文件对应的inode标记脏:执行inode->dirtied_when = jiffies更新inode的脏时间,然后把inode加入bdi->wb.b_dirty链表。实际进行脏页回写时,是从bdi->wb.b_dirty链表取出脏inode,然后回写该inode上的脏页:由inode找到address_space,然后就能找到对应文件的page cache,脏页就在这里,把里边的数据回写到磁盘。后边也有相关讲解。
需要说明一点,bdi_wakeup_thread_delayed()函数的源码层层分析后,它可能会唤醒脏页回写进程,见__queue_work()->insert_work()->wake_up_worker()。
在块设备初始化时会执行到bdi_start_writeback->__bdi_start_writeback->bdi_queue_work(),这也是把dwork直接插入到bdi_wq脏页回写工作队列,而不是没有定时5s后再插入bdi_wq脏页回写工作队列,源码如下:
static void bdi_queue_work(struct backing_dev_info *bdi,
struct wb_writeback_work *work)
{
//把work加入到bdi->work_list链表
list_add_tail(&work->list, &bdi->work_list);
//把bdi->wb.dwork 加入bdi_wq脏页回写工作队列
mod_delayed_work(bdi_wq, &bdi->wb.dwork, 0);
}
可以发现,还执行了list_add_tail(&work->list, &bdi->work_list)把struct wb_writeback_work *work插入到bdi->work_list。可以去前边看看bdi_writeback_workfn()函数源码,最后有if (!list_empty(&bdi->work_list))判断,这里这是简单提一下。这种情况应该只在块设备初始化时才会成立。
3 bdi脏页回写的关键流程
这里把相关源码先画个流程图,本节基本就是按照这个流程图讲解的。
3.1 wb_do_writeback、wb_check_old_data_flush、wb_check_background_flush函数讲解
前一节提到,脏页回写的入口函数是bdi_writeback_workfn()->wb_do_writeback(),这里把wb_do_writeback()源码列下,有删减。
long wb_do_writeback(struct bdi_writeback *wb, int force_wait)
{
struct backing_dev_info *bdi = wb->bdi;
struct wb_writeback_work *work;
long wrote = 0;
//回写历史脏数据,脏页脏30s就回写就是在这里
wrote += wb_check_old_data_flush(wb);
//脏页数超过阈值则回写脏页走这里
wrote += wb_check_background_flush(wb);
clear_bit(BDI_writeback_running, &wb->bdi->state);
//返回值是回写的脏页总数
return wrote;
}
这个函数执行两个关键子函数: wb_check_old_data_flush()和wb_check_background_flush(),前者负责回写历史脏页脏数据,默认情况脏页在内存中保存30s就要回写到磁盘,后者保证脏页超过阀值后就一直回写脏页直到脏页总数小于阀值。文章后续分别用old_data_flush模式和background_flush模式简称这两种脏页回写。两个函数源码如下:
//回写历史脏数据,脏页脏30s就回写就是在这里
static long wb_check_old_data_flush(struct bdi_writeback *wb)
{
unsigned long expired;
long nr_pages;
//dirty_writeback_interval 默认5s
expired = wb->last_old_flush +
msecs_to_jiffies(dirty_writeback_interval * 10);
/*每两次执行该函数的时间差大于dirty_writeback_interval,才会继续执行,保证
脏页回写进程5s执行一次*/
if (time_before(jiffies, expired))//jiffies<expired返回true
return 0;
wb->last_old_flush = jiffies;
/*获取脏页数,来自nr_pages=NR_FILE_DIRTY+NR_UNSTABLE_NFS+ dirty inodes
,竟然包含脏inode个数*/
nr_pages = get_nr_dirty_pages();
//如果有脏页则执行wb_writeback()回写脏页
if (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,
};
//回写脏页,返回值是回刷的脏页总数,不包含回写完脏页的inode数
return wb_writeback(wb, &work);
}
return 0;
}
//脏页超过阀值则执行wb_writeback()回刷脏页
static long wb_check_background_flush(struct bdi_writeback *wb)
{
//脏页超过阀值返回true
if (over_bground_thresh(wb->bdi)) {
struct wb_writeback_work work = {
.nr_pages = LONG_MAX,
.sync_mode = WB_SYNC_NONE,
.for_background = 1,
.range_cyclic = 1,
.reason = WB_REASON_BACKGROUND,
};
//回写脏页,返回值是回刷的脏页总数,不包含回写完脏页的inode数
return wb_writeback(wb, &work);
}
return 0;
}
可以发现二者流程类似,都是声明一个struct wb_writeback_work work结构,然后按照各自情况填充其成员,然后执行wb_writeback()函数。需要说明一点,wb_check_background_flush()->over_bground_thresh()->global_dirty_limits()判断脏页是否超过阀值,用到了dirty_background_ratio/ dirty_background_bytes,vm_dirty_ratio/vm_dirty_bytes这几个可设置的脏页回写参数,源码简单列下:
void global_dirty_limits(unsigned long *pbackground, unsigned long *pdirty)
{
unsigned long background;
unsigned long dirty;
unsigned long uninitialized_var(available_memory);
struct task_struct *tsk;
if (!vm_dirty_bytes || !dirty_background_bytes)
available_memory = global_dirtyable_memory();
if (vm_dirty_bytes)
dirty = DIV_ROUND_UP(vm_dirty_bytes, PAGE_SIZE);
else
dirty = (vm_dirty_ratio * available_memory) / 100;
//dirty_background_bytes 和 dirty_background_ratio都表示脏页阀值,对应/proc目录设置脏页阀值
if (dirty_background_bytes)
background = DIV_ROUND_UP(dirty_background_bytes, PAGE_SIZE);
else
background = (dirty_background_ratio * available_memory) / 100;
if (background >= dirty)
background = dirty / 2;
tsk = current;
if (tsk->flags & PF_LESS_THROTTLE || rt_task(tsk)) {
background += background / 4;
dirty += dirty / 4;
}
*pbackground = background;
*pdirty = dirty;
//这个trace可以直接打印脏页总数
trace_global_dirty_state(background, dirty);
}
3.2 wb_writeback和queue_io函数讲解
继续讲解脏页回写,wb_writeback()实际完成脏页回写,源码精简后如下:
//回刷脏数据的核心入口函数,返回值是回刷的脏页总数,不包含回写完脏页的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;
//work->older_than_this指向局部变量oldest_jif
work->older_than_this = &oldest_jif;
for (;;) {
//回写脏页数达到预期则brask
if (work->nr_pages <= 0)
break;
/*background_flush模式work->for_background是1,这里是判断脏页总数
小于脏页阀值则停止回写脏页*/
if (work->for_background && !over_bground_thresh(wb->bdi))
break;
/*old_data_flush回写历史脏页模式,oldest_jif表示的时间是30s前,
work->older_than_this指向它,下边的queue_io()函数会用oldest_jif来判断一个
脏inode是否脏了30s,超过了30s就要回写该inode上的脏页*/
if (work->for_kupdate) {
oldest_jif = jiffies -
msecs_to_jiffies(dirty_expire_interval * 10);
/*background_flush超过脏页阀值则回写脏页模式,oldest_jif赋值当前时间,
表示只要是inode脏了,不管脏了多久,都回写它的脏页*/
} else if (work->for_background)
oldest_jif = jiffies;
trace_writeback_start(wb->bdi, work);
//如果wb->b_io空,把wb->b_more_io或者wb->b_dirty上的dirty inode移动到wb->b_io
if (list_empty(&wb->b_io))
queue_io(wb, work);
if (work->sb)//正常回写脏页一般不走这里,但是也有抓到执行过
progress = writeback_sb_inodes(work->sb, wb, work);
else/*从wb->b_io链表取出一个个脏inode,回刷inode的脏数据,返回值是回
刷的脏页数+脏页回写完的inode数*/
progress = __writeback_inodes_wb(wb, work);
trace_writeback_written(wb->bdi, work);
wb_update_bandwidth(wb, wb_start);
//实际测试下来progress大部分情况大于0。如果回写脏页数是0,progress就是0
if (progress)
continue;
//b_more_io保存临时没来得及传输的inode,下次传输
if (list_empty(&wb->b_more_io))
break;
if (!list_empty(&wb->b_more_io)) {
trace_writeback_wait(wb->bdi, work);
//从wb->b_more_io取出inode
inode = wb_inode(wb->b_more_io.prev);
//休眠等待inode脏页回写完成
inode_sleep_on_writeback(inode);
}
}
spin_unlock(&wb->list_lock);
/*返回值是回刷的脏页总数,不包含回写完脏页的inode数,与
__writeback_inodes_wb()和writeback_sb_inodes()的返回值不一样*/
return nr_pages - work->nr_pages;
}
首先看到了dirty_expire_interval,它对应可设置的脏页参数dirty_expire_centisecs,默认30s,表示脏页在内存中停留30s就要被回写到磁盘。
wb_writeback()函数循环调用__writeback_inodes_wb()或writeback_sb_inodes()回写脏页,这个稍后讲解。如果回写脏页数达到预期,则跳出循环,见if (work->nr_pages <= 0)那里的判断。还有就是background_flush模式,如果脏页总数超过已经小于脏页阀值则也会跳出循环,见if (work->for_background && !over_bground_thresh(wb->bdi))那个判断。还有一个重点函数是queue_io(wb, work),它负责把wb->b_more_io或者wb->b_dirty上的dirty inode移动到wb->b_io链表,这些链表都啥意思?先看下源码:
//把wb->b_more_io或者wb->b_dirty上的dirty inode移动到wb->b_io
static void queue_io(struct bdi_writeback *wb, struct wb_writeback_work *work)
{
int moved;
assert_spin_locked(&wb->list_lock);
//把wb->b_more_io上的dirty inode移动到wb->b_io
list_splice_init(&wb->b_more_io, &wb->b_io);
//把wb->b_dirty上的dirty inode移动到wb->b_io
moved = move_expired_inodes(&wb->b_dirty, &wb->b_io, work);
trace_writeback_queue_io(wb, work, moved);
}
static int move_expired_inodes(
struct list_head *delaying_queue,//即wb->b_dirty链表
struct list_head *dispatch_queue,//即wb->b_io链表
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;
while (!list_empty(delaying_queue)) {
inode = wb_inode(delaying_queue->prev);
/*inode_dirtied_after():old_data_flush模式,inode脏时间>=30s返回
false。background_flush模式只要inode是脏inode就返回false。返回false,就
把inode从wb->b_dirty链表移动到wb->b_io链表*/
if (work->older_than_this &&
inode_dirtied_after(inode, *work->older_than_this))
break;
//如果inode属于不同的超级快则do_sb_sort=1
if (sb && sb != inode->i_sb)
do_sb_sort = 1;
sb = inode->i_sb;
//把inode移动到tmp
list_move(&inode->i_wb_list, &tmp);
//每移动一个inode则moved加1
moved++;
}
//如果inode都是一个超级快的,直接把所有inode移动到dispatch_queue链表
if (!do_sb_sort) {
list_splice(&tmp, dispatch_queue);
goto out;
}
//如果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);
//属于同一个超级块的inode彼此挨着放到dispatch_queue链表
if (inode->i_sb == sb)
list_move(&inode->i_wb_list, dispatch_queue);
}
}
out:
//返回值是移动到dispatch_queue链表(即wb->b_io链表)的脏inode数
return moved;
}
//old_data_flush模式,inode脏时间>=30s返回false。background_flush模式只要inode是脏inode就返回false.
static bool inode_dirtied_after(struct inode *inode, unsigned long t)
{
/*old_data_flush模式,t=jiffes-30,如果inode的脏时间
inode->dirtied_when > jiffes-30返回true,否则返回false。就是说,如果
inode脏时间>=30s返回false,inode脏时间<30s返回true。background_flush
模式t=jiffes,只要inode脏时间<=jiffies返回false*/
bool ret = time_after(inode->dirtied_when, t);
#ifndef CONFIG_64BIT
/*
* For inodes being constantly redirtied, dirtied_when can get stuck.
* It _appears_ to be in the future, but is actually in distant past.
* This test is necessary to prevent such wrapped-around relative times
* from permanently stopping the whole bdi writeback.
*/
//这里我觉得这里返回false可能性很低
ret = ret && time_before_eq(inode->dirtied_when, jiffies);
#endif
return ret;
}
前文提过,在写文件产生脏页时,会执行__mark_inode_dirty()把inode加入wb->b_dirty链表,queue_io()->move_expired_inodes()就负责把wb->b_dirty链表上的inode移动到wb->b_io链表,之后执行的__writeback_inodes_wb()或writeback_sb_inodes()正是从wb->b_io链表取出脏inode,回写该inode上的脏页。move_expired_inodes()有两种情况,如果是old_data_flush模式,即要把脏30s的inode从wb->b_dirty链表移动到wb->b_io链表;如果是background_flush模式,则不管三七二十一直接把wb->b_dirty链表上的inode移动到wb->b_io链表。
wb->b_more_io 有什么存在意义?当从wb->b_io链表选出的inode一次脏页回写没把脏页全部回写完,要把inode移动wb->b_more_io链表,下轮脏页回写把wb->b_more_io链表上的脏inode移动到wb->b_io,再从wb->b_io链表取出该inode继续进行脏页回写。可以看下queue_io()源码,是先执行list_splice_init(&wb->b_more_io, &wb->b_io)把wb->b_more_io链表上的inode移动到wb->b_io,就是这个道理。
这里再插一句,在执行__mark_inode_dirty()把inode加入wb->b_dirty链表前,是先inode->dirtied_when=jiffies设置inode脏时间。wb->b_dirty链表上的inode是按照inode->dirtied_when由大到小的从左到右顺序排列。毕竟后插入wb->b_dirty链表头的inode,inode->dirtied_when是最新的系统时间,也是最大的。
3.3 __writeback_inodes_wb和writeback_sb_inodes函数讲解
回到wb_writeback()函数,上一小节提到该函数中实际执行__writeback_inodes_wb()和writeback_sb_inodes()函数进行脏页回写,__writeback_inodes_wb()函数中其实也是循环调用writeback_sb_inodes(),源码如下:
static long __writeback_inodes_wb(struct bdi_writeback *wb,
struct wb_writeback_work *work)
{
unsigned long start_time = jiffies;
long wrote = 0;
while (!list_empty(&wb->b_io)) {
struct inode *inode = wb_inode(wb->b_io.prev);
wrote += writeback_sb_inodes(sb, wb, work);
}
//返回值是回写脏页数+回写完脏页的inode数
return wrote;
}
所以重点还是writeback_sb_inodes()函数,源码精简后如下:
//从wb->b_io取出一个个脏inode,回写inode上的脏页,返回值是回写脏页数+回写完脏页的inode数
static long writeback_sb_inodes(struct super_block *sb,
struct bdi_writeback *wb,
struct wb_writeback_work *work)
{
struct writeback_control wbc = {
.sync_mode = work->sync_mode,
.tagged_writepages = work->tagged_writepages,
.for_kupdate = work->for_kupdate,
.for_background = work->for_background,
.range_cyclic = work->range_cyclic,
.range_start = 0,
.range_end = LLONG_MAX,
};
unsigned long start_time = jiffies;
long write_chunk;
long wrote = 0; /* count both pages and inodes */
while (!list_empty(&wb->b_io)) {
/*从wb->b_io.prev取出有脏页的inode,这是先从wb->b_io的链表尾取出脏inode,为什么?因为链表尾
的inode脏的更久,更应该先回写它的脏页。*/
struct inode *inode = wb_inode(wb->b_io.prev);
//如果inode是I_NEW状态,或者inode对应文件关闭正在释放文件数据
if (inode->i_state & (I_NEW | I_FREEING | I_WILL_FREE)) {
spin_unlock(&inode->i_lock);
//把inode移动到wb->b_dirty,终止本轮脏页回写
redirty_tail(inode, wb);
continue;
}
/*如果inode的脏页已经在回写,并且是wb_check_old_data_flush和
wb_check_background_flush发起的脏页回写*/
if ((inode->i_state & I_SYNC) && wbc.sync_mode != WB_SYNC_ALL) {
//把inode->i_wb_list移动到到wb->b_more_io,终止本轮脏页回写
requeue_io(inode, wb);
continue;
}
//inode->i_state设置I_SYNC标记,表示在进行脏页回写
inode->i_state |= I_SYNC;
//计算本次预期回写脏页数
write_chunk = writeback_chunk_size(wb->bdi, work);
//wbc.nr_to_write初值是本次预期回写脏页数
wbc.nr_to_write = write_chunk;
wbc.pages_skipped = 0;
__writeback_single_inode(inode, &wbc);//这里真正回写脏页
/*write_chunk是本次循环预期回写脏页数,wbc.nr_to_write是还剩下的
待回写的脏页数,相减就是已经回写的脏页数*/
work->nr_pages -= write_chunk - wbc.nr_to_write;
wrote += write_chunk - wbc.nr_to_write;//wrote累计回刷的脏数
//如果inode的脏页回写完成了,wrote还要加1
if (!(inode->i_state & I_DIRTY))
wrote++;
/*如果inode还有脏页,把inode移动到到wb->b_more_io或者wb->b_dirty。
如果inode没有脏页则把inode从脏页链表清除掉*/
requeue_inode(inode, wb, &wbc);
//清除I_SYNC,wake_up_bit(&inode->i_state, __I_SYNC)
inode_sync_complete(inode);
if (wrote) {
/*HZ/10=100,就是100ms,jiffes>start_time+100返回true,最大回
写时间100ms,为了避免长时间回写脏数据影响其他进程*/
if (time_is_before_jiffies(start_time + HZ / 10UL))
break;
if (work->nr_pages <= 0)
break;
}
}
//这里的返回值wrote是回写脏页数+回写完脏页的inode数
return wrote;
}
简单来说,writeback_sb_inodes()函数就是循环从wb->b_io链表取出一个个脏inode,然后执行__writeback_single_inode(),这里边会调用文件系统有关接口函数实际把脏页回写到磁盘文件系统。__writeback_single_inode()调用结束后,会执行requeue_inode()函数,根据inode的脏页状态,把inode加入wb->b_more_io或者wb->b_dirty链表,或者inode没有脏页了直接把inode从这些链表中全部剔除掉,源码下一小节讲解。有一点需要注意,就是writeback_sb_inodes()函数的返回值是回写脏页数+回写完脏页的inode数,这点挺奇怪的,为什么要加上回写完脏页的inode数?
3.4 requeue_inode和redirty_tail函数讲解
先把requeue_inode()源码贴下,有删减。
/*如果inode还有脏页,把inode移动到到wb->b_more_io或者wb->b_dirty。Inode
没有脏数据则把inode从脏页链表清除掉*/
static void requeue_inode(struct inode *inode, struct bdi_writeback *wb,
struct writeback_control *wbc)
{
//如果inode有脏页page
if (mapping_tagged(inode->i_mapping, PAGECACHE_TAG_DIRTY)) {
//wbc->nr_to_write <= 0 表示本次预期的脏数据已经刷回磁盘了,一般不成立吧
if (wbc->nr_to_write <= 0) {
/*把inode临时移动到wb->b_more_io,下次回写脏页再把wb->b_more_io
链表上的脏inode移动到wb->b_io链表*/
requeue_io(inode, wb);
} else {
//否则把inode移动到wb->b_dirty链表
redirty_tail(inode, wb);
}
}//如果没有脏页,但是inode有脏标记,把inode移动到wb->b_dirty链表
else if (inode->i_state & I_DIRTY) {
redirty_tail(inode, wb);
} else {
//inode没有脏页,把inode从脏页链表wb->b_more_io或者wb->b_dirty清除掉
list_del_init(&inode->i_wb_list);
}
}
writeback_sb_inodes()->__writeback_single_inode()调用结束后,会执行requeue_inode()函数。此时如果inode上还有脏页,则把inode加入wb->b_more_io链表,等下轮回写脏页继续,把该从wb->b_more_io链表移动到wb->b_io,再从wb->b_io取出该inode,继续回写该inode的脏页;如果inode上没有脏页,但inode还有脏标记,则还要执行redirty_tail()把inode移动到wb->b_dirty;如果inode没有脏页了,则要把inode从wb->b_more_io或者wb->b_io或者wb->b_dirty链表全部剔除,没有脏页了,不再有牵涉关系。redirty_tail()函数源码如下:
/*重新把inode移动到wb->b_dirty链表,并可能会再次更新inode的脏时间
inode->dirtied_when*/
static void redirty_tail(struct inode *inode, struct bdi_writeback *wb)
{
assert_spin_locked(&wb->list_lock);
if (!list_empty(&wb->b_dirty)) {
struct inode *tail;
//取出wb->b_dirty链表上的第一个脏inode
tail = wb_inode(wb->b_dirty.next);
/*如果inode的脏时间比wb->b_dirty链表上的第一个脏inode的脏时间还小还
老,则把inode的脏时间更新为当前时间*/
if (time_before(inode->dirtied_when, tail->dirtied_when))
inode->dirtied_when = jiffies;
}
/*把inode移动到wb->b_dirty链表头,wb->b_dirty链表头的inode脏时间肯定
是最新的,最大的*/
list_move(&inode->i_wb_list, &wb->b_dirty);
}
3.5 __writeback_single_inode函数讲解
__writeback_single_inode()函数源码简单多了,源码如下,有删减:
static int
__writeback_single_inode(struct inode *inode, struct writeback_control *wbc)
{
struct address_space *mapping = inode->i_mapping;
long nr_to_write = wbc->nr_to_write;
unsigned dirty;
int ret;
//调用文件系统的接口把脏页数据回写到磁盘文件系统
ret = do_writepages(mapping, wbc);
//这里先把inode脏标记清除掉
dirty = inode->i_state & I_DIRTY;
inode->i_state &= ~I_DIRTY;
//如果inode还有脏页则再给inode加上I_DIRTY_PAGES标记
if (mapping_tagged(mapping, PAGECACHE_TAG_DIRTY))
inode->i_state |= I_DIRTY_PAGES;
return ret;
}
里边的do_writepages()其实是调用文件系统有关的接口把脏页数据回写到磁盘文件系统,这里不再介绍。当回写完脏页,则inode->i_state &= ~I_DIRTY清除inode的脏标记,然后再判断该inode的address_space还有没有脏页,有脏页则还要inode->i_state |= I_DIRTY_PAGES给inode加上I_DIRTY_PAGES脏标记。
关于I_DIRTY、I_DIRTY_PAGES,这里简单介绍一下:
#define I_DIRTY_SYNC (1 << 0)//inode脏
#define I_DIRTY_DATASYNC (1 << 1)
#define I_DIRTY_PAGES (1 << 2)//只是有脏页,inode可能是干净的
#define I_DIRTY (I_DIRTY_SYNC | I_DIRTY_DATASYNC | I_DIRTY_PAGES)
__mark_inode_dirty()经常看到传递进去的flags是I_DIRTY或I_DIRTY_PAGES,I_DIRTY是I_DIRTY_PAGES的大集合,I_DIRTY_PAGES表示有脏页,但并不一定inode脏。
总结
可以发现内核脏页回写流程还是稍微有点复杂的,但是关键的流程其实也还好,把关键函数流程掌握清楚了就比较简单了。遇到过一例问题,就是脏页有好几十G过了很长时间还是降不下去,最后发现问题出在块设备的脏页回写的dwork上,还是挺有意思的。
水平有限,如有错误请指出。更详细的内核源码注释见https://github.com/dongzhiyan-stack/kernel-code-comment。