前言
前一篇博客中我们仔细描述了Linux文件系统的主动一致性,即文件系统对外提供的用于实现文件一致性的接口,应用程序可以调用这些接口同步文件/系统的脏数据和元数据。但诚如前一篇博客中所说,一个成熟的系统不仅应该只有这些由用户控制的同步方式,系统需要提供一些方式来保证文件数据/元数据的一致性。本篇博客我们就详细描述Linux内核中这种被动一致性的实现框架以及部分细节。
思考
所谓被动一致性是指系统后台存在定期的任务刷新某些文件的脏数据以及元数据。稍加思索知道,这些定期任务应该以内核线程的形式出现,于是,这些后台线程在设计的时候存在如下问题需要解决:
- 需要创建多少个内核线程来完成同步任务,根据何种标准来确定线程数量?多线程采用何种架构,所有线程处于同等地位还是存在一个集中管理线程(类似lighthttp架构)?
- 多线程如何处理并行的问题?这个问题其实又和如何确定创建的线程数量息息相关。
- 内核线程作为被动地刷新脏文件,其执行流必然会和主动刷新并行执行,如何设计一个统一的框架来管理这些任务流地执行?
总体框架
针对上述思考中的各个问题,Linux内核采取了如下的解决办法:
- 创建的针对回写任务的内核线程数由系统中持久存储设备决定,操作系统中有N个存储设备,那么在系统初始化时就会为其创建N个刷新线程。
- 关于多线程的架构问题,Linux内核采取了Lighthttp的做法,即系统中存在一个管理线程和多个刷新线程(每个持久存储设备对应一个刷新线程)。管理线程监控设备上的脏页面情况,若设备一段时间内没有产生脏页面,就销毁设备上的刷新线程;若监测到设备上有脏页面需要回写且尚未为该设备创建刷新线程,那么创建刷新线程处理脏页面回写。而刷新线程的任务较为单调,只负责将设备中的脏页面回写至持久存储设备中。
- 刷新线程刷新设备上脏页面大致设计如下:
- 每个设备保存脏文件链表,保存的是该设备上存储的脏文件的inode节点。所谓的回写文件脏页面即回写该inode链表上的某些文件的脏页面。
-
系统中存在多个回写时机,第一是应用程序主动调用回写接口(fsync,fdatasync以及sync等),第二管理线程周期性地唤醒设备上的回写线程进行回写,第三是某些应用程序/内核任务发现内存不足时要回收部分缓存页面而事先进行脏页面回写,设计一个统一的框架来管理这些回写任务非常有必要。

图1 回写线程总体框架

