由于页高速缓存的缓存作用,写操作实际上会被延迟。当页高速缓存中的数据比后台存储的数据更新时,那么该数据就被称为脏数据。在内存中积累起来的页最终必须被写回磁盘。在以下两种情况发生时,脏页被写回磁盘:
1. 当空闲的内存低于一个特定的阈值时,内核必须将脏页写回磁盘,以便释放内存。
2. 当脏页在内存中驻留时间超过一个特定的阈值时,内核必须将超时的脏页写回磁盘,以确保脏页不会无限期地驻留在内存。
在老内核中,这是由两个独立的内核线程分别完成。但是在2.6版本中,由一组内核线程统一执行这两种工作—pdflush后回写线程。这两个目标是如何实现的?
首先,pdflush线程在系统中的空闲内存低于一个阈值时,将脏页刷新回磁盘。该后台回写例程的目的在于在可用物理内存过低时,释放脏页以重新获得内 存。特定的内存与之可以通过dirty_background_ratio sysctl系统调用设置(看kernel/Sysctl.c)。
在mm/Page_writeback.c中
/* Start background writeback (via pdflush) at this percentage
*/
int dirty_background_ratio = 10;
当空闲内存比阈值dirty_background_ratio还低时,内核便会调用函数wakeup_bdflush()唤醒一个pdflush线程,随后pdflush线程进一步调用函数background_writeout()开始将脏页写回磁盘。该函数的参数指定试图写回的页面数目,该函数会连续的写出数据,直到满足一下两个条件:
1. 已经有指定的最小数据的页被写出到磁盘
2. 空闲内存数已经回升,超过阈值dirty_background_ratio
在mm/Page-writeback.c中
/*
* writeback at least _min_pages, and keep writing until the amount of dirty
* memory is less than the background threshold, or until we’re all clean.
*/
static void background_writeout(unsigned long _min_pages)
{
long min_pages = _min_pages;
struct writeback_control wbc = {
.bdi = NULL,
.sync_mode = WB_SYNC_NONE,
.older_than_this = NULL,
.nr_to_write = 0,
.nonblocking = 1,
.range_cyclic = 1,
};
for ( ; ; ) {
long background_thresh;
long dirty_thresh;
get_dirty_limits(&background_thresh, &dirty_thresh, NULL);
if (global_page_state(NR_FILE_DIRTY) +
global_page_state(NR_UNSTABLE_NFS) < background_thresh
&& min_pages <= 0)
break;
wbc.encountered_congestion = 0;
wbc.nr_to_write = MAX_WRITEBACK_PAGES;
wbc.pages_skipped = 0;
writeback_inodes(&wbc);
min_pages -= MAX_WRITEBACK_PAGES - wbc.nr_to_write;
if (wbc.nr_to_write > 0 || wbc.pages_skipped > 0) {
/* Wrote less than expected */
congestion_wait(WRITE, HZ/10);
if (!wbc.encountered_congestion)
break;
}
}
}
pdflush后台例程会被周期唤醒,将那些在内存中驻留时间过长的脏页写出,确保内存中不会有长期存在的脏页。如果系统发生崩溃,由于内存处于混乱中,所以那些在内存中还没来得及写回磁盘的脏页就会丢失,所以周期性同步页高速缓存和磁盘非常重要。
在系统启动时,内核初始化一个定时器,让它周期地唤醒pdflush线程,随后使其运行函数wb_kupdate()。该函数将把所有驻留时间超过百分之 dirty_expire_centisecs秒的脏页写回。然后定时器将再次被初始化为百分之dirty_expire_centisecs秒后唤醒 pdflush线程。
在mm/Page-writeback.c中
/*
* Periodic writeback of “old” data.
*
* Define “old”: the first time one of an inode’s pages is dirtied, we mark the
* dirtying-time in the inode’s address_space. So this periodic writeback code
* just walks the superblock inode list, writing back any inodes which are
* older than a specific point in time.
*
* Try to run once per dirty_writeback_interval. But if a writeback event
* takes longer than a dirty_writeback_interval interval, then leave a
* one-second gap.
*
* older_than_this takes precedence over nr_to_write. So we’ll only write back
* all dirty pages if they are all attached to “old” mappings.
*/
static void wb_kupdate(unsigned long arg)
{
unsigned long oldest_jif;
unsigned long start_jif;
unsigned long next_jif;
long nr_to_write;
struct writeback_control wbc = {
.bdi = NULL,
.sync_mode = WB_SYNC_NONE,
.older_than_this = &oldest_jif,
.nr_to_write = 0,
.nonblocking = 1,
.for_kupdate = 1,
.range_cyclic = 1,
};
sync_supers();
oldest_jif = jiffies - dirty_expire_interval;
start_jif = jiffies;
next_jif = start_jif + dirty_writeback_interval;
nr_to_write = global_page_state(NR_FILE_DIRTY) +
global_page_state(NR_UNSTABLE_NFS) +
(inodes_stat.nr_inodes - inodes_stat.nr_unused);
while (nr_to_write > 0) {
wbc.encountered_congestion = 0;
wbc.nr_to_write = MAX_WRITEBACK_PAGES;
writeback_inodes(&wbc);
if (wbc.nr_to_write > 0) {
if (wbc.encountered_congestion)
congestion_wait(WRITE, HZ/10);
else
break; /* All the old data is written */
}
nr_to_write -= MAX_WRITEBACK_PAGES - wbc.nr_to_write;
}
if (time_before(next_jif, jiffies + HZ))
next_jif = jiffies + HZ;
if (dirty_writeback_interval)
mod_timer(&wb_timer, next_jif);
}
总而言之,pdflush线程周期地被唤醒并把超过特定期限的脏页写回磁盘。
系统管理员可以在/proc/sys/vm中设置回写的相关参数,也可以通过sysctl系统调用设置它们。
pdflush线程的实现代码在文件mm/pdflush.c中,回写机制的实现代码在文件mm/page-writeback.c和fs-writeback.c中。
pdflush设置
变量 描述
dirty_expire_centisecs 该数值以百分之一秒为单位,它描述超时多久的数据将被周期性执行的pdflush线程写出
dirty_ratio 占全部内存百分比,当一个进程产生的脏页达到这个比例时,就开始被写出
dirty_writeback_centisecs 该数值以百分之一秒为单位,它描述pdflush线程的运行频率
laptop_mode 一个布尔值,用于控制膝上型电脑模式
dirty_background_ratio 占全部内存的百分比。当内存中空闲页到达这个比例时,pdflush线程开始回写脏页
膝上型电脑模式
膝上型电脑模式是一种特殊的页回写策略,该策略主要意图是将硬盘转动的机械行为最小化,允许硬盘尽可能长时间的停滞,以此延长电池供电时间。该模式可通过 /proc/sys/vm/laptop_mode文件进行配置。通常,该配置文件内容为0,即膝上型电脑模式关闭,写1则启用该模式。
该模式的页写回行为与传统方式相比只有一处变化。除了当缓存中的页面太旧要执行回写脏页以外,pdflush还好找准磁盘运转的实际,把所有其他的物理磁盘IO、刷新脏换成等统统写回磁盘,以便保证不会专门为了写磁盘而去主动激活磁盘运行。
所述Linux发布版会在电脑接上或拔下电池时,自动开启或禁止膝上型电脑模式及其需要的pdflush可调节开关。因此机器可在使用电池电源时自动进入膝上型电脑模式,而在插上交流电源时恢复到常规的页回写模式。
避免拥塞的方法:使用多线程
因为磁盘的吞吐量有限,如果只有惟一线程执行回写操作,那么这个线程很容易等待对一个磁盘上的操作。为了避免出现这样的情况,内核需要多个回写线程并发执行,这样单个设备队列的拥塞就不会称为系统的瓶颈了。
2.6内核使用多个pdflush线程,每个线程可以相互独立地将脏页刷新回磁盘,而且不同的pdflush线程处理不同的设备队列。线程的数目可以根据 系统的运行时间进行调整。pdflush线程数量取决于页回写的数量和拥塞情况,动态调整。如果所有存在的pdflush线程都忙着写回数据,那么一个新 线程就会被创建,确保不会出现一个设备队列处于拥塞状态,而其他设备队列却在等待—不是接收–回写数据的情况。如果堵塞,pdflush线程的数量 便会自动减少,以便节约内存。
为了避免每一pdflush线程都挂起在同一个堵塞的队列上,pdflush线程利用了拥塞避免策略,它们会积极的试图写回那些不属于拥塞队列的页面。这样一来,pdflush线程通过分派回写工作,阻止多个线程在同一个忙设备上纠缠。