Distributed Operating Systems
A **distributed operating system** tries to make a network of machines look like one computer. By definition it hides the network, so a user sees a single machine rather than many. The goal is transparency; the obstacle is that networks fail partially, and no abstraction fully hides a machine that has stopped answering.
The Transparency Goals
A distributed operating system manages a collection of independent machines and presents them as a single coherent system. The measure of success is transparency, and the literature breaks it into specific kinds: access (local and remote resources use the same operations), location (a name does not encode where the thing is), migration (a resource can move without breaking references), replication (copies are invisible to users), concurrency (sharing does not require coordination by the user), and failure (partial failures are hidden).
This is a much stronger claim than a network operating system, which is the distinction most courses test. NFS, SSH, and a shared printer give you a network OS: the machines remain visibly separate and you address them explicitly. A distributed OS would let you run a process without knowing or caring which machine executes it.
The value is real — pooled resources, transparent load distribution, fault tolerance through redundancy, incremental scaling by adding nodes. The 1980s expectation was that this would become the normal way to build systems.
- Access — the same call local or remote
- Location — the name encodes no host
- Migration — resources move while in use
- Replication — copies stay invisible
Why Partial Failure Broke the Model
The obstacle was not bandwidth or latency, both of which improved enormously. It was partial failure: in a single machine, a component failure takes down the whole machine and the failure is unambiguous. In a distributed system a remote node can be slow, crashed, or unreachable, and no timeout can distinguish these cases.
This is why failure transparency — hiding failures from the programmer — turned out to be the goal that could not be delivered. Waldo and colleagues argued the point directly in 1994's A Note on Distributed Computing: making a remote call look identical to a local one is a leaky abstraction, because a local call cannot fail halfway and a remote one can. Systems that pretended otherwise produced applications that broke unpredictably at scale.
Consistency compounds it. Keeping distributed state coherent means consensus, consensus means round trips, and the CAP theorem formalises that during a network partition you must choose availability or consistency. A single-system image needs strong consistency almost everywhere, which is exactly the expensive choice.
- Partial failure has no single-machine equivalent
- A timeout cannot separate crashed from slow
- A lost reply is indistinguishable from a lost request
- CAP forces a choice during any partition
What The Research Systems Proved
Distributed operating system examples from research are worth knowing because their ideas survived even where the systems did not. Amoeba, from Tanenbaum's group at VU Amsterdam, pooled CPUs into a processor pool and allocated them per-process — and produced Python, written by Guido van Rossum for Amoeba scripting. Plan 9 from Bell Labs took 'everything is a file' to its conclusion: every resource, local or remote, appears in a per-process namespace assembled from 9P file servers.
Sprite at Berkeley demonstrated transparent process migration, moving running processes between workstations to exploit idle machines. MOSIX brought migration to Linux clusters and remained in production use for HPC for years.
Plan 9's influence is the clearest: its 9P protocol, per-process namespaces, and UTF-8 (invented by Thompson and Pike for it) are all in wide use, and Linux namespaces — the mechanism containers are built from — are recognisably the same idea.
- Amoeba pooled processors, and produced Python
- Plan 9 gave us 9P, namespaces and UTF-8
- Sprite demonstrated transparent process migration
- Linux namespaces descend from these ideas
Four transparency goals tested against one node that stops answering
# The distributed OS promise, and the one part of it that cannot be kept.
def resolve(name, directory):
# LOCATION TRANSPARENCY: the name carries no host. The lookup does.
return directory[name]
def migrate(proc, target):
# MIGRATION TRANSPARENCY: freeze, ship the state, rebind the handles.
proc["node"] = target
return proc
def remote_read(node, reachable, node_known_dead):
# FAILURE TRANSPARENCY: this is the one that does not work.
if reachable:
return "completed"
if node_known_dead:
return "failed"
# A timeout cannot distinguish these three cases:
# 1. the node crashed before doing the work
# 2. it did the work and the REPLY was lost
# 3. it is merely slow and will answer later
return "unknown"
DIRECTORY = {"/data/readings": "B"}
proc = {"pid": 41, "node": "A"}
print("resolved:", resolve("/data/readings", DIRECTORY), "(no host in the name)")
print("migrated to:", migrate(proc, "B")["node"], "(same pid", str(proc["pid"]) + ")")
for reachable, dead, label in [(True, False, "healthy"), (False, True, "confirmed dead"),
(False, False, "silent")]:
print("%-15s -> %s" % (label, remote_read("B", reachable, dead)))
print("a local call has two outcomes; a remote call has three")
// The distinction the abstraction cannot hide.
#include <iostream>
#include <chrono>
#include <optional>
struct Handle {
int node;
};
// A LOCAL call: cannot fail halfway. It returns, or the whole process dies.
int local_read(Handle h, char* buf, std::size_t n);
// A REMOTE call: three outcomes, and a timeout tells you only that you
// are in one of the last two -- never which.
enum class Outcome {
Completed, Failed, Unknown
};
std::optional<int> rpc_wait(int node, std::chrono::milliseconds);
bool node_confirmed_dead(int node);
Outcome remote_read(Handle h, char*, std::size_t,
std::chrono::milliseconds timeout) {
if (rpc_wait(h.node, timeout)) return Outcome::Completed;
if (node_confirmed_dead(h.node)) return Outcome::Failed;
return Outcome::Unknown; // slow? crashed? reply lost? unknowable.
}
// Waldo et al., "A Note on Distributed Computing" (1994): making a remote
// call look identical to a local one is a leaky abstraction, because the
// failure modes are not the same set. Unknown has no local equivalent.
int main() {
std::cout << "a local call has 2 outcomes; a remote call has 3\n"
<< "the third is why failure transparency cannot be delivered\n";
}// Modern systems achieve the goal by making failure EXPLICIT, which is
// the opposite of what a classical distributed OS attempted.
interface ClusterStore {
// Location transparency: kept. No host in the key.
byte[] get(String key) throws Unavailable, Timeout;
// ^^^^^^^^^^^^^^^^^^^^
// Failure transparency: deliberately abandoned. The caller is
// REQUIRED to handle partial failure -- it is in the signature.
}
class Caller {
byte[] read(ClusterStore s, String k) {
try {
return s.get(k);
}
catch (Timeout t) {
// The application decides: retry, fall back, degrade, fail.
// A distributed OS tried to make this decision invisibly,
// and there is no correct invisible answer.
return fallback(k);
}
}
}Step through it
Running on a 3-node cluster running a migrated process; node B becomes unreachable at t=4
Read all 14 Steps
- three machines presenting as one system Nodes A, B and C run a distributed operating system. A user logs in and sees one namespace, one process table, one pool of CPUs. Whether that illusion holds is what the next steps test — and the goals are specific, not vague: access, location, migration, replication, concurrency and failure transparency.
- access transparency — the same call for local and remote The process opens /data/readings. It uses the same open() it would use for a local file; there is no separate remote_open in the interface. This one is genuinely achievable and has been achieved many times — NFS, 9P and countless RPC layers do exactly this.
- location transparency — the name carries no host The path is /data/readings, not //nodeB/data/readings. Nothing in the name says where the data is, so the file can live anywhere and the name still resolves. Plan 9 took this furthest: every resource, local or remote, appears in a per-process namespace assembled from 9P file servers.
- the process is migrated from A to B The scheduler notices A is loaded and B is idle, checkpoints the process, ships its state to B, and rebinds its open handles. Sprite at Berkeley demonstrated exactly this in the 1980s, moving running processes between workstations to exploit idle machines.
- migration transparency — the process never noticed It keeps executing on B with the same PID, the same open files and the same view of the filesystem. Its own code contains nothing about migration. This works, and MOSIX kept it in production use on Linux clusters for years.
- replication transparency — three copies, one file The readings file is replicated across all three nodes for durability. The process reads it as one file and never learns there are copies, which node served the read, or that a write must reach several places. While the network is healthy, this holds cleanly.
- t=4 — node B stops answering The process is running on B, and B goes silent. Here is the question the whole model turns on: what happened? B could have crashed. B could be alive but partitioned by a failed switch. B could be alive, healthy, and merely slow because of a garbage collection pause or a saturated link.
- no timeout can tell these apart A short timeout declares healthy-but-slow nodes dead. A long timeout leaves the system hung on genuinely dead ones. There is no correct value, because the information needed to choose is exactly the information the network is failing to deliver. This is not an implementation weakness — it is a property of asynchronous networks.
- and the request may already have executed Worse: if B received the write and its reply was lost, the operation completed. Retrying applies it twice. If B never received it, not retrying loses it. The caller must choose, and cannot know which case it is in — which is why idempotency became a design requirement rather than a nicety.
- why this is different from a local failure In a single machine, a component failure takes down the whole machine, and the failure is unambiguous — there is no state where half your memory is reachable and half is not answering. Partial failure has no local equivalent, so an abstraction that maps remote calls onto local ones has no way to represent it.
- Waldo's argument, 1994 Waldo, Wyant, Wollrath and Kendall made this precise in A Note on Distributed Computing: making a remote call look identical to a local one is a leaky abstraction, because a local call cannot fail halfway and a remote one can. Systems that pretended otherwise produced applications that broke unpredictably at scale — which is the historical record of transparent RPC frameworks.
- consistency compounds the cost Keeping the three replicas coherent requires consensus, consensus requires round trips, and CAP formalises that during a partition you must choose availability or consistency. A single-system image needs strong consistency nearly everywhere, which is precisely the expensive side of that choice.
- what the successors did instead Kubernetes schedules containers across a cluster, restarts what dies, and abstracts placement — location and migration transparency delivered at the orchestration layer rather than the kernel. The difference is that a pod can be evicted, a gRPC call returns UNAVAILABLE, and a distributed database states its consistency level. Failure is in the interface, not hidden behind it.
- the ambition was right; one goal was wrong Pooling resources across machines is now completely standard — nobody provisions per-machine any more, and that was the point of the exercise. What did not survive is pretending the network is not there. The research systems earned their place regardless: Amoeba pooled processors and produced Python along the way, written by van Rossum for Amoeba scripting; Plan 9 gave us 9P, per-process namespaces and UTF-8, and Linux namespaces — the mechanism containers are built from — are recognisably the same idea; Sprite proved transparent process migration. Five of the six transparency goals are routinely delivered today. The sixth was not a matter of insufficient engineering, and treating it as achievable is what produced a generation of systems that failed in ways their users could not diagnose.
What Replaced It
The practical goal, running work across many machines without managing each one, was achieved — just not by a shared kernel. Kubernetes schedules containers across a cluster, restarts failures, and abstracts placement; from the operator's view a deployment runs 'on the cluster'. That is location and migration transparency delivered at the orchestration layer instead of the kernel layer.
The difference is that these systems make failure explicit rather than hiding it. A Kubernetes pod can be evicted, a gRPC call returns UNAVAILABLE, a distributed database tells you its consistency level. The application is required to handle partial failure, which is precisely the thing the classical distributed OS tried to abstract away.
So the honest verdict is that the ambition was right and one specific goal was wrong. Pooling resources across machines is now completely standard. Pretending the network is not there is what did not survive contact with production.
- Kubernetes delivers placement and restart transparently
- gRPC returns UNAVAILABLE rather than hiding it
- Applications are required to handle partial failure
- Five of the six transparencies are routinely achieved