由于页高速缓存的缓存作用,写操作实际上会被延迟。当页高速缓存中的数据比后台存储的数据更新时,那么该数据就被称为脏数据。在内存中积累起来的页最终必须被写回磁盘。在以下两种情况发生时,脏页被写回磁盘:
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线程通过分派回写工作,阻止多个线程在同一个忙设备上纠缠。