17内核数据同步

17内核数据同步

物理内存和硬盘空间在很大程度上是可以互换的。如果有大量的物理内存是空闲的,则内核使用一部分内存来缓冲块设备的数据。反过来,如果物理内存太少,可以将数据换出内存,转移到磁盘空间。二者有一个共同点,数据总是在物理内存中操作,随后在随机的时间点写回(或刷出)到磁盘,以持久保存修改。在这里,块存储设备通常称为物理内存的后备存储器

Linux提供了各种缓存方法,已经在第16章详细讨论。但上一章没有讨论数据是如何从缓存回写到磁盘的。内核为此仍然提供了几个选项,可分为如下两类:

  1. 后台线程重复检查系统内存的状态,周期性地回写数据
  2. 在系统缓存中脏页过多,而内核需要干净页的情况下,将进行显式刷出

17.1 概述

在页的刷出(flushing)、交换(swapping)、释放(releasing)操作之间,有着明确的关系。不仅需要定期检查内存页的状态,还需要检查空闲内存的大小。在完成检查后,未使用或很少使用的页将自动换出,但在换出前,其中包含的数据将与后备存储器同步,以防数据丢失。对动态产生的页,系统交换区充当后备存储器。对映射自文件的页来说,其交换区就是底层文件系统中与页对应的部分。如果内存发生严重的不足,必须强制刷出脏数据,以获得干净的页

内存/缓存与后备存储器之间的同步,概念上分为两部分

  1. 策略例程(policy routine)控制数据交换的时机。系统管理员可以设置各种参数,帮助内核判定何时交换数据,这实际上是系统负荷的一个函数。
  2. 实现处理缓存和后备存储器之间同步操作的硬件相关细节,并确保策略例程发出的指令得以执行

同步和交换不能彼此混淆。同步只保证物理内存中保存的数据与后备存储器一致,而交换将导致从物理内存刷出数据,以释放空间,用于优先级更高的事项。在数据从物理内存清除之前,将与相关的后备存储器进行同步。

可能因不同原因、在不同的时机触发不同的刷出数据的机制:

  1. 周期性的内核线程,将扫描脏页的链表,并根据页变脏的时间,来选择一些页写回。如果系统不是太忙于写操作,那么在脏页的数目,以及刷出页所需的硬盘访问操作对系统造成的负荷之间,有一个可接受的比例
  2. 如果系统中的脏页过多(例如,一个大型的写操作可能造成这种情况),内核将触发进一步的机制对脏页与后备存储器进行同步,直至脏页的数目降低到一个可接受的程度。而“脏页过多”和“可接受的程度”到底意味着什么,此时尚是一个不确定的问题,将在下文讨论
  3. 内核的各个组件可能要求数据必须在特定事件发生时同步,例如在重新装载文件系统

前两种机制由内核线程pdflush实现,该线程执行同步代码,而第三种机制可能由内核中的多处代码触发

下图给出了实现数据同步的各个函数之间的依赖关系,但它不是一个恰当的代码流程图,只说明了函数彼此间的关联,以及可能的代码路径。着重说明由pdflush线程、系统调用以及文件系统相关组件的显式请求所发起的同步操作

在这里插入图片描述

内核可以从代码中任意位置发起数据同步,但所有的代码路径都在sync_sb_inodes结束。该函数负责同步属于给定超级块的所有脏的inode,对每个inode都使用writeback_single_inode。sync系统调用和各种通用的内核抽象层(如分区代码或块层)都利用了这种做法

另一方面,系统也可能出现需要将所有超级块的脏inode进行同步的情况。对周期性回写和强制回写,特别需要该操作。内核会在文件系统代码修改数据之前就启动同步操作,确保脏页的数目不失去控制。

对文件系统来说,同步一个超级块的所有脏inode通常粒度太粗了。它们通常需要同步一个脏的inode,因而要直接使用writeback_single_inode

即使同步实现围绕inode展开,但这并不意味着该机制只适用于已装载的文件系统所包含的数据。在10.2.4节曾讨论过,裸块设备由bdev伪文件系统的inode表示。因而,该同步方法也会影响到裸块设备,就像是普通的文件系统对象那样,这对想要直接访问数据者而言,是个好消息。

下文提到inode同步时,总是既包括inode元数据的同步,也包括inode管理的二进制裸数据的同步。对普通文件来说,这意味着同步代码不仅要传输时间戳、属性等信息,还要将文件的内容传输到底层块设备

17.2 pdflush 机制

pdflush机制实现在一个文件中:mm/pdflush.c。这与内核更早的版本中同步机制支离破碎的实现,形成了鲜明的对照

pdflush是用通常的内核线程机制启动的:

//mm/pdflush.c
//启动一个pdflush线程
static void start_one_pdflush_thread(void)
{
	kthread_run(pdflush, NULL, "pdflush");
}

内核通常会同时使用几个线程。应该注意到,特定的pdflush线程并不总负责同一个块设备。线程分配可能随时间变化,因为线程的数目不是常数,而且可能随系统负载而变化

实际上,内核在初始化pdflush子系统时,会启动MIN_PDFLUSH_THREADS个线程。通常,在普通负荷的系统上该数目是2,通过ps可以看到进程列表中有两个活动的pdflush实例:

wolfgang@meitner> ps fax 
2 ? S< 0:00 [kthreadd] 
... 
 206 ? S 0:00 _ [pdflush] 
 207 ? S 0:00 _ [pdflush] 
...

pdflush线程的数目有下限和上限。MAX_PDFLUSH_THREADS指定了pdflush实例的最大数目,通常为8。并发线程的数目保存在nr_pdflush_threads全局变量中,但并不区分活动和睡眠的线程。用户空间可通过/proc/sys/vm/nr_pdflush_threads查看当前值

何时创建/销毁pdflush线程的策略是很简单的。如果1秒内都没有空闲的pdflush线程可用,内核将创建一个新的线程。反之,如果某个pdflush线程的空闲时间已经超过1秒,则将被销毁。并发pdflush线程数目的上下限分别定义为MIN_PDFLUSH_THREADS(2)和MAX_PDFLUSH_THREADS(8),内核总是遵守这两个限制。

为什么需要多个线程现代系统通常具备多个块设备。如果系统中存在许多脏页,那么内核需要使这些设备尽可能忙于回写数据不同块设备的队列是彼此独立的,因而数据可以并行写入。数据传输速率主要受限于I/O带宽,而不是当前硬件上CPU的计算能力。下图概述了pdflush线程和回写队列之间的关联。下图说明,pdflush线程的数目是动态变化的,这些线程向底层块设备传输那些必须进行同步的数据。请注意,一个块设备可能有多个可以传输数据的队列,而一个pdflush线程可能服务于所有队列,也可能只向其中一个提供数据

在这里插入图片描述

此前的内核版本只采用了一个刷出守护进程(那时称为bdflush),但这导致了一个性能问题:如果一个块设备队列因为过多的待决回写操作而拥塞,那么守护进程就不能向其他设备的队列再提供数据。这些队列将处于空闲状态。可以通过动态创建和销毁pdflush内核线程来解决该问题,这种方法可同时使许多队列处于忙碌状态

17.3 pdflush线程结构体

pdflush机制由两个主要部分构成,数据结构描述线程的工作,策略例程帮助执行工作。数据结构定义如下:

//mm/pdflush.c

