Directory Implementation
Chapter One Hundred Six
Syllabus topic Module 2, "File System Implementation - Directory Implementation"
Pages 429 to 432 of 452
In one line
The names in a directory are kept either in a list, which is simple and slow to search, or in a hash table, which is fast and fixed in size.
Why the choice matters
Every path resolution is a directory search. Opening /home/student/notes/os.txt searches four directories, and that happens on every open, every stat, every ls of a path. The directory search is the most frequent operation in the file system, which is why the choice of structure is worth a chapter.
A linear list
A list of entries, each holding a name and the inode number. It is what the ext4 image of Chapter one hundred five had: the debugfs listing printed a name and a number for each entry and nothing else.
| Operation | What it costs |
|---|---|
| search for a name | a linear scan: on average half the entries, and all of them if the name is absent |
| create a file | a full search first, to be sure the name is not already there, then add an entry |
| delete a file | a search, then release the entry |
| list the directory | read it through: this one is ideal |
Creating a file costs a search of the whole directory, not half. The system must prove the name is absent, and proving absence means looking everywhere. That is the answer to why creating files in a huge directory is slow.
What to do with a deleted entry
| Method | What happens |
|---|---|
| mark it unused, with a special name or a zero inode number | simple; the directory never shrinks and the holes are searched over |
| move the last entry into the hole | keeps the list packed; loses any ordering |
| keep a free list of the holes | a new entry goes into the first hole that fits |
A sorted list allows a binary search, which turns 500 comparisons into about 10, but every insertion has to move the entries after it. Keeping the list as a linked list in sorted order, or as a B tree, is how real systems get the search without the insertion cost.
The cost as a number
A directory of 1,000 entries in a file system with 1024 byte blocks, each entry taking about 16 bytes:
entries per block = 1024 / 16 = 64
blocks in the directory = 1000 / 64, about 16
| Operation | Comparisons | Block reads |
|---|---|---|
| a successful search, on average | 500 | about 8 |
| an unsuccessful search, or a create | 1000 | about 16 |
| a binary search of a sorted list | about 10 | about 4, one per probe |
The comparisons are cheap and the block reads are not: Chapter ninety two priced a block read at about nine milliseconds. A linear directory search of a large directory is a disk operation, and that is the real cost.
Directory Implementation
A hash table
Hash the name to a number, and keep the entry in the bucket with that number. The search is then one hash and a short chain instead of a scan.
| Part | What it does |
|---|---|
| the hash function | turns a name into a bucket number |
| the table of buckets | each bucket holds the entries whose names hashed to it |
| a chained overflow list | for names that collide, which they will |
| Operation | What it costs |
|---|---|
| search | one hash, then the length of one chain |
| create | one hash, a scan of that chain to check the name, then an insertion |
| list in order | badly: the entries are in hash order, so listing sorted means reading everything and sorting it |
The weakness is the fixed size of the table. The number of buckets is chosen when the directory is made; as the directory grows, the chains grow with it, and the search becomes linear again. The answer is to rehash into a larger table, which means recomputing every entry's bucket, so it is done rarely and it is expensive when it is.
The same directory, with 64 buckets
average chain = 1000 / 64 = 15.625
About 16 comparisons instead of 500, and usually one block read instead of eight, because a bucket and its chain are small enough to sit together.
Real systems do better again: ext4's directories are hashed B trees, which keep the fast lookup and also grow gracefully. The feature is recorded in the superblock, and it is in the image built in Chapter one hundred five:
$ mkfs.ext4 -q -F -b 1024 -I 128 -O ^has_journal dir.fs 4096 2>&1 | grep -v deprecated
Creating regular file dir.fs
$ dumpe2fs -h dir.fs 2>/dev/null | grep '^Filesystem features' | tr -s ' ' '\n' | grep -c dir_index
1The 1 is the dir_index feature being present: this file system indexes its directories rather than scanning them.
A directory is a file that grows
The measurement that makes the whole chapter concrete. 120 files are created in the root directory of the image, and the directory's own size is read from its inode before and after.
$ printf 'x\n' > one.txt
$ debugfs -R "stat <2>" dir.fs 2>/dev/null | awk '/^User/ {print "the root directory is", $6, "bytes"}'
the root directory is 1024 bytes
$ { for i in $(seq 1 120); do printf 'write one.txt file-%03d\n' "$i"; done; } > script.debugfs
$ debugfs -w -f script.debugfs dir.fs > /dev/null 2>&1; echo done
done
$ debugfs -R "stat <2>" dir.fs 2>/dev/null | awk '/^User/ {print "after 120 files the root directory is", $6, "bytes"}'
after 120 files the root directory is 2048 bytes
$ debugfs -R "ls /" dir.fs 2>/dev/null | tr -s ' ' '\n' | grep -c '^file-'
120Directory Implementation
One block became two. The root directory is inode 2, as Chapter one hundred five showed, and it is an ordinary file: it was 1024 bytes, one block; 120 more names did not fit, so the file system gave it a second block and it is now 2048.
entries = 120 + 3 = 123
2048 / 123, about 17 bytes an entry
123 entries, counting ., .. and lost+found, in 2,048 bytes: about 17 bytes each, which is a name of a few characters, an inode number, and the lengths that go with them. The arithmetic of the earlier section was not a model of something else: it is this.
Distinctions that carry marks
| Linear list | Hash table | |
|---|---|---|
| Search | linear: half the entries on average | one hash and a chain |
| Create | a full search, then insert | one hash, one chain, then insert |
| 1,000 entries, 64 buckets | 500 comparisons | about 16 |
| Listing in order | easy | hard: hash order is not name order |
| Fixed size | no | yes, until it is rehashed |
| Programming difficulty | trivial | some, and the hash function matters |
| Successful search | Unsuccessful search | |
|---|---|---|
| Comparisons in a list of n | n / 2 on average | n, always |
| Happens when | opening a file | creating one |
What it does not mean
A directory is not a special kind of object. It is a file, and it grows in blocks like any other, as the measurement shows.
A hash table does not remove the search. It makes it short.
A linear list is not always wrong. Most directories hold a few dozen entries, where a scan of one block is cheaper than a hash.
Rehashing is not something the user sees. It is the file system's own work, and it is why the fixed table size is tolerable.
The 16 bytes an entry is not a rule. It depends on the length of the names, and the image measured about 17.
Quick revision
- Every path resolution is a directory search, so the structure matters more than any other
choice in the file system.
- A linear list of name and inode number: a search costs n / 2 on average and n when
the name is absent, so creating a file costs a full search.
- Deleted entries are marked unused, filled by the last entry, or kept on a
free list. A sorted list allows a binary search but costs on insertion; real systems use a B tree.
Directory Implementation
- Worked: 1,000 entries of 16 bytes in 1024 byte blocks is 64 entries a block, about
16 blocks; a search averages 500 comparisons and about 8 block reads.
- A hash table costs one hash and one chain: with 64 buckets, 1000 / 64 = 15.625,
about 16 comparisons. It lists badly in order, and its fixed size means rehashing as it grows.
- ext4 records dir_index in the superblock: its directories are hashed B trees.
- Measured: the root directory was 1024 bytes, and after 120 files it is 2048. With
., .. and lost+found that is 123 entries in two blocks, about 17 bytes each.
Test yourself
- Why does the directory structure matter so much? Because every path resolution searches
one directory per component, which makes it the most frequent operation in the file system.
- What does a linear list cost to search, and why is creating a file worse? Half the entries
on average for a successful search; creating a file must prove the name is absent, which means searching all of them.
- Give the three ways of handling a deleted entry in a linear list. Mark it unused, move the
last entry into the hole, or keep a free list of holes. 4. A directory holds 1,000 entries of about 16 bytes in 1024 byte blocks. How many blocks is it, and what does a search cost? 64 entries a block, so about 16 blocks; a successful search averages 500 comparisons and about 8 block reads.
- How does a hash table change the cost, with 64 buckets and 1,000 entries? The search is
one hash and one chain of 1000 / 64 = 15.625 entries, about 16 comparisons and usually one block read.
- Give two disadvantages of a hash table for a directory. Listing the entries in name order
needs a sort, and the table is a fixed size, so the chains lengthen as the directory grows until it is rehashed.
- What does ext4 do, and how can you tell? Its directories are hashed B trees; the
dir_index feature is recorded in the superblock and printed by dumpe2fs. 8. The root directory of an image was 1024 bytes and became 2048 after 120 files were added. What does that show? That a directory is an ordinary file: the names did not fit in one block, so the file system allocated a second one.
The rest of this subject
These notes are cut from the University's printed syllabus. Open the syllabus itself, or the past papers, for the same subject.