Implementing the VFS Interface

Recall that the kernel supports many filesystems, each managing their own disk blocks. The filesystems all have to support a common interface for this to work. Specifically, they must implement superblocks, inodes, files, and directories. If the kernel were written in C++, we might create an abstract superclass for each of those types and then have each filesystem implementation extend it, implementing the functions and possibly adding fields to the class.

The kernel is not written in C++, though, so we use another solution. The VFS layer defines structs for superblocks and inodes. These structures define the minimum information all implementations must store, for example, all inodes must contain the owner's user id. But VFS can't possibly know what fields an implementation will need in addition, so it provides an out: it adds a field that the particular implementation can use (u) to store whatever it wants.

That takes care of adding new fields to the class (using object-oriented terminology), but what about implementing the methods? This is done by having the filesystem implmentation declare a set of structs listing the supported methods for each data type. For example, in file.c, cse451fs declares a struct file_operations. That struct contains 4 function pointers. As it happens, cse451fs is similar enough to standard filesystems that 3 of these are the "generic" versions, and only one actually need be implemented by cse451fs. A pointer to this struct will wind up in every inode the represents a cse451fs file (in the i_fop field, put there by cse451_read_inode). Then, when an method needs to be invoked on an inode, the invoker just looks up the method in that struct.

Combined, we've got a somewhat clumsy object-oriented system. Interestingly, in implementation, it is very similar to what a C++ compiler does behind the scenes.

To make things slightly more confusing, inodes and superblocks also need to be stored on disk. Again, VFS does not care what format they are stored on disk. In cse451fs, the structure actually stored on disk for an inode is struct cse451_inode, while the actual superblock structure is struct cse451_super_block (both defined in cse451fs.h).

Using the Buffer Manager

First, learn to use Cross-Referencing Linux and grep(1). The linux source is large, and the code you'll need to understand spread thin; these tools can make your life significantly easier.

There are two pieces of the buffer manager: the data structures that it manages (struct buffer_head) and the actual data (pointed to by the b_data field of buffer_head). For any given disk block, the buffer manager may be:

  1. Completely unaware of the block - no buffer_head, data from block not in memory.
  2. Aware of the block's information (a buffer_head exists for it), but the block data is not in memory.
  3. Have the block's information and data in memory.
(We hope the buffer manager never lacks a buffer_head but has the data in memory.)

Thus, when the filesystem asks for a block (by calling bread), the buffer manager first needs to find or create a buffer_head. It then needs to make sure the data is available.

The following is intended to serve as a quick-reference to the functions of the buffer manager. Throughout, bh refers to a struct buffer_head *, block and size are int, and dev is kdev_t.

Buffer Manager FunctionDefined AtDescription
bh = bread(dev, block, size) fs/buffer.c:1177 Get the buffer_head for the given disk block, ensuring that the data is in memory and ready for use. Increments ref count; always pair with a brelse.
bh = getblk(dev, block, size) fs/buffer.c:982 Get the buffer_head for the given disk block. Does not guarantee anything about the state of the actual data. Increments ref count; always pair with a brelse.
bh = get_hash_table(dev, block, size) fs/buffer.c:547 Find the buffer_head for the given block in the hash table. Does not guarantee anything about the state of the actual data. Increments ref count; always pair with a brelse.
ll_rw_block(rw, numbufs, bufs) drivers/block/ll_rw_blk.c:1021 Lock buffer, begin an IO (either read or write), schedule a callback that will unlock buffer when IO finishes, and return (does not wait for IO completion - see wait_on_buffer).
wait_on_buffer(bh) include/linux/locks.h:17;
see __wait_on_buffer fs/buffer.c:145
Wait for the buffer to be unlocked (e.g. read from disk to complete)
mark_buffer_dirty(bh) fs/buffer.c:1098 Mark the buffer modified, meaning needs to be written to disk at some point.
mark_buffer_uptodate(bh) include/linux/fs.h:1023 Indicate that the data pointed to by bh is valid.
is_valid = buffer_uptodate(bh) include/linux/fs.h:264 Return whether the bh points to valid data.
brelse(bh) see __brelse, fs/buffer.c:1138 Decrement the ref. count of the given buffer.

Confusingly, cse451fs also defines some functions that are similar in name and purpose to those in the buffer manager (basically, they differ in that they take inodes and file block numbers rather than disk devices and disk block numbers):
cse451fs FunctionDefined AtDescription
bh = cse451_bread(inode, block, create) super.c:187 Similar to bread, but takes an inode and block number relative to that inode.
bh = cse451_getblk(inode, block, create) super.c:166 Similar to getblk, but takes an inode and block number relative to that inode. Zeros out new blocks (required for security).
err = get_block(inode, block, &bh, create) super.c:118 Fills in b_blocknr field of bh, allocating a new data block if necessary (and create is true). Does not guarantee data is in memory.