static LIST_HEAD(pdflush_list);/*pdflush 线程链表的表头*/
static unsigned long last_empty_jifs;//记录全局 pdflush 任务链表空了以后的时间,如果空的时间超过1s,pdflush 线程会在工作结束后创建新的 pdflush 线程
//pdflush 机制的结构体,描述任务
struct pdflush_work {
	struct task_struct *who;	/*pdflush 线程*//* The thread */
	void (*fn)(unsigned long);	/*该结构的主干,指向了完成实际工作的函数。在调用该函数时,arg0 作为参数传递*//* A callback function */
	unsigned long arg0;		/* An argument to the callback *///fn的参数
	struct list_head list;		/*链表元素,链接 pdflush 线程,表头为全局变量 pdflush_list*//* On pdflush_list, when idle */
	unsigned long when_i_went_to_sleep;/*线程上一次进入睡眠的时间,单位为 jiffies,用于从系统删除多余的 pdflush 线程(即在内存中已经有一段比较长的时间处于空闲状态的线程)*/
};

17.4 pdflush线程执行流程

pdflush线程在创建之后进入睡眠,直至内核的其他部分为线程指派任务,任务由pdflush_work描述。因而,pdflush线程的数目无须与要执行的任务的数目匹配。创建的线程都处于待命状态,等待内核分配任务

pdflush的工作方式:

在这里插入图片描述

  • pdflush 线程执行的函数
    • 优先级设为0级,限制能执行该线程的CPU为授予其父进程的CPU
    • __pdflush
      • 无限循环
        • 本线程设为可中断
        • 本线程的 pdflush_work 加入全局链表
        • 记录当前时间为休眠时间
        • 调度线程,去执行其他线程,等待被唤醒
        • 如果内核需要一个工作线程,它可以设置全局链表中某个pdflush_work实例的工作函数,并唤醒对应的线程
        • 线程被唤醒后,执行注册的工作函数
        • 检查工作线程是否太多或太少.如果已经有1秒多的时间没有空闲工作线程,则start_one_pdflush_thread创建一个新线程。如果睡眠时间最长的线程(在pdflush_list链表末尾)已经睡眠超过1秒,则退出无限循环,这使得当前线程被从系统删除

17.5 为pdflush线程指定工作,并唤醒pdflush线程开始工作 pdflush_operation

pdflush_operation为pdflush线程指定了一个工作函数,并唤醒该线程。如果没有可用的线程,则返回-1。否则,从链表移除一个线程并唤醒。为简化阐述,我们已经省略了代码中需要进行的锁操作:

  • pdflush_operation 为pdflush线程指定工作,并唤醒pdflush线程开始工作
    • 从全局pdflush链表中取出一个 pdflush_work 对象
    • 如果取出以后pdflush链表空了则设置空的时间为当前时间
    • 为 pdflush 线程设置工作函数和参数
    • 唤醒该 pdflush 线程

17.6 周期性内存数据同步机制介绍

这里描述实际的同步工作函数,这些函数负责将缓存的内存与相关的后备存储器同步。前面已经讲过,有两种方案可用,一种是周期性的,另一种是强制的。下面首先讨论周期性的回写机制

在较早的内核版本中,使用一个用户态应用程序来执行周期性写操作。该应用程序在内核初始化时启动,每隔一定时间调用一个系统调用来回写脏页。与此同时,这个不那么优雅的过程被一个更为现代的方案代替,后者不通过用户态迂回,因而不仅更高效,而且更为优雅。

早期同步函数的名称是kupdate。该名称会作为某些函数的一部分出现,通常用于描述刷出机制。

周期性地刷出脏的缓存数据需要两个组件:借助pdflush机制执行的工作函数,以及定期激活该机制的相关代码

17.7 周期同步相关的数据结构

mm/page-writeback.c中的wb_kupdate函数负责刷出操作的技术实现。它基于地址空间概念(在第4章讨论),这一概念建立了物理内存与文件或inode和底层块设备之间的关联

17.7.1 管理内存中所有页的状态相关的数据结构 vm_stat

wb_kupdate基于两个数据结构,二者控制了该函数的运作。其中一个是全局数组vm_stat,可用于查询所有系统内存页的状态

atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];//用于描述每个CPU的所有内存页的状态.系统中的每个CPU都对应该结构的一个实例

//vm_stat中收集了下列统计量
enum zone_stat_item {
	/* First 128 byte cacheline (assuming 64 bit words) */
	NR_FREE_PAGES,
	NR_INACTIVE,
	NR_ACTIVE,
	NR_ANON_PAGES,	/* Mapped anonymous pages */
	NR_FILE_MAPPED,	/* pagecache pages mapped into pagetables.
			   only modified from process context *///被页表机制映射的页的数目(只计算基于文件的页,直接的内核映射不包含在内)
	NR_FILE_PAGES,
	NR_FILE_DIRTY,//脏页数
	NR_WRITEBACK,//当前正在回写的页的数目
	/* Second 128 byte cacheline */
	NR_SLAB_RECLAIMABLE,//slab缓存可回收的页数目
	NR_SLAB_UNRECLAIMABLE,//slab缓存不可回收的页数目
	NR_PAGETABLE,		/* used for pagetables *///用于存放页表的页的数目
	NR_UNSTABLE_NFS,	/* NFS unstable pages */
	NR_BOUNCE,
	NR_VMSCAN_WRITE,
#ifdef CONFIG_NUMA
	...
#endif
	NR_VM_ZONE_STAT_ITEMS };

该数组保存了一组全面的统计信息,用于描述每个CPU的内存页面的状态。因而,系统中的每个CPU都对应该结构的一个实例。各个实例群集在一个数组中,以简化访问。

该结构的成员只是简单的基本类型的数字,表示具有特定状态的页的数目。要找出具备这些状态的具体的内存页,还需要其他的手段,将在下文中详细讨论。

在内存域结构体中也维护了一个统计信息的数组:

//include/linux/mmzone.h
struct zone {
    ...
    /* 维护了大量有关该内存域的统计信息 .如当前活动和不活动页的数目
	 * 函数 zone_page_state 用来读取 vm_stat 中的信息
	 * */
	atomic_long_t		vm_stat[NR_VM_ZONE_STAT_ITEMS];
    ...
};

维护全局和特定于内存域的数组,使其状态能够反映最新的使用情况,是内存管理子系统的工作。我们目前主要关注的是,这些信息是如何使用的。为获得整个系统状态的概述,必须合并各数组项中的信息,才能获得整个系统的数据,而不是特定于CPU的数据。内核提供了辅助函数global_page_state,它可以提供vm_stat的一个特定字段的当前值

//include/linux/vmstat.h
static inline unsigned long global_page_state(enum zone_stat_item item)

因为各个vm_stat数组及其数组项并未由锁机制保护,在global_page_state执行时,可能数据已经发生改变。所返回的结果不是准确值,而是近似值。这一点不成问题,因为该值只是工作分配的有效程度的一个一般性标志。在实际数据和返回值之间存在微小的差别,是可以接受的

17.7.2 控制脏页回写的参数的结构体 writeback_control

另一个数据结构保存了用于控制脏页回写的各种参数。上层使用该结构,将如何进行回写的相关信息传递给底层。但该结构也可以用来反向传播状态信息(自底向上)

