众所周知,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中)
SYSCALL_DEFINE1(fdatasync, unsigned int,fd) { return do_fsync(fd, 1); } static int do_fsync(unsigned int fd, intdatasync) { struct file*file; int ret =-EBADF; file =fget(fd); if (file) { ret =vfs_fsync(file, datasync); fput(file); } return ret; } int vfs_fsync(struct file *file, intdatasync) { returnvfs_fsync_range(file, 0, LLONG_MAX, datasync); }
最终的实现是调用vfs_fsync_range()来控制同步文件file某一段范围内的脏数据。fsync同步整个文件范围内的脏数据,因此,范围设置是0~LLONG_MAX。
int vfs_fsync_range(struct file *file,loff_t start, loff_t end, int datasync) { struct address_space *mapping = file->f_mapping; int err, ret; if (!file->f_op || !file->f_op->fsync) { ret = -EINVAL; goto out; } ret = filemap_write_and_wait_range(mapping, start, end); /* * We need to protect against concurrent writers, which could cause * livelocks in fsync_buffers_list(). */ mutex_lock(&mapping->host->i_mutex); err = file->f_op->fsync(file, datasync); if (!ret) ret = err; mutex_unlock(&mapping->host->i_mutex); out: return ret; }
与内核中很多其他函数一样,该函数是典型的三段式。首先检查参数等信息,然后调用filemap_write_and_wait_range()同步文件脏数据。等文件脏数据写入完成以后,调用具体文件系统的fsync方法,该方法主要是回写文件元数据,即inode信息。在此之前,需要对inode加锁,即mutex_lock。
int filemap_write_and_wait_range(structaddress_space *mapping,loff_t lstart, loff_t lend) { int err = 0; if (mapping->nrpages) { err = __filemap_fdatawrite_range(mapping,lstart, lend,WB_SYNC_ALL); /* See comment of filemap_write_and_wait() */ if (err != -EIO) { int err2 =filemap_fdatawait_range(mapping,lstart, lend); if (!err) err = err2; } } return err; }
该函数主要作用是回写文件的脏页面并等待脏页面写入完成。参数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。
int ext2_fsync(struct file *file, int datasync) { int ret; structsuper_block *sb = file->f_mapping->host->i_sb; structaddress_space *mapping = sb->s_bdev->bd_inode->i_mapping; ret =generic_file_fsync(file, datasync); if (ret == -EIO ||test_and_clear_bit(AS_EIO, &mapping->flags)) { /*We don't really know where the IO error happened... */ ext2_error(sb,__func__,"detected IO error when writing metadata buffers"); ret= -EIO; } return ret; } 这里主要调用通用函数generic_file_fsync来完成接下来的同步工作。 int generic_file_fsync(struct file *file,int datasync) { structwriteback_control wbc = { .sync_mode= WB_SYNC_ALL, .nr_to_write= 0, /* metadata-only; caller takes care of data */ }; struct inode*inode = file->f_mapping->host; int err; int ret; //what does it do? ret =sync_mapping_buffers(inode->i_mapping); if(!(inode->i_state & I_DIRTY)) return ret; if (datasync&& !(inode->i_state & I_DIRTY_DATASYNC)) return ret; err =sync_inode(inode, &wbc); if (ret == 0) ret = err; return ret; }该函数中主要完成两个任务:1.将associate mapping中的private中的buffer_head中的block数据同步至磁盘中,暂时还不清楚该buffer中保存的数据是什么,不过初步猜测可能保存文件数据间接块。2.步骤1完成后,将inode同步至磁盘中,现在我们主要关注sync_inode函数。
传递给该函数的参数有2:参数1. inode代表要写入的inode结构,参数2. wbc表示控制写入的参数,如在调用者中设置了以下几个控制参数:
struct writeback_control wbc = {
.sync_mode = WB_SYNC_ALL,//表示数据完整性写入,一定等到同步完成才可以返回
.nr_to_write = 0, //要写入的页面数,因为目前文件的所有脏页面已经全部同步,因此在回写inode的时候没有必要再同步文件脏数据页面
};
int sync_inode(struct inode *inode, structwriteback_control *wbc) { int ret; spin_lock(&inode_lock); ret =writeback_single_inode(inode, wbc); spin_unlock(&inode_lock); return ret; } staticint writeback_single_inode(struct inode *inode, struct writeback_control*wbc) { structaddress_space *mapping = inode->i_mapping; unsigned dirty; int ret; if(!atomic_read(&inode->i_count)) WARN_ON(!(inode->i_state& (I_WILL_FREE|I_FREEING))); else WARN_ON(inode->i_state& I_WILL_FREE); if (inode->i_state& I_SYNC) { /* * If thisinode is locked for writeback and we are not doing *writeback-for-data-integrity, move it to b_more_io so that * writebackcan proceed with the other inodes on b_io. * * We'll haveanother go at writing back this inode when we * completeda full scan of b_io. */ if(wbc->sync_mode != WB_SYNC_ALL) { requeue_io(inode); return0; } /* * It's adata-integrity sync. We must wait for this inode to be synced. */ inode_wait_for_writeback(inode); } BUG_ON(inode->i_state& I_SYNC); /* Set I_SYNC,reset I_DIRTY_PAGES */ inode->i_state|= I_SYNC; inode->i_state&= ~I_DIRTY_PAGES; spin_unlock(&inode_lock); ret =do_writepages(mapping, wbc); /* * Make sureto wait on the data before writing out the metadata. * This isimportant for filesystems that modify metadata on data * I/Ocompletion. */ if(wbc->sync_mode == WB_SYNC_ALL) { interr = filemap_fdatawait(mapping); if(ret == 0) ret= err; } /* * Somefilesystems may redirty the inode during the writeback * due todelalloc, clear dirty metadata flags right before *write_inode() */ spin_lock(&inode_lock); dirty =inode->i_state & I_DIRTY; inode->i_state&= ~(I_DIRTY_SYNC | I_DIRTY_DATASYNC); spin_unlock(&inode_lock); /* Don't write theinode if only I_DIRTY_PAGES was set */ if (dirty & (I_DIRTY_SYNC| I_DIRTY_DATASYNC)) { int err =write_inode(inode, wbc); if (ret == 0) ret= err; } spin_lock(&inode_lock); inode->i_state&= ~I_SYNC; if(!(inode->i_state & I_FREEING)) { if(mapping_tagged(mapping, PAGECACHE_TAG_DIRTY)) { /* * We didn'twrite back all the pages. nfs_writepages() * sometimesbales out without doing anything. */ inode->i_state|= I_DIRTY_PAGES; if(wbc->nr_to_write <= 0) { /* * slice usedup: queue for next turn */ requeue_io(inode); } else { /* * Writebackblocked by something other than *congestion. Delay the inode for some time to * avoidspinning on the CPU (100% iowait) * retryingwriteback of the dirty page/inode * thatcannot be performed immediately. */ redirty_tail(inode); } } else if(inode->i_state & I_DIRTY) { /* *Filesystems can dirty the inode during writeback *operations, such as delayed allocation during * submissionor metadata updates after data IO *completion. */ redirty_tail(inode); } else if(atomic_read(&inode->i_count)) { list_move(&inode->i_list,&inode_in_use); } else { list_move(&inode->i_list, &inode_unused); } } inode_sync_complete(inode); return ret; }
该函数的主要作用是向磁盘中写回脏的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)中会对该参数的设置做出判断。
int generic_file_fsync(struct file *file,int datasync) { structwriteback_control wbc = { .sync_mode= WB_SYNC_ALL, .nr_to_write= 0, /* metadata-only; caller takes care of data */ }; struct inode*inode = file->f_mapping->host; int err; int ret; //what does it do? ret =sync_mapping_buffers(inode->i_mapping); if(!(inode->i_state & I_DIRTY)) returnret; if (datasync&& !(inode->i_state & I_DIRTY_DATASYNC)) returnret; err =sync_inode(inode, &wbc); if (ret == 0) ret= err; return ret; }
可以发现,在同步完成mapping_buffers以后,在决定是否需要同步inode时会判断datasync参数:
因此,对比fsync和fdatasync的实现我们发现,fdatasync仅刷新了脏页面以及必要时刷新inode,而fsync实现的更为苛刻。