Module 4 · Unit 10 · Laboratory 9
File Systems
This laboratory examines how the operating system presents persistent storage as a hierarchy of files and directories. You will inspect the on-disk layout of an ext4 file system, read and interpret inodes directly, build a directory walker, and compare the three classical file-allocation strategies — contiguous, linked, and indexed — with a working simulator. The laboratory closes by demonstrating journaling's crash-consistency guarantee and by reasoning about why modern file systems are layered on top of a log-structured or copy-on-write design.
01 Lab overview
| Course | COMP 3150 / CMIS 3150 — Operating Systems |
|---|---|
| Unit | Unit 10 — File Systems |
| Weight | 1.25% of the final grade (1/12 of the 15% laboratory component) |
| Estimated duration | 4–5 hours of hands-on work |
| Submission | One archive containing the C source files, the Makefile, the output logs, and a short lab report (see § 06) |
| Environment | Linux VM, gcc, make, debugfs, dumpe2fs, stat, ls -li, getdents, strace, an ext4 image (created with mkfs.ext4 on a loop device) |
This lab contributes to U10.1 through U10.7, and to course learning outcomes CLO5 and CLO8.
02 Learning objectives
By the end of this laboratory you will be able to
- Explain the file abstraction and the operations the file system provides.
- Interpret the metadata of a file — inode number, mode, size, timestamps, and block pointers — using
statanddebugfs. - Explain the structure of an inode and how it stores a file's metadata and data-block pointers.
- Describe how directories are organised as files that map names to inode numbers, and read a directory programmatically.
- Compare contiguous, linked, and indexed file allocation, and evaluate the trade-offs.
- Explain journaling and demonstrate its role in crash consistency.
- Describe the design of a modern file system (ext4, XFS, Btrfs, ZFS) and the problems each addresses.
03 Background
3.1 The file abstraction
A file is a named, persistent
sequence of bytes. The file system provides a small
set of operations on files: create,
open, read,
write, lseek,
close, unlink, and a few
others. Everything else — buffering, blocking,
caching, allocation — is the file system's
responsibility. The abstraction hides the
physical layout of storage: the application sees a
linear array of bytes, and the file system maps
that array onto disk sectors, possibly scattered
across the device.
3.2 Inodes
An inode (index node) is the data structure that describes a file. It contains the file's metadata — owner, permissions, size, timestamps, link count — and the pointers to the file's data blocks. It does not contain the file's name: names are stored in directories, which map names to inode numbers. On ext4, the inode contains 15 block pointers: 12 direct pointers, one single-indirect pointer, one double-indirect pointer, and one triple-indirect pointer. With 4 KB blocks, this allows files up to about 4 TB in size.
3.3 Directory organisation
A directory is itself a file whose contents map names to inode numbers. In ext4, the directory is stored as a hash tree (htree), which makes lookup O(1) rather than O(n). In older systems (and in simple file systems like FAT), the directory is a linear list of entries and lookup is O(n). The directory file has its own inode, so it has metadata and (unlike a regular file) a size that reflects the length of the name-to-inode mapping.
3.4 File allocation
- Contiguous allocation. The file's blocks are consecutive on disk. Fast sequential access; simple to implement. Suffers from external fragmentation; growing a file may require moving it.
- Linked allocation. Each block contains a pointer to the next. No external fragmentation; files grow easily. Poor random access (a lookup requires traversing the chain), and a single corrupted pointer can truncate the file.
- Indexed allocation. An index block holds the addresses of the file's data blocks. Supports efficient random access and growth, at the cost of an extra block per file and a limit on the maximum file size (unless multi-level indices are used).
3.5 Journaling
A journaling file system writes metadata (and, optionally, data) to a log before applying the changes to the file system's structures. After a crash, the file system replays the log to bring the structures to a consistent state. The alternative — an fsck pass over the entire file system — can take hours on a large volume. The journal can record metadata only (fastest, but data may be lost while metadata is consistent), metadata plus data in ordered mode (data is written before metadata; the default in ext4), or metadata plus data in journal mode (slowest, but the strongest guarantee).
3.6 Modern file systems
Ext4 is a journaling file system with extents (runs of contiguous blocks), delayed allocation, and a hash-tree directory index. XFS is a high-performance journaling file system designed for large files and parallel I/O. Btrfs and ZFS are copy-on-write file systems with snapshots, checksums, and (for ZFS) pooled storage and RAID integration. The copy-on-write design writes new data to fresh blocks and atomically updates the tree, so it never overwrites live data — a crash leaves either the old tree or the new one, never a mix.
04 Guided activities
Activity 4.1 — Inspecting file metadata
- Create the lab directory:
mkdir -p ~/comp3150/lab9 && cd ~/comp3150/lab9 - Create a small file and inspect it with
stat,ls -li, andfile. Record the inode number, size, mode, timestamps (atime, mtime, ctime), link count, and block count. - Create a hard link to the file with
lnand a symbolic link withln -s. Runstaton the original, the hard link, and the symbolic link. Explain why the hard link has the same inode number and the same link count while the symbolic link has a different inode and a small file size. - Remove the original file with
rm. Check the contents of the hard link and the symbolic link. Explain the result: the hard link still has the data because its link count is now 1; the symbolic link is now dangling because it points to a name that no longer exists. - Use
stat --format='%i %s %h %a %n'to print the inode number, size, link count, permissions, and name for a set of files. Automate the check for a directory tree.
Activity 4.2 — Reading inodes directly
- Create a 32 MB ext4 image:
dd if=/dev/zero of=fs.img bs=1M count=32 mkfs.ext4 fs.img mkdir mnt sudo mount -o loop fs.img mnt - Create a file inside the mounted file system and fill it with a known pattern (for example, 1 MB of zeros). Record its inode number with
ls -i. - Unmount the image and use
debugfs -R "stat <inode>" fs.imgto print the inode. Identify the direct, single-indirect, double-indirect, and triple-indirect pointers, and the block numbers they reference. - Use
dumpe2fs -h fs.imgto print the file system's superblock: block size, inode size, number of inodes, number of blocks, journal size, and features. Explain what each field means. - Use
debugfs -R "blocks <inode>" fs.imgto list the data blocks of the file. Confirm that a 1 MB file with 4 KB blocks uses 256 data blocks and (for a file that fits within the 12 direct pointers) no indirect blocks. - Create a file larger than 12 × 4 KB = 48 KB and repeat. Identify which indirect pointer is used and how many levels of indirection are required.
Activity 4.3 — Reading a directory
- Write
readdir.cthat opens a directory and lists its entries. Use the system callgetdents64directly (viasyscall(SYS_getdents64, ...)) rather thanreaddir(3), so that you see the raw on-disk directory format. - For each entry, print the inode number, the record length, the file type, and the name. The structure is
struct linux_dirent64:
struct linux_dirent64 {
ino64_t d_ino; /* inode number */
off64_t d_off; /* offset to next dirent */
unsigned short d_reclen; /* length of this record */
unsigned char d_type; /* file type */
char d_name[]; /* filename, null-terminated */
};
- Run the program on the directory
/etc. Verify that the first two entries are.and..and that the last entry is empty (a zero-length record marks the end of the buffer). - Compare the output of your program with
ls -lai /etc. Every inode number should match. - Modify the program to print a histogram of file types (regular, directory, symbolic link, other). Explain the distribution you observe on
/etc.
Activity 4.4 — File allocation simulator
- Write
alloc.cthat simulates a disk with N blocks. It supports three allocation strategies, selectable by a flag:--contig,--linked,--indexed. - The simulator reads a trace of operations:
create <name> <size>,read <name> <offset> <len>,append <name> <size>,delete <name>. It reports the number of disk accesses for each operation and, at the end, the total number of accesses and the external fragmentation (number of free blocks that are not adjacent to any allocated block, or the largest free run). - Run the same trace under all three strategies. Compare:
- Number of accesses for a sequential read of a whole file.
- Number of accesses for a random read of a single block.
- Number of accesses to append to the end of a file.
- Total external fragmentation after the workload.
- Generate a workload that favours contiguous allocation (all files created at once, no deletions) and one that favours linked or indexed allocation (interleaved creations and deletions). Report which strategy wins on each.
Activity 4.5 — Journaling and crash consistency
- Unmount the ext4 image from Activity 4.2 and inspect its journal with
dumpe2fs. Record the journal size and location. - Remount the image with different journaling modes using the
data=mount option:sudo mount -o loop,data=journal fs.img mnt sudo mount -o loop,data=ordered fs.img mnt # default sudo mount -o loop,data=writeback fs.img mnt - Run a workload that creates and writes many small files. After the workload, use
syncto flush, then unmount and check the journal's activity withdebugfs -R "logdump" fs.img | head -100. Identify the descriptor blocks and the commit blocks. - Simulate a crash: while a write workload is running, kill the mount with
sudo umount -f mnt(or power off the VM if permitted). Remount the image withmount -o loop fs.img mntand rune2fsck -fn fs.imgto verify that the file system is consistent. Explain what the journal replay did. - Repeat with
data=writebackand explain why the metadata is consistent but the data may be lost — the writeback mode does not order data before metadata, so after a crash the file system may have a metadata entry for a file whose data blocks were never written.
05 Exercises
- Write your own ext2 reader. Write a program that opens a raw ext2 image and prints the superblock, the block group descriptors, and, for a given inode number, the inode's metadata and data block list. This is the classic "read a file system without the file system" exercise; it forces you to understand the on-disk structures exactly.
-
Hash-tree directory benchmark.
Create two directories: one with 100,000 files
and one with 1,000,000 files. Time a
statlookup on a file in each. Then disable the directory index (tune2fs -O ^dir_index) and repeat. Explain the difference in terms of linear vs hashed lookup. -
Fragmentation measurement.
On an ext4 image, create and delete files in a
pattern that produces fragmentation, then use
filefrag -vto report the number of extents for each file. Compare with a fresh image where files are written sequentially. Explain the difference. -
Copy-on-write experiment.
If your VM supports it, create a Btrfs file
system and take a snapshot. Modify the original
and observe that the snapshot is unchanged.
Use
btrfs subvolume listandbtrfs filesystem duto show that the snapshot and the original share the unmodified blocks. Explain how the shared tree structure makes snapshots cheap. -
Filesystem-level I/O accounting.
Use
/proc/<pid>/ioto measure the read/write byte counts of a program that copies a large file. Comparecopyimplemented withread/writeagainst one implemented withsendfileorsplice. Explain the reduction in system-call overhead and copying. -
Direct vs. buffered I/O.
Write a program that writes a 1 GB file twice:
once with
write, once withO_DIRECT. Measure the wall-clock time and the number of blocks written (usingiostator/proc/<pid>/io). Explain why direct I/O is faster in some cases (no page-cache copy) and slower in others (no read-ahead, no write coalescing). -
Journal replay cost.
Measure how long
e2fscktakes on a cleanly unmounted file system versus one that was crashed while writing. The difference is the journal's contribution to recovery speed. Report both times and the file system size.
06 Deliverables
Submit a single archive named
lab9-<lastname>-<studentnumber>.tar.gz
containing:
readdir.c— Activity 4.3.alloc.c— Activity 4.4 with all three allocation strategies.ext2_reader.c— Exercise 1.- The
Makefilewith one target per program and acleantarget. inodes.txt— thedebugfs,dumpe2fs, andstatoutput for Activities 4.1 and 4.2, with your annotations.journal.txt— thelogdumpexcerpt ande2fsckoutput for Activity 4.5.outputs.txt— the output logs of all programs with the commands that produced them.report.md(orreport.pdf) — the lab report described below.
Lab report
The report should be 5–7 pages (excluding code) and contain:
-
Metadata and inodes.
The
statoutput for the original, hard link, and symbolic link, with an explanation of each field and of the difference between the two link types. -
Inode structure.
The
debugfsoutput for one inode, with the direct and indirect pointers annotated and the corresponding data blocks identified. -
Directory format.
A description of the
linux_dirent64records for one directory, with the inode numbers verified againstls. - Allocation comparison. The results from Activity 4.4, presented as a table with a row per strategy and a column per operation, and a discussion of which strategy is appropriate for which workload.
-
Journaling.
The journal descriptor and commit blocks, the
crash-recovery demonstration, and a discussion
of the three
data=modes. - Reflection. A discussion of how copy-on-write file systems (Btrfs, ZFS) avoid the consistency problem that journaling solves, and what trade-offs they introduce.
07 Grading rubric
| Criterion | Points | CLOs | Exemplary (90–100%) | Proficient (80–89%) | Developing (70–79%) |
|---|---|---|---|---|---|
| Metadata and inode analysis | 25 | CLO5 | All metadata fields interpreted correctly; direct and indirect pointers identified and verified. | Most fields interpreted; pointer analysis mostly correct. | Metadata or pointer analysis incomplete. |
| Directory reading | 15 | CLO5, CLO6 | Directory entries read correctly with getdents64; inode numbers verified against ls. |
Directory entries read; verification partial. | Program has bugs or does not use getdents64. |
| Allocation simulator | 25 | CLO5, CLO8 | All three strategies correct; comparison is quantitative and workload-driven. | Strategies correct; comparison adequate. | Strategies have bugs or comparison is superficial. |
| Journaling | 20 | CLO5 | Journal structure identified; crash recovery demonstrated and explained. | Journal identified; recovery discussed but not demonstrated. | Journal or recovery analysis incomplete. |
| Report quality | 15 | CLO5, CLO8 | Report is evidence-based, well-structured, and reflective. | Report is clear; some claims unsupported. | Report is disorganised or inaccurate. |
Late submission penalty: 10% per day, up to a maximum of three days, after which the lab is accepted only with documented extenuating circumstances.
08 Knowledge check
Check your understanding
Question 1
What is an inode, and what does it contain?
Show answer
Question 2
A file system uses 4 KB blocks and the inode has 12 direct pointers, one single-indirect pointer, one double-indirect pointer, and one triple-indirect pointer. What is the maximum file size?
Show answer
Question 3
Explain the difference between a hard link and a symbolic link.
Show answer
Question 4
Compare contiguous, linked, and indexed file allocation. Which is best for sequential access, which for random access, and which is used by modern file systems?
Show answer
Question 5
What is journaling, and what problem does it solve? Describe the three journaling modes of ext4.
Show answer
- Journal (
data=journal): both metadata and data are written to the journal. The strongest consistency guarantee, the slowest, because data is written twice. - Ordered (
data=ordered): only metadata is journaled, but data blocks are written before the metadata that references them. The default; the data is consistent but the journal is smaller and faster. - Writeback (
data=writeback): only metadata is journaled, and data may be written at any time. The fastest, but after a crash the metadata may reference data blocks that were never written — a file may contain garbage (though the file system's structures remain consistent).
09 References
- Silberschatz, A., Galvin, P. B., & Gagne, G. Operating System Concepts, 10th ed., Chapters 13–15.
- Arpaci-Dusseau, R. H., & Arpaci-Dusseau, A. C. Operating Systems: Three Easy Pieces, Chapters 39–43.
- Tanenbaum, A. S., & Bos, H. Modern Operating Systems, 4th ed., Chapter 4.
- McKusick, M. K., Joy, W. N., Leffler, S. J., & Fabry, R. S. A Fast File System for UNIX. ACM Transactions on Computer Systems, 1984.
- Rosenblum, M., & Ousterhout, J. K. The Design and Implementation of a Log-Structured File System. ACM Transactions on Computer Systems, 1992.
- Mathur, A., Cao, M., Bhattacharya, S., Dilger, A., Tomas, A., & Vivier, L. The New ext4 File System. Ottawa Linux Symposium, 2007.
- Linux man-pages:
man 2 stat,man 2 open,man 2 readdir,man 5 proc,man 8 debugfs,man 8 dumpe2fs.