//include/linux/writeback.h
//控制脏页回写参数
struct writeback_control {
	struct backing_dev_info *bdi;	/* If !NULL, only write back this
					   queue *///底层存储介质的信息,其中有两个成员相关,一个保存回写队列的状态(这意味着,例如,如果写请求太多,则可以通知调用方发生拥塞).另一个标记出基于物理内存的文件系统,此类文件系统没有(块设备作为)后备存储器,回写操作对此类文件系统是无意义的
	enum writeback_sync_modes sync_mode;/*回写模式*/
	unsigned long *older_than_this;	/*如果数据变脏的时间已经超过 older_than_this 指定的值,那么将回写,如果指针为 NULL ,那么不进行变脏时间的检查,所有的对象,无论何时变脏,都进行同步*//* If !NULL, only write back inodes
					   older than this */
	long nr_to_write;		/*限制应该回写的页的最大数目,上限为 MAX_WRITEBACK_PAGES,通常设置为1024,将nr_to_write 设置为 0 ,会禁用对回写页数目的上限限制*//* Write this many pages, and decrement
					   this for each page written */
	long pages_skipped;		/*在回写过程中,因各种原因跳过的页的数目(如:回写的页被别的函数锁定导致回写失败)*//* Pages which were not written */

	loff_t range_start;
	loff_t range_end;

	unsigned nonblocking:1;		/*指定了回写队列在遇到拥塞时是否阻塞(拥塞,是指待决写操作的数量比实际能够满足的写操作数目要多)。如果被阻塞,则内核将一直等待,直到队列空闲为止。否则,内核将交出控制权。写操作将在稍后恢复*//* Don't get stuck on request queues */
	unsigned encountered_congestion:1; /*通知上层在数据回写期间发生了拥塞。它接受的值为1或0*//* An output: a queue is full */
	unsigned for_kupdate:1;		/*如果写请求由周期性机制发出,则 for_kupdated 设置为1。否则,其值为0*//* A kupdate writeback */
	unsigned for_reclaim:1;		/*如果回写操作是由内存回收发出,则为1*//* Invoked from the page allocator */
	unsigned for_writepages:1;	/*如果回写操作是由do_writepages 函数发出,则为1*//* This is a writepages() call */
	unsigned range_cyclic:1;	/*如果 range_cyclic 设置为0,则回写机制限于对 range_start 和 range_end 指定的范围进行操作。该限制是对回写操作的目标映射设置的。如果 range_cyclic 设置为1,则内核可能多次遍历与映射相关的页*//* range_start is cyclic */
};

//同步回写模式
enum writeback_sync_modes {
	WB_SYNC_NONE,	/*发送回写请求,不等待回写完成,称为刷出回写(flushing writeback)*//* Don't wait on anything */
	WB_SYNC_ALL,	/*发送回写请求,等待回写完成,称为数据完整性回写(data integrity writeback),__sync_single_inode函数*//* Wait on every mapping */
	WB_SYNC_HOLD,	/*用于sync系统调用,类似 WB_SYNC_NONE,差别微妙*//* Hold the inode on sb_dirty for sys_sync() */
};

17.7.3 系统同步操作的各阈值默认值(如脏页百分比,脏页存在时间等,导出到用户层,用户可以修改)

内核支持通过参数对同步操作进行微调。这些参数可以由管理员设置,以帮助内核评估系统的使用情况和负荷。第10章描述的sysctl机制即用于此目的,这意味着proc文件系统成为了操作这些参数的固有的接口,这些参数位于/proc/sys/vm/。有如下4个参数可以设置,都定义在mm/page-writeback.c(由于历史原因,sysctl的名称与变量名不同)。

  1. dirty_background_ratio指定脏页的百分比,当脏页比例超出该阈值时,pdflush在后台开始周期性的刷出操作。默认值为10,当与后备存储器相比,有超过10%的页变脏时,pdflush机制将开始运转
  2. vm_dirty_ratio(对应的sysctl是dirty_ratio)指定了脏页(相对于非高端内存域)的百分比,脏页比例超出该阈值时,将开始刷出。默认值是40。为何将高端内存排除在比例计算之外?实际上,在2.6.20之前的内核版本是不区分高端和普通内存的。但如果高端内存和低端内存的比例过大(即在32位处理器上主内存远超4 GiB),在回写机制初始化时,dirty_background_ratiodirty_ratio的默认值需要稍微降低一些。使用原有的默认值将导致buffer_head实例的数量过多,这些都要占用宝贵的低端内存。通过将高端内存排除在计算之外,内核无须处理对比例的回缩,一定程度上简化了工作。
  3. dirty_writeback_interval定义了周期性刷出例程两次调用之间的间隔(对应的sysctl是dirty_writeback_centisecs)。间隔的单位是百分之一秒(源代码中也称作厘秒,centisecond)。默认值是500,相当于两次调用的间隔为5秒。在进行大量写操作的系统上,降低该值将提高性能,但在写操作数量很少的系统上,增加该值只能带来很少的性能增益
  4. 一页可以保持为脏状态的最长时间,由dirty_expire_interval指定(对应的sysctl是dirty_expire_centisecs)。该时间值的单位仍然是百分之一秒。默认值为3 000,这意味着一个脏页在写回之前,保持脏状态的时间最长可达30秒

17.8 pdflush机制线程使用的周期回写函数 wb_kupdate

周期性刷出操作中,关键性的一个组件是定义在mm/page-writeback.c中的wb_kupdate函数。它负责指派底层函数在内存中查找脏页,并将其与底层块设备同步。代码流程图如下:

在这里插入图片描述

//mm/page-writeback.c
//pdflush 线程回写工作函数,回写脏页,用于周期性回写,与background_writeout比较
static void wb_kupdate(unsigned long arg)
  • wb_kupdate 回写工作函数
    • sync_supers 同步超级块
    • global_page_state 获取要回写页数
    • while中进行回写
      • writeback_inodes 回写,通过inode获取数据进行回写,根据回写控制结构体的信息
      • 如果有回写失败的页,判断是否发生队列拥塞,如果队列拥塞,则等待拥塞减轻再继续往下执行
      • 要回写总页数减去回写成功的页数.减到0退出while
    • 启动定时器,确保 dirty_writeback_interval 时间间隔后再次调用

通常,wb_kupdate函数两次调用之间的间隔由dirty_writeback_centisecs指定。但如果wb_kupdate花费的时间比dirty_writeback_centisecs指定的时间更长,会出现一种特殊情况。在这种情况下,将推迟下一个wb_kupdate调用的时间,到当前wb_kupdate调用结束之后1秒钟。这不同于通常情况,因为这里的间隔不是按两个连续调用的开始时间来计算的,而是按照第一个调用结束到下一个调用开始的时间间隔来计算的

下面这个函数执行完后,整个机制就开始运转,内核在该函数中第一次启动相关的定时器

//mm/page-writeback.c
//初始化页回写机制
void __init page_writeback_init(void)

pdflush 线程调用 wb_kupdate 流程说明:

  • static DEFINE_TIMER(wb_timer, wb_timer_fn, 0, 0); 定义全局定时器 wb_timer
  • page_writeback_init 初始化页回写机制
    • mod_timer(&wb_timer, jiffies + dirty_writeback_interval); 启动定时器
  • wb_timer_fn 定时器到期执行函数
    • pdflush_operation(wb_kupdate, 0) //为 pdflush 线程设置工作函数 wb_kupdate 并唤醒 pdflush 线程执行工作
    • 如果pdflush链表为空上面的函数调用失败,mod_timer(&wb_timer, jiffies + HZ)重设定时器1秒后超时
  • start_one_pdflush_thread 启动一个pdflush线程
  • pdflush pdflush线程执行函数
    • __pdflush
      • 进入无限循环
      • (*my_work->fn)(my_work->arg0);//执行工作函数 wb_kupdate
  • wb_kupdate pdflush线程回写工作函数,回写脏页,用于周期性回写
    • writeback_inodes 回写脏页
    • mod_timer(&wb_timer, next_jif) 重设定时器,在dirty_writeback_interval之后超时

