Linux 采用内存页来缓存磁盘文件内容,从而提高系统整体IO访问性能,这就是我们熟知的pagecache机制,对于进程的一次写文件操作,内核只是简单的把修改写到内存,并把页面标记为脏页,然后直接返回,具体的回写操作,由内核周期性的启动线程来完成,这个我们称为writeback机制。
1 脏页的产生
修改文件内容和属性都会产生脏页,当进程调用write进行写操作时,最终会通过set_page_dirty函数标记脏页,并唤醒内核后台回写线程。
如果进程只写了page中的一部分,是否有必要回写整个页了?当然是没必要的,我们只需要标记page中的某个block为脏页就可以了,所以脏页的标记有两种模式
自上往下标记
当一个page都是脏页时,会同步标记所有的buffer_head为dirty状态。
set_page_dirty-> /*标记整个page为dirty*
__set_page_dirty_buffers-> /*把page对应的buffer_head标记为dirty*/
__set_page_dirty-> /*标记page为dirty*/
__mark_inode_dirty /*标记inode为dirty*/
自下往上标记
当一个page的部分buffer_head为dirty状态时,没必要回写所有的buffer_head
mark_buffer_dirty-> /*标记单个buffer为dirty*/
__set_page_dirty-> /*标记page为dirty*/
__mark_inode_dirty /*标记inode为dirty*/
2 writeback回写时机
既然数据是异步落盘,那么内核必须要在适当的时机,把内存中的数据写回磁盘,内核总体有五种情况会触发回写:
1)当系统显示执行sync操作,或者进程调用fsync系统调用时,强制脏数据落盘;
2)内核周期性(for_kupdate 5秒)的启动回写线程(wb_workfn,dirty_writeback_centisecs = 500),回刷驻留时间超过dirty_expire_centisecs(3000)30秒的脏页。
3) 内核后台(for_background)检查脏页比例达到系统可用内存的vm.dirty_background_ratio(10%)时,就会回写时间超过dirty_expire_centisecs的脏页;此时业务进程写脏页仍然不受影响;
4)当进程write数据时,检查脏页比例达到系统可用内存的dirty_ratio(20%)时,阻塞当前写进程,然后进行脏页平衡(balance_dirty_pages_ratelimited),唤醒后台回写进程,回写时间超过dirty_expire_centisecs的脏页
5)内存紧张时,业务进程申请内存触发direct reclaim,会直接唤醒kworker线程(wakeup_flusher_threads)
6) 最后一种回写触发时保证dirtytime类型的inode能够被回写,一般要12小时触发一次kworker线程
3.数据结构与初始化
每块磁盘对应着一个BDI设备,用struct backing_dev_info表示,系统上所有BDI设备通过struct list_head bdi_list链表串在一起,一个BDI设备对应一个struct bdi_writeback结构,存放该设备需要处理的脏页及脏页回写函数,而一次具体的回写操作由wb_writeback_work表示。
3.1数据结构关系
3.2 注册和初始化
bdi在add_disk()函数中进行注册,以链表的形式组织到全局变量bdi_list中;bdi初始化过程中会创建名为writeback的bdi_wq,提供回写的kworker;request queue分配过程中会初始化bdi;
staticint __init default_bdi_init(void)
{
bdi_wq = alloc_workqueue("writeback", WQ_MEM_RECLAIM | WQ_FREEZABLE |
WQ_UNBOUND | WQ_SYSFS, 0);
}
每个request_queue队列对应一个BDI设备
blk_mq_init_queue->blk_alloc_queue_node
struct request_queue *blk_alloc_queue_node(gfp_t gfp_mask, int node_id)
{
q->backing_dev_info = bdi_alloc_node(gfp_mask, node_id);
err = bdi_init(&q->backing_dev_info);
}
回写线程初始化
static int wb_init(struct bdi_writeback *wb, struct backing_dev_info *bdi,
int blkcg_id, gfp_t gfp)
{
wb->bdi = bdi;
/* 脏inode链表*/
INIT_LIST_HEAD(&wb->b_dirty);
/*超过dirty_expire_centisecs时间的inode链表,回写进程直接从b_io取脏页回写 */
INIT_LIST_HEAD(&wb->b_io);
/* 更多需要被回写的inode链表*/
INIT_LIST_HEAD(&wb->b_more_io);
/* time被修改的inode链表*/
INIT_LIST_HEAD(&wb->b_dirty_time);
/* 待回写work链表*/
INIT_LIST_HEAD(&wb->work_list);
/* 回写work线程*/
INIT_DELAYED_WORK(&wb->dwork, wb_workfn);
wb->dirty_sleep = jiffies;
}
4.回写线程wb_workfn分析
函数调用层级关系
五种链表,三种模式
五种链表
bdi_writeback结构体中有5个关键链表,实现了脏页的周期性回写
work_list: BDI设备需要高优先级回写的任务
b_dirty:mark_inode_dirty标记的脏inode直接链接到b_dirty
b_bio:回写线程需要回刷脏页时,会从b_ditry链表截取满足回写模式的超时要求的inode到b_bio链表
b_more_io:当inode的状态与work回写模式不匹配时,会先将当前回写的inode链表挂入b_more_io
b_dirty_time: 需要修改meta信息的inode链表
回写模式
从函数调用上看,可以把理解有三种回写模式
for_kupdate:周期性的回刷(5秒),会把dirty 时间超过30s的inode进行回写(b_dirty->b_bio)
for_background: 当系统的dirty page超过dirtyable memory的dirty_background_ratio(10%)时,开始backgound writeback,当系统的dirty page低于dirtyable memory的10%时,停止回写。这里dirtyable memory(最大可变成脏页的内存数)=total_free_page + inactive_file + active_file - totalreserve_pages
writeback_work回刷:如果非回刷线程(比如sync)需要回写数据,可以构造bdi_writeback_work,回刷线程会优先回刷,且不会检查inode的dirty时间。
kupdate保证dirty page能够及时回写,避免数据丢失风险,而background模式则保证系统总的dirty page保持在一定阈值以下。
代码分析
1.wb_workfn 线程
void wb_workfn(struct work_struct *work)
{
struct bdi_writeback *wb = container_of(to_delayed_work(work),
struct bdi_writeback, dwork);
set_worker_desc("flush-%s", dev_name(wb->bdi->dev));
current->flags |= PF_SWAPWRITE;
if (likely(!current_is_workqueue_rescuer() ||
!test_bit(WB_registered, &wb->state))) {
do {
/* 调用wb_do_writeback回写*/
pages_written = wb_do_writeback(wb);
trace_writeback_pages_written(pages_written);
} while (!list_empty(&wb->work_list));
}
/*最后检测一次work_list,work_list都是高优先级的回刷任务,需要立即执行 */
if (!list_empty(&wb->work_list))
wb_wakeup(wb);
/* 如果wb上有脏页,且使能了周期性的回刷,则启动Delay work进行下一个周期的回刷*/
else if (wb_has_dirty_io(wb) && dirty_writeback_interval)
wb_wakeup_delayed(wb);
current->flags &= ~PF_SWAPWRITE;
}
2.wb_do_writeback
static long wb_do_writeback(struct bdi_writeback *wb)
{
struct wb_writeback_work *work;
long wrote = 0;
set_bit(WB_writeback_running, &wb->state);
/* 优先处理work_list的回刷任务*/
while ((work = get_next_work_item(wb)) != NULL) {
trace_writeback_exec(wb, work);
wrote += wb_writeback(wb, work);
finish_writeback_work(wb, work);
}
/*
* Check for a flush-everything request
*/
/*一般是内存回收触发的回刷 */
wrote += wb_check_start_all(wb);
/* 检测周期性的回刷*/
wrote += wb_check_old_data_flush(wb);
/*检测background回刷 */
wrote += wb_check_background_flush(wb);
clear_bit(WB_writeback_running, &wb->state);
return wrote;
}
3.wb_writeback
static long wb_writeback(struct bdi_writeback *wb,
struct wb_writeback_work *work)
{
unsigned long wb_start = jiffies;
long nr_pages = work->nr_pages;
oldest_jif = jiffies;
work->older_than_this = &oldest_jif;
spin_lock(&wb->list_lock);
for (;;) {
/*work定义的page已经回刷完成 */
if (work->nr_pages <= 0)
break;
/*如果work_list上存在高优先级的回刷任务,则停止background和kupdate回刷 */
if ((work->for_background || work->for_kupdate) &&
!list_empty(&wb->work_list))
break;
/*如果系统dirty page低于dirty_background_ratio定义的值,则结束background回写 */
if (work->for_background && !wb_over_bg_thresh(wb))
break;
/*如果是kupdate回写,则设置inode dirty超时时间为30s */
if (work->for_kupdate) {
oldest_jif = jiffies -
msecs_to_jiffies(dirty_expire_interval * 10);
} else if (work->for_background)/*background回刷不会检查inode dirty时间,只会检查系统的整体dirty page水位 */
oldest_jif = jiffies;
trace_writeback_start(wb, work);
/*这里根据设置的oldest_jif时间,把inode从b_dirty链表移到b_bio链表,并把属于同一个
super_block的inode放到一起。b_bio上的inode会立即被回刷
*/
if (list_empty(&wb->b_io))
queue_io(wb, work);
/*先回刷特定super_block的inode*/
if (work->sb)
progress = writeback_sb_inodes(work->sb, wb, work);
else /* 回刷普通的inode*/
progress = __writeback_inodes_wb(wb, work);
trace_writeback_written(wb, work);
if (progress)
continue;
if (list_empty(&wb->b_more_io))
break;
trace_writeback_wait(wb, 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;
}
5 writeback参数调节
这两个参数决定系统dirty page超过多少时开启background回写,同时只能一个生效
dirty_background_bytes
dirty_background_ratio(default for 10%)
这两个参数决定系统dirty page超过多少时开启脏页平衡(),同时只能一个生效
dirty_bytes
dirty_ratio(default for 20%)
dirty_writeback_centisecs(default for 500):回刷线程扫描周期,默认为5秒
dirty_expire_centisecs:脏页回写时间,默认dirty 时间超过30秒就会被回写
dirtytime_expire_seconds:meta信息被修改的inode回写时间,默认为12H一次。