Arch-Engineer
Why the fork() ?
Foreseeing a fork()
I remember taking a systems programming class as a freshman, where we learned how to create a new process in Unix: you make a copy of the current process, and then modify it to run a different program. "How odd," I thought. Yet the arguments were strong and commonplace, and I quietly accepted it, albeit with reservations.
Earlier this year, I read the essay "A fork() in the road" by a group of OS researchers, which argued what my freshman self had wanted to hear: that the Unix fork()
API was a relic from an earlier time, unsuitable for most use-cases, and a design liability.
Their case is ironclad, their proposed alternatives detailed, and I'll let you read it yourself. I want to ask a question with broader software design relevance: how could the original Unix developers have foreseen the problems with fork()
?
I teach that the answers to software design questions are to be found not in the code, but in the specifications. And the specification for fork()
, written in English, looks something like this:
Preconditions: The calling program is a valid process.
Postcondition: There are now two processes identical to the caller's process. Everything is the same about these two processes, except that one is considered the parent of the other in the OS process tree; fork()
returns the PID of the child process to the parent, and returns 0 to the child.
And this is already enough for a trained software designer to recognize the disaster. Because "everything" is a very scary word to appear in a specification.
"Everything" means that the code must interact with every feature added to processes. When fork()
was first created, this was not significant, but its growth was entirely predictable. As the authors write:
Fork's semantics have infected the design of each new API that creates process state. The POSIX specification now lists 25 special cases in how the parent's state is copied to the child: file locks, timers, asynchronous IO operations, tracing, etc. [...] Any non-trivial OS facility must document its behaviour across a fork, and user-mode libraries must be prepared for their state to be forked at any time. The simplicity and orthogonality of fork is now a myth.
When you're facing an "everything" in a specification, you have a few options.
The naive option is to let your program be, and accept that one part of your code will need to be modified for every other feature added.
An alternative is to try to contain the "everything." If you're implementing a trading card game which each card can do "anything," then you can try to structure everything a card may affect into a GameState
, and design special categories of cards that only affect a well-defined portion of this GameState
, so that only the most insidious of cards require facing the full complexity.
Or, you can take the option proposed by these essay authors: get rid of the need to deal with "everything" in the first place.
Read: A fork() in the road