17.9 超级块同步 sync_supers

超级块数据通过一个专用函数sync_supers进行同步,这使得它与普通的同步操作区分开来。该函数及其他与超级块相关的函数都定义在fs/super.c。其代码流程图如下:
在这里插入图片描述

回想第8章的内容,内核提供了全局链表super_blocks来保存所有装载文件系统的super_block实例。sync_supers最初的任务就是遍历所有超级块,并根据超级块结构s_dirt成员来检查超级块是否是脏的。如果是,则通过write_super将超级块数据的内容写到数据媒体。

实际的写入操作由特定于超级块的super_operations结构所包含的write_super方法完成。如果该函数指针未设置,则该文件系统不需要超级块同步(例如虚拟的和基于物理内存的文件系统)。例如,proc文件系统就使用了一个NULL指针。当然,块设备上的普通文件系统,如Ext3或Reiserfs,都提供了适当的方法(例如ext3_write_super)来与块层通信并写回相关的数据。

  • sync_supers 同步超级块
    • list_for_each_entry(sb, &super_blocks, s_list) 遍历所有超级块
      • if(sb->s_dirt) 检查脏的超级块
        • write_super -> sb->s_op->write_super 超级块数据回写

17.10 inode 和对应的页数据同步 writeback_inodes

writeback_inodes通过遍历系统的inode对安装的映射进行回写(为简单起见,这称为inode回写,但事实上不只是inode,连同相关的脏数据都进行了回写)。该函数肩负着主要的同步工作,因为大多数系统数据都是以地址空间映射的形式提供的,均利用了inode。下图给出了writeback_inodes的代码稍微简化版本的流程图。
在这里插入图片描述

该函数利用了第8章讨论的数据结构,来建立超级块、inode和相关数据之间的关联

  • writeback_inodes inode和对应的脏页回写
    • for (; sb != sb_entry(&super_blocks); sb = sb_entry(sb->s_list.prev)) 遍历所有超级块
      • if(sb_has_dirty_inodes(sb)) 有脏数据的inode存在
        • sync_sb_inodes 回写单个超级块inode回写链表中的inode页数据
          • 遍历超级块回写链表
            • __writeback_single_inode 单个inode脏页回写
              • 如果当前是在进行普通回写(异步回写),则回写inode关联的页,不回写inode do_writepages
              • inode被锁定,休眠等待解锁
              • 如果进行数据完整性回写(即同步回写),将inode和inode关联的页都回写 __sync_single_inode
          • if (wbc->nr_to_write <= 0) 达到回写限制,退出循环
      • if (wbc->nr_to_write <= 0) 达到回写限制,退出循环

17.10.1 遍历有脏页的超级块

在逐inode回写各映射时,最初的路径是通过系统中表示已装载文件系统的所有超级块实例。对每个超级块实例都会调用sync_sb_inodes以回写超级块inode数据。在以下两种情况下,对超级块链表的遍历会结束

  1. 已经顺序扫描了所有超级块实例。内核已经到达链表末尾,因而已经完成工作
  2. writeback_control实例中指定的回写页的最大数目已经达到。由于回写需要获得各种重要的锁,因此不应该让回写操作干扰系统的正常运作太长时间,以便内核的其他部分能够恢复对相关inode的访问

17.10.2 回写单个超级块回写链表中的页 sync_sb_inodes

在借助于超级块结构确认文件系统确实包含带有脏数据的inode之后,内核将工作转交给sync_sb_inodes,该函数将同步有脏页的超级块inode。

如果内核每次为区分干净和脏的inode时,都需要遍历文件系统inode的完整列表,那就需要巨大的工作量。因而内核将所有脏inode置于特定于超级块的链表super_block->s_dirty上,实现了一个代价小得多的方案。请注意,该链表中的inode是按照时间逆序排列的。inode变脏的时间越靠后,它就越接近链表的尾部

为对这些inode进行同步,还需要两个链表头。super_block结构的相关部分如下:

//超级块结构体
struct super_block {
    ...
    struct list_head	s_dirty;	/*表头,链表元素为inode->i_list,用于脏的inode链表,磁盘同步时使用该链表而不需要扫描全部inode,效率更高.VFS层的相关代码会自动更新该链表*//* dirty inodes */
	struct list_head	s_io;		/*表头,回写时回写该链表中的页.如果同步请求不是源于周期性机制,那么将脏链表上(s_dirty链表和s_more_io链表)所有的inode都放置到s_io链表中*//* parked for writeback */
	struct list_head	s_more_io;	/*表头,包含那些已经被选中进行同步的inode,也置于s_io链表中,但不能一次处理完毕.最简单的解决方案是由内核将这些inode放回到s_io链表,但这可能导致新近变脏的inode无法得到同步处理,或导致锁方面的问题,因此又引入了一个链表*//* parked for more writeback */
    ...
};

sync_sb_inodes的第一个任务是填充s_io链表。必须区分如下两种情况:

  1. 如果同步请求不是源于周期性机制,那么将脏链表上所有的inode都放置到s_io链表中如果s_more_io链表上有inode,则将其置于s_io链表的末尾。内核提供了辅助函数queue_io来执行这两个链表操作。这种行为确保前一次同步剩余的inode仍然能够得到处理,但将优先考虑新近变脏的inode。这样,即使有比较大的脏文件存在,也不会导致小的脏文件不能得到同步处理
  2. 如果同步操作由周期性机制wb_kupdate触发,仅当s_io链表为空时,才补充额外的脏inode。否则,内核将等待,直至s_io中所有inode的回写操作都完成为止。对于周期性机制来说,没有什么特别的压力要求其在尽可能短的时间内回写尽可能多的inode。相反,更重要的是,稳健地写出一定数目的inode

如果回写控制参数指定了older_than_this条件,那么在标记为脏的inode中,只有变脏超过一定时间的那些才纳入同步处理。如果该成员中保存的时间早于映射的dirtied_when成员中的时间值,那么同步的必要条件不满足,内核不会将该inode从脏链表s_dirty移动到s_io链表

在选择了s_io链表的成员之后,内核开始遍历各链表元素

