Linux常用的回写脏数据的参数可通过sysctl -a | grep dirty
来查看:
其中比较重要的参数有dirty_background_ratio, dirty_expire_centisecs, dirty_ratio, dirty_writeback_centisecs, dirtytime_expire_seconds。这些参数是如何影响回写脏数据的,下面我们来深入探寻一下。在此之前需要回顾一下writeback的刷盘流程。
每个块设备在初始化的时候都会分配一个request_queue, request_queue里有一个成员backing_dev_info(struct backing_dev_info类型)用于管理该块设备的刷盘回写。backing_dev_info里有个重要的变量wb(struct bdi_writeback类型),负责将页缓存的脏页异步刷新到对应的块设备上。
块设备格式化文件系统执行挂载时,会将文件系统superblock的s_bdi指向磁盘的bdi,以ext4文件系统为例,具体流程如下:
ext4_mount
mount_bdev
sget(fs_type, test_bdev_super, set_bdev_super, flags | SB_NOSEC, bdev);
sget_userns
err = set(s, data); // 调用set_bdev_super设置文件系统超级块的superblock
当ext4文件系统有脏数据时,会调用__mark_inode_dirty对文件inode置脏,调用过程参见如下堆栈:
__mark_inode_dirty 关键流程:
- 调用具体文件系统的dirty_inode函数置脏inode
sb->s_op->dirty_inode(inode, flags);
- 根据dirty类型将inode添加到wb的b_dirty或b_dirty_time链表里。
- 以delay timeout方式唤醒刷盘回写线程。
queue_delayed_work(bdi_wq, &wb->dwork, timeout);
wb_workfn 回写线程:
if (likely(!current_is_workqueue_rescuer() ||
!test_bit(WB_registered, &wb->state))) {
// 通用路径
do {
pages_written = wb_do_writeback(wb);
trace_writeback_pages_written(pages_written);
} while (!list_empty(&wb->work_list));
} else {
// bdi_wq没有足够的worker
pages_written = writeback_inodes_wb(wb, 1024,
WB_REASON_FORKER_THREAD);
trace_writeback_pages_written(pages_written);
}
if (!list_empty(&wb->work_list))
wb_wakeup(wb); // 如果在回写过程中,加入了新的worker, 立即唤醒
else if (wb_has_dirty_io(wb) && dirty_writeback_interval)
wb_wakeup_delayed(wb); // 延迟唤醒
wb_do_writeback 回写流程:
- 依次遍历wb中所有的work执行回写;
while ((work = get_next_work_item(wb)) != NULL) {
wrote += wb_writeback(wb, work);
}
- 周期性回写历史数据;
// 如果设置了dirty_writeback_interval参数,当前时间超过了上次记录的回写时间+dirty_writeback_interval阈值,则会启动回写。
wrote += wb_check_old_data_flush(wb);
// 如果系统当前脏页数量超过后台回写阈值,则启动脏页回写。
wrote += wb_check_background_flush(wb);
wb_writeback 流程参见下图:
wb_writeback传入参数work(struct wb_writeback_work类型)的nr_pages控制了本次回写的脏页数,脏页数会根据写入的数据量实时修改。流程里会根据work的类型计算本次回写的时间阈值,即在时间阈值之内的inode才会启动回写。wb_writeback里的queue_io会遍历wb->b_dirty/wb->b_dirty_time中记录的置脏inode链表,挑选时间阈值以内的inode放入wb->b_io链表。wb->b_io链表为本次回写的inode链表,里面是按照文件系统来排序(注:一个块设备可能存在多个分区,每个分区可格式化为一个文件系统)。接下来会对每个文件系统启动回写。
每个文件系统回写核心流程writeback_sb_inodes:
writeback_sb_inodes
while (!list_empty(&wb->b_io)) {
wbc_attach_and_unlock_inode
__writeback_single_inode
do_writepages(mapping, wbc);
mapping->a_ops->writepages(mapping, wbc);
...
wbc_detach_inode
requeue_inode // 这里b_io出链
/* The inode is clean. Remove from writeback lists. */
inode_io_list_del_locked(inode, wb);
}
接下来我们来分析刷写脏数据的主要参数:
vm.dirty_expire_centisecs:关联内核的dirty_expire_interval变量(单位:10ms)
wb_writeback:
// for_kupdate是周期性回写类型
if (work->for_kupdate) {
dirtied_before = jiffies -
msecs_to_jiffies(dirty_expire_interval * 10);
}
可见该参数是周期性回写时,每个inode的过期时间,默认是30s。即每个inode置脏后,最多30s就会写入到磁盘。
vm.dirty_writeback_centisecs: 关联内核的dirty_writeback_interval参数(单位10ms)。
wb_wakeup_delayed
timeout = msecs_to_jiffies(dirty_writeback_interval * 10);
...
queue_delayed_work(bdi_wq, &wb->dwork, timeout);
该参数启动回写线程delay的时间,默认为5s。即唤醒启动回写时,会开启一个定时器,在5s后调用回写线程。
vm.dirtytime_expire_seconds: 关联内核的dirtytime_expire_interval变量(单位:s)
queue_io
if (!work->for_sync)
time_expire_jif = jiffies - dirtytime_expire_interval * HZ;
moved += move_expired_inodes(&wb->b_dirty_time, &wb->b_io, EXPIRE_DIRTY_ATIME, time_expire_jif);
该参数为非透写(非direct io下),脏页过期时间。(注:inode涉及时间的更改,会放入wb->b_dirty_time链表里)
vm.dirty_ratio: 关联内核的vm_dirty_ratio变量,指定系统中脏页占可用内存的比例,默认30%,与vm.dirty_bytes互斥。
global_dirty_limits
domain_dirty_limits
unsigned long bytes = vm_dirty_bytes;
unsigned long ratio = (vm_dirty_ratio * PAGE_SIZE) / 100;
...
// 如果指定了dirty_bytes,会根据dirty_bytes算ratio
if (bytes)
ratio = min(DIV_ROUND_UP(bytes, global_avail), PAGE_SIZE);
vm.dirty_background_ratio: 关联内核的dirty_background_ratio变量,指定后台回写进程认为脏页最多占可用内存的比例。达到该比例后,后台回写进程开始将脏页写回磁盘。默认10%,与vm.dirty_backgroup_bytes互斥。
global_dirty_limits
domain_dirty_limits
unsigned long bg_bytes = dirty_background_bytes;
unsigned long bg_ratio = (dirty_background_ratio * PAGE_SIZE) / 100;
...
// 如果指定了dirty_backgroup_bytes, 会根据该值重算bg_ratio
if (bg_bytes)
bg_ratio = min(DIV_ROUND_UP(bg_bytes, global_avail), PAGE_SIZE);