About 40 years ago, when the UNIX operating system was designed, its creators thought that a powerful virtual filesystem, shell scripts, and text I/O streams ought to be enough for everybody. And this is how Linux ended up nowadays with many incompatible way to do such fundamental things as boot-time initialization, GUI rendering, audio input and output, and now hardware accelerated graphics.
“Them fools !”, probably thought the Windows NT designers some 20 years later. “We shall make sure that every system functionality is specified in a standard way, so that this never happens to us.”. And nowadays, there are several incompatible APIs for pretty much every single functionality of Windows.
If there’s a lesson to be learned from the past there, I think it’s that operating systems need standard ways to do important things, but that doing standards right is unfortunately fiendishly hard.
What filesystems are good for
As I discussed here before, the idea of turning the virtual filesystem into a full-blown repository of system resources is not, in itself, fundamentally wrong. It is very nice to be able to discover and access all system resources through standard filesystem queries. The only part where I disagree with UNIX designers on this front, is on the statement that data streams are good enough as a universal metaphor.
Consider what happens when system resources have to be queried and accessed without a filesystem. Application programs run in isolated processes, so they have, a priori, no way of knowing what is available. The kernel can make this information available, through a chunk of shared memory or a system call interface, but that burdens it with the great complexity of being able to describe every kind of system resource there is… and good OS designers don’t want their vulnerable kernel code to be complex.
Or maybe they can contact a process with that information. But which process? How do they know its PID? Or filename, even? How do they know which IPC primitive is best suited to communicate with it? How can this primitive support further OS evolution without backward compatibility breakage? How is it used?
Answering all of these questions, one ends up essentially devising a protocol for system resource enumeration, query, and access. And I think that a virtual file system, which any OS will have anyway, does just fine as such a protocol. Without requiring more than a small number of resource primitives such as flat files (randomly accessible chunks of data), data streams (half-duplex communication channels transmitting data of homogeneous or heterogenous type), and functors (ways to make a request to an OS component).
A well-designed filesystem-based system resource abstraction frees application software from the duty to know which process implements which system functionality, and in the way gives OS developers much flexibility for implementation refactoring when things start to get a little messy. It also makes it possible to change the inner nature of a system resource at run time, without affecting the behavior of running applications. For instance, an audio application which is hard-coded to send its output to a given process, is much less powerful than one whose audio output may be dynamically routed to another application at run time, without the emitting application being even aware of it.
Why we need more than filesystem primitives
Filesystem primitives, for all the good they bring, must be kept simple and general-purpose. If we start to make one dedicated filesystem primitive for audio output, one for video streams, one for network connections, and so on, our filesystem abstraction will gradually grow in complexity until the point where it tries to standardize every single OS functionality in existence and collapses under its own weight. No one wants that to happen.
Yet there is a need for such specialized interfaces. There is a need for standard audio and video streams, for standard timing routines, for standard UI primitives. We can implement such standards using filesystem primitives, but that’s only half of the story, as much like with system resource access before, defining a standard OS resource involves a lot more work than just creating a resource in a virtual filesystem, no matter how fancy said virtual filesystem is.
Take, for example, GUI display. Typically, to display application user interfaces, one will either go through a computer’s GPU, or through the limited but standard framebuffer abstraction of it provided by the computer’s firmware (VESA VBE, UEFI GOP…). Of these, dedicated GPU drivers bring the most powerful functionality, but firmware framebuffer interfaces are a lot more likely to be available in a community-driven OS project. So we may want to support both in our GUI toolkit.
But wait! There’s more to this! Firmware framebuffer interfaces come in many size and shapes. Sometimes, even different versions of a given firmware interface may differ dramatically, as is the case with VBE. Each form of a framebuffer interface requires specific handling precautions. And as for GPUs, there basically aren’t two chips in the wild with the same interface and capabilities, even when these are accessed using such complex abstractions as OpenGL.
Should a GUI toolkit need to support all these individual hardware variations? Of course not. And addressing this problem is what system abstractions are for. To solve this specific problem, one could use a range of solutions, from building a standard framebuffer interface to every kind of graphics hardware (which is easiest, but precludes later use of hardware-accelerated rendering), to building a full 2D rendering stack like Cairo, AGG, or Direct2D.
The important thing to realize there is that there is no God-given, universally right answer to this OS design problem. The only golden rule is that there has to be a standard way to do things, to avoid Linux-style fragmentation issues, and that the standard has to be good enough, to avoid Windows-style constant standard renewal.
Accessing standard interfaces through the filesystem
As described above, it would seem unwise to make every operating system standard a dedicated filesystem primitive. However, there is value in exposing and accessing such standard interfaces through the filesystem. And although I cannot prove it in a general way, I can showcase how fun and useful it can be for some previously mentioned examples of standardized resources.
For audio I/O, consider a situation where an application starts to emit audio data. It can do so by creating an output data stream containing specially formatted data that are pretty much audio data frames along with some metadata. As any system resource, the application’s output data stream is made accessible through the filesystem. It is, in addition, connected to another data stream corresponding to the main system audio output, then sound starts streaming through it.
Now, imagine that the application’s audio output has a high dynamic range. It is sometimes very quiet, and sometimes very loud. Unfortunately for our user, he is in a loud place where the quiet parts of the audio are not understandable. What should he do? If he amplifies the overall system volume, he risks being deafened by othe system sounds, which are designed to be pretty loud. If he amplifies the application volume, he will experience saturation in the loud parts of the application’s audio output, which is not enjoyable either. The application developers did not think of anything else, so at first sight, our user is stuck.
But in an operating system where all audio I/O channels are exposed through the filesystem, a user with some audio processing knowledge knows how to address this problem. The fix is to insert an audio filter known as a compressor, which amplifies quiet parts with respect to loud parts, inbetween the application and the hardware audio output. This will deteriorate the audio quality somewhat, but not to the extent that full-on amplification would, and it will make the entire application audio output well understandable. And it can be done without modifying the application, not even shutting it down.
Framebuffer graphics in a filesystem? Easy. Just expose it as a flat file, to which software can write using standard memory-mapping or sequential I/O techniques. And if you are lucky enough to have accelerated filling and blitting operations available, expose them through a functional interface. With this, taking a screenshot becomes as simple as making a copy of the flat file representing the virtual framebuffer.
In general, as most programming done these days is imperative, a lot of system libraries can be transparently translated to filesystem primitives in the form of functional interfaces. Call a filesystem functor, and don’t care how exactly it is implemented under the hood. Don’t hunt processes by name, and don’t rely on a fragile shared library system to implement the stuff which should be process-isolated.
I’m pretty convinced that the core insight from UNIX and Plan 9 according to which every system resource should be accessible through the file system was pure genius. But that to actually make it work in practice, one must expand current notions of a filesystem somewhat, and be clear as for what filesystems cannot do.
Filesystem primitives provide a uniform interface for accessing system resources no matter where they originate from, and how they are actually implemented. They allow querying which resources are and aren’t there, and their level of indirection can also be used to increase operating systems’ fault tolerance, by greatly increasing the amount of run-time modifications that can be brought to said resources. Transparently restarting or replacing the process that implements a given filesystem resource, for example, is not very difficult, provided that the filesystem design takes such usage patterns into account.
At the same time, there needs to be more to filesystem primitives than just sequential data streams. Myself, I propose the extra addition of flat files, which are necessary to random access and memory mapping (the latter allowing for e.g. fast data sharing between processes), and functors, which transparently map the imperative interfaces that are at the core of all modern programming languages.
And finally, it’s important to remember that filesystem primitives do not void the need for system standards in vital areas such as audio I/O or GUI rendering. They simply provide a standard set of building blocks from which these standard resources can be exposed to the rest of the running operating system. But there’s a lot more to operating system standard design than this.
Thank you for reading!