在实际回写之前,会进行一些检查,以确认相关的inode适合进行同步:

  1. 纯粹基于内存的文件系统如RAM磁盘,或伪文件系统或纯粹的虚拟文件系统,都不需要与底层块设备同步。这通过在相关文件系统的映射所属的backing_dev_info实例中,设置BDI_CAP_NO_WRITEBACK来表示。如果遇到此类inode,可以立即放弃处理。
    但有一个文件系统,其元数据是纯粹基于内存的,没有物理上的后备存储器,但该文件系统的inode不能跳过:块设备伪文件系统bdev。回想第10章的内容,bdev用于处理对裸块设备或其中分区的访问。对每个分区都提供了一个inode,对裸设备的访问通过该inode处理。尽管inode的元数据在内存中是重要的,但持久存储是没有意义的,因为它们只是用于实现一个统一的抽象机制。但这并不意味着块设备的内容不需要同步。事实上完全相反。对裸设备的访问照例由页缓存进行缓冲,任何修改都会反映到基数树数据结构中。在修改块设备的内容时,数据会通过页缓存。因而,相关的页必须像页缓存中其他页一样,定期与底层硬件同步
    块设备伪文件系统bdev因而并不设置BDI_CAP_NO_WRITEBACK。但相关的super_operations中并不包含write_inode方法,因此不进行元数据同步。另一方面,其数据同步的运行类似于任何其他文件系统。
  2. 如果同步队列发生拥塞(backing_dev_info实例的status字段的BDI_write_congested标志位置位)而且writeback_control中选中了非阻塞回写,那么需要向更高层报告拥塞。这是通过将writeback_control实例中encountered_congestion字段设置为1来完成的。
    如果当前inode属于一个块设备,那么将使用辅助函数requeue_io将该inode从s_io移动到s_more_io。同一块设备的不同inode可能由不同的队列处理,例如,在将多个物理设备合并为一个逻辑设备时。内核因而会继续处理s_io链表中其他的inode,以期它们属于其他未发生拥塞的队列
    但如果当前inode源自一个普通文件系统,那么可以假定其他inode也是由同一队列处理。由于该队列已经拥塞,继续同步其他的inode是没有意义的,因此将放弃循环。未处理的inode将保持在s_io链表中,在下一次调用sync_sb_inodes时处理。
  3. 可以通过writeback_control指示pdflush专注于某一队列。如果遇到了一个使用不同队列的普通文件系统inode,则可以放弃处理。如果该inode表示一个块设备,则跳过该inode,去处理s_io链表中的下一个inode,原因与写操作中映射的情形相同
  4. 在sync_sb_inodes开始时,将以jiffies为单位的当前系统时间保存在一个局部变量中。内核现在将要检查当前处理的inode被标记为脏的时间,是否在sync_sb_inodes函数开始执行时间之后。如果是,则完全放弃同步操作。未处理的inode仍然保留在s_io中
  5. 还有一种情况会导致sync_sb_inodes结束。如果一个pdflush线程已经处于回写当前被处理队列的过程中(由backing_dev_info的status成员的BDI_pdflush标志位表示),则当前线程会让正进行处理的pdflush线程自行其是

在内核确认上述条件都满足之前,是不会发起inode回写的.inode是使用__writeback_single_inode回写的,下文将详细介绍该函数。可能发生这样的情况是:在应该回写的所有页中,可能某些页的回写操作没有成功,例如,页可能被内核的其他部分锁定,或者,网络文件系统的连接可能是不可用的。在这种情况下,该inode将再次移回s_dirty链表,如果inode在回写时被再次弄脏,那么将需要更新dirtied_when字段。在接下来某一次运行同步时,内核将自动重试同步该inode的数据。此外,内核还需要确保s_dirty上的所有inode维持了时间逆序。辅助函数redirty_tail可对此进行维护

上述处理进程一直重复下去,直至下列两个条件之一得到满足

  1. 该超级块的所有脏inode都已经回写
  2. 达到了页同步的最大数目(在nr_to_write指定)。为支持上文描述的逐单元同步机制,这是必须的。s_io中剩余的inode将在下一次调用sync_sb_inodes时处理
  • sync_sb_inodes 回写单个超级块inode回写链表中的inode页数据
    • queue_io 当超级块s_io链表为空或同步请求不是源于周期性机制,将超级块s_more_io和s_dirty链表中的页移动到超级块s_io链表尾部
    • 遍历超级块回写链表 s_io
      • 处理不用回写的页,如内存文件系统
      • 处理写同步队列拥塞
      • 如果设置了只处理某一个写队列,其他队列的回写不处理
      • 如果inode在sync_sb_inodes函数开始之后才变脏的,放弃处理
      • __writeback_single_inode 单个inode脏页回写
        • 如果当前是在进行普通回写(异步回写),则回写inode关联的页,不回写inode
        • inode被锁定,休眠等待解锁
        • 如果进行数据完整性回写(即同步回写),将inode和inode关联的页都回写
    • if (wbc->nr_to_write <= 0) 达到回写限制,退出循环

17.10.3 回写单个inode __writeback_single_inode

内核将同步与一个inode相关联数据的任务委托给__writeback_single_inode 流程图如下:

在这里插入图片描述

  • __writeback_single_inode 单个inode脏页回写

    • 如果当前是在进行普通回写(异步回写),则回写inode关联的页,不回写inode
      • 将inode放回s_more_io链表,让下次回写继续考虑这个inode
      • 调用 do_writepages 将与该inode关联的页数据回写,inode数据先不回写
        • 调用回写 mapping->a_ops->writepages,提交了bio请求
        • 上一个回写函数没有就调用该函数回写 generic_writepages
      • 函数结束
    • 如果页被锁定,休眠等待锁定解除
    • 完整性回写(即同步回写),将inode和inode关联的页都回写调用 __sync_single_inode
      在这里插入图片描述
  • __sync_single_inode 将inode和inode关联的页都回写

    • do_writepages 将inode关联的页数据回写
      • mapping->a_ops->writepages 对ext3系统为ext3_writepages方法
      • 上面的函数没有则调用 generic_writepages 函数进行回写.该函数将找到映射的所有脏页,并使用地址空间操作的writepage方法回写每页.如果writepage方法也不存在,则调用mpage_writepage,它调用了 submit_bio 来回写数据
    • 如果I_DIRTY_SYNC或I_DIRTY_DATASYNC有置位
      • write_inode 回写inode元数据
    • 如果当前同步的目的在于保证数据完整性,即设置了WB_SYNC_ALL
      • filemap_fdatawait 等待所有待决写操作(通常是异步处理的)完成.该函数以页为单位等待写操作完成
    • 如果映射的数据未能全部回写
      • 将inode放回s_more_io或s_dirty链表,在后续的同步操作中处理
    • 如果inode数据在此期间再次变脏
      • 将inode放回s_dirty链表,在后续的同步操作中处理
    • 如果inode访问计数器(i_count)的值大于1
      • 将inode插入到全局的inode_in_use链表
    • 如果访问计数器降低到0
      • 将该inode置于保存未使用inode实例的全局链表中(inode_unused)
    • inode_sync_complete 调用wake_up_inode唤醒等待回写完毕的进程

17.11 拥塞,每个队列读写请求过多,最好等一段时间再提交读写请求

已经几次使用了术语拥塞(congestion),但没有确切定义其语义。在直觉上该术语不难理解,在某个内核块设备队列负荷了过多的读/写操作时,向队列增加更多与块设备通信的请求是没有意义的。在提交新的读/写请求之前,最好等待一段时间,使得一定数目的请求被处理完成,而队列能变得短一些。

接下来,将讲述内核在技术层次上对该定义的实现。

17.11.1 拥塞相关数据结构

实现拥塞方法需要两个等待队列:

//mm/backing-dev.c
//实现拥塞方法需要两个等待队列
static wait_queue_head_t congestion_wqh[2] = {
		__WAIT_QUEUE_HEAD_INITIALIZER(congestion_wqh[0]),/*用于读请求 fs.h中的READ宏*/
		__WAIT_QUEUE_HEAD_INITIALIZER(congestion_wqh[1])/*用于写请求 fs.h中的WRITE宏*/
	};

