Linux Namespaces Are a Poor Man's Plan 9 Namespaces
yotam.net
yotam.net
First, it's not always easy to map every object operation into either an open read or write. With time, we should have seen a lot of ugly interfaces resulting from this limitation.
Second, hardware progress, the web, etc., introduced a lot of heterogeneity and complexity. People could no more keep up with simple general designs. And to squeeze every bit of performance, everyone was doing things different based on the hardware and the workloads. They use whatever makes their software, drivers, and OS objects work as fast as they could.
And this is how we ended with the extreme fragmentation and heterogeneity we have in Linux, which explains the complex and less general implementation of its namespaces and its other features.
Edit: fix typos
Heck, even for basic files on disk, and even more so for sockets, the traditional open/read/write/close is starting to feel not so great. There is reason why iouring is hailed as the second coming, and it solves just part of the problems; stuff like fsync apocalypse comes to mind.
And ioctls are imho completely disgusting hack.
Ultimately IO is intrinsically complex topic, and trying to paper over that complexity with simple interfaces is disingenuous and falls flat on edge cases.
This is a bad design. Process is not a file because you cannot send signals to a file, or cannot debug a file. Network socket is not a file because you cannot get file's peer address. Shared memory is not a file. And so on.
Debugging is hard to imagine, yes. One could look at /proc/12345/mem, write breakpoints in there, but I'm not sure about how to do the more exotic things.
npm install -g styled-linux
The serialized streams make it easier to think of these ioctl-alikes as something you can easily access over a network (9p).
And that’s how resource sharing is done in plan 9.
The question isn't "will it be just as bad" but "would some things become easier, and if so, how many things would become easier?".
Because if the latter, then it's a worthwhile topic to think about even if we never use it in any commercial sense of of the word.
It's far too easy to pretend reality is not complex and that "elegant" solution somehow will fit everything
The situation there is exactly the opposite of a syscall, it is rather that the kernel calls into userspace to perform a helper function.
Communication with the socket is still read()/write()/..., so there are still syscalls. The userspace program will do a read() to get the next struct+packet out of the socket.
The modern, syscall-less interface for stuff would be io_uring. There, you do not need to read(), you can just get your data written into a userspace buffer that you can mwait or poll on.
Outside of lots of non-obvious problems with synchronization, this is your example that best fits the idea. This interface is probably a good one.
> And of course you can get a peer address from /dev/tcp
You will have lots and lots of problems with access controls if this is your only interface.
> Shared memory can be a file
Coercing random access memory into a serial file just to go and emulate a random access over that file is... not a great way to deal with a high-performance primitive.
The file doesn't need to have an on-disk representation. I don't see why an mmap-ed file should behave any different from a SHM segment. They are basically the same thing, the SHM segment even has a file descriptor. It just doesn't have a name somewhere in the filesystem hierarchy. https://man7.org/linux/man-pages/man7/shm_overview.7.html
Because if you just add some high-level interface without any concern for performance, yeah, you get what Linux does today. But if you make them a core concern of your shared memory interface, you will certainly lose performance on the cases it's mapped as memory. And "everything is a file, but this one here is actually all about random access" doesn't give you much abstraction.
As somebody already said on the comments, the nice (maybe IMO, I'm not sure) thing about Plan9 is that every resource is named somewhere in a tree. The fact that those things are "files" only detracts from the value and makes the system less fit for modern usage.
But well, if the proposal is to unify everything, you will have to unstream network connections too.
This interface is a bad one because if you want to filter syscalls then it will be difficult to distinguish write to a file from sending a signal.
Try `strace|grep open` on any program, and you will be spammed with a number of shared libraries. You need to filter them out anyway.
In Plan 9, you send signals and messages to a file by writing to it. And read messages by reading from it.
It's in essence no different than OOP or actor model or what have you.
It’s really “Everything has a File Descriptor”, nothing about the concept of a file has changed.
It's in essence no different than OOP or actor model or what have you.
I’m sure there’s an isomorphism that could be drawn but it does nothing to show that it’s an equally good paradigm to write software in. IMO, it’s a tortured abstraction.
This is akin to how shell piping in Unix is also just... text and bytes. This is limiting and produces many ad-hoc protocols.
But take what Plan 9 was trying to do, and add to it what Microsoft's PowerShell tried to do, where you stream objects, structured information, instead of just bytes, on the way in and out of commands and files...
And suddenly... we got ourselves an Erlang.
Many perfect and awesome systems have been built that never saw significant adoption.
I agree with ESR's observation: "Plan 9 failed simply because it fell short of being a compelling enough improvement on Unix to displace its ancestor. Compared to Plan 9, Unix creaks and clanks and has obvious rust spots, but it gets the job done well enough to hold its position. There is a lesson here for ambitious system architects: the most dangerous enemy of a better solution is an existing codebase that is just good enough." Source: https://www.catb.org/~esr/writings/taoup/html/plan9.html
Another amazing OS that never caught on was BeOS: https://en.wikipedia.org/wiki/BeOS Neal Stephenson's description of BeOS as "fully operational Batmobiles" is accurate and it is sad that BeOS did not become more mainstream. See https://people.cs.georgetown.edu/~clay/classes/spring2010/os...
interface UniversalInterface {
fun open(…)
fun read(…)
fun write(…)
fun close(…)
}
If you went to a software engineering design review meeting, proposing that several different kinds of objects representing everything from files, network sockets, to arbitrary devices should all use the interface above, you’d be laughed out of the room.Ultimately, when someone does end up implementing something like the above as the sole interface for some major component, it’ll result in libraries that return a wrapping object with a more useable and pleasing interface that abstracts away the ugliness of using UniversalInterface to interact with said component.
I see two ways forward with this. Either: (a) present the option of using UniversalInterface to interact with X object, in addition to interface that’s much more idiomatic and closer to how X object actually behaves.
Or, (b) come up with an alternative universal (or flexible) object interaction interface that’s much more flexible than UniversalInterface.
> Or, (b) come up with an alternative universal (or flexible) object interaction interface that’s much more flexible than UniversalInterface.
That’s, basically, what COM/Corba/… are. There, UniversalInterface has an additional call “If you are a Foo, give me your Foo interface”, with Foo as an argument to that call.
That's really good. The fact that the file interface just gives you a string of bytes, with no concept of structure or type safety in the interface itself is a major flow of it. Type safety is immensely valuable.
You're describing a meeting in which people seem unaware of the purpose of uniform interfaces. Not sure we could call such a meeting "engineering design" meeting, because engineers tend to know better.
If I went up and proposed the same interface for all cables, displays, mice, keyboards, speakers, hard drives, phones... and called it USB, would I also be laughed out of the room?
There's no benefit to reinventing open/close/read/write in 50 different ways. The goal is to do it once, and then build more complex interfaces on top of it. That is, unless you're paid by the number of lines of code you write.
There is an universal interface, it is called "system call". Rather than build unnecessary layer on top of it, improve the syscalls if you don't like them, and get rid of ioctls, /proc, /sys and other pseudo-syscall abstractions.
Hence open is not part of your UniversalInterface, but it is a way to obtain a reference to it. It seems reasonable to have a generic way to dispose of an UniversalInterface (hence close). read/write are simply a generic ways to send and receive messages from UniversalInterface, not unlike a dynamically typed object. Ideally you would do a checked down cast to your actual interface [3], but this was designed to work with C so you have to make do.
[1] I don't subscribe to the Everything is an Object in the OO sense, but having a common base class for most OS resources seem a reasonable solution.
[2] or at the very least there isn't always a cross-resource-type namespace.
[3] Not unlike COM QueryInterface, and in fact UniversalInterface is equivalent to IUnknown
This is very obviously an acceptable solution because the whole world runs on TCP.
The metadata around files - ownership, permissions, creation time - apply to a lot more than files.
I don't really see it being laughed out of the room.
Doubtful. Why are the main paradigms?
Everything is an object
Everything is a function
Everything is a resource (REST)
Uniform interfaces are common for good reason: you gain a lot of flexibility for redirection, introspection and policy control.
Yes? All interfaces do that, especially system interfaces. It's the whole purpose of interface - to provide implementation logic in a usable form. Try to call the kernel by hand and see the difference.
Wouldn't the same be applicable to files, processes, devices and so on?
And like it or not OOP shows that it's possible for one idiom to describe all the things when it's flexible enough.
I've got a lot of experience working in systems that model everything as a series of resources passing messages and it works very well. The entire QNX operating system right down to its POSIX support uses this underlying primitive and, while you would never see it in your own code their system-wide profiler leans on this design to make it easy to see how control flow moves between isolated threads and processes within the software system.
Both "special" and "device" files are still "files".
1. ioctls can make any "syscall" on a file.
2. a process does not have to be a singular file. All processes have most if not all their attributes exposed as files /proc/$PID/ as files, and can have this arbitrarily extended. In plan9, passing a signal (technically a note) is done by writing to /proc/$PID/note, and there is no technical reason for not allowing the same on Linux.
3. the entire concept of memory mapping is based around files, with anonymous memory - i.e., non-disk-backed memory - just being a subset of this. POSIX shared memory (shm_open) is provided through /dev/shm, which is a tmpfs folder and is indeed just files.
4. sockets are file descriptors, and file descriptors is what makes a file, and as such you can get a peers address of a file descriptor when such is present. Ways to expose creating sockets in the filesystem also exist, and not just for plan. The special socket-bits could easily be made less special, with the only justification for the current BSD socket API being that it became dominant and so everyone copied it.
A file descriptor is exactly nothing like a file. You cannot write to a pidfd. You cannot waitid an eventfd. You cannot getsockopt on a regular file. The only operations that all file descriptors have in common is close, dup, poll and some other basic operations.
So "file descriptor" basically just means "kernel interface object" and the available operations depend on the type of object.
A file is a container for arbitrary data that I can read from, write to, and reposition the read/write cursor in. If you call anything else a "file" then you haven't made everything a file, you've just redefined "file" to mean "thing."
What business does a socket have in a physical, on-disk filesystem? (Let alone a clunky hack to invoke the much simpler `signal` syscall in a roundabout way?) The socket "file" is completely meaningless unless the process that opened it is currently alive and still listening on it. So why the fuck should it get written to a persistent storage device?
How do I specify the socket type, which is a meaningless concept for an actual file, when I open a socket "file"? Oh that's right, I don't. Because I don't open a socket. I bind or connect. I don't use "file" APIs because they're not applicable. I use a dedicated socket API that's fit for the purpose.
Pidfd was not introduced to treat processes as "files," it was introduced so they could share the operations that they do meaningfully share with other kernel objects (e.g. poll).
The overloaded ioctl syscall is bad design. The proc "filesystem" is bad design. /dev/shm is ridiculous design. So I have to mock a fake filesystem in memory so I can create a fake file in that "filesystem" just so I can get the same memory pages mapped into my virtual address space as some other process, all of which has absolutely nothing to do with files or a filesystem (and is much lower level than that). lolwat?
If not being able to write makes it not a file, then files stop existing when a disk is full, and means that /dev/zero and /dev/null are not files - despite being at the heart of the whole "everything is a file" paradigm.
Pidfd was not made to make processes behave like files - that is what /proc is - but to solve problems with process related syscalls and PIDs, which are flawed and racey. The solution to that was to make APIs that treat processes as files, which gives you the ability to poll it like a file.
A streaming socket is exactly like a normal file. You read, write and poll. The only thing that is special is how to create it, but that is a design decision, not a technical limitation - see the plan9 file based API for making TCP sockets, which is trivially implementable in Linux.
Domain sockets are a bit different because of their side channel and would require more ctl files, but Linux's API is 99% magic files and ioctls so this is not that weird.
ioctls are not themselves bad design. In fact, scoping kernel functionality onto file handles is a great design and why that's almost the entirety of the kernel (device driver calls dwarf syscalls). The problem is not the design itself, but the fact that ioctl was not originally meant for it and got overloaded through several design iterations. This is what happens when you do organic design through more than 3 decades.
If you start out by defining a way to do file-scoped syscalls - and file does and always will mean "an fd" to a kernel - then you wouldn't have that awkwardness. That is what plan9 did: Take the learnings, and implement them clean instead of on legacy.
Which is what I said. It's a "file" in name only.
> If not being able to write makes it not a file, then files stop existing when a disk is full, and means that /dev/zero and /dev/null are not files - despite being at the heart of the whole "everything is a file" paradigm.
Great example that showcases the idiocy of Everything Is A File. /dev/null and /dev/zero are basic parts of the Unix API, so the basic OS API is broken-by-default at boot until one mounts a file system that had these dummy "device" nodes at a specific path that's hardcoded everywhere.
Instead of providing a sensible API like memfd_create or timerfd_create, for example.
> A streaming socket is exactly like a normal file. You read, write and poll.
That doesn't make it a file, that makes it an object that shares common traits with file objects. Datagram sockets do not read/write because they're not bound to a fixed remote address. And that's perfectly fine. They're not files.
> The only thing that is special is how to create it, but that is a design decision, not a technical limitation
And it's a good design decision. The socket API is pretty decent except for the dumb "file" nodes it creates when listening on standard unix sockets.
> see the plan9 file based API for making TCP sockets, which is trivially implementable in Linux.
But thank God it's not implemented in Linux.
> ioctls are not themselves bad design. In fact, scoping kernel functionality onto file handles is a great design
Agreed, the only bad part is that too much was shoehorned into the same syscall. It's certainly better than magic "files" that pretend to be "files" by having you read and write structs from, but you're only allowed to read and write whole structs per syscall, which has no resemblance whatsoever to how reading from and writing to a file work. (Maybe Plan9 doesn't have this limitation and tries harder to keep up the charade, I wouldn't know.)
No, it is the very definition of a file from the OS perspective. There is no other applicable definition to the OS. You seem to conflate files with disk storage, in which case not just plan9, not just Linux but the entirety of UNIX history seems to have flown past you.
That files are a nothing but abstract handles that implements the VFS interface to serve every conceivable function - where regular file is treated no differently than a device driver - is the entire point of modern UNIX. If this is the part you are stuck on it is not a surprise that both the existing Linux kernel APIs and trivial (and quite frankly, perfectly ergonomic and efficient) alternatives like the plan9 API seem so foreign to you.
"read and write structs" is the most normal thing for an application to do, whether you are reading JSON from disk, communicating over UNIX domain sockets with raw C structs, or sending protobuf over the network.
"But thank God it's not implemented in Linux" - Linux has many of these APIs already and it constantly grows, see for example all of /dev, /sys and /proc, not to mention FUSE and support for the 9P protocol to use all of plan9's services as-is.
You don't get one object == one file node, you get one object == a subdirectory with lots of file nodes that you have to open and close independently. That should tell you that your pattern doesn't work. (With enough effort, you can of course always shoehorn everything into an ill-conceived concept (and you did), but if you have to bend over backwards to make it fit, you should just admit it doesn't fit.)
It works for /dev, but definitely not for the mess that is /sys and /proc.
"Reading and writing structs" is the most natural thing to do when you're forced to serialize your data. You then design wrappers around that serialization that expose a sane, type-safe API on each end.
https://man7.org/linux/man-pages/man2/pidfd_send_signal.2.ht...
Also sockets have the concept of ancillary channels used to deliver out of band data (see recvmsg). If every system resource had such channel, it could be used for control similar to how sockets do it.
With a remarkable similarity to certain mainframe systems from half a century ago.
I’m not against pointing out the edge cases in the Plan 9 file model[1,2], but the thing is, I haven’t seen complex I/O interfaces that aren’t a horror show, either. Granted, I haven’t seen that many of those at all, but I’ve had a thorough look at the ones in OS/2, Win32, and NT, and none of them seem particularly inspiring.
I’d very much like to see some nice alternatives, to be clear!
The reference to io_uring also doesn’t seem all that strong of an argument, honestly. I’d like to say there are three layers to the idea of “traditional” “Unix” “files” as an OS (not storage) interface:
- System and user resources you have access to are identified by unforgeable references (called “fds”; the merits of allowing userspace to control their naming as opposed to having the kernel assign the names are debatable). You can feed bytes into these, (ask to) get bytes out of them, and perhaps have a out-of-band call to e.g. transmit one of the other references you hold to a peer. You can of course also delete a reference.
So far this is just dynamically typed object-capabilities by another name. It’s going to require higher-level protocols on top, but so does basically everything else on this level of generality.
Plan 9 mostly (if not completely) eliminated ioctls here by using separate control files instead.
(The part where the OS merges the payloads of write calls into a byte stream then cuts it back up into reads is more opinionated, but I don’t think even Bell Labs systems ever adhered to that principle strictly[1].)
- You obtain (most of) these references by navigating a stringy hierarchical namespace. There are additional calls to do so and to modify that namespace.
This is less of an obviously correct least common denominator, and as history shows the consensus is less strong here as well. Mountpoints, symlinks, namespaces per TFA, even the *at() calls all change how this part functions. On the other hand, I don’t think anybody longs for version numbers or nesting limits (or even drive letters) of other systems that have used naming approaches similar enough for a comparison.
- You access these services through synchronous system calls write(), read(), ioctl(), close(), open(), etc.
This is the part that is changed by the introduction by io_uring... But I don’t feel it’s all that important for the conceptual model, unlike the preceding points.
[1] Cutting a bytestream into packets is as always a tedious slog of buffering so some of the protocol implementations use write() / read() boundaries thus actually (depend on being able to) use the API in a datagram-like fashion (which 9P enables IIUC).
[2] Auth is based on a /proc/self-like hack wherein the kernel-side implementation of the “file” inspects the opening process through kernel-side knowledge you can’t access nor proxy from userspace.
I’m not sure how much stock to put into the multimedia claims, however—a lot has changed since then, both in the state of human knowledge about low-latency multimedia, A/V sync, network streaming, etc., and in what we can and can’t afford on machines we perform multimedia processing on. How relevant and how commonly known are the insights that BeOS incorporated today?
(You can see that I expect the answers to be “not very” and “extremely”, but sometimes life surprises us. For example—did you know that Microsoft shipped a renderer for 2D animation based on FRP ideas, designed with the direct participation of Conal Elliott himself, in 1998? It was called DirectAnimation and released as part of DirectX 5.)
It doesn't seem like it. Linux has a habit of multiplexing alternate functions through a single handle with additional and somewhat scary methods like ioctl. Plan9 manages this with servers, directories, and more than one path available for a single resource depending on what you're trying to access. This is far more sane.
> With time, we should have seen a lot of ugly interfaces resulting from this limitation.
They don't seem any more complicated than they need to be. Compare implementing a fuse server vs implementing a plan9 server. Yet I don't see where all of this complication adds anything or enables implementation of technologies that couldn't be implemented on plan9 with a few additional paths.
> People could no more keep up with simple general designs. And to squeeze every bit of performance,
These are contrary goals, and I'm not sure what you mean people can't "keep up" with "general designs." What is there to "keep up" with? And in exchange for that performance we got one of the most insane /class/ of unfixable CPU bugs ever imagined.
> And this is how we ended with the extreme fragmentation and heterogeneity we have in Linux,
And yet.. many of these systems are now being unified into generalized file descriptor based systems, that have wacky open methods, but boil down to allowing simpler interfaces through read(2) and write(2).
What do Spectre and Meltdown have to do with “everything as a file” system architecture?
Given how horribly asynchronous filesystems are I take one handle owned by the current process over a dozen free hanging files that might be reused for different resources at any time. Hell I am quite sure Linux had to kill suid on scripts because the kernel could not guarantee that the script file wasn't swapped out before the interpreter would load the path.
Almost more importantly, they don't claim to be universal. Instead, you can still send messages where that makes sense. The approaches are much simpler and powerful when combined than when each tries to be everything. Yes, you are allowed to say "synergies".
With the corresponding object streams (that can be specialised to byte-streams as well), we have something I like to call Plan A from Userspace.
https://2019.splashcon.org/details/splash-2019-Onward-papers...
There is no real practical difference in capability between this and dedicated syscalls. For Linux, the distinction is usually just whether the functionality is "global" or isolated to a certain area like devices or drivers.
https://www.jerf.org/iri/post/2931/
The idea there is that the particular way interfaces work in Go is possibly the way that Plan 9 should have worked. In reality, trying to fit everything into a file is still non-functional, because not everything is a file. But if you instead have a hierarchy of interfaces, starting at the very bottom with "this is a stream", working up to "this is a stream you can close", and so on and so forth up through "this is a seekable, sparse, appendable chunk of bytes that can have ACLs set and has the following ioctls", you can get what you're looking for out of common interfaces, while at the same time not having to run all around the system putting "do nothing" methods on things just to conform to interfaces. ("Do nothing" methods are a valid tool for a bit of fitting into an interface, and actually quite important, but only when the methods are themselves something that can be fulfilled by a do-nothing implementation. "Set this ACL" shouldn't be satisfied by a do-nothing method.)
For many things even a file is overkill; what you care about is that you can stream bytes in or out once you have it, not whether you can change the ownership of the thing you are working with. And with ioctls you see cases where files aren't anywhere near good enough.
(Note this is not advocacy for Go as a language you might want to program in; it's more a suggestion for a Plan 10 by drawing on Go as a particular combination of features. It would take non-trivial work to figure out how to turn this into an OS feature, but it's not inconceivable amounts of work IMHO.)
That said, I have no idea how you get "poor man's Erlang" out of this specific post. As a practical matter, Erlang is a standard dynamically-typed language in this matter; as a theoretical matter it has some limited support for protocols as used by things like gen_server but I don't think I ever saw a single use outside of the standard library, and I'm about 90% sure there's no implicit satisfaction of them; you must declare what you are implementing. It is irrelevant to my point as Python or Perl. We already know what this sort of dynamically-typed interface looks like in those systems, namely, "a lot less nice in practice than in theory but still useful enough most of the time". Nice for writing scripts, sufficient for reasonably-sized programs, not a sufficient foundation for an OS with Plan 9's level of aspirations.
Now a days people are mapping everything to JSON, and it works. The Plan9 directory structure was basically the JSON of the 80s, a hierarchical data structure were the hierarchy is easily navigable in a standard way, and the leafs can then hold any non-standard stuff you may need.
And then comes concurrency and exceptions... UNIX "design" anticipated none of the above. And it's not like these things were somehow unknown at the time. The "designers" thought they are making something remarkable by cutting corners and making a "simple" (but really a half-baked) OS.
It's such a shame that UNIX became the vector for the spread of the Internet and eventually infected virtually every computer system on Earth. It's even a greater disappointment that its "design" decisions are still revered as the holy Bible in academia and in the industry.
I personally liked VMS for concurrency, but for dealing with code and coding there was no contest that Unix was far better. I think the reality is they're all half-baked since none can satisfy every need.
Unfortunately, today, UNIX is by and large all we have. Other things are either some kind of UNIX but with a twist, or very underdeveloped.
Looking into the future, I'm very enthusiastic about OS-as-a-library approach, but I don't think we have a solid contender there yet.
I think fundamentally, there are only a few things it has to do: Allow applications to run (on the CPU/other hardware), Allow applications to communicate between themselves (establish communication standards), Allow access to disk, Manage the time given to each application well/fairly, Define permissions around resources (who can talk to whom and who can access what). Depending on who you ask, shells/desktop environments should be part of the OS too.
When I think of the minimum that can accomplish that, I think something like a simple communication standard (with authentication mechanisms) would be interesting (with possibility to support more specialized standards). A standard for defining app. share of resources (CPU/disk). Maybe hardware resources like disks, devices like cameras, etc.. could be treated as applications via a driver (or application <-> disk application <-> disk driver). Then instead of 'opening' files, you're just communicating your intentions with a disk driver, and you can do essentially anything. Maybe then instead of a Filesystem Hierarchy standard, there could simply be "OS (standard) applications" that for example list what users are in the OS, what applications are available, and so on (without a filesystem hierarchy at all, if you wish!). An FHS could be provided for legacy reasons.
I also think permissions should take a more fine grained approach than Unix/Linux does (closer to Android permission systems), I think by default applications should have minimum permissions and should be whitelisted as needed.
One concrete example of its design: exercising administrator (sudo) permissions causes a User Account Control (UAC) dialog to pop up, which takes over the screen from the current WindowStation -- making it impossible for any software to mess with approval or password entry [1].
IO Completion Ports (IOCP) also stand out as a powerful way to perform asynchronous IO that NT has had since ... I'm not sure how long, but I believe probably since the 90s. As one HN commenter wrote in 2016, "IOCP is the top item on my (short) list of things Windows simply does better. The performance boost you see from designing a server for IOCP from the ground up is jaw-dropping." [2] And it works for a variety of different IO tasks (including disk IO).
Windows gets a lot of hate, but most of the criticism you read about it online is not usually a well-considered critique of how its kernel APIs operate.
[1] https://learn.microsoft.com/en-us/windows/security/applicati...
Also, while IOCP has the advantage of being older w.r.t. availability, I wonder how io_uring competes - I haven't used IOCP myself, but from what I've seen io_uring seems to fill its role quite well and I've seen several people complain about various problems with IOCP (in particular poor documentation)
But, what I said about developing software stands: I'd much rather be using a unix .
Linux is not derived from Unix, but supports Unix (Posix) APIs. As does modern MS Windows, and Mac OS X.
This also means that Google could experiment with OSes like Haiku or Fuchsia that support Posix, but are built in a different way.
A lot of what defines REST works well here. The "Uniform Interface" creates a simple API that can scale to a surprising amount of functionality.
Not saying it doesn't have it's downsides, but you can accomplish a lot with minimal increase in the surface area of that "uniform interface".
Tangentially, I've long wondered why the Linux "API" is such a mess, in particular why are there multiple tracing frameworks? Multiple security frameworks? Which to choose??
It turns out, this is a consequence of Linux's stand-alone development, along with their (good) "never break userspace" mantra. The two together mean they can never deprecate an API.
Contrast this with the *BSD's development, where the kernel is developed alongside the libc and (a core set of) user-space applications. This allows them to evolve and deprecate their APIs, because they can update the clients.
(From a runtime perspective, all is not lost in Linux, as I suppose they can move an API to a module or allow "users" (distros etc) to disable it at compile-time.)
Plan 9 lived in the goddamned future.
Which is what ChromeOS and Android are mostly today, leaving C to the kernel, or tiny special purpose libs, and everything else in a managed language, Limbo.
Edit: fixed a typo
[1]: https://9p.io/wiki/plan9/Unix_to_Plan_9_command_translation/...
ls | where size > 10mb | sort-by modified
It seems so elegant! Down with strings! …But it's not my life yet, since last time I wanted to try Nushell they didn't have scripting yet, and I have yet to circle back around to it. There's also Elvish and PowerShell and Oil Shell but they all have their own issues. ls -t *(Lm+10)
"L" for the "file size" qualifier (why "L"? For "less than size", I think – probably because "s" is already used for setuid), "m" for megabytes, "+" for "larger than". You can also add "om" or "Om" to sort by mtime, but ls will reorder it so you need the -t flag (or the -U flag to not sort things).The syntax is not easy, even byzantine, but it's a lot less to type and pretty convenient in interactive shells.
If you mean you couldn't run batch scripts from a file, they can do that now. There's an example repo with some community scripts. My favourite is how they handle CLI parsing: https://github.com/nushell/nu_scripts/blob/main/sourced/nu_1...
I think a good mini language bridges the gap between interactive commands and programming. Sometimes you need to spend an hour writing a program to do something complicated. But because of various "extraneous features" built into UNIX commands, you rarely need to spend an hour manually poking at the filesystem. The mini language gets you almost as productive as a full-fledged programming language, without feeling like you're programming. (How do you know you're programming? If you "git init" and start committing stuff, you're probably programming. If your carefully-crafted thing scrolls into .history obscurity, then you're interacting.)
Plan 9 is not an answer to every problem, but it's just impressive how much it can do with so little code.
Piping programs together is almost always going to use significantly more resources than one program doing it all. More code doesn’t necessarily imply more resource usage.
Plan 9 is not an answer to every problem, but it's just impressive how much it can do with so little code.
I don’t want to be all around negative about plan 9, but I don’t see it as really solving any interesting problems. Indeed it is far easier to write slim, elegant systems when forgoing feature parity and/or competitive performance.
That's true! Yet, somehow we found ourselves in a situation where most people use computers, to chat, read news etc, all of which would be possible with much less resources. All done with the use of platforms that we literally can't rewrite, as they're too complex. It's certainly not find's fault, but the complexity creep starts somewhere.
Plan 9, as a research OS, is not a useful platform to base your next business on, but it does serve as a baseline we can relate our "real" platforms to.
Just having a framebuffer with page flipping for seamlessly redrawing the screen costs 64MB of memory at 4K resolution. People did real work and gaming on computers with a small fraction of that.
Early on in the history of computing the framebuffer was discussed as a theoretical construct, like "what could we do with this concept if we had the memory to implement it".
find(1) is a bad mini language. Its arguments are in a legacy format and its solution for composability is a hack, and an unstable one.
Different programs introducing their own specific programmatic layers defeats the holistic small pieces, loosely coupled premise.
When I saw the start of this thread, I thought abusing "du" for "find" sounded insane - but after mulling it over - I guess du is just a recursive stat(1).
And I can see the logic; have a tool that builds a tree of metadata, filter with a tool that... filters.
However - as far as i can tell, du/grep on plan9 can't fill in for find(1) - but the idea (above) would probably fit with PowerShell or other "typed/rich streams" kind of shell...
All the time arguments and size parameters and rules about depth and boolean syntax and the print0 for pipeline integration ... it makes me long for DOS interfaces from the 80s. There has to be a way to do it with less intellectual lifting every time.
Maybe just a simple set of bash reads will help - that's how I do ssh port forwarding - I was tired of getting confused.
Integration with /etc/mime would be nice as well so I can just search for, say, "image" or "video". (This would be at the frontend in this (currently) fictional helper script)
The existence of things like `-print0` is the downside of Unix's "all files/pipes are byte streams" design decision.
The IBM mainframe implementation of the pipeline idea – CMS Pipelines [0] – makes pipes record-based instead. Since the pipes are not streams of bytes, rather records with out-of-band boundaries, there is no need to reserve a special character (whether LF or NUL) to serve as a record separator.
> Integration with /etc/mime would be nice as well so I can just search for, say, "image" or "video"
It is a pity that Unix never had a "file type" field in the filesystem, unlike classic MacOS, Acorn RISC OS, among others. I suppose both those systems had the limitation that the file type was just a number, subsequent experience has demonstrated it needs to be a much longer string (such as a MIME type or Apple UTI). The problem with file extensions is the same extension ends up being used by completely unrelated applications for completely unrelated file formats – e.g. nowadays .doc is normally assumed to be legacy binary Microsoft Word, but many older archives it is a plain text file instead, or sometimes even some other word processing format.
That's the direction Powershell took, and to some extent was what other OSes were doing at the time of Unix. But Unix has become so ubiquitous and influential that we've forgotten that programs could pass more than ill-specified strings around, and that resources could implement richer interfaces rather than trying to force the file IO interface on every single one, regardless of how little sense that makes
Worse is Better may have been a useful expedience in the 20th century, but we're long overdue repaying that technical dept, to get back some of the rich OS/environment features that other camps had got working almost half a century ago. The first step would be to stop putting "Unix philosophy" on a throne
Well yes, but I think it will be hard or rather impossible to convince the unix crowd of anything good coming from windows.
For example, I found a flatpak of a system monitor and thought it was portable. Turns out, it parses `ps` output directly, but it's not compatible with BusyBox `ps` output
https://github.com/hakandundar34coding/system-monitoring-cen...
the author literally doesn't care since it works with coreutils `ps`
it's a culture problem
Sed has a fairly well designed although limited interface you again pass in.
By contrast find tries to have separate pieces strung together each as an argument.
On the other hand, the lack of find is not a particularly good rebuttal to the original point. If I rolled up in a prototype personal transport which could go 1000 miles on a single AA battery, would you complain that the seats were poorly stitched?
If the poor stitching were taken as a point of pride and the community refused to fix it, then yeah, I'd assume that that community values purity over practicality. Which is exactly how I feel about Plan 9: it had good ideas and was an improvement over Unix in many ways, but it was a regression in others, in large part due to choosing minimalist aesthetics over usability.
In a parallel universe where Unix did not take on so much baggage, perhaps it could have even naturally evolved towards Plan 9 and Plan 9 would have not been a necessary break. But then, like Plan 9, maybe Unix would have never rose up to see any widespread use to make that evolution significant.
Tradeoffs, as always.
Hardest thing that comes to mind is there's some slight portability differences to look out for between GNU find and BSD find that may require a quick man dive with relation to the max depth handling, but that's about it.
I've used find extensively in the past two decades and even read the man page (gasp) on occasion, but it's one coreutils command that I have always found cumbersome. I recently discovered fd and I have a feeling that I will be switching to that where I can, muscle memory be damned.
So they chose to just never finish the OS instead.
find . -exec grep -Hn regex \{\} \;
How would you do that with the du | grep combo?
This will also be faster, because you fork less.
du -a will need to do a stat to get the file size, which is comparatively expensive; I don't think find will (not sure)?
Yes, those stat() calls can be very slow in aggregate on a large tree, especially from HDD (due to seeking) or network filesystem, if the stat information isn't already in cache.
Im not sure how one can do \0 separator with du, plan9's du(1) doesn't list such option.
http://git.9front.org/plan9front/plan9front/HEAD/sys/src/cmd...
329 lines of code.
You can also use dmenu's stest to do the equivalent to find's -type f, { walk $PWD | stest -f }
I agree, but it seems to me that this is partly because it's stuck in its tiny little niche and never got the rough edges worn down by exposure to millions.
I was a support guy, not a programmer. I started Unixing on SCO Xenix and later dabbled in AIX and Solaris, and they were all painful experiences with so many rough edges that I found them really unpleasant to use.
Linux in the 1990s was, too. Frankly WinNT was a much more pleasant experience.
But while Win2K was pleasant, XP was a bloated mess of themes and mandatory uninstallable junk like Movie Maker. So I switched to Linux and found that, 5-6 years after I first tried Slackware and RHL and other early distros with 0.x or 1.0 kernels, it was much more polished now.
By a few years later, the experience for non-programmers was pretty good. It uses bold and underline and italics and colour and ANSI block characters, right in the terminal, because it assumes you're using a PC, while the BSDs still don't because you might be on a dumb terminal or a VAX or a SPARCstation or something. (!)
Linux just natively supports the cursor keys. It supports up and down and command-line editing, the way Windows does, the way PC folk expect. BSD doesn't do this, or very poorly.
Linux just natively supports plain old DOS/Windows style partitions, whereas BSD did arcane stuff involving "slices" inside its own special primary partitions. (GPT finally banishes this.)
I've taken this up with the FreeBSD and OpenBSD devs, and they just plain do not understand what my problem is.
But this process of going mainstream on mainstream hardware polished the raw Unix experience -- and it was very raw in the 1990s. Linux from the 2nd decade of the 21st century onwards got refined into something much less painful to use on
Plan 9 never got that. It still revels in its 1990s-Unix weirdness.
If Plan 9 went mainstream somehow, as a lightweight Kubernetes replacement say, it would soon get a lot of that weirdness eroded off. The purists would hate it, of course, just as BSD purists still don't much like Linux today.
Secondly, Plan 9 did a tonne of cleaning up the C language, especially (AFAICT) after it dropped Alef. No includes that contain other includes is obvious and sensible and takes orders of magnitude off compilation times. That rarely gets mentioned.
The other vital thing to remember is that despite 9front (and HarveyOS and Jeanne and so on), Plan 9 was not the end of its line.
After Plan 9 came Inferno.
I have played around with both and just by incorporating late-1990s GUI standardisation into its UI, Inferno is much more usable than Plan 9 is.
Plan 9 made microkernels and loosely-couple clustering systems obsolete >25y ago.
A decade or so later, Inferno made native code compilation and runtime VMs and bytecode and all that horrible inefficient 1980s junk obsolete. It obsoleted WASM, 2 decades before WASM was invented.
With Plan 9, all the machines on your network with the same CPU archuitecture were parts of your machine if you wanted.
(A modernised one should embed a VM and a dramatically cut-down Linux kernel so it can run text-only Linux binaries in system containers, and spawn them on other nodes around the network. Inelegant as all get-out, but would make it 100x more useful.)
But with Inferno, the restrictions of CPU architecture went away too. The dream of Tao Group's Taos/Intent/Elate, and AmigaDE, delivered, real, and FOSS.
When considering Plan 9, also consider Inferno. It fixed some of the issues. It smoothed off some of the rough edges.
I feel, maybe wrongly, that there could be some mileage in somehow merging the two of them together into one. Keep Plan 9 C and native code as an option for all-X86-64 or all-Arm64 clusters. Otherwise, by default, compile to Dis. Maybe replace Limbo with Go.
That seems like an insane concept. I honestly can't see how it can be justified in a world where pretty much every compiler since the 90s has support for `#pragma once` and detects include guards automatically without re-reading the header.
Can you illustrate this with examples? Ones that are from outside of the Unix and C family, just to make it clear that we aren't talking about tweaks to old tools?
(note: if you want examples of compilers that have `#pragma once` then I don't know what to say because I'd be hard-pressed to name a compiler that does not support it - similar situation w.r.t. header-guard detection)
> pretty much every compiler since the 90s has support for `#pragma once`
I asked for examples.
Specifically I am asking for examples that are not C compilers, do not compile languages derived from or related to C, and which do not run on Unix-like OSes, because what we are talking about here is what came after Unix. Plan 9 is effectively Unix 2.0: it is what the people who designed and built Unix did next.
Inferno is Unix 3.0. It's what they did after Plan 9.
Tweaks to the project that they had moved on from (or clones thereof, e.g. Linux) are not particularly interesting or relevant in this context, IMHO.
You want me to cite compilers that don't compile C that support a C extension ? And you want the language it compiles to be completely unrelated to C ?
Frankly, I'd already be quite hard-pressed to even cite a compiler that compiles a language that is completely unrelated to C in the first place and doesn't run on any Unix-like OSes (I guess maybe something like cmd.exe (...though it's an interpreter, not even a compiler) or some assembler written entirely in assembly - I think there's a few I've seen for Windows (though an assembler isn't typically considered much of a compiler) or something like that ? Does Windows even count as "not Unix-like" ? Does Wine mean it runs on Unix-like OSes ?)
This just makes your criteria seem like it can't match any compiler at all, quite frankly, unless you're asking to trawl through truly ancient stuff from the 60s that I wouldn't even be able to test at all in the first place (since by the criteria you've given it must not be able to run on my laptop)
Also, I'll add that you're likely to be correct in the most literal sense - I don't expect many compilers for languages that don't directly descend from C to include anything like `#pragma once`, because they're using a completely different model from C: almost no languages these days use "copy-paste" style file inclusion to handle libraries, and if they do anything like that, `#pragma once` is implied and never needs to be manually specified.
But in practice, `#pragma once` allows C libraries headers to work just like in many other languages - they can freely include other libraries without forcing the user to do so themselves manually.
Just imagine if you had to follow the Plan 9 C scheme on other languages, imagine if everyone that did `import argparse` in Python had to then add `import os`, `import re`, `import gettext`, `import warnings` and `import sys` because `argparse` can't do it itself without fear of double inclusion (...and then you'd also have to manually add `import abc`, `import stat`, `import enum`, `import functools`, `import unicodedata`, `import copyreg`, `import posix`, `import posixpath`, `import nt`, `import ntpath`, `import subprocess`, `import io`, `import locale`, `import builtins`, `import linecache`, `import tracemalloc`, `import traceback`, `import types`, `import operator`, `import reprlib`, `import collections`, `import weakref`, `import typing`, `import genericpath`, `import pwd`, `import errno`, `import time`, `import signal`, `import threading`, `import contextlib`, `import fcntl`, `import msvcrt`, `import select`, `import selectors`, `import grp`, `import encodings`, `import tokenize`, `import fnmatch`, `import pickle`, `import itertools`, `import textwrap`, `import keyword`, `import atexit`, `import gc`, etc. as the nested dependencies of os/re/gettext/warnings/sys/etc.)... I can't imagine anyone would consider that a good thing.
> You want me to cite compilers that don't compile C that support a C extension ?
No.
There are, hmm, I am not sure, at least 2, probably 3, and I suspect more than 3 assumptions in here.
#0, general context:
The bigger picture here is that there is a whole world of OS and language development which is completely outside the world of Unix, C, and things that derive, directly or indirectly from it.
However, the Unix+C world is so big, so commercially successful, that a lot of people in it can't see that there is anything else. To quote Gaiman and Pratchett, it's the same as the reason that "people in Trafalgar Square can't see England".
I wrote about this recently: https://www.theregister.com/2022/03/29/non_c_operating_syste...
#1. Plan 9 is not a Unix. It's what came after Unix. As such, I would say it's fair to say that Plan 9 C is not plain ol' Unix C. While the Unix flavour of C has continued on its own path, it's a very conservative path: it's terrified of breaking backwards compatibility. The developers of Plan 9 were not scared like this, and quite cheerfully made big sweeping changes.
#2. So, yes, I would consider that carefully implementing and adding what you yourself call an extension to an existing compiler (or set or family of compilers, it doesn't really matter) is not the same thing as redefining the semantics of a language to say "you are not allowed recursive includes".
One is a big sweeping change; the other is a minor tweak.
#4. The core, important point here is that Plan 9 made significant changes which in places have big ramifications — for instance, performance improvements on the order of several orders of magnitude — simply by tightening up the rules around an existing, well known language. IMHO, adding a new compiler directive is not comparable to this.
Adding a new directive is patching a hole, while changing the rules of how the languages compiled is fixing the design. What I was enquiring about was other designs that fix this problem in the core design of traditional compilers, in other words UNIX C and its descendants.
> And you want the language it compiles to be completely unrelated to C ?
Well, yes! Why not? There are lots of them!
> Frankly, I'd already be quite hard-pressed to even cite a compiler that compiles a language that is completely unrelated to C in the first place and doesn't run on any Unix-like OSes
Really? OK then, let me see how many I can name of the top of my head without googling: Pascal, Lisp, APL, Fortran, Cobol, Algol, BCPL, Forth, Ada, Prolog, Bliss... Okay I'm starting to struggle a little bit now, but I think the point is made.
I wasn't asking for languages that don't run on UNIX. I was asking for languages that weren't built out of UNIX tools. There are legions of languages today that were built pretty much entirely from the existing UNIX toolchain, from Python to JavaScript. My point is that there are dozens to hundreds of languages which are outside of that family.
Plan 9 is, I would argue, outside of that family because it's what came after that family.
Operating systems from outside of the UNIX family? Classic MacOS, AmigaOS, CP/M, MS-DOS, Atari ST TOS, Concurrent CP/M, Concurrent DOS, DR FlexOS, Novell Netware, OS/2 and all of its descendants, GEOS, Palm OS, Newton OS, Microware OS9, Interlisp/Medley, OpenVMS, OpenGenera.
And of course basically all mainframe operating systems... Tools that are still worth billions of dollars today, whose daily use affect everyone who uses a bank account or everyone who ever travels on an aeroplane.
So, yeah, a pretty significant technological and economic market segment… and one that UNIX people often totally forget exists at all. As it seems you did.
That sure is quite a useful precision to make - personally I found what you were asking for confounding given I can't really think of much of any language that doesn't run on UNIX, save for particularly obscure, often long-defunct ones (i.e. languages that basically don't run on anything at all or on stuff I couldn't possibly check out myself anyway). The fact you've also implicitly added the stipulation that only languages that derive from C and not all those that are related to it are excluded is also quite useful - I was excluding languages like BCPL or Algol on that basis (and I was quite iffy on Fortran/Pascal and a few others too, though since most ran on UNIX I wasn't even considering most of them anyway).
Also, while I broadly agree with your point about most people not even thinking stuff outside of Windows/UNIX exists and indeed find it quite acutely accurate in many cases, I'd say I'd hope I wouldn't be counted in it - I could have named most of the OSes and languages you did, save for a few like Microware OS9 or Bliss, simply I did not name the languages when prompted to because I'm not really aware of any that can't run on UNIX-like OSes, which I had then thought you were excluding before you clarified things.
Anyway, I'm aware of my ignorance of many of the details that are involved, but more importantly for what we were discussing, I would find it remarkably unlikely that any of these languages has a dependency handling system quite alike to that which is involved in Plan 9.
Are you actually telling me that you know of many examples of languages and/or compilers that handle dependencies like in Plan 9 ? That want you to manually specify all the dependencies of all the libraries you want to use before you can use that library, and which consider this sane in any way ?
If any actually exist, then I'd expect there's either: 1. Tooling around them specifically made to work around this exact issue by automatically specifying the dependencies when needed without having to spell them out manually 2. Preferred alternatives that don't have this problem in the first place
Meanwhile, the `#pragma once`-style model (i.e. make it so that imported libraries can use a separate library without ending up with asinine "double definition lol" errors if the program that uses the first library also separately uses the second one) seems like the evident standard that all the other languages use, such as:
- Ada
- Fortran
- Pascal
- APL
- Cobol
- Algol
- Prolog
within which I've found no examples of needing to do something like `import definitions_every_single_library_or_module_relies_on` and `import definitions_from_the_library_almost_every_single_other_library_or_module_uses` at the top of almost every file.
In general, what I am getting at is that Unix is a (?) uniquely weird situation that happens to have grown like Kudzu. It's an early generation of a long running project, where that early generation caught on and became massive and thus ignores simple but profound improvements from later versions of the same project.
And since in that ecosystem, almost everything is built on a foundation of C, problems with C affect everything layered on top... even though they do not in any other ecosystem. But something like 99% of inhabitants of the ecosystem are not even aware that other ecosystems even exist, let alone knowing anything about them.
If they know of any, it's macOS or Windows. macOS is also UNIX, and modern Windows is NT, which is Windows built with Unix tools and a Unix type design... so they are not really different at all.
So what I am getting at is that Plan 9 has aspects other than the namespaces and the cosmetic aspects of the design. It makes changes to the language and how it's compiled that are just as important, and hacks to gain some of that on modern versions of the parent OS family are not of comparable significance.
Famous quote:
<<
So... the best way to compare programming languages is by analogy to cars. Lisp is a whole family of languages, and can be broken down approximately as follows:
* Scheme is an exotic sports car. Fast. Manual transmission. No radio.
* Emacs Lisp is a 1984 Subaru GL 4WD: "the car that's always in front of you."
* Common Lisp is Howl's Moving Castle.
>>
Comparison: if somehow you make a pruned-down Howl's Moving Castle, you can probably never ever, whatever you do, no matter how much effort you invest, make it into something as small and light as the Subaru, let alone the sports car.
In more detail:
Let's imagine one team invented first the train, then the bicycle, then the car.
The bicycle takes the idea of a wheeled vehicle from the train but reduces it down to an incredibly minimal version. It needs no fuel, just 2 wheels, but it is very simple, very light, and can go almost anywhere.
Although you do need 1 per passenger, it's true, whereas a single carriage train can carry 100 people, you can make 100 bicycles for those 100 people with fewer materials than that 1 train, even excluding consideration of the rails etc.
If someone still making trains looked at bicycles and inspired by them came up with a train with just 2 wheels, that balanced or hung from the track, they do not get to claim that they have successfully imported the simplicity of the bicycle.
They have retained just one aspect and may have achieved savings in moving parts, or slight reduction of complexity, or a slightly simpler machine... but it's still a train.
If you tweak a conventional C compiler to not waste time rereading text that has already been read once, or a disk cache obviates it, then you have not caught up with the advance I am trying to discuss here.
For clarity, this is not my original thinking. This idea is from a Go talk over a decade ago:
> Rust's "fd" implementation is under 7,000 lines of code
Not counting the 23.8k files, not even lines of code, of rust (https://github.com/search?q=repo%3Arust-lang%2Frust++languag...). Comparison should happen on a similar baseline
Also, if you're gonna try it do it either with a proper three-button mouse or with a large-ish and easy to press scroll wheel button. Otherwise it's really frustrating.
After longer usage you do that without any thinking
Moving to a mouse primes my brain to start thinking spatially about things. Which is not something I'm typically doing when looking at textual/symbolic things. I can see some value in doing it, of course.
Plan 9 in general seems very much like a system "by the designers, for its designers" with not all that much attention to user-friendliness. To some degree that's also the case for Unix, but with Unix other people took the thing they made at Bell Labs and made it somewhat user-friendly (and even then, its received plenty of criticism for it). In Plan9 that step never really happened.
I think the main thing people get confused by besides the chording is the teleporting of the cursor, but then you realize it's actually really useful once you get used to it (at least I did).
I don't know exactly; this is one of those things where I'd have to implement some things and play around to see what works. Something like some text popping up maybe? I don't know. More advanced users can always just disable these sort of things (I also set up my Vim to not show "-- INSERT --" because at this point it's never helpful for me and it looks a bit nicer this way, but obviously for loads of people it's a very helpful thing).
It definitely is that. That's a really great thing when the designers needs and assumptions match your own, but if they don't, you end up wondering what these bozos were thinking.
For what it's worth, Rob Pike was a big proponent of the mouse-driven design, and he has some very thoughtful essays/articles/emails about why he feels that way. I can't say I agree with him 100%, but it's clear that the decisions weren't arbitrary.
Edit to add: I don't know that this actually makes Plan 9 better, but I'm pretty sure it doesn't make it worse (Although from our perspective in 2023, it's definitely alien and difficult)
Technological progress likes to reinvent itself, looping back to the same idea that did not work last time, and maybe making it a hit finally. Two examples:
- Apple Newton, 1992 (a flop) -> Palm Pilot, 1997 (niche success) -> Apple iPhone, 2007 (world domination).
- Java Virtual Machine, 1994 (server-side world domination, a flop in the browser) -> Inferno OS, from the makers of Unix and Plan9, 1996 (a flop) -> WASM (prospects of world domination in the browser).
WASI also seems like a wrong approach, considering it inherits the limitations of WASM, like problems with memory, allocation and multithreading and hacks needed to overcome them.
VM-s are also kind of a wash for me too, since nowadays the most popular way to run software in a portable/sandboxed manner is Docker, which is NOT cpu-agnostic, even if you ran a JVM app in it, the bundled JVM would be CPU arch dependent.
The point of VMs like WASM is not (specifically) performance though, even if performance is desirable. It's the minimal, fixed standard, and the isolation.
If you badly need native performance, develop a native application %)
> The point of VMs like WASM is not (specifically) performance though
I feel like it is though. There was another thing, called asm.js back then, which was a subset of javascript that was easily translatable to machine code. It was a giant hack, and performed worse than NaCl.
I feel like WASM is more of a spiritual successor of that thing.
> If you badly need native performance, develop a native application
And pass up the ease of distribution/compatibility browsers offer?
No. Android won. The concept from iPhone won, but Android it's the world leader.
Unix as a concept (a set of ideas and interfaces) achieved world domination, but it's not the original AT&T kernel, and not the original AT&T userland.
It's an entirely separate, kludgy and vastly inelegant, reinvention of a concept that had already been done much better.
First in Tao Group's Taos, later much refined and improved in Elate and Intent.
Then done in a more Unixy way in Inferno.
Inferno embeds the cross-platform runtime VM right into the kernel and makes it the default.
WASM bodges a cross-platform runtime VM together out of chunks of web browser and Javascript tech, and then others rip it out of the browser and make it run standalone.
Instead I'm trying to show the same general idea tried over and over, at different times and from different angles. The arrows just show the sequence in time.
I haven't really used it but from what I understood at first most of it is very elegant because at the time it was developped all nodes in a network could still be trusted.
Worse is better than nothing, but when better has been demonstrated to exist, worse is just worse
All because I couldn't care less about 3rd party services like github.
Go was designed by:
- Robert Griesemer, known for nothing else,
- Rob Pike, primarily known for sam(1), acme(1) and several other Plan 9 tools, the Blit (Unix's own graphical terminal), UTF-8 (with Ken Thompson), Inferno and Limbo,
- and Ken Thompson, ancient god.
I mean, as editors, for instance, Vi and Emacs are horrible ugly things that were rendered obsolete by the Apple Lisa in 1982, let alone by the mainstream success of the Mac a couple of years later.
And yet, they persist.
X11 is a horrid lashup for what most people actually use it for. And yet, it remains way more mainstream than Wayland, say.
Disagree. In the vi/TECO model and emacs to a slightly lesser extent, you operate on text, using text, with an input device that has a 1:1 mapping to units of text. In the Lisa model, you operate on pictures of text.
As far as the UI goes, the critical distinction is not about how it looks on screen or how it is rendered, it's about modal versus nonmodal user interfaces. I happened to side with Larry Tesler on this: "don't mode me in!"
As far as Emacs goes, I was more thinking about its strange set of user interface conventions and terminology, which pre-date (and conflict with) industry standards such as the IBM CUA set of standards which came to almost completely dominate DOS, Windows, and Linux.
How the appearance of the thing is rendered on the screen is completely irrelevant to this.
In (q)ed/ex/vi and their direct and/or spiritual successors, you'll reach Command Mode by pressing Escape. In Emacs, it's Ctrl/Alt/Meta. In Acme, it's the mouse. Even editors which (almost?) follow the CUA standards have a Command Mode - using the Ctrl modifier key.
All editors have commands of some kind, yes. Otherwise, it's not an editor.
But holding down a modifier key is not a mode in the software. You could sort of argue it's a mode of the keyboard,. or of the key, but I think it's over reaching.
I've seen editors with no menus, no visible UI at all, just hotkeys that do stuff. It is still a UI even if it is not visible. There are apps for blind Windows users with no visible presence on the screen at all, such as the Qwitter Twitter client.
If pressing the Ctrl key suddenly made the app switch into a different type of operation until you pressed it again, that might count, but it doesn't. The app doesn't even need to know. It just knows "keycodes #F to #Z enter letters, but #A to #E are commands". There are no modes here.
Granted, one is Shift and one is Caps Lock…
It's like saying that cars, motorbikes and bicycles were irrelevant, because trains already existed.
A lot of terminal driven software design pivoted around lots of modes. Input mode, edit mode, command mode, extended command mode, etc etc etc. It's powerful but it's very hard to learn and everything becomes very context sensitive. What any key or instruction does depends heavily on what you did before.
That means you have to remember. That means you have to think much more.
The design of second generation GUIs (the Apple Lisa etc.) strove hard to eliminate this totally. The user can do anything at any point without modes of interaction.
The pivotal event that is often missed, and much of the Unix industry even today does not get, is that in the years after the Lisa and the Mac (and, don't forget, their affordable cousins such as the ST, Amiga etc.) appeared, that this stuff then filtered down to DOS and revolutionised DOS apps too.
And those (DOS and DOS apps) were the commercial mainstream, and that ecosystem is what evolved into Windows and all modern computers. Yes including the ones running Linux, and including Linux at the GUI level.
The Mac now is a descendant of NeXT, and is a Unix, remember. It's unrelated to Classic MacOS.
But the UI design of the GUI layer comes directly from Apple R&D. The UI at the shell layer comes from a decade or 2 earlier and is totally different.
Unless you know this, the differences between Unix' and Windows' GUIs and shells makes little sense.
Windows modernised its shell layer.
Unix didn't.
- Strongtalk: https://www.strongtalk.org/history.html
- Object Oberon: https://en.wikipedia.org/wiki/Object_Oberon
- Sather: https://www.gnu.org/software/sather/docs-1.2/tutorial/intro2...
Hilarious how in any thread on HN tangentially related to Go, somebody will inevitably find a way to construct that bridge to Go purely to shit on it (and it's rarely anything more interesting than "Go sucks, right?").
But it will be slower, and won't have any useful software written for it, also you won't be able to use internet or your GPU. (To be fair several years ago I tried the HURD distribution ArchHurd. I just installed on it on my laptop, and by luck internet just worked, and had a firefox running so it was good.)
All these "X is poor man's Y" are a cope. Plan9 is poor man's <insert any operating system that is supposedly inferior to Plan9> because noone uses it. I'm sure on most people the irony of this statement will be lost, so I'm gonna explain: yeah, plan9 is better on paper, too bad we live in glass world.
Linux moves towards massive process sharing, and thus various of sharing controls are key to the future that is missing from 90s operating systems.
Just enable sound, run your browser and music app, and count number of processes living with `ps ef|wc -l`. I see 282 kernel threads, and 536 total processes. You will see that we live in the future, because of extensive modularization of system services, and cannot get back without sacrificing reliability and comfort.
It doesn't need a microkernel, because the concept of microkernels is to split a big monolithic kernel into lots of small simple "servers" running in user space, and have them communicate by passing messages over a defined communications protocol, some kind of RPC type thing. It works, and QNX is the existence proof. But it's really hard and it's really inefficient -- of which, the HURD and Minix 3 are the existence proofs.
So most of the actual working "microkernel" OSes kludge it by embedding a huge in-kernel "Unix server" that negates the entire microkernel concept but delivers compatibility and performance. Apple macOS and iOS are the existence proof here. (It could be argued that Windows NT 4 and later are also examples.)
Plan 9 achieves the same result, without the difficulties, by default by having most things user space processes and communicating via the filesystem.
Disclaimer: this is my very rudimentary understanding. I am not an expert on Plan 9 by any means.
But I think the core point here is that, as with much of the Plan 9 design, by including a more elegant and powerful abstraction in the core design, the need for a much more powerful and much more complicated abstraction layer on was obviated, if not eliminated.
Remember CPUs were largely single threaded, and you had expensive multi-cpu systems that ran 2, 4, and 8 CPUs. Compute heavy processes consumed entire systems, or CPU since SMT was also not common. You couldn't effectively microsegment CPU usage, because you were only really multiplexing idle time. Additionally, your OS had to consume more cycles to do that resource sharing.
Today, the story is pretty similar, but we have systems with multiple cores, and SMT. We can share resources more effectively, but its also that our systems have 16x more scheduling slots.
This also caused appearance of syscalls like ioctl - very ugly solution. Ioctl is a large and undocumented API. Pseudo-filesystems like /proc or /sys are also examples of undocumented APIs.
Instead of "everything is a file", "everything implements well-documented object-oriented interfaces" would be much better idea.
The argument of GP is that file-oriented interfaces like `rewind` or `seek` are ill-suited for anything that doesn't behave like a seekable file. `read` and `write` work well for anything with a streaming interface, but are utterly unsuitable for things like processes. You'd have to layer protocols on top of it.
They do have many advantages, but lots of people don't like being compelled to use just one thing, and I think that's a huge factor in how the Unix/WinNT model did so well.
Do you really think that something like this is suitable to be embedded as the core internal communications mechanism of an entire operating system? 'Cause I don't.
Do you remember, for example, that the original plan for the GNOME desktop environment was that components would communicate over CORBA? They abandoned that after version 1.
Processes being real objects /kernel/proc/1363 with interfaces like org.kernel.proc.Process1 that allow you to manipulate them with generic message passing/function calls would be amazing.
Okay then.
Me, I'd prefer to throw the whole lot out and start over with something clean that incorporated this into its central design.
Not every file there is seekable/readable - some only exist as an input interface to change some property.
The very same way there could exist a 'signals' file to write signals to.
The presence of PowerShell softened my annoyance somewhat, but it never the less often feels unnecessary over-complex and overtly limiting. Tasks I can do in a few seconds on Linux often require reading long convoluted documentations and writing quite long PowerShell scripts or even full blown programs on Windows.
My experience is that the more basic and simple the task at hand the more annoying having to search thru pages upon pages of documentation and writing scripts became. But the more complex a task became the more I was happy not having to always deal with (and error handle) parsing an integer out of a string or splitting a string by a certain character.
This doesn't map well to hardware
[0]: https://yarchive.net/comp/linux/everything_is_file.html
Systems like /proc/ are cornerstones of the “Everything is a file” concept.
The idea is that files are just byte streams/arrays, and pretty good to be optimized.
If your point is simply that “files are efficient because they are just dumb bags of bytes”, then you are in a rude awakening.
Another L4 fan?
It mentions aspects such as:
"At the most basic level, the kernel identifies specific zones in the same fashion as it does processes, by using a numeric ID. The zone ID is reflected in the cred and proc structures associated with each process. The kernel can thus easily and cheaply determine the zone membership of a particular process. This mapping is at the heart of the implementation."
BeOS still looks more advanced in some aspects than Windows, Linux and MacOS.
I expected lots of advancements to take place in the OS space in 20 years.
In what ways?
* Combining the simplicity of the classic MacOS Finder with the keyboard controls and hierarchical menus and customisable taskbars of the MS Win95 Explorer, without the horrid bloat of Win98 and all later versions, including copies such as KDE >=2.
* Removal and elimination of a huge amount of legacy bloat, making a tiny fast OS. (e.g. No text mode, no console, at all.)
* Removal of complexity of multiple languages and C as the lowest common denominator; everything is in C++ and nothing but.
* Richly multithreaded: ran extremely well on SMP machines in the 1990s, when this was strictly limited to huge workstations that cost as much as a house.
It was gorgeous. Haiku is lovely but IMHO a little bloated and sluggish by comparison.
Note that most terminal applications still use virtual terminals (`/dev/pty*`), and it is important to provide standard interface here.
Also, terminal makes it easier to debug failing UI, and host headless OS.
You forget that _all_ modern OS transcend the desktop: they support phones, tablets, and headless devices.
Despite what Haiku's sole full time developer says, Haiku is a FOSS recreation of BeOS, and BeOS was clearly and explicitly designed not to be another flavour of Unix.
If you feel that the OS world begins and ends with Unix, then we have nothing more to talk about. It's one data point on a vast and branching tree, and while it's shifted a lot of units, that doesn't make it especially significant or interesting.
In the wider space of graphical desktops, Unix is a poor, third-rate entrant. Anyone half serious about talking about OS development and implementation in general should be able to name half a dozen non-Unix OSes that do at least some things better in various ways.
Your points all assume that BeOS is trying to be a better Unix than Unix. It was not. It was not trying to be a Unix at all.
Historically, Unix has had two main APIs for establishing TCP/IP connections - Berkeley sockets, and the AT&T Streams-based TLI (which later evolved into XTI). In Berkeley sockets, although a socket is a file descriptor, you can't create one just using `open()`, you have to use the `socket()` system call instead. Whereas in TLI, a network protocol such as TCP is actually a device file (e.g. `/dev/tcp`), and you create a socket by opening it – although instead of `open()` you have to use `t_open()`. Arguably, TLI is closer to "everything is a file" in this regard than Berkeley Sockets is. Alas, Berkeley Sockets won and TLI lost. TLI was more Unix-like because it was invented on Unix; Berkeley Sockets was copied from TOPS-20.
Originally with TLI, `/dev/tcp` was an actual device file on disk. In principle, you could have an alternative TCP stack using some other name, e.g. `/dev/tcp2`, although I'm not sure if any systems ever did that. The later XTI standard moved away from "everything is a file" by stating that `/dev/tcp` didn't actually have to exist in the filesystem, instead the kernel could just interpret `/dev/tcp` as an opaque string requesting the TCP protocol.
`/dev/tcp` in bash is possibly inspired by TLI but uses Berkeley sockets, and I don't think TLI ever let you do `/dev/tcp/HOST/PORT`, instead you had to use `t_bind()` to bind and `t_connect()` to connect. I wonder why Linux/etc never added support for bash-style `/dev/tcp` (and `/dev/udp`) in the kernel so other programs could use it. Nowadays, I suspect many would object to that on the grounds that it could potentially be abused into a security vulnerability.
z/OS is unusual in supporting multiple concurrent TCP/IP stacks, and technically being a Unix (it is certified as one). If you have more than one TCP/IP stack, you can control which one your application uses by setting the `_BPXK_SETIBMOPT_TRANSPORT` environment variable, or by calling the `setibmopt()` API on the socket. If you don't do either, z/OS extracts the routing table from each stack and uses that to forward requests to the appropriate stack by matching the IP address against those routing tables.
I loved a lot of the ideas on plan 9, but it really was unusable as a day to day environment.
The best namespace design should come from the microkernel world, where isolation can be achieved by simply rerouting outgoing API calls to alternate servers. This will also allow injecting all kinds of crazy/complicated policies in the middle.
Current complexities stem from years and years of development and edge cases of new, previously not known use cases.
Therefore Plan 9 filesystem abstractions are either really simple and often not comparable to Linux equivalents, or complicated but hiding internals behind the quasi-elegant interface. The latter being prone to fail on various edge cases.
What strikes me as unnecessarily convoluted is "mounting remote server /net directory to create proxy". Imagine what obscene things must be going behind the scenes.
There isn't a lot of options when it comes to the basic interface to an API. You have functions and their arguments. They can be strongly typed like most random syscalls or generic like ioctl or some microkernels. io_uring's API is not so nice in the sense it is very generic on the surface and typing is handled in the data structure rather than signature so it needs wrapping for any language level typing support. And that that point, besides some internal details, what separates ioctl from io_uring? Could I not send similar data to ioctl and have it act similarly? At the end of the day it is just a way to shuffle data to and from the kernel. How the kernel acts on that is what matters.
Abstractions with 3 orders of magnitude difference in latency / bandwidth are not abstractions.
Wonder how far will you fare, and how long will it take.
Or try PlanD: https://github.com/rvs/planD
1. We could gradually port Linux `ioctl` to Plan9 namespaces, if there was understanding they greatly simplify Docker.
2. Docker and virtualization are one of most important applications of Linux, but there is still not even a proof-of-concept implementation of such on Plan9.
3. We don't need a complete rebuilding of Linux to make it more like Plan9. Both Windows and Mac OS X is an example of system that underwent radical redesigns of several subsystems.
4. All features of Plan9 could be brought to Linux by adding modules for alternative system interfaces, and deprecating old ones or blocking them with security capabilities.
Quote from https://utcc.utoronto.ca/~cks/space/blog/unix/WhyNoUserNames...:
> Imagine that Unix had this feature and still had setuid, and you would like root privileges. No problem; make a custom namespace for /etc that has a version of /etc/shadow, /etc/group, and /etc/sudoers that have known passwords and list you as authorized. Now run sudo. Done.
Fixing this in Linux means removing everything that doesn't fit in this interposable, redirectable, nameable world with a uniform way to interact with all resources.
That's most of Linux.
Ioctl is a particularly egregious violation of the model.
that a little above your docker usecase already...
Regarding the rest: I was an early Plan9 user and still use some of their tools. They had some good ideas and some bad ones. I don't want people to rewrite linux to make it a plan9 clone. The good parts have already been added to Linux anyway
Hierarchical nouns, a fixed collection of verbs and a stream of bytes is aesthetically pleasing and can be helpful for developers hacking around, but is too limited and low level a vocabulary to express many important APIs. An obvious example is the Plan 9 windowing/graphics server, which in theory is just files but because that's way too low level is actually accessed via a client library written in C. That's how it would have always gone in the end: in theory it'd be files, in practice it'd be RPCs squashed and squeezed to look a bit like files as long as you don't think too hard, with a C API wrapping it for convenience.
APIs and files are very different things. Every modern platform has a fairly sophisticated inter-process RPC system at its core for that reason. Windows has (D)COM, Apple has XPC, Linux has DBUS (but doesn't use it as much), Android has the Binder, Chrome(OS) has Mojo. The designers of these platforms were all quite familiar with Plan 9 and yet none chose to implement the everything-is-a-file model, which I think is good evidence that this is a dead-end design wise. Fundamentally you don't write complex programs in shell scripts but the Plan 9 design assumes you do.
One place this model does live on is HTTP, but HTTP isn't enough and is thus always used with extensions, at minimum JSON or XML but also things like multipart, websockets, headers, CORS, maybe Swagger etc. And of course HTTP servers only look like a file system on the surface, in reality you can't actually browse them or do most of the things you'd expect of a filing system, and you don't use the filing system APIs to access them.
Networking and filesystems are an especially difficult combination because the UNIX APIs for file access (which Plan 9 largely also uses) are too impoverished to provide necessary functionality. They all assume relatively reliable and low latency access, so even quite basic things like being able to get progress information from operations isn't easy.
Still, the underlying ideas in Plan 9 are worth iterating on. My company has an internal Kotlin based scripting tool designed to bridge the world of shell scripting and 'real' programming languages. It exposes a shell-like API with functions like mv, cp, wget, and so on which are all implemented internally. One of the things it does is expose progress events via a unified progress reporting API. You can assign a progress event handler and then do things like file copies or archival operations, and get information on what's going on. By default it renders a nice animated progress bar on the terminal but you can also do things like serialize these events across network boundaries. The filesystem API is pluggable so you can do things like browse into zips, and there's also an ssh function that lets you connect to the file system of a remote server: strings can be turned into path objects that remember their home filesystem and those can then be used to do things like copy/browse/execute things on remote systems:
val local = dir / "local-archive.tar.gz"
ssh("//foo.com/home/bar") {
cp("remote-archive.tar.gz", local)
}
extract(local)
... etc ...It also acts as a sort of testbed for vaguely Plan9-ish ideas. For instance I want to experiment with how to adapt a regular POSIX-ish file API to allow nodes to be both files and directories simultaneously. But you don't need an OS for this. It can all be done in userspace.
I can also imagine doing the reverse of what you suggest in that libraries could implement fundamental mechanisms like RPCs and the file model would just be one form of interface to them. This would enable all programs to work to a minimum level with a new source or sink of data.
So you wouldn't need to rewrite code to be able to make it send data via your new RPC mechanism or dump data to your strange new distributed backup mechanism. That would be tremendously powerful.
If you had to do something very specific then the choice to access it via a more complex API would be there.
In fact if we forgot about files per se we could just look at models of access like: 1) sequential serial 2) low latency random access 3) heirarchical namespace 4) search-based namespace 5) high latency request/response
That way a file manager for example, could treat a namespace like my remote ssh drive differently from my local disk which offered low latency random access by not trying to generate thumbnails on it and not blocking while reading the names in it.
i.e. the file model is possibly too simple for its own good but that doesn't mean we couldn't make it better.
It's fairly common for RPC systems to grow a little shell at some point that lets you arrange objects hierarchically and invoke methods on them. Files then just become objects that support the IFile interface. Mojo had one but it was deleted, and before doing Android the BeOS folks did "OpenBinder" which also had such a shell. There were some attempts at shell-like things in the JVM space as well, I've used CRaSH in the past.
There's a lot of scope for interesting OS research here. One issue is that OOP is a bit too dogmatic. Filesystems separate code from data, but OOP is all about fusing them together. If you try and embed an object with an API into the filesystem (e.g. /dev files) then you get stuck when wanting to change the code whilst keeping data constant, which is something that happens all the time with things like image formats, HTML, text files ... operating systems have relatively crude notions of file associations which could be improved a lot.
There are still leftovers of that original design all over the system; IIRC, some COM functionality make use of a hidden window to send messages between processes.
https://learn.microsoft.com/en-us/windows/win32/dataxchg/abo...
COM (ab)uses window messages primarily for objects with thread affinity. Microsoft don't (didn't?) have a standard way to post lambdas to a thread's message loop like most platforms do, so they hack it by using GUI messages to a hidden window.
COM is used both to expose in-process APIs that you invoke and also to do RPCs between services, but that's an implementation detail that only sometimes matters. Historically most stuff ran in the kernel (not any more) so there wasn't a big need for inter-process COM. These days there's more microkernel design but the underlying APIs were all 1980s era flat C APIs so indeed COM is of less relevance for remoting them.
The restrictions on eBPF programs are... legion, just count all the "No, you can't" answers in the FAQ: https://www.kernel.org/doc/html/latest/bpf/bpf_design_QA.htm...
-- Someome, somewhere, sometime.
Edit: Oh, it looks like it is indeed!^[1]
Golang for example borrows lots of stuff from 1-9c compilers from plan9/9front, Limbo and Plan9 design such as static binaries and cross compiling from anywhere to everywhere.