图2 回写机理
需要特别注意的一点是:系统为每个设备创建一个回写线程,而不是每个磁盘分区创建一个回写线程。这就导致可能出现如下问题:图2中的脏inode链表中的inode可能并不属于同一个文件系统,因为每个文件系统可能会建立在设备的一个分区之上。
具体实现
相关数据结构
为了实现相关我们上面构思的回写框架,内核中必须设计一些相关的数据结构,以下部分我们就主要阐述与回写相关的一些数据结构,我们重点放在思考为何必须这些数据结构。
首先,上面描述中我们知道,必须为每个设备创建相关的脏inode链表以及刷新线程,这些信息必须都记录在设备信息中,因此,设备信息中必须增加额外的成员变量以记录这些信息。同时,对于刷新线程部分,我们除了记录刷新线程的task结构外,还必须记录与该刷新线程相关的一些控制信息,如为了实现周期性地回写,必须记录上次回写时间等,也可以将脏inode链表记录在该结构体之中。
另外,为了实现周期性回写和释放缓存而导致的回写,可为每次回写构造一个任务,发起回写的本质是构造这样一个任务,回写的执行者只是执行这样的任务,当然,发起者需要根据其回写的意图(如数据完整性回写、周期性任务回写、释放缓存页面而进行的回写)设置任务的参数,执行者根据任务的参数决定任务的处理过程,结构相当清晰。为了增加这样一个任务数据结构,必须在设备中添加一个任务队列,以记录调用者发起的所有任务。
因此,根据上面的思考,我们总结出Linux内核中为了回写而引入的数据结构。
1. struct backing_dev_info
2. struct bdi_writeback
3. struct wb_writeback_work
4. struct writeback_control
1. struct backing_dev_info
系统中每个设备均对应这样一个结构体,该结构体最初是为了设备预读而设计的,但内核后来对其扩充,增加了设备回写相关的成员变量。与设备脏文件回写的相关成员如下列举:
struct backing_dev_info {
struct list_head bdi_list;
.........
struct bdi_writeback wb;
spinlock_t wb_lock;
struct list_head work_list;
.........
};
系统提供bdi_list将所有设备的bdi结构串联成链表,便于统一管理;wb是该设备对应的回写线程的数据结构,会在下面仔细描述;work_list是设备上所有任务的链表,发起回写的调用者只是构造一个回写任务挂入该链表即可;wb_lock是保护任务链表的锁。
2. struct bdi_writeback
前面我们说过,每个设备均为其创建一个写回线程,每个写回线程不仅需要记录创建的进程结构,还需要记录线程的上次刷新时间以及脏inode链表等,因此,内核中为此抽象出的数据结构为struct bdi_writeback。
struct bdi_writeback {
struct backing_dev_info *bdi; /* our parent bdi */
unsigned int nr;
unsigned long last_old_flush; /* last old data flush */
unsigned long last_active; /* last time bdi thread was active */
struct task_struct *task; /* writeback thread */
struct timer_list wakeup_timer; /* used for delayed bdi thread wakeup */
struct list_head b_dirty; /* dirty inodes */
struct list_head b_io; /* parked for writeback */
struct list_head b_more_io; /* parked for more writeback */
};
成员bdi指向设备结构体,last_old_flush记录上次刷新的时间,这是用于周期性回写之用,last_active记录回写线程的上次活动时间,该成员可用于销毁长时间不活跃的回写线程。task是刷新线程的进程结构,b_dirty是脏inode链表,每当一个文件被弄脏时,都会将其inode添加至所在设备的b_dirty链表中,至于b_io和b_more_io链表的作用在后面将仔细描述吧。
3. struct wb_writeback_work
我们前面说过,回写的过程实质上就是发起者构造一个回写任务,交给回写执行者去处理。这个任务就详细描述了本次回写请求的具体参数,内核中每个回写任务的具体参数如下描述:
struct wb_writeback_work {
long nr_pages;
struct super_block *sb;
enum writeback_sync_modes sync_mode;
unsigned int for_kupdate:1;
unsigned int range_cyclic:1;
unsigned int for_background:1;
struct list_head list; /* pending work list */
struct completion *done; /* set if the caller waits */
};
nr_pages表示调用者指示本次回写任务需要回写的脏页面数。sb表示调用者是否指定需要回写设备上属于哪个文件系统的脏页面,因为前面我们说过,每个设备可能会被划分成多个分区以支持多个文件系统,sb如果没有被赋值,则由回写线程决定回写哪个文件系统上的脏页面。sync_mode代表本次回写任务同步策略,WB_SYNC_NONE代表什么?WB_SYNC_ALL又代表什么?for_kupdate代表本回写任务是不是由于周期性回写而发起的,range_cyclic表示什么?for_background表示什么?list和done又分别代表了什么?
4. struct writeback_control
该结构可当做上面所描述回写任务的子任务,即系统会将每次回写任务拆分成多个子任务去处理,原因会在后面仔细说明。
回写流程
前面我们叙述了与被动(隐式)回写相关的数据结构,接下来我们就要思考回写流程到底该如何设计。
因为内核对回写采取了单管理线程+多工作线程的框架。因此,回写的流程分为管理线程设计和工作线程流程设计。
管理线程
对于管理线程来说,其主要工作是监视工作线程的运行状况,根据设备上的脏页面状况调整工作线程的运行,如设备上无脏页面且设备的工作线程已经有一段时间未被激活那么就kill该设备的回写线程,如果设备上有回写页面但尚未创建回写线程,那么为设备创建回写线程并启动线程运行。因此,总结来说,管理线程的主要流程如下:
- 遍历系统中所有的设备,判断设备目前的状态,如果设备脏inode链表不为空或者设备任务队列不为空且该设备当前尚未创建回写线程,那么为设备创建回写线程;如果设备当前脏inode链表为空且设备的回写线程已经有较长一段时间未活跃,那么就需要kill该设备的回写线程。当然,在对每个设备进行处理的过程中,是需要有很多细节问题需要考虑的。以下是管理线程的运行函数:
static int bdi_forker_thread(void *ptr)
{
struct bdi_writeback *me = ptr;
current->flags |= PF_FLUSHER | PF_SWAPWRITE;
set_freezable();
/*
* Our parent may run at a different priority, just set us to normal
*/
set_user_nice(current, 0);
//线程运行在一个大的循环之中
for (;;) {
struct task_struct *task = NULL;
struct backing_dev_info *bdi;
enum {
NO_ACTION, /* Nothing to do */
FORK_THREAD, /* Fork bdi thread */
KILL_THREAD, /* Kill inactive bdi thread */
} action = NO_ACTION;
/*

本文详细介绍了Linux内核中的被动一致性,即后台定期刷新文件的脏数据和元数据的实现。文章围绕管理线程、工作线程的架构,讨论了回写任务的创建、数据结构、回写流程以及如何处理设备的脏inode链表。通过分析,揭示了内核如何确保文件系统的一致性,并避免活锁等问题。
最低0.47元/天 解锁文章
1170

被折叠的 条评论
为什么被折叠?



