246 lines
9 KiB
HTML
246 lines
9 KiB
HTML
<title>High-performance File Systems</title>
|
|
<html>
|
|
<head>
|
|
</head>
|
|
<body>
|
|
|
|
<h1>High-performance File Systems</h1>
|
|
|
|
<p>Required reading: soft updates.
|
|
|
|
<h2>Overview</h2>
|
|
|
|
<p>A key problem in designing file systems is how to obtain
|
|
performance on file system operations while providing consistency.
|
|
With consistency, we mean, that file system invariants are maintained
|
|
is on disk. These invariants include that if a file is created, it
|
|
appears in its directory, etc. If the file system data structures are
|
|
consistent, then it is possible to rebuild the file system to a
|
|
correct state after a failure.
|
|
|
|
<p>To ensure consistency of on-disk file system data structures,
|
|
modifications to the file system must respect certain rules:
|
|
<ul>
|
|
|
|
<li>Never point to a structure before it is initialized. An inode must
|
|
be initialized before a directory entry references it. An block must
|
|
be initialized before an inode references it.
|
|
|
|
<li>Never reuse a structure before nullifying all pointers to it. An
|
|
inode pointer to a disk block must be reset before the file system can
|
|
reallocate the disk block.
|
|
|
|
<li>Never reset the last point to a live structure before a new
|
|
pointer is set. When renaming a file, the file system should not
|
|
remove the old name for an inode until after the new name has been
|
|
written.
|
|
</ul>
|
|
The paper calls these dependencies update dependencies.
|
|
|
|
<p>xv6 ensures these rules by writing every block synchronously, and
|
|
by ordering the writes appropriately. With synchronous, we mean
|
|
that a process waits until the current disk write has been
|
|
completed before continuing with execution.
|
|
|
|
<ul>
|
|
|
|
<li>What happens if power fails after 4776 in mknod1? Did we lose the
|
|
inode for ever? No, we have a separate program (called fsck), which
|
|
can rebuild the disk structures correctly and can mark the inode on
|
|
the free list.
|
|
|
|
<li>Does the order of writes in mknod1 matter? Say, what if we wrote
|
|
directory entry first and then wrote the allocated inode to disk?
|
|
This violates the update rules and it is not a good plan. If a
|
|
failure happens after the directory write, then on recovery we have
|
|
an directory pointing to an unallocated inode, which now may be
|
|
allocated by another process for another file!
|
|
|
|
<li>Can we turn the writes (i.e., the ones invoked by iupdate and
|
|
wdir) into delayed writes without creating problems? No, because
|
|
the cause might write them back to the disk in an incorrect order.
|
|
It has no information to decide in what order to write them.
|
|
|
|
</ul>
|
|
|
|
<p>xv6 is a nice example of the tension between consistency and
|
|
performance. To get consistency, xv6 uses synchronous writes,
|
|
but these writes are slow, because they perform at the rate of a
|
|
seek instead of the rate of the maximum data transfer rate. The
|
|
bandwidth to a disk is reasonable high for large transfer (around
|
|
50Mbyte/s), but latency is low, because of the cost of moving the
|
|
disk arm(s) (the seek latency is about 10msec).
|
|
|
|
<p>This tension is an implementation-dependent one. The Unix API
|
|
doesn't require that writes are synchronous. Updates don't have to
|
|
appear on disk until a sync, fsync, or open with O_SYNC. Thus, in
|
|
principle, the UNIX API allows delayed writes, which are good for
|
|
performance:
|
|
<ul>
|
|
<li>Batch many writes together in a big one, written at the disk data
|
|
rate.
|
|
<li>Absorp writes to the same block.
|
|
<li>Schedule writes to avoid seeks.
|
|
</ul>
|
|
|
|
<p>Thus the question: how to delay writes and achieve consistency?
|
|
The paper provides an answer.
|
|
|
|
<h2>This paper</h2>
|
|
|
|
<p>The paper surveys some of the existing techniques and introduces a
|
|
new to achieve the goal of performance and consistency.
|
|
|
|
<p>
|
|
|
|
<p>Techniques possible:
|
|
<ul>
|
|
|
|
<li>Equip system with NVRAM, and put buffer cache in NVRAM.
|
|
|
|
<li>Logging. Often used in UNIX file systems for metadata updates.
|
|
LFS is an extreme version of this strategy.
|
|
|
|
<li>Flusher-enforced ordering. All writes are delayed. This flusher
|
|
is aware of dependencies between blocks, but doesn't work because
|
|
circular dependencies need to be broken by writing blocks out.
|
|
|
|
</ul>
|
|
|
|
<p>Soft updates is the solution explored in this paper. It doesn't
|
|
require NVRAM, and performs as well as the naive strategy of keep all
|
|
dirty block in main memory. Compared to logging, it is unclear if
|
|
soft updates is better. The default BSD file systems uses soft
|
|
updates, but most Linux file systems use logging.
|
|
|
|
<p>Soft updates is a sophisticated variant of flusher-enforced
|
|
ordering. Instead of maintaining dependencies on the block-level, it
|
|
maintains dependencies on file structure level (per inode, per
|
|
directory, etc.), reducing circular dependencies. Furthermore, it
|
|
breaks any remaining circular dependencies by undo changes before
|
|
writing the block and then redoing them to the block after writing.
|
|
|
|
<p>Pseudocode for create:
|
|
<pre>
|
|
create (f) {
|
|
allocate inode in block i (assuming inode is available)
|
|
add i to directory data block d (assuming d has space)
|
|
mark d has dependent on i, and create undo/redo record
|
|
update directory inode in block di
|
|
mark di has dependent on d
|
|
}
|
|
</pre>
|
|
|
|
<p>Pseudocode for the flusher:
|
|
<pre>
|
|
flushblock (b)
|
|
{
|
|
lock b;
|
|
for all dependencies that b is relying on
|
|
"remove" that dependency by undoing the change to b
|
|
mark the dependency as "unrolled"
|
|
write b
|
|
}
|
|
|
|
write_completed (b) {
|
|
remove dependencies that depend on b
|
|
reapply "unrolled" dependencies that b depended on
|
|
unlock b
|
|
}
|
|
</pre>
|
|
|
|
<p>Apply flush algorithm to example:
|
|
<ul>
|
|
<li>A list of two dependencies: directory->inode, inode->directory.
|
|
<li>Lets say syncer picks directory first
|
|
<li>Undo directory->inode changes (i.e., unroll <A,#4>)
|
|
<li>Write directory block
|
|
<li>Remove met dependencies (i.e., remove inode->directory dependency)
|
|
<li>Perform redo operation (i.e., redo <A,#4>)
|
|
<li>Select inode block and write it
|
|
<li>Remove met dependencies (i.e., remove directory->inode dependency)
|
|
<li>Select directory block (it is dirty again!)
|
|
<li>Write it.
|
|
</ul>
|
|
|
|
<p>An file operation that is important for file-system consistency
|
|
is rename. Rename conceptually works as follows:
|
|
<pre>
|
|
rename (from, to)
|
|
unlink (to);
|
|
link (from, to);
|
|
unlink (from);
|
|
</pre>
|
|
|
|
<p>Rename it often used by programs to make a new version of a file
|
|
the current version. Committing to a new version must happen
|
|
atomically. Unfortunately, without a transaction-like support
|
|
atomicity is impossible to guarantee, so a typical file systems
|
|
provides weaker semantics for rename: if to already exists, an
|
|
instance of to will always exist, even if the system should crash in
|
|
the middle of the operation. Does the above implementation of rename
|
|
guarantee this semantics? (Answer: no).
|
|
|
|
<p>If rename is implemented as unlink, link, unlink, then it is
|
|
difficult to guarantee even the weak semantics. Modern UNIXes provide
|
|
rename as a file system call:
|
|
<pre>
|
|
update dir block for to point to from's inode // write block
|
|
update dir block for from to free entry // write block
|
|
</pre>
|
|
<p>fsck may need to correct refcounts in the inode if the file
|
|
system fails during rename. for example, a crash after the first
|
|
write followed by fsck should set refcount to 2, since both from
|
|
and to are pointing at the inode.
|
|
|
|
<p>This semantics is sufficient, however, for an application to ensure
|
|
atomicity. Before the call, there is a from and perhaps a to. If the
|
|
call is successful, following the call there is only a to. If there
|
|
is a crash, there may be both a from and a to, in which case the
|
|
caller knows the previous attempt failed, and must retry. The
|
|
subtlety is that if you now follow the two links, the "to" name may
|
|
link to either the old file or the new file. If it links to the new
|
|
file, that means that there was a crash and you just detected that the
|
|
rename operation was composite. On the other hand, the retry
|
|
procedure can be the same for either case (do the rename again), so it
|
|
isn't necessary to discover how it failed. The function follows the
|
|
golden rule of recoverability, and it is idempotent, so it lays all
|
|
the needed groundwork for use as part of a true atomic action.
|
|
|
|
<p>With soft updates renames becomes:
|
|
<pre>
|
|
rename (from, to) {
|
|
i = namei(from);
|
|
add "to" directory data block td a reference to inode i
|
|
mark td dependent on block i
|
|
update directory inode "to" tdi
|
|
mark tdi as dependent on td
|
|
remove "from" directory data block fd a reference to inode i
|
|
mark fd as dependent on tdi
|
|
update directory inode in block fdi
|
|
mark fdi as dependent on fd
|
|
}
|
|
</pre>
|
|
<p>No synchronous writes!
|
|
|
|
<p>What needs to be done on recovery? (Inspect every statement in
|
|
rename and see what inconsistencies could exist on the disk; e.g.,
|
|
refcnt inode could be too high.) None of these inconsitencies require
|
|
fixing before the file system can operate; they can be fixed by a
|
|
background file system repairer.
|
|
|
|
<h2>Paper discussion</h2>
|
|
|
|
<p>Do soft updates perform any useless writes? (A useless write is a
|
|
write that will be immediately overwritten.) (Answer: yes.) Fix
|
|
syncer to becareful with what block to start. Fix cache replacement
|
|
to selecting LRU block with no pendending dependencies.
|
|
|
|
<p>Can a log-structured file system implement rename better? (Answer:
|
|
yes, since it can get the refcnts right).
|
|
|
|
<p>Discuss all graphs.
|
|
|
|
</body>
|
|
|