内核提供了两个队列。<fs.h>中定义了两个预处理器常数(READ和WRITE)来访问数组元素,在不直接使用数组索引的情况下,明确区分这两个队列

该数据结构并不区分系统中的不同设备。对可能发生的拥塞,块层的数据结构包含了特定于请求队列的信息

这两个队列不能直接用标准的等待队列方法来操作。内核为此提供了若干辅助函数,声明在<backing-dev.h>中。

17.11.2 请求队列是否拥塞的阈值

请求队列是否拥塞只需检查请求队列中请求的数目是否超出最小值和最大值(或阈值)

内核对此并不使用固定的常数。相反,它根据系统主存储器来定义这些限制值,因为阻塞请求的数目是随着主存储器大小而变化的。

回想第6章的内容,每个块设备都有一个请求队列,由struct request_queue定义。字段,如下所示:

//请求队列,块设备的读写请求放在这个队列结构体中
struct request_queue
{
    ...
    unsigned long		nr_requests;	/*定义每个队列中request结构的最大数目。通常,该数目设置为BLKDEV_MAX_RQ,其值为128,但可以使用/sys/block/<device>/queue/nr_requests修改.请求数目的下界由BLKDEV_MIN_RQ给出,其值为4*//* Max # of requests */
	unsigned int		nr_congestion_on;//表示队列请求数目达到拥塞的阈值。发生拥塞时,空闲request结构的数目必定小于该值
	unsigned int		nr_congestion_off;//指定了一个阈值,该阈值表示队列不再被认为拥塞。当空闲request结构的数目多于该值时,内核认为该队列不是拥塞的
	unsigned int		nr_batching;
    ...
};

queue_congestion_on_thresholdqueue_congestion_off_threshold函数用于读取当前的阈值。尽管这两个函数都很简单,但仍然必须使用,而不能直接读取相应的值。如果后续内核版本对阈值的实现进行修改,用户仍然能够使用同样的接口,而无须修改

拥塞阈值由blk_congestion_threshold计算:

//block/ll_rw_blk.c
//计算拥塞阈值
static void blk_queue_congestion_threshold(struct request_queue *q)

下图显示了对给定长度的请求队列计算的拥塞阈值。congestion_oncongestion_off的值稍有不同。这种微小的差别(内核源代码中称之为hysteresis,即滞后,从物理学借用的一个术语),在空闲请求的数目接近拥塞阈值时,可防止队列不断在两个状态之间切换。

在这里插入图片描述

17.11.3 拥塞状态的设置和清除 blk_set_queue_congested/blk_clear_queue_congested

内核提供了两个标准函数(声明在<blkdev.h>中)来设置和清除的队列的拥塞状态,分别是blk_set_queue_congested和blk_clear_queue_congested。两个函数都会获得所述请求队列的backing_dev_info,然后分别将工作移交给set_bdi_congestedclear_bdi_congested,后两个函数都定义在mm/backing-dev.c

为修改该状态需要操作两个数据结构。首先,必须修改块设备的请求队列(从第6章,读者已经熟悉了相关的request_queue数据结构),其次,必须注意维护全局拥塞数组(congestion_wqh)。

blk_set_queue_congested用于将一个请求队列标记为拥塞。值得注意的是,它只在内核中的一处调用,即get_request(这是不完全准确的,也可以从queue_request_store中调用blk_set_queue_congested。但该代码路径只在系统管理员通过sysfs改变请求队列的nr_requests字段时才会被激活)。第6章讨论过,get_request的作用是为一个队列分配一个request实例,或从适当的缓存取出一个request实例。这是检查拥塞的理想位置。如果空闲request实例的数目低于阈值,set_queue_congested通知其余代码已经发生了拥塞。

set_bdi_congested的实现非常简单。只需要设置请求队列中的一个比特位,当然,拥塞的方向不同,设置的比特位也是不同的

//mm/backing-dev.c
void set_bdi_congested(struct backing_dev_info *bdi, int rw)

但内核也负责将在拥塞队列上等待的进程添加到congestion_wqh等待队列。稍后会描述这是如何完成的

用于清除队列拥塞状态的函数是clear_queue_congested,也比较简单。同样,它也只在内核中一处使用(这里也忽略了系统管理员可能修改队列的nr_requests设置的可能性),由_freed_request调用,该函数位于发源于blk_put_request的代码路径中,该代码路径负责将不再需要的request实例返还给内核缓存。此时,很容易检查空闲request实例的数目是否已经超出了可以清除拥塞状态的阈值

在指定方向的拥塞标志位已经清除之后,在congestion_wqh队列上等待进行I/O操作的进程将由第14章描述的wake_up函数唤醒。回想前文,clear_queue_congested只是clear_bdi_congested的一个前端

//mm/backing-dev.c
void clear_bdi_congested(struct backing_dev_info *bdi, int rw)

17.11.4 在拥塞队列上等待 congestion_wait

内核提供了在拥塞的请求队列上等待,直至队列再次空闲的机制。读者已经看到,内核为此采用了一个等待队列,因此还需要讨论如何将进程添加到该等待队列。

内核为此使用了congestion_wait函数。它在拥塞发生时将一个进程添加到congestion_wqh等待队列。该函数需要两个参数,数据流的方向(读或写操作)以及一个超时时间,在经过超时时间指定的时间间隔之后,总是会唤醒进程,即使队列仍然是拥塞的。这个超时设置用于防止出现长时间的停滞,毕竟,队列可能拥塞比较长的时间。

//mm/backing-dev.c
//在拥塞发生时将一个进程添加到congestion_wqh等待队列,超时时间到后唤醒进程,即使队列仍然是拥塞的
long congestion_wait(int rw, long timeout)
  • congestion_wait 在拥塞发生时将一个进程添加到congestion_wqh等待队列,直到超时唤醒或不在拥塞时被其他进程唤醒
    • 获取全局拥塞等待队列 wait_queue_head_t *wqh = &congestion_wqh[rw]
    • prepare_to_wait 将进程放入等待队列头
      • __add_wait_queue 将进程增加到等待队列头
    • io_schedule_timeout 进程调度直到设置的超时时间到,重新唤醒(对于后台同步,超时设置为1秒钟)
      • delayacct_blkio_start
        • __delayacct_blkio_start 获取系统时间戳保存在 current->delays->blkio_start 中
          • delayacct_start
            • do_posix_clock_monotonic_gettime/ktime_get_ts 获取单调时钟的时间戳格式
      • schedule_timeout 调度当前进程,在 timeout 时间后唤醒
      • delayacct_blkio_end
        • __delayacct_blkio_end
    • finish_wait 将进程从等待队列移除,以继续工作

17.12 强制回写 wakeup_pdflush

在系统负荷不高时,上述以后台活动来回写内存页的机制工作得很好。内核能够确保脏页的数目一定不会失去控制,并且在物理内存和底层块设备之间,数据可以充分交换。但是,如果某些进程的缓存数据快速变脏,致使需要比普通方法更多的同步操作时,情况就会发生改变

在内核接收到对内存的紧急请求,而同时因为有大量脏页而不能满足该请求时,内核必须设法尽快将脏页的内容传输到块设备,以尽快释放物理内存用于其他目的。在这种情况下,使用的方法与后台刷出数据所用的方法是相同的,但同步操作不是由周期性过程发起,而是由内核显式触发,这种回写是“强制”的

