An Overview of the Everything Is a File Philosophy, Sockets, Spawning Processes, and Notes (signals). #80
KaiNorberg
announced in
Announcements
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
PatchworkOS strictly follows the "everything is a file" philosophy in a way inspired by Plan9, this can often result in unorthodox APIs that seem overcomplicated at first, but the goal is to provide a simple, consistent and most importantly composable interface for all kernel subsystems, more on this later.
Included below are some examples to familiarize yourself with the concept. We, of course, cannot cover everything, so the concepts presented here are the ones believed to provide the greatest insight into the philosophy.
Sockets
The first example is sockets, specifically how to create and use local seqpacket sockets.
To create a local seqpacket socket, you open the
/net/local/seqpacketfile. This is equivalent to callingsocket(AF_LOCAL, SOCK_SEQPACKET, 0)in POSIX systems. The opened file can be read to return the "ID" of the newly created socket which is a string that uniquely identifies the socket, more on this later.PatchworkOS provides several helper functions to make file operations easier, but first we will show how to do it without any helpers:
Using the
sread()helper which reads a null-terminated string from a file descriptor, we can simplify this to:Finally, using use the
sreadfile()helper which reads a null-terminated string from a file from its path, we can simplify this even further to:Now that we have the ID, we can discuss what it actually is. The ID is the name of a directory in the
/net/localdirectory, in which the following files exist:data: Used to send and retrieve datactl: Used to send commandsaccept: Used to accept incoming connectionsSo, for example, the sockets data file is located at
/net/local/[id]/data.Say we want to make our socket into a server, we would then use the
ctlfile to send thebindandlistencommands, this is similar to callingbind()andlisten()in POSIX systems. In this case, we want to bind the server to the namemyserver.Once again, we provide several helper functions to make this easier. First, without any helpers:
Using the
F()macro which allocates formatted strings on the stack and theswrite()helper that writes a null-terminated string to a file descriptor:Finally, using the
swritefile()helper which writes a null-terminated string to a file from its path:If we wanted to accept a connection using our newly created server, we just open its accept file:
The file descriptor returned when the accept file is opened can be used to send and receive data, just like when calling
accept()in POSIX systems.For the sake of completeness, to connect the server we just create a new socket and use the
connectcommand:Documentation
File Flags?
You may have noticed that in the above section sections the
open()function does not take in a flags argument. This is because flags are directly part of the file path so to create a non-blocking socket:Multiple flags are allowed, just separate them with the
:character, this means flags can be easily appended to a path using theF()macro. Each flag also has a shorthand version for which the:character is omitted, for example to open a file as create and exclusive, you can door
For a full list of available flags, check the Documentation.
Permissions?
Permissions are also specified using file paths there are three possible permissions, read, write and execute. For example to open a file as read and write, you can do
or
Permissions are inherited, you can't use a file with lower permissions to get a file with higher permissions. Consider the namespace section, if a directory was opened using only read permissions and that same directory was bound, then it would be impossible to open any files within that directory with any permissions other than read.
For a full list of available permissions, check the Documentation.
Spawning Processes
Another example of the "everything is a file" philosophy is the
spawn()syscall used to create new processes. We will skip the usual debate onfork()vsspawn()and just focus on howspawn()works in PatchworkOS as there are enough discussions about that online.The
spawn()syscall takes in two arguments:const char** argv: The argument vector, similar to POSIX systems except that the first argument is always the path to the executable.spawn_flags_t flags: Flags controlling the creation of the new process, primarily what to inherit from the parent process.The system call may seem very small in comparison to, for example,
posix_spawn()orCreateProcess(). This is intentional, trying to squeeze every possible combination of things one might want to do when creating a new process into a single syscall would be highly impractical, as those familiar withCreateProcess()may know.PatchworkOS instead allows the creation of processes in a suspended state, allowing the parent process to modify the child process before it starts executing.
As an example, let's say we wish to create a child such that its stdio is redirected to some file descriptors in the parent and create an environment variable
MY_VAR=my_value.First, let's pretend we have some set of file descriptors and spawn the new process in a suspended state using the
SPAWN_SUSPENDEDflagAt this point, the process exists but its stuck blocking before it is can load its executable. Additionally, the child process has inherited all file descriptors and environment variables from the parent process.
Now we can redirect the stdio file descriptors in the child process using the
/proc/[pid]/ctlfile, which just like the socket ctl file, allows us to send commands to control the process. In this case, we want to use two commands,dup2to redirect the stdio file descriptors andcloseto close the unneeded file descriptors.Next, we create the environment variable by creating a file in the child's
/proc/[pid]/env/directory:Finally, we can start the child process using the
startcommand:At this point the child process will begin executing with its stdio redirected to the specified file descriptors and the environment variable set as expected.
The advantages of this approach are numerous, we avoid COW issues with
fork(), weirdness withvfork(), system call bloat withCreateProcess(), and we get a very flexible and powerful process creation system that can use any of the other file based APIs to modify the child process. In exchange, the only real price we pay is overhead from additional context switches, string parsing and path traversals, how much this matters in practice is debatable.For more on
spawn(), check the Userspace Process API Documentation and for more information on the/procfilesystem, check the Kernel Process Documentation.Notes (Signals)
The next feature to discuss is the "notes" system. Notes are PatchworkOS's equivalent to POSIX signals which asynchronously send strings to processes.
We will skip how to send and receive notes along with details like process groups (check the docs for that), instead focusing on the biggest advantage of the notes system, additional information.
Let's take an example. Say we are debugging a segmentation fault in a program, which is a rather common scenario. In a usual POSIX environment, we might be told "Segmentation fault (core dumped)" or even worse "SIGSEGV", which is not very helpful. The core limitation is that signals are just integers, so we can't provide any additional information.
In PatchworkOS, a note is a string where the first word of the string is the note type and the rest is arbitrary data. So in our segmentation fault example, the shell might produce output like:
All that happened is that the shell printed the exit status of the process, which is also a string and in this case is set to the note that killed the process. This is much more useful, we know the exact address and the reason for the fault.
For more details, see the Notes Documentation, Standard Library Process Documentation and the Kernel Process Documentation.
But why?
I'm sure you have heard many an argument for and against the "everything is a file" philosophy. So I won't go over everything, but the primary reason for using it in PatchworkOS is "emergent behavior" or "composability" whichever term you prefer.
Take the
spawn()example, notice how there is no specialized system for setting up a child after it's been created? Instead, we have a set of small, simple building blocks that when added together form a more complex whole. That is emergent behavior, by keeping things simple and most importantly composable, we can create very complex behavior without needing to explicitly design it.Let's take another example, say you wanted to wait on multiple processes with a
waitpid()syscall. Well, that's not possible. So now we suddenly need a new system call. Meanwhile, in an "everything is a file system" we just have a pollable/proc/[pid]/waitfile that blocks until the process dies and returns the exit status, now any behavior that can be implemented withpoll()can be used while waiting on processes, including waiting on multiple processes at once, waiting on a keyboard and a process, waiting with a timeout, or any weird combination you can think of.Plus its fun.
PS. For those who are interested, PatchworkOS will now accept donations through GitHub sponsors in exchange for nothing but my gratitude.
This is a cross-post from Reddit. From now on all big Reddit posts will be associated with a new GitHub discussion.
Beta Was this translation helpful? Give feedback.
All reactions