(If you are curious how an physical hard disk works, take a look at this Scientific American diagram. We'll focus on the higher levels.)

Given the following file system, diagram the data stored on the disk.

The filesystem:

/ -+- etc/ -+- passwd
   |        `- fstab
   `- bin/ -+- sh
            `- date
This is on a 4MB disk with 1KB blocks (4096 blocks total). The disk has only one partition, used for the cse451fs v1.0 with structure shown above; the directory entries for . and .. are not shown. The file sizes are as follows:
FileSize
/etc/passwd1024 bytes
/etc/fstab100 bytes
/bin/sh10,000 bytes
/bin/date5,000 bytes

From the description of cse451fs, we know the on-disk data structures will have the general layout:

But how large will each portion of the disk be? The boot and superblock are defined to be 1 block each. Reading mkfs.cse451fs.c (in the project file distribution) reveals that there will be 1 inode for every 3 blocks on the disk, meaning we will have 4096/3 = 1365 inodes. This requires 1365/CSE451_INODES_PER_BLOCK = 85 inode blocks (inodes are 64 bytes each). (If we had a larger disk, the number of inodes might be limited by the size of the inode map in the superblock - see below.) This leaves 4096 - 2 - 85 = 4009 blocks for the data map and data blocks.

Again from mkfs.cse451fs.c, we see that the numDataMapBlocks = 4009 / (BLOCK_SIZE*8) + 1 = 1 (rounds down). This means we have 4008 data blocks. We can now draw the picture with sizes (not to scale; click on a segment to examine it):
Boot Block Superblock Data Map Inode Blocks Data Blocks

Boot Block

On many architectures, including i386, the first block of a bootable disk has to contain the bootloader executable. Since it's not part of the filesystem, we won't discuss it here, though the curious may see the bootloader description for more information.

Superblock

The superblock has the master information about the filesystem. On cse451fs, the block also contains the inode map after the filesystem meta-data. Zooming in on it, it would look like:

From cse451fs.h, the superblock record has the following format:

struct cse451_super_block {
	__u16 s_nNumInodes;		// inode map is tail of superblock
	__u16 s_nDataMapStart;		// block # of first data map block
	__u32 s_nDataMapBlocks;		// data map size, in blocks
	__u32 s_nInodeStart;		// block # of first inode block
	__u32 s_nNumInodeBlocks;	// number of blocks of inodes
	__u32 s_nDataBlocksStart;	// block # of first data block
	__u32 s_nDataBlocks;		// number of blocks of data

	__u32 s_nBusyInodes;		// number of inodes in use
	__u16 s_magic;			// magic number 
	char  s_imap[0];		// name for inode map
};
The superblock field values for our little disk would be:
FieldValue
s_nNumInodes1365
s_nDataMapStart2
s_nDataMapBlocks1
s_nInodeStart3
s_nNumInodeBlocks85
s_nDataBlocksStart88
s_nDataBlocks4008
s_nBusyInodes7
s_magicCSE451_SUPER_MAGIC

Following the superblock record (but on the same disk block) is the inode map. The inode map tracks which inodes are currently in use. Looking at the filesystem, we see 3 directories and 4 files, for a total of 7 inodes required. A possible inode map is thus the low 7 bits in the 33rd byte of the superblock set to 1, with the remaining 7937 bits in the inode map set to 0.

Data Map

The data map tracks which data blocks are currently in use. Like the inode map, it is a bit array. Our data block requirments are as follows:
File/DirectorySizeData Blocks
/4 entries+1 null entry1
/etc/4 entries+1 null entry1
/bin/4 entries+1 null entry1
/etc/passwd1024 bytes1
/etc/fstab100 bytes1
/bin/sh10,000 bytes10
/bin/date5,000 bytes5
Total:20

Again, one possible data map would be the first 20 bits set, and the remaining bits all 0s.

Inode Blocks

There are 85 inode blocks allocated, but we've only got 7 inodes. Assuming the inode map described above is used, that means the first 7 inodes (which fit on a single block, since 7*64<1024) are valid. From cse451fs.h, the inodes have the following format:
struct cse451_inode {
	__u16 i_mode;
	__u16 i_nlinks;
	__u16 i_uid;
	__u16 i_gid;
	__u32 i_filesize;
	__u32 i_datablocks[CSE451_NUMDATAPTRS];
};
Values would be as follows. The order I've choosen to assign inodes to dirs and files is arbitrary except for the first inode, which must always be "/". I've abbreviated CSE451_DIRENTRYSIZE to ENTRYSIZE and made assumptions about permissions and ownership. Also note that the dir/file names are included just for reference; they are not stored in the inodes.
Values for inode number:
Field 1 ("/") 2 ("/etc") 3 ("/bin") 4 ("/etc/passwd") 5 ("/etc/fstab") 6 ("/bin/sh") 7 ("/bin/date")
i_mode S_IFDIR+0755 S_IFDIR+0755 S_IFDIR+0755 0644 0644 0644 0644
i_nlinks 2 3 3 1 1 1 1
i_uid 0 0 0 0 0 0 0
i_gid 0 0 0 0 0 0 0
i_filesize 4*ENTRYSIZE 4*ENTRYSIZE 4*ENTRYSIZE 1024 100 10,000 5,000
i_datablocks[13] [88,0,...,0] [89,0,...,0] [90,0,...,0] [91,0,...,0] [92,0,...,0] [93,94,...,102,0,0,0] [103,104,...,107,0,...,0]

On disk, these would be layed out as a sequence of cse451_inodes.

Data Blocks

As described above, we are assuming the first 20 data blocks are in use. Examining the inodes, we can see that data blocks 88 through 90 represent directories while 91 through 107 contain file data. We will examine just the first data block (88), which contains the directory entries for "/". Other directory blocks are very similar. The file blocks contain arbitrary data - exactly whatever the file contains.

Since data block 88 represents a directory, it contains an array of struct cse451_dir_entrys. Again from cse451fs.h, the format is (CSE451_MAXDIRNAMELENGTH is 30):

struct cse451_dir_entry {
	__u16 inode;
	char name[CSE451_MAXDIRNAMELENGTH];
};
The contents of block 88 would look like (the values in doublequotes are c strings, null-terminated. Note that despite the short length of the names, each element is still the full 32 bytes):
ElementField   Value
0inode1
name"."
1inode1
name".."
2inode2
name"etc"
3inode3
name"bin"
4inode0
name0

Once again, the ordering of entries is somewhat arbitrary (the . and .. entries will always be first). The 0/0 entry terminates the entry array. After this terminator, the data in the block is undefined.

The other two directory data blocks would be very similar. The "." entries would point to the inode for the directory, and the ".." to the inode for their parent (in this case, inode 1). Note that the directory listing does not distinguish between files and subdirs; that's left to the meta-data stored in the inode.