Skip to content
text
6.1810 2024 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

Review: why do we care about logging?
  crash recovery for on-disk file-system data structures
  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 tree 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!
  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 block cache, not disk
  when system call is done -- commit:
    1. write all updated blocks to log on disk
    2. then mark on-disk log as committed (i.e. contains all of operation's writes)
    3. then write blocks to home locations in FS
  recovery after crash+reboot:
    if log marked as committed, copy blocks from log to home locations
    otherwise, ignore log

Review: two critical rules
  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+reboot.
  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
  modern ext4 similar to ext3

ext3 has higher performance than xv6:
  better asynchrony and batching
    xv6: usually log commit per syscall; and syscall waits
    ext3: syscalls rarely have to wait for log writes; many per xaction
          batching allows write absorbtion, thus fewer writes
  ordered data mode:
    does not write file content to journal, so only written once

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 reflects updates of many syscalls -- 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, in cache
       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

ext3 syscalls are "asynchronous"
  they write cached blocks, and return -- fast!
  they do not wait for log write or commit
  multiple transactions is necessary for this asynchrony
    to allow syscalls to proceed without waiting for prev transaction to commit
    batching -> big transactions -> take a long time to commit

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
  and transactions are committed only every few seconds
  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
    and call fsync() to enforce their own ordering rules

main goals of logging:
  1. protect FS's own invariants despite crash
     e.g. a block cannot be both free and used in an i-node
  2. high performance
  but *not* to provide application-level transactions!

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() may decide to close current xaction, start a new one
  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()

note: syscalls update cached blocks, but do not add to log

note: syscalls lock i-nodes &c, not shown here, to prevent conflicting updates

committing a transaction to disk
  (this happens in the background)
  1. start blocking any 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 now-oldest transaction in log (T3)

"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 some writes that were in-progress at time of crash
    at sector granularity (i.e. no partial sector writes)
  logging does *not* help with non-fail-stop failures
    e.g. bugs/errors in FS code, disk firmware or medium, CPU, RAM

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
         or new directory entry refers to i-node, but i-node not initialized

how ext3 recovery works
  SB T8 xT5 T6 T7
    SB points to T6
    T8 has overwritten start of T5
  0. reboot with intact disk
  1. look in log superblock for offset and seq# at which to start
  2. find the end of the log
     scan forward over valid transactions
       descriptor/commit pairs with correct magic and seq #
     end of log is xaction before first invalid xaction
  3. write all blocks to homes, through end of log, in log order
     thus completing those partially-written operations
  4. start normal file-system operation

write-ahead rule ensures that transactions after last committed
  cannot have started writing any home locations, so after
  recovery it will be as if they never happened.

why does recovery write log blocks to homes in log order?
  to ensure newer block versions replace older ones

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 some 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 T2's unlink while T1's create is executing
    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 include some of T2's writes!
    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, indirect blocks, 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 deleted
      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?
  again, goal of the log is to protect FS's own invariants
  from this point of view, file content is not relevant.

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 prevents so many syscalls starting that log isn't big enough?
  each 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
  no class next week
  then: 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