这种立即同步请求不仅可能由内核发起,也可能来自于用户空间。我们熟悉的sync命令(和对应的sync系统调用)通知内核将所有脏数据刷出到块设备。内核为此还提供了其他系统调用,将在17.14节描述

同步是基于wakeup_pdflush的,该函数实现在mm/page-writeback.c中。刷出页的数目作为参数传递给函数。如果传入0,则回写所有脏页

//mm/page-writeback.c
//激活回写线程,强制回写nr_pages页,如果nr_pages为0,全部回写
int wakeup_pdflush(long nr_pages)
  • wakeup_pdflush
    • pdflush_operation(background_writeout, nr_pages) 唤醒pdflush线程执行 background_writeout 函数

background_writeout 与wb_kupdate相比,不要求页在回写之前已经脏了一定时间,不会同步超级块,没有设置定时器来周期性地重启回写机制

在两个地方以非0参数调用 wakeup_pdflush 回写部分脏页,其他调用都回写所有脏页(以0为参数)

  1. 在free_more_memory中,在内存不足以生成页缓存时,总会使用该函数。在这种情况下,使用的参数是固定值1024
  2. 在try_to_free_pages中,即第18章讨论的页面回收,采用了wakeup_pdflush方法,来回写扫描缓存时认为多余的页中的脏数据。(在使用膝上模式时,try_to_free_pages也会以0参数调用wakeup_pdflush,参见17.13节。)

可以理解,回写所有脏页是代价很高、很耗时的操作,因而其使用应该非常谨慎,在内核中只用于下述少量情况

  1. 在sync系统调用明确请求同步脏数据时
  2. 在紧急同步时,或使用magic system request key请求紧急重新装载时
  3. balance_dirty_pages通知background_writeout尽可能多回写一些页。在文件系统(或内核的其他部分)在一个映射上产生脏页时,VFS层将调用该函数。如果系统中脏页的数目过大,那么将使用background_writeout开始同步。与上文讨论的所有这些情形都不同,pdflush线程不会对系统的所有请求队列进行操作。只有脏页所属的后备存储器设备的队列,才会得到考虑

17.13 膝上模式(笔记本降低功耗) laptop_flush

在笔记本电脑不插电源使用时,在有些情况下,pdflush可以发挥作用减少耗电。对当今的硬件来说,硬盘在物理上确实是由一些“盘”实现的,以固态元件实现的替代方案已经出现了,但尚未达到广泛应用的程度。硬盘的运作需要旋转。这会消耗电力,在不需要使用硬盘时,降低旋转速度有助于减少耗电量

与匀速运转的磁盘相比,加速旋转的硬盘更糟糕,因为这需要更多能量。因而,优化内核有如下两方面

  1. 使硬盘尽可能低速旋转。这可以通过将写操作延迟更长的时间来做到
  2. 在磁盘旋转加速不可避免的情况下,执行所有未执行的写操作,即使在普通情况下,这些操作仍然要继续延迟(读操作要求磁盘处于运转状态,因此,除了避免无用的读操作,实在没什么可做的)。这有助于防止硬盘来回升高/降低旋转速度

本质上,磁盘操作是猝发执行的:如果必须从设备读取数据,那么所有待决写操作都可以执行,因为无论如何设备现在已经激活了

为了实现这些目标,内核提供了一种膝上模式,可以通过/proc/sys/vm/laptop_mode激活。全局变量laptop_mode充当一个标志,表示当前膝上模式是否激活。例如,用户层守护进程可以根据供电是否来自于电池,使用该文件来启用或禁用膝上模式。请注意, Documentation/laptop-mode.txt提供了一些有关该技术的文档

膝上模式对同步代码的改变极少。

  1. 使用了一个新的pdflush工作例程:laptop_flush只调用sys_sync来同步系统中所有的脏数据(效果与调用sync系统调用相同)。因为这将产生大量磁盘I/O,所以仅当我们知道磁盘处于活动状态时,才有必要激活该线程。
    在处理请求时,块设备使用标准函数end_that_request_last表示一系列请求中的最后一个已经提交。由于这确保了磁盘已经处于运转状态,该函数又调用了laptop_io_completion,后者安装了一个定时器laptop_mode_wb_timer,从现在起1秒钟后将执行laptop_timer_fn。laptop_timer_fn用laptop_flush作为工作函数来启动pdflush线程。这导致pdflush将执行一个系统范围内的完全同步.
  2. 回想上文,如果脏页在内存中比例过高,则balance_dirty_pages激活一个pdflush线程。但在膝上模式中,只要写了一些数据,就会启动pdflush。
  3. try_to_free_pages也稍有修改。如果该例程决定使用一个pdflush线程,那么回写的页数是不受限制的。如果磁盘需要加快旋转,这样做是有意义的,这种情况下应该触发更多的I/O。

最后请注意,如果将 /proc/sys/vm/dirty_writeback_centisec/proc/sys/vm/dirty_expire_centisec设置为比较大的值,膝上模式会受益。这将导致写操作比普通情况下延迟更长时间。在写操作最后发生时,在上文所述的膝上模式中所进行的改动,会自动确保磁盘恢复运转

17.14 用于同步控制的系统调用

可以从用户空间通过各种系统调用来启用内核同步机制,以确保内存和块设备之间(完全或部分)的数据完整性。有如下3个基本选项可用

  1. 使用sync系统调用刷出整个缓存内容。在某些情况下,这可能非常耗时。
  2. 各个文件的内容(以及相关inode的元数据)可以被传输到底层的块设备。内核为此提供了 fsync 和 fdatasync 系统调用。尽管 sync 通常与上文提到的系统工具 sync 联合使用,但 fsync 和 fdatasync则专用于特定的应用程序,因为刷出的文件是通过特定于进程的文件描述符(在第8章介绍)来选择的。因而,没有一个通用的用户空间工具可以回写特定的文件
  3. msync用于同步内存映射

17.15 同步系统调用(同步所有脏页数据) sys_sync

按内核惯例,sync系统调用在sys_sync中实现。其代码位于fs/buffer.c文件,相关的代码流
程图在下图给出

该例程的结构非常简单,由 wakeup_pdflush 开始的一串函数调用(通过 do_sync )组成,
wakeup_pdflush的调用参数为0。如上文所述,这将导致回写系统中所有的脏页

下一步是通过sync_inodes同步inode的元数据。这是我们第一次遇到这个回写所有inode的过程。
我们在下文将仔细考察该函数

sync_supers遍历super_blocks链表中的所有超级块,如果super_block->write_super例程
存在,则调用。这会导致将特定于超级块的信息写回到对应的各文件系统

sync_filesystems 通过再次遍历 super_blocks 链表并对每个以读/写模式装载并提供了sync_fs方法的文件系统调用sync_fs例程,来同步装载的各文件系统。仅在通过系统调用请求显式同步时,才会调用该方法,它向各文件系统提供了挂钩到进程中的能力。例如,Ext3文件系统就利用了该时机,对当前所有运行的事务起动了一个提交(commit)操作

如下图所示,sync_inodes和sync_filesystems会调用两次,首先用参数0,然后用参数1。该参数指定了函数是要等待写操作结束(1),还是异步执行(0)。将操作分为两遍,使得写操作可以在第一遍发起。这将触发与inode相关的脏页的同步操作,并使用write_inode同步元数据。但具体的文件系统可能选择只将包含元数据的缓冲区或页标记为脏,而不向块设备发生写请求。由于 sync_inodes将遍历所有脏inode,各个inode元数据的修改可能只有一点数据,但累积起来,就形成了比较大量的脏数据
在这里插入图片描述

