Filesystem Layers
When designing systems, it's often the case that the design process becomes more manageable when we break the system down into layers. For filesystems, we can think of the relevant layers as follows:
- High-Level Filesystem: This layer understands concepts like filenames, directories, and permissions. It allows files to be of arbitrary length in bytes, and allows reading and writing starting from any offset in a file. This layer is what most users interact with when they use a computer. This functionality is built on top of the low-level filesystem.
- Low-Level Filesystem: This layer provides a more rudimentary concept of files. Files are just numeric file IDs, and the filesystem provides a way to read and write logical blocks of data to these files. Sizes of low-level files are typically multiples of the logical block size.
- Block Device: This layer provides a way to read and write logical blocks of data to a disk, and is what the low-level filesystem uses to read and write data to the disk. The block device layer may provide buffering of writes and caching of reads to improve performance.
Although we might prefer to have strict and clear boundaries between these layers, in practice, in real operating systems, the layers are often more intertwined. For example, the low-level filesystem might have some knowledge of the block device and store some special information on behalf of the high-level filesystem.
In this lesson, we'll focus on the low-level filesystem, and in the next lesson, we'll build on that to understand the high-level filesystem.
A Low-Level Filesystem Interface
Let's envision a low-level filesystem interface, starting with the following functions:
create(): Create a new file and return its file ID.delete(fileID): Delete a file.read(fileID, blockNum, count): Read data from a file starting at a block number and reading a certain number of blocks.write(fileID, blockNum, count, data): Write data to a file starting at a block number and writing a certain number of blocks.
Can I write to a block that isn't already part of the file? If not, how do I add blocks to a file?
How do I know how big a file is?
Can I make a gap in the middle of a file by not writing to some of the blocks? If so, can I later find out where the gaps are?
What about putting MORE blocks into a file? And can I put them in the middle of a file?
Let's clarify and extend the interface to address some of these questions:
extend(fileID, count): Extend a file by a certain number of blocks.size(fileID): Return the size of a file in blocks.
We'll make some features optional, including
- Files with gaps in the middle.
- Inserting blocks into the middle of a file.
By default, we'll assume that our low-level filesystem doesn't support these features as they're not essential, but we'll note when it would technically be possible to add them.
Hay! Wouldn't it be useful to have a
create(size)function to create a file of a particular size?
But you can always
createa(n empty) file and thenextendit to the desired size, right?
Yes, we can make do without
create(size), but it would certainly be convenient for the low-level layer to know how big a file is supposed to be from the start. In practice, the higher-level layer doesn't provide any way to specify the size of a file when it's created, so in practice it would always be called ascreate(0), so we don't bother with it here.
Actually, sometimes the high-level API provides a
clonefileorcopyfilecall that creates a new file of the same size as an existing file, so there are cases where the size of a file is specified at creation time in the high-level API.
Meh. Whatever. Worse is better and all that.
(When logged in, completion status appears here.)