6.1810 2023 Lecture 15: Linux ext3 crash recovery This lecture: a case study of logging in Linux's ext3 file system with an eye to performance and real-world design details Why do we care about logging? crash recovery for complex on-disk file-system data structures good performance, good semantics, not too complex ideas widely used e.g. in databases and distributed systems "log" == "journal" lots to chew on for performance and correctness Review: the problem [diagram: FS on disk, free bitmaps] FS on disk is a complex data structure -- a tree Crash while updating can leave it permanently broken, if not repaired e.g. file append, add block to i-node, mark block non-free if crash w/o repair, that block can be allocated for a 2nd file! e.g. file delete, delete directory entry, mark i-node free if crash w/o repair, dirent still present but i-node marked free, can later be allocated to different file why crashes are threatening: crash w/o repair can leave file system permanently broken typically seemingly OK but with mystery problems that will surface later the bad pattern: an update that involves writing multiple disk blocks, and crash occurs after some but not all writes Review: the basic idea of logging (both ext3 and xv6) [diagram: cached blocks, FS on disk, log on disk] the goal: make system calls ("operations") atomic w.r.t. crashes, after recovery atomic = all-or-nothing system calls initially only write the buffer cache, not disk when system call is done -- commit: 1. write all updated blocks to log on disk 2. mark on-disk log as committed (i.e. contains all of operation's writes) 3. later, write blocks to home locations in FS recovery after crash+reboot: if log marked as committed, copy blocks from log to home locations write-ahead rule: don't start home FS writes until all writes committed in log on disk i.e. don't start modifying FS until *all* the operation's writes are in the log, so the operation can be completely finished if there's a crash. freeing rule: don't erase/overwrite log until all home FS writes are done ext3 is the modern name of the file system in today's reading for many years ext3 was the main Linux FS similar to more recent ext4 ext3 compared to xv6: more concurrency/overlap/asynchrony -- less waiting batching to reduce disk writes ordered data mode: does not write file content to journal, so only written once but: concurrency and ordered data add complexity ext3 data structures: [cache, info, disk FS tree, disk log] in memory: write-back block cache current xaction info: sequence # block #s to be logged outstanding "handles" -- one per syscall prev xaction info... on disk: FS circular log -- in a special pre-allocated fixed-size file. the ext3 log can hold multiple transactions |SB: offset+seq #| ... |D4|...blocks...|C4|D5|... log superblock: log offset and seq # of earliest valid transaction (this is not the FS superblock; it's a block at start of log file) descriptor blocks: magic, seq, block #s data blocks (as described by descriptor) commit blocks: magic, seq an ext3 transaction is really a "compound" transaction ext3 "batches" updated blocks from many system calls into each transaction ext3 commits the current "open" transaction every few seconds ext3's batching reduces time spent writing the log: 1. spreads fixed transaction costs over many system calls fixed costs: descriptor and commit blocks, seek/rotate, syscall barrier 2. "write absorbtion" many syscalls in the batch may modify the same block e.g. an app creating many files in the same directory thus one disk write for many syscalls' updates ext3 also gets performance from two kinds of concurrency: 1. multiple system calls can add to the current transaction 2. multiple transactions may be at various stages: * one "open" transaction that's accepting new syscalls * committing transactions doing their log writes * committed transactions writing to home locations in FS on disk how does concurrency help performance? CPU concurrency: many processes can execute FS ops in parallel adding to the current "open" transaction I/O concurrency: processes can operate on FS in cache while previous transactions are writing the disk and, a syscall doesn't wait for disk writes to complete (or even start) a crash may "forget" the last few seconds of operations even if the system calls completed and returns b/c system calls return before effects are committed to disk why? to allow batching lots of operations into each transaction. careful applications need to be aware that crash may lose last few ops databases, mail systems, editors, &c need some combination of careful writing and post-crash recovery but at the application layer, to ensure application invariants e.g. databases layer their own logging on top of FS kernel implementation of ext3 system calls: sys_unlink() { h = start() get(h, block #) ... modify the block(s) in the cache stop(h) } start(): tells the logging system about a new system call can't commit until all start()ed system calls have called stop() start() can block this sys call if needed get(): tells logging system we'll modify cached block added to list of blocks to be logged in current transaction stop(): stop() does *not* cause a commit transaction can commit iff all included syscalls have called stop() committing a transaction to disk (this happens in the background) 1. block new syscalls 2. wait for in-progress syscalls to stop() 3. open a new transaction, unblock new syscalls 4. write descriptor to on-disk log w/ list of block #s 5. write modified blocks from cache to on-disk log 6. wait for descriptor and data log disk writes to finish 7. write the commit record to on-disk log 8. wait for the commit disk write to finish 9. now modified blocks allowed to go to homes on disk (but not forced) though only after all previous transactions have committed too when can ext3 re-use log space? log is circular: SB T4 T5 T2 T3 a transaction's log space can be re-used if: it's the oldest transaction in the log -- T2 in this case T2 has committed T2's blocks have all been written to home locations in FS on disk ==> no longer any need to replay from log after a crash before re-use, must write log superblock with offset/seq of resulting now-oldest transaction in log "freeing" a transaction in the log means noting that the space can be re-used and advancing SB seq/offset over it. (*not* putting the blocks in the FS free bitmap). what if a crash? what kinds of crashes can logging help with? "fail-stop" followed by reboot e.g. power failure, then power restored crash causes RAM (and thus cached disk blocks) to be lost disk h/w is assumed to be readable after restart and to reflect all *completed* writes up to the time of the crash and perhaps parts of in-progress writes at sector granularity (i.e. no partial sector writes) after a crash, on-disk log may have a bunch of complete xactions, then some partial may also have written some of block cache to disk before crash i.e. FS *partially* updated, not safe to use e.g. file i-node has new block, but bit not cleared in free bitmap how ext3 recovery works SB T8 xT5 T6 T7 SB points to T6 T8 has overwritten start of T5; T5 previously freed 0. reboot with intact disk 1. look in log superblock for offset and seq# of oldest transaction 2. find the end of the log scan forward over valid complete transactions descriptor/commit pairs with correct magic and seq # end of log is xaction before first invalid xaction 3. re-write all blocks through end of log, in log order thus completing those partially-written operations write-ahead rule ensures that transactions after end of log cannot have started writing any home locations, so after recovery it will be as if they never happened. what if block after last valid commit block looks like a log descriptor? i.e. looks like the start of the next transaction? perhaps some file data happens to look like a descriptor? ext3 prevents this from happening ext3 replaces magic number in data blocks in journal with zero and sets a flag for that block in the descriptor perhaps a descriptor block left over from an old freed transaction? seq # will be too low -> end of log what if another crash during log replay? after reboot, recovery will replay exactly the same log writes that was the straightforward part of ext3. there are also a bunch of tricky details! why does ext3 delay start of T2's syscalls until all of T1's syscalls finish? i.e. why this: T1: |-syscalls-| T2: |-syscalls-| ---time---> this barrier sacrifices some performance example problem that this barrier prevents: T1: |-create(x)-| T2: |-unlink(y)-| X crash ---time---> if we allow unlink to start before create finishes unlink(y) free y's i-node -- i.e. marks it free in block cache create(x) may allocate that same i-node -- i.e. read unlink's write if T1 commits, but crash before T2 commits, then y will exist (the unlink did not commit) x will exist but use the same i-node as y! we can't let T1 see T2's updates! so: don't start T2's syscalls until T1's syscalls are done (note ext3's copy-on-write doesn't fix this: it gives T1 a copy of all blocks as of the end of T1, but T2's unlink executed before T1 ended.) (note we can't give T2 its own copies of the blocks, separate from T1's copies, since generally we do want later system calls to see the effects of earlier system calls.) why commit needs to write on-disk log from a snapshot of cached blocks consider this: T1: |--create(d/f)--| ... T1's log writes ... T2: |--create(d/g)--| both create()s write d's directory content block suppose T1 writes log to disk *after* T2's create() we can't let T1 write T2's update to the log! then a crash+recovery might replay some but not all of T2 e.g. make the directory entry d/g but without initializing g's i-node ext3's solution: give T1 a private copy of the block cache as it existed when T1 closed T1 commits from this snapshot of the cache it's efficient using copy-on-write the copies allow syscalls in T2 to proceed while T1 is committing so far we've been talking about EXT3's "journaled data" mode in which file content blocks are written to log as well as meta-data (i-nodes, directory content, free bitmaps) so file content is included in atomicity guarantee logging file content is slow, every data block written twice to disk data is usually much bigger than meta-data can we entirely omit file content from the log? if we did, when would we write file content to the FS? can we write file content blocks after committing write() meta-data to log? no: if metadata is committed first, crash may leave i-node pointing to blocks from someone else's deleted file, with previous file's content! ext3 "ordered data" mode: write file content blocks to disk *before* commiting log (and don't write file content to the log) thus, for a write(): 1. write file content blocks to FS on disk, 2. wait until content writes finish, 3. commit i-node (w/ block numbers and new size) and block free bitmap if crash after data write, but before commit: block on disk has new data but not visible, since i-node not updated w/ new block no metadata inconsistencies neither i-node nor free bitmap were updated, so blocks still free most people use ext3 ordered data mode it's much faster: avoids writing data twice why does it make sense for the log to omit file content? goal of the log is to protect FS's own invariants e.g. a block cannot be both free and used in an i-node from this point of view, file content is not relevant. in any case, the file system doesn't know which groups of application writes need to be atomically written. instead, applications call fsync() when they need to wait for data to be safe on the disk. correctness challenges w/ ordered data mode: * 1. rmdir() frees directory content block, #27 2. file write() (in same compound xaction) allocates block #27 3. write() writes block #27 on disk 4. crash before rmdir is committed after recovery, as if rmdir never happened, but directory content block (#27) has been overwritten! fix: no re-use of freed block until freeing syscall committed * rmdir, commit, re-use block in file, ordered file write, commit, crash+recover, replay rmdir 1. rmdir erases "." and ".." in content block 28, which is logged then frees 28 2. rmdir commits 3. write() allocates 28, and writes it (with ordered data mode) 4. write commits 5. crash, reboot, log is replayed, including the rmdir's write but write()'s data write was not logged, not replayed result: file is left w/ directory content fix: add "revoke" log record when freeing dir block, prevents *prior* log records from writing that block. note: both problems due to changing the type of a block (content vs meta-data) so another solution might be to never change a block's type what if there's not enough free log space for a transaction's blocks? free oldest transaction(s) i.e. install its writes in the on-disk FS what if so many syscalls have started that the entire log isn't big enough to hold their writes, even if all older transactions have been freed? syscall passes max number of blocks needed to start() start() waits if total for current transaction is too high and works on freeing old transactions another (complex) reason for reservations: T1 updates block 17 T1 commits T1 finishes writing its log and commit record but has not yet written block 17 to home location T2 also updates block 17 in the cache a T2 syscall is executing, does a write for which no log space can ext3 install T1's blocks to home locations on disk, and free T1's log space? no: the cache no longer holds T1's version of block 17 (ext3 could read the block from the log, but it doesn't) but once T2 commits, logged block 17 will reflect T1's writes so freeing of T1 can just wait for T2 to commit that is, freeing of early transactions may have to wait for commit of later transactions this means any transaction (T2) *must* be able to commit without having to wait for freeing of earlier transaction (T1) log space reservations help ensure this checksums recall: transaction's log blocks must be on disk before writing commit block ext3 waits for disk to say "done" before starting commit block write risk: disks usually have write caches and re-order writes, for performance sometimes hard to turn off (the disk lies) people often leave re-ordering enabled for speed, out of ignorance bad news if disk writes commit block before the rest of the transaction solution: commit block contains checksum of entire logged transaction on recovery: compute checksum of transaction if matches checksum in commit block: install transaction if no match: don't install transaction ext4 has log checksumming, but ext3 does not next week creative uses of virtual memory at application level References: Stephen Tweedie 2000 talk transcript "EXT3, Journaling Filesystem" http://olstrans.sourceforge.net/release/OLS2000-ext3/OLS2000-ext3.html log format details https://ext4.wiki.kernel.org/index.php/Ext4_Disk_Layout https://www.usenix.org/system/files/login/articles/04_tso_018-021_final.pdf