因而,出于以下两个原因,需要第二遍处理

  1. write_inode调用标记为脏的页需要写回磁盘(与裸块设备的同步确保了这一点)。由于元数据的改变无须一点一点处理,这种方法提高了写操作的性能。
  2. 内核现在显式等待已经触发的所有写操作完成,这是可以保证的,因为第二遍处理设置了WB_SYNC_ALL

这种两遍处理的行为模式,要求对sync_sb_inodes进行一项修改,此前尚未讨论到。第二遍需要等待所有已经提交的页写入完成。这包括了在第一遍期间提交的页。对应的等待操作是在 __sync_single_inode 中发出的。但在调用sync_sb_inodes时,该函数只能看到inode位于超级块的3个链表s_dirty、s_io和s_more_io中的某一个中。如果在第一遍处理即用WB_SYNC_NONE调用sync_sb_inodes,那么inode就不会再处于这些链表上,这导致无法进行等待。

为此,内核专门引入了回写模式WB_SYNC_HOLD。它几乎等同于WB_SYNC_NONE。重要区别在于,在sync_sb_inodes中不会将已经同步的inode从s_io移除,而是放回到s_dirty链表上。这样,在第二遍处理时,这些inode仍然是可以访问的,并能够进行等待。但块层在两遍处理之间即可开始写出数据。

在sync系统调用期间,对函数的冗余调用会额外消耗一定量的CPU时间。但与缓慢的I/O操作所需的CPU时间相比,这是可以忽略的,因而是完全可接受的。

  • sys_sync
    • do_sync
      • wakeup_pdflush(0)
      • sync_inodes(0) 异步
      • sync_supers
      • sync_filesystems(0) 异步
      • sync_filesystems(1) 同步
      • sync_inodes(1) 同步

17.15.1 inode的同步 sync_inodes

sync_inodes会同步所有脏inode。其代码流程图在下图给出。

在这里插入图片描述

sys_sync是一个前端,真正的同步工作在__sync_inodes中进行。在调用__sync_inodes之前,对所有超级块,内核都使用set_sb_syncing将struct super_block的s_syncing成员设置为0。这有助于避免从多个地方对超级块进行同步

__sync_inodes函数将遍历所有超级块,并对每个超级块调用几个方法。该函数有一个参数:

//fs/fs-writeback.c
static void __sync_inodes(int wait)

wait是一个布尔变量,用于决定内核是否应该等待写操作完成。回想上文所述,这种行为对sync系统调用是必不可少的。

以下是__sync_inodes所完成的任务。

  1. 如果超级块当前正在由内核的另一部分进行同步(即struct super_block的s_syncing设置为1),则跳过该超级块。否则,将s_syncing设置为1,向内核的其他部分表示,该超级块当前正在进行同步
  2. sync_inodes_sb同步与超级块相关的所有脏inode。它使用get_page_state查询当前页状态,然后创建一个writeback_control实例。
  3. 大多数文件系统的底层同步例程只是将缓冲区或页标记为脏,但并不进行实际的回写。为此,内核接下来调用sync_blockdev,来同步文件系统所在块设备上的所有映射(在这一步,内核并不局限于某个特定的文件系统)。这确保了数据实际写回到块设备
  • sync_inodes
    • __sync_inodes
      • sync_inodes_sb 同步与超级块相关的所有脏inode
        • sync_sb_inodes 回写单个超级块inode回写链表中的inode页数据
      • sync_blockdev 同步文件系统所在块设备上的所有映射

17.15.2 单个文件的同步

同步单个文件的内容,这是不需要同步系统中所有数据的。该选项由应用程序使用,以确保它们在内存中修改的数据总是写回到适当的块设备。因为普通的写访问操作总是先进入缓存,该选项为真正重要的数据提供了额外的安全性(当然,另一种选择是使用直接I/O操作,绕过缓存)。

如上所述,有如下几个系统调用可用于此

  1. fsync同步一个文件的内容,并将与文件的inode相关的元数据写回到块设备。
  2. fdatasync仅回写数据内容,忽略元数据
  3. sync_file_range是一个相对较新的系统调用,在内核版本2.6.16引入。它可以对打开文件中精确定义的部分进行受控同步。本质上,其实现将选择目标内存页进行回写,可能会等待结果。由于这与上述系统调用采用的方法没有太多不同,所以这里不会详细讨论sync_file_range。

fsync和fdatasync的实现只有一处不同

  • sys_fsync
    • __do_fsync(fd, 0)
  • sys_fdatasync
    • __do_fsync(fd, 1)

__do_fsync的代码流程图如下:
在这里插入图片描述

单个文件的同步相对简单。fget用于根据文件描述符找到适当的file实例,然后将工作委托给如下3个函数

  1. filemap_fdatawrite ( 通过迂回到 __filemap_fdatawrite 和__filemap_fdatawrite_range)首先创建一个writeback_control实例,其nr_to_write值(刷出页的最大数目)设置为映射中页数的两倍,以确保能够回写所有页。然后,使用我们熟悉的 do_writepages 方法,调用文件所在的文件系统底层的写例程
  2. 使用文件的file_operations结构,找到特定于文件系统的fsync函数,然后调用该函数来回写缓存的文件数据。这也是fsync和fdatasync的不同之处,file_operations的fsync方法有一个参数,指定是只刷出普通的缓存数据,还是连同元数据一同刷出。该参数对fsync设置为0,对fdatasync设置为1
  3. 接下来,调用filemap_fdatawait等待在filemap_fdatawrite中发起的写操作结束,然后同步操作即告完成。这确保了异步写操作对用户应用程序表现为同步语义,因为该系统调用直到在块层和文件系统层的层次上完成指定数据的回写后,才将控制返回给用户空间

大多数文件系统对file_operations->fsync提供的方法都非常相似。图17-13给出了一个通用方法的代码流程图

在这里插入图片描述

该代码执行如下两个任务:

  1. sync_mapping_buffers将mapping实例的private_list中所有私有的inode缓冲区写回。这些通常用于保存间接块或其他文件系统内部数据,它们不是inode管理数据的一部分,而用于管理数据自身。该函数将工作委托给给fsync_mapping_buffers,该函数遍历所有的缓冲区。缓冲区数据通过ll_rw_block函数写到块层,读者在第6章应该已经熟悉了该函数。借助于osync_buffers_list,内核接下来一直等到写操作完成(块层也会对写访问进行缓冲),然后确保sync_buffers_list之外的元数据的同步表现为一个同步操作
  2. fs_sync_inode 回写inode的管理数据(即直接保存在特定于文件系统的inode结构中的数据)。注意,调用该方法时,fsync的datasync参数必须设置为0。这是fdatasync和fsync的唯一区别。

inode管理数据的回写是特定于文件系统的,请参见第9章。

  • __do_fsync
    • do_fsync 同步一个文件的内容,是否同步inode取决于datasync,0同步inode
      • filemap_fdatawrite 标记页缓存的的页回写
        • __filemap_fdatawrite
          • __filemap_fdatawrite_range
            • do_writepages
              • mapping->a_ops->writepages/ext2_writepages
                • mpage_writepages
                  • generic_writepages
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值