Hi George,
Brief (ha!) description of how objects come into being and how actors get access to them. And how their interactions are controlled and revoked.
[mixture of Mach-speak and each of the other lexicons we've loosely employed]Sketching as you read may help!
When a task is created (by /), in addition to the typical resources packed in that container (memory, executable, etc) there is a *Handle* to a Namespace_t. The Handle is effectively a send right to a particular port_t. That Handle is *handled* by the Namespace Server (Handler). I.e., the Namespace Handler holds the receive right for that port_t.
(remember the task is an object and *also* is represented by a Handle!)
Any communications directed to that port_t (Handle) end up *at* the Namespace Handler -- arriving on that specific port ("assigned" to that particular task). I.e., the Handle represents the task's Namespace.
All Namespace operations (methods) expect a Handle BACKED BY SOMETHING THAT UNDERSTANDS NAMESPACE IDL as an argument. Other arguments vary with the operation (method) being invoked. For example, lookup(name), create(name), destroy(name), etc.
The / that caused this Namespace object to be created (evident below), may have prepopulated the namespace with certain names that the task will recognize, by convention (can vary with task; e.g., may also include a name like "[parent]").
The may elect to allow the task to modify this namespace (add, delete, rename, etc). Or, it may *lock* the namespace from the task's perspective.
Once the task is executing, it can lookup() particular names in its namespace that have been previously agreed upon, by convention. [note that the conventions need only be consistent between a task and he creator of its namespace! one task can know an object by the name "trunk release" and another could know an object by the name "boot opener". Names are just convenient mnemonics.]
Invoking the lookup() stub with the args required causes an IPC/RPC message to be created, args marshalled, etc. and then delivered to the port *in* the Namespace Handler that is associated with that task's Handle (Namespace).
*Because* the task holds a valid send right for the port, the kernel allows the message to be delivered.The Namespace Handler receives the message, examines the method being sought and decides if messages of this type (method) are permitted ON THIS PORT! I.e., if the Handle has the appropriate "authorizations" defined when created this binding. If not, it returns FAIL (NO PERMISSION).
Assuming the Handle for the task's namespace ALLOWS the lookup() method to be invoked by that task (WTF?), the args are unmarshalled and the operation (method) attempted. A result (if required) is then propagated back to the calling task (using a *reply* port that the task has included in the message -- i.e., you can support async methods if you plan on watching that reply port *later* for the reply to this operation!)
[the IDL allows various types of "methods" to be designed -- including "routines" that return no value.]In the event of a lookup(), the task is often trying to establish initial contact with some other Handler -- to create an object of the type Handled by the Handler. Pretend we're interested in Files (bad choice but easy to spell! :> )
[Of course, objects of that type may already have been created by and placed into task's Namespace before the task began executing! The Handles for any of these will already have been bound to appropriate names in the task's Namespace. E.g., "log".]Now it gets clever...
For initial contact with a named Handler, you need a "public" access method that is enabled "for all". The Handles to this perhaps just implement create() -- for the type of object managed by that Handler (server). So, a task can contact the File Server to create a new file!
For untrusted tasks, when builds the initial Namespace for the task, he doesn't bind the "trusted" public Handle into the server. But, instead, has the Handler creates *another* such Handle. Same permissions (typically). Then, binds the name for this Handler into the Namespace that it has been building for the task. So, "__File_Server__" is bound to a particular port *in* the File Server.
And, this port may be different for each such task!
All such "public" Handles into the File Server are serviced by one or more threads. I can assign a thread per port (Handle), a thread for the entire service INTERFACE (not counting threads that actually BACK specific Files), or lump groups of ports into port sets so a single thread can handle requests coming in on ANY of these ports.
The ask creates a File, for example. This causes the File Server to create a new port for that File and pass a send right (Handle) to that "object" back to the task in the reply to the create() method.
Note that this object now exists yet has no *name*. No problem! The task knows it by its handle. It can read and write it and even destroy it -- without ever giving it a formal *name*! Furthermore, it can pass the Handle on to someone else and *they* can read and write it as well (assuming the task doesn't tell the File Server to reduce the authorizations on a *new* Handle passed instead!). As long as there are Handles referencing the File, it exists.
If it wants to *name* the File, it can (assuming gave him that authorization for HIS Namespace) bind an Name to the object. THIS NAME ONLY MEANS SOMETHING IN THE TASK'S NAMESPACE! Meanwhile, some other task holding a copy of that Handle can create a different name for this same object (File) in *its* namespace! Or, remove the name, etc.
[Namespaces are just convenience objects. BUT, have the effect of limiting what you can talk to!! If you can't reference it, you can't talk to it!]Now, assume someone holding a Handle to this File starts misbehaving. Keeps trying to write to the file. Doing this repeatedly when you have already been told (error return) that you don't have permission is abusive -- wastes the File Server's resources!
But, because that offender holds a valid send right to this object, the kernel will keep passing along those RPC's!
The File Server can take matters into its own hands and destroy the port on which the RPCs are arriving. The kernel will then know not to pass the RPC along -- the task has effectively lost the right to send to that port!
Ah, but a determined adversary just reconnects to the File Server via the public port and starts the process all over again!
*BUT*, the only way the adversary can talk to the File Server is via the public port that was created JUST FOR HIM! I.e., __File_Server__ maps to a specific port that, effectively, identifies *him* (his "public" connection).When the File Server gave permission to access that particular File that was later abused, it knew which pubic port was associated with it! So, the File Server can delete *that* port thereby severing the task's only means of talking to the File Server publicly.
[You can also create initial "credentialed" connections whereby each actor identifies itself with a unique token -- a *port* that it creates exclusively for that particular connection creation operation. As ports are unforgeable, only a task holding this port can act *as* this actor!]He can still exercise any Handles that he has outstanding for other File objects -- until he gets abusive with those, etc.
So, a server can isolate itself from its clients, selectively. (remember, everything is essentially a server -- for *something*!)
From the other side, when everything drops (destroys, discards) every Handle to an object, that object can no longer be accessed! So, it can be deinstantiated and its resources freed. (this of course is done by the Handler for that type of object)
E.g., when the last Handle into a particular Namespace is dropped, the Namespace itself can disappear (which, in turn, causes handles to all the objects referenced in the namespace to go away... etc.)
Simiarly, when the last Handle into the Namespace Handler (!!) goes away, the Namespace Handler itself can go away!!
[an unreferenceable object is just a waste of resources!]This is where the notification of Handler death comes into play. Automatic self-GC without a sweeper!
So, "init()" can do its job and then release the Handles that it held to all these "top level servers" *knowing* that the last task to reference any of them will effectively determine their fate -- without init() acting like a zombie clinging to them.
As I said, a picture makes things more intuitive.