The simple, everything is a file, model of Plan9 is what makes the namespaces API as clear and as general as it is. All the objects export the same file API. Every interaction with the OS objects is done through file open, create, read, write, etc. But it has it's drawbacks.
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.
> First, it's not always easy to map every object operation into either an open read or write.
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.
io_uring is a more modern API to file access. Actually I think it would be great if everything was a file and the communication with the kernel was only with io_uring.
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.
Why would (pseudocode, nonexisting but possible example) write(SIGKILL, "/proc/12345/signals") not be possible? For the other direction, there is signalfd in Linux. And of course you can get a peer address from /dev/tcp (https://andreafortuna.org/2021/03/06/some-useful-tips-about-... ). Yes, /dev/tcp is not an OS primitive but a bash builtin, but there isn't really a reason you cannot do this in the OS. Shared memory can be a file, you just have to mmap() it. mmap is always left out of the open/close/read/write-enumeration of the traditional file API, but I think it is actually extremely useful and should be the fifth alongside those.
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.
This means layering additional protocols on top of the file APIs. Of course you can do that, but eventually there will be a similar explosion such as on top of `ioctl`, and it's doubtful whether the resulting interfaces will be any easier to use than the existing ones.
On the other hand, layering tons of stuff on unsuitable interfaces has been all the rage in the last 25 years. Just think of everything-over-http, stuff-it-in-xml/json, program-it-in-yaml and similar industry trends. ;)
Let me introduce you to the Linux Audio Stack. Laying protocols on top of each other, when everything is just files, is no different from layering protocols on top of each other when some things are files and some thing aren't.
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.
The main problem with Linux audio is the bloom of APIs that appeared over the years. While a file-based interface would be cool, it would be just yet another API that competes with all the others for its place in the ecosystem. Fortunately, ALSA and PulseAudio seem to dominate right now.
mmap() is very different in its semantics from read() and write(). The latters have a clear, precise definition, a byte stream received by your file driver. mmap() is a different beast, it can be a ring buffer, it can a spinlock, it can be anything. The moment you have mmap(), the "eveything is a file" model is already broken and the namespace API can longer work reliably over the network. How would you transport an mmap() over the network? if /dev/tcp in plan9 uses mmap to asynchronously write the packets, you can no longer rely on the 9p filesystem protocol to implement a proxy by mounting the remote computer's /dev/tcp, because there is no clear and reliable way to map remote memory. It's no more about transporting a random sequence of bytes to another computer. You need more than that to make it work. It's just one example where this "everything is a fike" model is overly simple for the harsh real world.
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.
> 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
To reverse that question, what do you gain by defining a `read` and `write` API over that memory segment?
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.
Yeah, "everything has a filename" and "everything is a file" are very similar concepts but they aren't quite the same, and it might be that most of the value comes from the former.
Netlink sockets in Linux input and output packed C structures. Doesn't get more efficient than that. Such an interface definitely doesn't have to be strings-only. But of course, you cannot use it with just 'echo' in that case.
Passing data in registers is more efficient, that's what you lose with serialization. Serialization gets you generality at the cost of some performance.
Why not make real syscalls rather than emulate them using socket-like interface? Doesn't make much sense to me. For example, if you want to filter syscalls then it becomes more difficult (need to remember which type of socket it is, need to parse the structures and so on).
The original idea for netlink sockets (where the name comes from) is to be able to do some network packet processing in userspace. E.g. to do stuff like virus-scanning on TCP connections.
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.
/proc pseudofiles could have a mode (perhaps on open) that determines whether the protocol is ascii or packed/binary. there could even be a side-band interface that provides the packed schema (assuming not everything is exploded to atomic type-evident items).
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.
> 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.
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.
A file descriptor is equivalent to the Object universal base class in many OO languages [1] and represents an handle to an OS resource. Now in UNIX you can have anonymous [2] resources, but in the Plan9 model most resources have a name and you can get an handle to it via open(<resource-name>).
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
That is quite literally how Kubernetes works at large, which Plan 9 is to Unix as Kubernetes is to Linux, sort of. When the system is built from the ground up to be distributed across heterogeneous systems, you basically must build higher order protocols on top of really really simple ones. It's not that opening a byte-stream socket is the best interface for everything, it's that it's the lowest common denominator that everything can agree on no matter what.
This is very obviously an acceptable solution because the whole world runs on TCP.
> 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.
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.
That's not far from the CRUD of a database, or POST/GET/PUT/DELETE of HTTP. It's also not far from what you'd expect at the transport layer of an RPC / IPC mechanism.
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.
>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.
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.
When everything is a file, what is a "file" becomes flexible.
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.
I wouldn't sit here and claim Plan 9 was perfect, because if it was, we'd be using it. In particular, the issue is that you're reading raw stream of content on the way in and out of those file descriptors.
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...
>> I wouldn't sit here and claim Plan 9 was perfect, because if it was, we'd be using it.
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
To elaborate on those points a bit further, past what I already said about pidfd being introduced exactly to treat processes as 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.
This is an absurd interpretation of "everything is a file." The kind that makes you go "arghwhat?!"
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?
A file descriptor is a handle to a file, and anything you have an fd to is a file. This file carries a vfs implementation, such as that of pidfd, a device driver, or disk storage. The kernel does not distinguish between these.
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.
> A file descriptor is a handle to a file, and anything you have an fd to is a file. This file carries a vfs implementation, such as that of pidfd, a device driver, or disk storage. The kernel does not distinguish between these.
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.)
> Which is what I said. It's a "file" in name only.
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.
I couldn't care less about the weird nomenclature, but it seems to have confused a lot of people since they insist that just because every object is called a "file," everything needs to be shoehorned into the concept of file nodes in a virtual filesystem tree.
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.
I am probably looking at it from a too high-level perspective, but the RESTful paradigm was widely adopted in every domain and proved that you can model pretty much any concept as resources, with CRUD primitives and hyperlinks between them.
Wouldn't the same be applicable to files, processes, devices and so on?
It is. I don't think people are well familiar with what Plan 9 calls "files". They're objects or resources, which also happen to be files. REST/OOP/Actors/Plan9 are very similar systems.
And like it or not OOP shows that it's possible for one idiom to describe all the things when it's flexible enough.
Yeah, it's not such a laughable idea when you consider just how much of the dynamic behavior of a software system can be modeled as a series of messages between independent resources. This is part of why UML was so pervasive. People had spent intense amounts of energy modeling systems using messaging and interconnection, and UML gave us a uniform way of doing this.
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.
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.
Maybe instead of saying everything is a file, perhaps it should be "all resources are organized in a tree structure". The nodes of the tree can be different things where different operations apply.
> Ultimately IO is [an] intrinsically complex topic, and trying to paper over that complexity with simple interfaces is disingenuous and falls flat on edge cases.
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 have never found the Be Book to be particularly engaging reading, but this might finally give me a reason to work through some parts. Thanks!
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.)
Wait until you realize you need asynchronous I/O operations, and then you also need to manage caching on I/O operations, and you also need to distinguish appends from over-writes, and that you also need to care about who allocates memory for the buffer being written to / read from...
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.
What's an existing example of a non-'half-backed' OS?
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.
The Windows NT kernel is surprisingly well-designed. Everything is an 'object' to the kernel (or file descriptor). Concepts like ACLs apply to all kernel objects. I summarized about some of its capabilities previously: https://news.ycombinator.com/item?id=34914776
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.
Isn't the kernel API banned from usage by userspace applications that aren't kernel32.dll (plus deliberately breaks ABI all the time) ? I can appreciate the strengths of the NT kernel, but if nobody is allowed to use it... (I suppose kernel drivers can, but that's still a very narrow range of applications)
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)
There's a quote that goes something like "the last piece of software ever written for a novel OS is the UNIX compatibility layer". kernel32.dll is essentially that, except it isn't even UNIX compatible!
Windows NT was designed by Dave Cutler who also designed VMS. And, I agree, for many things it is a very nicely designed system and doesn't deserve a lot of hate.
But, what I said about developing software stands: I'd much rather be using a unix .
I used "half-baked" in the context of UNIX history, where the legend goes that UNIX was the stop-gap solution for AT&T failing to come up with a better designed system on schedule.
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 one approach in (re)designing an OS should be to go back to fundamentals: what makes an Operating System?
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.
> First, it's not always easy to map every object operation into either an open read or write.
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).
The implication was that everything as a file _had_ to be abandoned to make way for performance "improvements." Which ended up just being a new class of CPU bugs. So.. was the trade worth it?
For an example, most high performance devices expose functionality as memory locations mapped into the CPU address space. In many cases it's necessary to allow userspace direct access to (part of) the memory the device exposes, such as with GPU's, RDMA NIC's and so forth. Not sure you could get high performance with a read/write stream based interface, as conceptually elegant such an interface is.
Devices have been doing that since forever, so Plan9 has an mmap equivalent with segment/segattach, where you use file I/O (only) to define a memory mapping and attach it to the current process. Everything from that point on is regular memory I/O.
> 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.
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.
Storage Combinators manage to unify a lot of this with a somewhat nicer, REST-based and object-oriented interface (they came out of something I called in-Process REST). Byte-oriented interface are much easier to fit on top of that than the other way around.
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.
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.)
Are you asking for six pages of text? I'm probably the worst person in the world to say that to.
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.
> First, it's not always easy to map every object operation into either an open read or write.
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.
In plan9 this is dealt with through a similar workaround as we use in Unix: ctl files with a per-file protocol. Linux uses a mix of this (sys and proc files) and ioctls (which are also per-file protocols) for many things.
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.
> And this is how we ended with the extreme fragmentation and heterogeneity we have in Linux
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.)
Looking at the Unix to Plan 9 translation [1] gives me a different opinion. To name one egregious example, omitting find(1) in favor of piping du(1) (what is supposed to be a disk usage analyzer) to grep(1) is not an improvement; it's just user-unfriendliness in service of minimalist aesthetics. (Contrary to popular belief, find(1) is not a particularly "bloated" program; Rust's "fd" implementation is under 7,000 lines of code [2], about a third of the size of Lua.)
I got used to `du | grep` after using Plan 9 a lot and still do it on Linux, but yeah really I think Plan 9 fans have had a tendency to ossify the pragmatic minimalism of the Bell Labs guys into a sort of cultishness... there's no reason we couldn't have find on Plan 9, but now it's almost a religious point not to have it.
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 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.
But the fact is that you could get a decent find into Plan 9 in a day's work. Hell, you could probably just compile some existing Go version of find without any changes. Trying to get Plan 9-style namespaces into Linux is not nearly so trivial.
It's not about "find" in particular, that was just an example brought up to illustrate the general point. If "find" was the only thing missing to bring Plan9 up to Linux usability standards, that would indeed be easy to fix.
The big lesson Plan 9 seemed to have learned is that maintenance is a drag and a hinderance. find may be objectively better than du | grep for the user, but once introduced then the developers have to essentially maintain the same thing twice and carry that baggage forever into the future.
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.
I always feel guilty that find . | grep <pattern> is easier for me than remembering how find's flags work. But I think it's really a case of my brain rejecting exactly the redundant baggage you're speaking of here.
What's hard about
"find . -regex <regex> -exec <stuff> /;"
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.
You just answered your own question because it's "\;" not "/;". And the "{}" placeholder syntax isn't exactly intuitive either (maybe there is some connection to awk or sed? I only know enough of those to be dangerous).
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.
You're right, find won't call stat() unless it needs to. It doesn't need to for recursing through the directory tree because readdir() returns dirent.d_type these days to determine if a name is a directory.
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.
Yeah find is a bad example for bloat, compared to GNU echo or GNU yes. find AFAIK is mentioned more for horror stories, Bernstein insults, as an example for alien parts of UNIX-like OSes which prevent from a streamlined, predictable-in-your-head experience. "It isn't windows server", a statement which devoids of meaning sometimes with powershell and with Azure cloud apis, but eh.
Is that a problem? A simple tool is good if it can solve complex problems, but a simple tool that can't is just an underdeveloped tool. Sometimes you need a complex tool to solve a complex problem.
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.)
Find is not simple - it's easy, but internally complex. It's fine to solve a complex problem with a complex tool, but it's troublesome when people mostly use it to solve simple problems. And the trouble is that this kind of approach spreads to every single component in the system, which means that we're using insane amount of code and cpu load to solve really simple problems on daily basis.
Plan 9 is not an answer to every problem, but it's just impressive how much it can do with so little code.
…we're using insane amount of code and cpu load to solve really simple problems on daily basis.
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.
> More code doesn’t necessarily imply more resource usage.
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".
After staring down find(1) on an AIX system I regret not going down the du | grep path. At least that way I wouldn't be stuck trying to mash GNU's parameters into completely incompatible version of find.
If your system supports `man 1p` (loading the posix manual for each supported utility), it is a good idea to reference that instead of the default manpage; it is more likely for your utility to support posix options than GNU extensions in legacy and non-linux systems.
Sure, which lends weight to the idea that the shell should be a more extensive programming language. Then you can pass an arbitrary Predicate<File> to the finder (which would likely just be a convenience function of about 5-20 lines), and receive a list of file objects for you to do what you want with
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
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
"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 ... 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.
I agree with you to some extent, but the solution is fd: change the tool's interface to make simple things easy. Deleting the tool entirely does nobody any favors. As bad as find(1) is for simple stuff, piping du(1) to grep(1) is worse.
That's half of the solution. The other half, I'm convinced, will be a tool that makes complex things possible. After all, if I really need some complex filtering, find's primitive logical operators probably wouldn't even be sufficient. To that end, I love how Nushell's leading example on their home page is
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.
"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.
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.
Hard to grep a list of path names for size or last modification date?
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...
Perhaps depends on your definition of “user friendly” - Find is certainly not going to be at the top of my own list. Half the time I use ls -lR | grep because I can’t be bothered to rediscover the right find option - which won’t even do what I want the first time I try it.
I would guess the minimalism was a practical choice at the time, given it was a pretty small team making an entire OS. Had Plan 9 ever become some kind of commercial (or even open source) success, I'm sure a find command would eventually have found its way in.
Personal experience: find gets hard to use, and I always have to resort to piping into grep anyway. Maybe I should learn how find works; maybe I should just stick with standard tools. And if `du | grep` gets too long to type, especially with arguments, then that's where I write a script for my common use.
> Rust's "fd" implementation is under 7,000 lines of code
«
To name one egregious example, omitting find(1) in favor of piping du(1) [...] to grep(1) is not an improvement
»
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.
> No includes that contain other includes is obvious and sensible and takes orders of magnitude off compilation times. That rarely gets mentioned.
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?
Examples of what ? Also, what exactly do you mean by "outside of the Unix and C family" ?
(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.
> 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.
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.
Thanks. You inadvertently completely confirm the point that I was seeking to validate.
> 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".
#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.
> I wasn't asking for languages that don't run on UNIX.
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:
I once saw Plan 9 described as "Imagine if all the stuff they told you about Unix was true."
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)
Plan 9 had many improvements over Unix of its times. It opened up too late to conquer the world though. Some good things from it were imported into Solaris and Linux later.
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).
I personally don't like WASM. I think the Native Client (which is more than a decade old today) was a much better technical solution. It used actual cpu native assembly bundled with a verifier, and had actual good integration into low-level platform APIs like graphics, threading, etc. instead of using this weird Javascript bridge approach. Due to it being native code from the start, it also had none of the startup performance issues, that plague WebAssembly.
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.
Actual CPU native assembly? It would need to support x86, x64, and aarch64 by now. (And likely RISC-5 in the near future.)
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 %)
Yeah, actual native assembly. I think doing a cross compilation for 2 architectures (x64 and aarch64) is more than doable, even 4, if you want to be thorough.
> 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?
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.
As I understand it, and I could be wrong, the origins of the project lay in trying to target the output from a compiler to a bytecode like intermediate language that the JavaScript runtime could execute. I am more than happy to believe that no trace of that is left now, but I think that is more or less how it got started.
Inferno did not "lead" to WASM. Palm Pilot did not "lead" to iPhone.
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.
Can't everyone design a better OS than the current operating systems? More features, easier to use as a user, safer, easier to write programs for it, less bugs, more compatibility, etc. I am using operating systems for years, and I have ideas how they should work on the surface.
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.)
And then replies slowly devolve into arguing about irrelevant things that were solved with the existence of Linux because plan9 sucks and Linux is just a kernel which is actually used by people and therefore they actually found ways to solve problems they were most annoyed by.
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.
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.
It is, but for what it's worth using the mouse is much more pleasant than what we're used to ime. It's weird at first, especially with the teleporting thing but it works really well.
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.
One problem I had is that I often just messed up the buttons, especially in the chords, and there's not a whole lot of feedback. I also messed up some things like letting go of the mouse button too soon by accident. Granted, I never spent that long with it, but it's not very mnemonic and requires some amount of motor skills I don't seem to have (e.g. in Vim "dw" is mnemonic for "delete word" and you get more feedback).
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.
>Plan 9 in general seems very much like a system "by the designers, for its designers"
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.
I learned the chords very quickly, but everyone works differently. What kind of feedback exactly would you expect from it? I can't immediately think of anything that would make it much better. I guess animations could help, but then they might just be distracting.
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).
> What kind of feedback exactly would you expect from it?
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).
A cursor change on the beginning of the chord would be the most obvious feedback. Maybe something like [1-] over the cursor, showing up the pressed key?
Just the idea of context switching from mouse to keyboard is so annoying to me outside of graphic editing. I installed vinium for firefox recently and it has been a game changer.
That's fair and I share the sentiment to some degree, especially in "regular" systems. In the time period I used plan 9 I quickly got used to it though, and the method of interaction made me a bit slower and much more considerate. I liked it a lot. I still miss it. I'll probably try it again some time.
Ish. Just changing where you are oriented can impact how some people are thinking. By nature of their orientation.
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.
Yes, Plan 9 made some bad choices in retrospect, at least in my opinion. The big dependency on the mouse is one of them but also assuming people will be in managed networks.
Article mentions that Plan9 missed `cgroups`, QoS.
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 cgroups or containers, because every process has its own namespace, its own view of the network-global filesystem, so everything is in a container by default.
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.
Cgroups are a lot more than just "namespaces". It is also the mechanism by which you can constrain how much CPU, Memory, Network Bandwidth, Storage IOPS or Throughput, etc., processes in a particular cgroup or container can use.
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.
It didn't have cgroups, because at the time it was thought of as a distributed system meaning it would run a single service, authentication for example, and there was no real ability to share resources in the way we do today.
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.
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.
Agreed. Go is a shining example of Unix philosophy - but I don't mean that as a compliment. I mean it prioritizes ease of implementation over any other concerns and hence forces the user to reinvent many wheels. Forces round things into square holes, regardless of if that interface actually makes sense. And either completely ignores good ideas from other camps or goes sour grapes and claims they're overcomplicated and bad (then quietly tries to hack them on later)
Worse is better than nothing, but when better has been demonstrated to exist, worse is just worse
I remember trying to figure out what's up with Go and left in complete disbelief when I was told that I need to install nginx on my local machine and fake a DNS entry just for go's package manager to "fetch" my local package from my local machine just so I could mess around with it how that works.
All because I couldn't care less about 3rd party services like github.
- 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,
All these things you listed are objectively bad and a tumor and I'm glad that those that are gone, are gone for good, because the one that stayed is a menace to any programmer caring about his sanity.
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.
> Vi and Emacs are horrible ugly things that were rendered obsolete by the Apple Lisa in 1982
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.
All text editors are modal editors though, all text editors have a command mode and an input mode.
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.
I think that may be over-generalising a point into irrelevance.
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.
I’m not even sure if the point IS relevant at all. The software-side difference between “the user pressed I, am I in command mode?” and “the user pressed I, did he also press Ctrl?” is not that big.
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.
> The same person who is involved with Plan 9 also made Go. Taking one look at that programming language doesn't leave me very optimistic about Plan 9.
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?").
"Everything is a file" is not a good idea, because different objects have different interfaces. For example, a network socket is not a file because you cannot seek it. A process is not a file because you cannot send signals to a file.
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.
>Instead of "everything is a file", "everything implements well-documented object-oriented interfaces" would be much better idea.
Why "object-oriented" specifically? Is this in any sense different from non object-oriented interfaces, eg. Rust's traits or Go's interfaces, or just to make it more clear that you're talking about interfaces in the programming languange sense?
The operation system already very much behaves like an object-oriented system, even in a languages without language support for this: you ask the OS for a handle, which has to be passed as the first argument to any system call. The kernel has figure out what object the handle refers to and redirect the call to the appropriate subsystem. This is abstraction and polymorphism. And of course, the kernel won't give you any access to internals by default, which gives you encapsulation.
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.
Not really, because then, the influence reaches deep into the programming language, and that means you get one of the many single-language type OSes, like Smalltalk, or Oberon, or Lisp Machines.
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.
Oh I absolutely think so, in fact I really hope kdbus gets merged and eventually it gets used to expose kernel structures
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.
It might not be the best example but in my experience Microsoft seemingly tries with Windows the approach of (almost) everything being an API. It might be the Windows API specifically or that I'm mainly driving Linux outside of work, but I often found the approach of (almost) everything being an API annoying.
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.
Aside from the fact that Linux increasingly has documentation for its ioctls and pseudo-filesystems, whether it is documented is a completely orthogonal issue to how these interfaces are designed. You can have well-documented APIs based on files and undocumented API based on object-oriented interfaces as well.
Everything as a file was an interesting architecture in the 70s. The whole system was dependent on that model so it made total sense. In the 00s and beyond I thought everything as a service was a better model. Called through an API. Basically RPC. But even that has failed. It's very difficult to approach a new system from that perspective. I think you can design a protocol to say what the model of interaction should be for a resource but essentially demonstrate that with a single application rather than the whole operating system. Hard to convince an engineer otherwise but the pursuit of technical perfection is often what stops something from achieving it.
Everything as a service is what modern operating systems do actually use under the hood. Look at the Apple stack. macOS/iOS are basically a large collection of RPC-driven microservices, wrapped behind some thin client libraries for convenience and abstraction.
Realistically neither do files. I think it really comes down to our common abstractions. Everything is IO. For the most part shared memory maps are what drives the most performance and ease of use.
/proc is for stuff like shell automation, not normal 'file' use. The idea is that files are just byte streams/arrays, and pretty good to be optimized. System calls will always be better than /proc, that's not the point at all.
I don't know how zones were implemented in Solaris, but since the article brings up docker, I feel the original zones implementation in Solaris is a still significnatly better user experience than docker or anything similar on Linux. The combined stack of zones + crossbow and zfs was just a pleasure to work with.
There is a paper called "Solaris Zones: Operating System Support for Consolidating Commercial Workload".
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."
* Incrementally improving on the WIMP model. (E.g. title bars as tabs.)
* 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.
I see about 6 main points here and I disgree with all of them.
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.
> Plan 9 had two major ideas, that everything else was built on. The first was the idea that everything is a file. You might think that in Unix everything was already a file, but it was only partially true. In Plan 9 they took this idea to the extreme. Everything including the input and output of the system, process management and network connections were all accessed through the file system instead of the usual syscalls.
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 managed to install a recent version in a VM and get it doing things, which I never managed with traditional Plan 9, despite several attempts. That impressed me.
To be fair, even the namespace design of Plan 9 isn't very elegant, because file-base API hits its own limit pretty quickly. Linux had to abuse `ioctl` to avoid that.
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.
Yes, but the simplicity is in a different level. Especially in nameserver-based designs, which is quite major, namespacing is a matter of using a different nameserver. The implementation boils down to one syscall that writes to a single uint value in the process struct (which would take, idk, 10 lines in total?).
I often see those "Plan 9 did everything right" posts and can't help but add "... because it hardly did anything at all"
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.
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.
The feature of plan 9 that makes it all hang together is that all features interact through a single, network transparent, universal layer.
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.
Just a nitpick: "docker" is not a technology, its a product. What you (and the author) mean is "kernel containers".
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
wrt #4: No, the big thing about Plan 9 is that each process has its own view of the filesystem, and can modify it without any privileges. This would break the unix security model because unix has setuid.
> 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.
The Plan 9 design wasn't as great as people like to think and it wouldn't have survived in the advertised form if Plan 9 actually had taken off. Actually it was already losing that aesthetic coherence quite early on in its lifetime.
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.
A model need not be copied to be better. IMO there are lots of reasons for people to pursue their own ideas or ideas that don't require fundamental upgrades to their operating system.
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.
DCOM is built on top of MS-RPC which is originally a DCE-RPC variant yes, but DCE-RPC isn't object oriented and all the interfaces inside Windows are. You can't just open a local socket and speak DCE-RPC to Windows services, you have to use COM.
Am I the only one who still remembers Windows existed before OLE and later COM? The original RPC in Windows was based on sending messages to windows (and suddenly the name of the operating system makes complete sense), each message having, besides the target window and the message type, a word parameter and a long parameter. If you needed to send more data, you allocated memory from the global heap (which was shared between all processes), and passed the handle to that memory as one of the two message parameters.
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.
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.
The RPC protocol used by Windows between machines (e.g. for registry access, file sharing, setting permissions, etc.) isn't COM-based. Similarly, the RPC used internally (e.g. to talk to win32k) isn't COM-based. My Windows knowledge is quite stale but when I was developing for it COM was used for APIs but it wasn't much used for RPC.
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.
This is the biggest advantage of single-vendor ecosystems like Apple's. The governance aspect is very simplified in this situation, and it becomes possible to ram through such extensive changes. It has to be balanced with backwards compatibility of course.
eBPF works because it only runs programs that pass its own static-analyzer - which basically means programs have to be specifically written for eBPF - you can't just take an arbitrary C program and have it run as an eBPF program.
More like the reverse. Coding in 9front's C from the book from Francisco J. Ballestero's felt and still feels like the future today.
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.
I use GNU Hyperbola as my main OS. Is not that I am a GNU/Linux hater.
But 9front has good points to borrow from.
Cross GCC (or clang) sucks a lot compared to [1-9]c compilers by a huge margin.
Current compilation suites are brain damaged. Don't get me started on cross compiling packages for foreign archs and hunting bugs. Or the compat32 disaster on Debian and derivatives like Trisquel/Ubuntu. Slackware does it fine, tho.
Also, I hope that FS from ori_b gets ported into the future Hyperbola BSD.
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