分类: LINUX
2015-08-13 10:44:29
众所周知,Linux内核采用了page cache来缓存文件数据以及元数据。既然采用缓存,就有可能会产生缓存数据与磁盘中的数据不一致的问题,本系列博客中我们重点关注Linux内核如何解决这种不一致。
一般来说,一个成熟的系统需要提供多种机制来保证数据一致性,其一是用户可控的,即用户能通过特定的接口去控制文件数据的一致性,这是对于文件数据一致性要求比较高的应用需要的语义。另一方面,某些用户或者应用程序对文件数据的一致性要求可能没有那么高,无需在每次写入的时候都调用相应的接口去保证文件缓存数据和磁盘上数据的一致性,此时,操作系统必须能够主动地承担起保证文件一致性的任务。
下面我们再来讨论下一致性的概念。操作系统中的文件除了数据外(最常见的形式就是字节流),还包含元数据,即文件=数据+元数据,元数据用来描述文件的各种属性,也必须存储在磁盘上。因此,我们说保证文件一致性其实包含了两个方面:数据一致+元数据一致。关于文件数据和元数据在内存中的缓存机制请参考我的另外一篇博客。
当前Linux下以两种方式实现文件一致性:1. 向用户提供特定接口,用户可通过接口来主动地保证文件一致性;2.系统中存在定期任务(表现形式为内核线程),周期性地同步文件系统中文件脏数据块。这两种方式各有优劣,1可保证文件数据的强一致性,但效率较为低下,每次写入必须等待落实磁盘,2克服了1的低效,但无法保证文件数据的always onsistent。
文件系统一致性接口
当前Linux主要对用户提供如下接口来保证文件数据一致性(均位于fs/sync.c文件)。
fsync(fd)将fd代表的文件的脏数据和脏元数据全部刷新至磁盘中。fdatasync(fd)将fd代表的文件的脏数据刷新至磁盘,同时对必要的元数据刷新至磁盘中,这里所说的必要的概念是指:对接下来访问文件有关键作用的信息,如文件大小,而文件修改时间等不属于必要信息。sync()则是对系统中所有的脏的文件数据元数据刷新至磁盘中。本篇博客中我们讲述文件系统的主动一致性也就是详细分析上述三个接口的实现原理。
fsync(int fd)
fsync的语义是同步fd代表的文件。要实现这种同步,让我们来看看需要有哪些棘手的问题需要解决:
1. 快速定位脏缓存数据。当前Linux以页面为单位组织文件缓存,且将所有的页面组织在radix tree中,那么该问题就变为如何快速定位脏页面?
2. 如何设计一个简单高效的框架来将主动同步和系统中的刷新线程纳入同步框架之中?
本篇博客中我们不太去关注问题2,待到下一篇中我们会详细思考同步框架的设计。因此下面我们简单描述fsync的实现。追踪它的实现流程如下:
fsync(int fd)(位于fs/sync.c中)
------->do_fsync(fd, 0)(位于fs/sync.c中)
------->vfs_fsync(file, datasync)(位于fs/sync.c中)
------->vfs_fsync_range(file, 0, LLONG_MAX, datasync)(位于fs/sync.c中)
------->filemap_write_and_wait_range(mapping,start, end)(位于mm/filemap.c中)
------->__filemap_fdatawrite_range(mapping,lstart, lend,WB_SYNC_ALL)(位于mm/filemap.c中)
------->filemap_fdatawait_range(mapping,lstart,lend)(位于mm/filemap.c中)
------->ext2_fsync(struct file*file, int datasync)(针对ext2文件系统,位于fs/ext2/file.c)
------->generic_file_fsync(file, datasync)(位于fs/libfs.c中)
点击(此处)折叠或打开
点击(此处)折叠或打开
点击(此处)折叠或打开
该函数主要作用是回写文件的脏页面并等待脏页面写入完成。参数lstart,lend分别表示文件要回写的脏页面范围。该过程主要调用了两个函数 __filemap_fdatawrite_range和filemap_fdatawait_range, __filemap_fdatawrite_range找到位于偏移范围内的脏页面,并将页面写入磁盘相应位置处,其中WB_SYNC_ALL表示回写的模式,表示本次回写是文件完整性回写,不同于为了内存紧张时回收页面而进行的回写。接下来,调用 filemap_fdatawait_range 等待上面的写页面全部完成,因为上面的函数只是发出写页面请求,而完整性回写必须要确保所有的页面回写完成才可返回。因此,filemap_fdatawait_range等待在所有上面已经发出的请求的页面上,发请求时给页面加上了PG_Writeback,到页面回写完成以后才会清除该标志位,而filemap_fdatawait_range等待在所有文件正回写页面的该标志位,直到该标志位被清除该函数才返回,意味着脏页面的回写已经完成,可以进行接下来的工作了。具体的页面写入的工作我们会在别的文章中仔细讨论。
在文件的所有脏页面写入完成以后,接下来需要同步文件元数据即inode结构,因为此时需要对inode进行独占,因此在这之前需要对inode进行加锁,接下来调用file->f_op->fsync(),对于ext2文件系统来说,该方法被实例化为ext2_fsync。
点击(此处)折叠或打开
传递给该函数的参数有2:参数1. inode代表要写入的inode结构,参数2. wbc表示控制写入的参数,如在调用者中设置了以下几个控制参数:
struct writeback_control wbc = {
.sync_mode = WB_SYNC_ALL,//表示数据完整性写入,一定等到同步完成才可以返回
.nr_to_write = 0, //要写入的页面数,因为目前文件的所有脏页面已经全部同步,因此在回写inode的时候没有必要再同步文件脏数据页面
};
点击(此处)折叠或打开
该函数的主要作用是向磁盘中写回脏的inode。每次在写入之前需要判断该inode是否正被写入,inode被写入之前会设置标志位I_SYNC,因此,只需对inode判断该标志位是否被设置即可。如果该标志位已被设置,说明别的内核线程/任务正在写回该inode,此时我们需要根据当前写的类型来决定下一步动作,如果只是为了内存回收而写回文件脏数据,那么只需将该inode添加到一个额外的链表中,直接返回;而如果是为了数据完整性而进行的回写(wbc->sync_mode == WB_SYNC_ALL),我们必须等待该回写任务完成才能进行接下来的处理,调用inode_wait_for_writeback(inode)等待在该inode的I_SYNC标志位上。
上述判断完成并执行以后,接下来就需要进行inode回写了,回写涉及两个任务,第一写回wbc中预先设置好的脏页面数,因为我们目前看到调用者将wbc_nr_pages设置为0,那么此时并不需要写回任何脏页面(因为脏页面已经被写回过了)。当然,在写回脏inode之前需要设置inode相应的标志位,如设置好inode的I_SYNC标志,然后写回inode代表的文件脏页面。
脏页面写回以后,接下来判断是否需要写回inode,if (dirty & (I_DIRTY_SYNC |I_DIRTY_DATASYNC))为真,表示该inode为脏,为什么这样就表示inode为脏呢,我们就需要弄清楚这两个标志位的含义,根据代码注视:
· I_DIRTY_SYNC代表inode被弄脏,但这种弄脏并不是由文件数据被修改而导致的,典型的如文件的访问时间被修改,此时文件数据没有被修改;
· I_DIRTY_DATASYNC表示由于文件数据被修改而导致文件inode变脏;
· I_DIRTY_PAGES表示仅仅文件数据被修改,但并未导致文件inode被改动,此时是不需要同步文件元数据的。
之所以为inode设置这么多标志位是从效率方面来考虑的,当我们不需要同步文件inode的时候,尽量不同步。如仅当I_DIRTY_PAGES被设置时,我们是无需去同步inode的。同时在fdata_sync调用中,如果仅仅I_DIRTY_SYNC被设置,此时亦是无需同步inode的。
因此if (dirty & (I_DIRTY_SYNC | I_DIRTY_DATASYNC))为真意味着该inode确实被修改过,调用函数write_inode(最终是调用具体文件系统的写回inode方法),注意该函数是同步的,即从该函数返回意味着inode已经写回或者在写入过程中出错,因此接下来的任务就是清除inode的I_SYNC标志位(inode->i_state &= ~I_SYNC)并唤醒阻塞在该标志位上的所有进程(inode_sync_complete(inode))。
但在修改inode标志位之前,我们需要获取inode_lock,在写入inode的过程中是不持有该锁的,写入inode完成修改inode的i_state的时候必须要持有该锁以实现串行修改。获取锁的过程中其他进程可能会修改inode的i_state,因此必须在获取锁以后作一个判断:
以上便是fsync的主要流程,可以看到,整体上的结构非常清楚,同步脏数据页面到同步脏inode。但在每一步的实现时候又是危机四伏,需要考虑的细节问题太多。
fdatasync(int fd)
fdatasync()的语义和fsync颇为接近,均是将文件脏数据页面写回磁盘上,他们的区别在于是否同步文件inode。参数fd亦代表需要同步文件的文件描述符。我们追踪fdatasync的实现流程:
fdatasync(intfd)(位于fs/sync.c中)
------->do_fsync(fd, 1)(位于fs/sync.c中)
------->vfs_fsync(file, datasync)(位于fs/sync.c中)
------->vfs_fsync_range(file, 0, LLONG_MAX, datasync)(位于fs/sync.c中)
------->filemap_write_and_wait_range(mapping, start, end)(位于mm/filemap.c中)
------->__filemap_fdatawrite_range(mapping,lstart, lend,WB_SYNC_ALL)(位于mm/filemap.c中)
------->filemap_fdatawait_range(mapping,lstart,lend)(位于mm/filemap.c中)
------->ext2_fsync(struct file*file, int datasync)(针对ext2文件系统,位于fs/ext2/file.c)
------->generic_file_fsync(file, datasync)(位于fs/libfs.c中)
对比它与fsync的函数流程可发现它们的处理路径完全相同,均是先刷新脏的缓存页面,在这就不赘述了,它们唯一的区别在于datasync这个参数的设置,fsync将其设置为0,而fdatasync的实现中将其设置为1。在函数generic_file_fsync(file, datasync)中会对该参数的设置做出判断。
点击(此处)折叠或打开
可以发现,在同步完成mapping_buffers以后,在决定是否需要同步inode时会判断datasync参数:
因此,对比fsync和fdatasync的实现我们发现,fdatasync仅刷新了脏页面以及必要时刷新inode,而fsync实现的更为苛刻。