To use some well known expressions, XOS is an fully object orientated, distributed, (preemptively) multithreaded/tasked, event (more precisely: message) driven sytsem. It is fully independent of anything a system can be dependent on =) - due to that option, it is also not compatible with anything yet, and there are things it inheretly can't be compatible with. To put it in simple terms, it is kind of a research project to have fun with (but god knows what it might become some day =).
XOS is at least flexible enough, that I can imagine a POSIX or Win32 emulator (if you like to code one =).
XOS's computational model is similar to the one used in the Java (language) system. It primarily knows objects. Objects are sort of algebraic structures - data structures which have the following properties:
An attribute is a string associated with data of arbritrary format (also no format) and size (also zero) (a hashtable entry). Attributes can be assigned to an object in three different ways:
A class codes (declares and implements) an object's methods (machine instructions) and data (variable-types (can certainly also be references to objects) and -sizes). Classes can be represented by data, or code. That means, that an object can create objects dynamically (class is represented by code), or by interpreting data as a (class) code that describes how to do so (actually the code (machine instructions) used in the latter case only is a generalized way to create objects that use the class (code) as parameters).
An instantiator is an object that instantiates (creates) new objects by using classes - it knows how to decode classes to create objects. An instantiated object by default is assigned the union of the class's and the instantiator's priviledge attributes.
Members are variables or methods declared in a class.
A class can be subclass of zero or more classes. A subclass inherits all members (methods or variables) of the superclass (that class, the (concerned) class is subclass from). That means the subclass can access them implicitly. If a subclass declares members that it's superclass declares, too, the members are overwritten. That means the subclass sees it's members implicitly and has to reference the members of the superclass explicitly. Members can be prevent from being overwritten by declaring them explicitly "final" (smells like steaming Java =).
Object's and their members can be declared public, private or protected. Public objects can be accessed by any other object, private objects by no other object and protected ones only by objects instantiated by the (declaring) object's instantiator, the instantiator itself and objects explicitly legitimated (certified by priviledge attributes).
Objects and members can additionally be declared static. That means they are instantiated only once for all further instances of a class (in case of an object, they can't be instantiated more than once).
A constructor is a method, that is called when an object is instantiated.
A process is an object, that is able to run without any surrounding context. It's constructor needs no further implicitly accessible members than it's own.
Processes are differentiated in 3 types:
This paragraph was written with an implementation on x86/Alpha/PPC/68xxx/SPARC/MIPS based workstations in mind. It might be slightly different for other systems (I think of embedded systems) - when I get to know more system types I will adjust...
The bootloader is a very special object. It doesn't exists in the system, but is essential to it's existance. The bootloader also doesn't care about any of the system's mechanisms, hence it doesn't really get in touch with any of the system's objects. But concerning the bootloader from the perspective of the system's computational model it is an instantiator, that uses classes to create instances of the main system components (i.e. KRNL, HEDS, BMDS, BMPS, BCFS, INIT).
XOS's computational model is very abstract and doesn't really care about system initialisation (besides the order in which the main sys comp's constructors are called). It expects some objects to be instantiated when it "opens it's eyes". How this state, in which the system can savely "open it's eyes" (we don't want our lovely system to see the mess out here, do we ? =) is established, is platform and configuration dependent, and is subject to the bootloader. What fileformat the classes of the main system components are present and how to actually load these classes is completely transparent to the system.
The kernel is, simply put, a hardware server for the CPU. And corresponding to the CPU's special function in the (hardware) system, the kernel has special functionality in the computational model. It is divided into 4 main modules, that partially could be seperated from the kernel totally, but not without significant loss of performance. For example, the exokernel design would have an external object for the IPC management or the scheduler and / or process- / thread-manager. But since these modules make heavy use of common data structures and tables, since many CPU's support many of their functions on the hardware level, it makes sense in context of performance orientation to implement them, together with the kernel, in one module.
In distributed systems IPC represents one of the major performance bottlenecks. To offensively fight this problem, I will try to realize the whole kernel in optimized assembler code - at least on CISC machines =). That enlarges the platform dependent part of the code enormously, but will help to reduce negative effects of performance bottlenecks in the most often called routines like IPC and scheduling routines. Beside the kernel, many servers have to be realized in (non-portable) ASM code, to not let further, heavy performance bottlnecks arise (I think of HDD, NET and GFX servers mainly).
The interface to the elementary kernel functions is provided by a single function dispatcher (KFD). It provides functions for memory- / process- and thread-management and IPC.
In XOS's computational model, there is "the memory". Processes aren't aware of different types of memory and how they are managed. They simply exist in the memory. The KFD provides memory management functions (alloc, free, resize, getHandles). It's up to the system to determine where that memory is really located and how it has to be loaded to be accessible to the requesting process. Since there is only one memory, virtual memory and memory mapped filesystem I/O has to be provided, since processes now expect anything to be accessible like RAM (that's definetly the end of file I/O operations - don't worry =). This way, files stored on permanent storing media like harddisks, are objects, that are instantiated by the corresponding MPS (Memory Protocol Server) at the moment it starts up.
The KFD-getHandles-call searches the first level memory (RAM and virtual memory) (actually it searches it's tables holding local objects) for any objects matching the given attributes and name. If no objects are found, it passes the request to MPS's. For example BMPS and IDPS (see 2.1.2.2 §4) are such a servers. If the specified objects are public, or they're protected and the refering object is permitted to access them (certified by priviledge attributes), handles are created depending on the location of the objects. The KFD-getHandles-call is atomic.
Note: synchronization problems can only arise concerning static, public and protected objects and members. So the system can transparently use semaphores or similar mechanisms to avoid inconsistance or deadlocks. Processes have to get all ObjectHandles they use in a critical section at once to indicate the system, which semaphores it has to request or to wait for. Access to static members has to be restricted, so that the system is always invoked, before the object actually gains access to them. This way, any synchronization (except that needed for concurrent threads) is transparently handled by the system.
IPC is the key to modularity. The more flexible a system's IPC mechanisms are, the more modular can it be built. And modularity is definetly one of the main goals to be achieved (think of code reusage, distribution, and simply non-redundant updating).
Identification of objects is very important, especially in distributed systems, since it's not always obvious which instance of which object is to be queried. That means a client doesn't always know the exact set of attributes or the exact name of the object, that provides a desired method. To solve this problem, ordinary attributes are used. They specify the "type" of an object. A reference to an object, that only specifies it's ordinary attributes, is ambiguous. It's up to the system to decide which object of those with the given ordinary attributes is to be queried. It decides by looking at the NAD-attributes of these objects. Nevertheless a reference can also contain NAD- and / or priviledge-attributes.
The invokation of methods can be divided into two parts (hmm, one and a half actually =). First, the client needs to have a reference to the object, that provides the method (it gets one by instantiating the object itself or calling KFD-getHandles successfully). Then the KFD has to make a method directly accessible to the calling object (in a way that it can be invoked using a simple CPU-level call instruction or similar low level mechanism (fast)). If the (destination) object is present on the same machine like the calling object, this method already is the destination object's method. Oherwise the method is a stub call that encapsulates RMI.
External objects (objects that exist on different machines) are accessed using the local IDPS (IPC Distribution Protocol Server). IDP is an application-level network protocol for inter-kernel communication (stay tune to learn more about this =). IDPS itself uses a NTPS (Network Transport Protocol Server), which itself is using a NCPS (Network Connection Protocol Server), which again is using a NDS (Network Device Server ). The kernel is dependent on other servers. There is a bunch of servers, that, together form the trusted part of the system - it's not only the kernel anymore.
The Process Manager creates a structure for each process holding information, mainly used by the scheduler. That structure contains:
Due to XOS's object orientated system design, each process structure, the Process Manager creates, has to carry very little extra information. Every device context is held by the corresponding hardware server object instance. This makes process context-switches very fast, since device contexts are switched by the corresponding hardware server only at appropriate times and when really necessary. A process, that uses very little or no devices is switched to and from very quickly, hence we don't have to be stingy with process context-switches. They still consume much performance, but not as much as they do in many OS's, which have to, due to this issue, sacrifice system security or go unlogical ways to not do so.
The Scheduler creates a heap structure at initialisation time, that holds all currently running processes. The root of the heap contains the currently running process. Every time this process is switched away from (due to quantum run-out or exceptional events), a heap-up, concerning the process's priority is performed, and the once running process is added to the most nested node (using level ordering) of the heap.
The HEDS encapsulates devices like interrupt controllers to provide a common way for objects to get noticed of special events, triggered by hardware.
The BMDS provides memory access on hardware level, whereat the term "memory" may stand for any device, that provides at least read transactions (e.g. network adapters, HDD, RAM, FDD, CDROM, (E)EPROM, etc.). It's primarily used by the BMPS and any other MPS's, respectively. The BMDS is a highly priviledged server.
The BMPS relies on the BMDS for lowlevel data transactions. It structures raw data and interpretes special hardware events (via HEDS) using a standardized protocol, that is provides an abstract filesystem. The BMPS also is a highly privilegded server.
The BCFS interprets class data from objects to instantiate at least the INIT process. The BCFS is a typical kind of server, which provides methods assigned to special objects - it's a typical instantiator, too.
The Init Process is responsible to do anything the system admin wants to. It follows no special definition, that means it is the first application-like process. Actually it's the same like the wellknown UNIX init.
Having system security as a major goal, XOS implements Method Invokation on x86 machines using CallGates. The KFD itself is implemented using a CallGate (the selector is 'X' (ASCII 0x0058) - for easier ASM coding =). It takes it's parameters in several registers, depending on the function to be called (KFD implements MMNG - alloc, free, resize, getHandles, etc.).
To get access to a server method, a process calls KFD-getHandles with the object handle (see 2.2.2) and the method name as parameter (KFD-getHandles is a universal, atomic call to make global, transparent synchronization possible) and receives a selector to a CallGate, the KFD generated in the process's LDT. When there has to be RMI done, the CallGate points to a stub call implementing all necessary IDP transactions.
On x86 machines, XOS processes have their own LDT and Page Dir, hence their own PDBR entry. All memory, no matter wether on harddrive or in RAM, is mapped in their virtual memory space. Following this concept, the procedure of memory allocation goes like this:
Instatiation of objects needs several segments to be created. If it's the first instance of an object, a segment holding the class's code and one holding it's static data has to be generated. For each object, private, protected and public data obviously have to be in their seperate segments (maybe having different access rights).
To instantiate a class, that is subclass of at least one class, all superclasses, beginning with the top most, are instantiated step by step, writing all code and data to the same segements. Inheritance is only a matter of relocation.
KFD-getHandles returns a 32-Bit value. The HiWord is an index into the process's LDT, indicating the first of the selectors created. The LoWord is an error code (not allowed to get Handles, Object not found, etc.).
To fullfill the demand of the system's routines being invoked before access to static members is given to objects, CallGates to static methods and selectors to segments holding static data are marked not present (the page fault handler then notices that these ones have to be synchronized rather than to be loaded from disk).
Every process occupies a seperate task. So all API-calls (Server MI) implicitly invoke a taskswitch. That'll be heavy work, even for those x86 machines >300Mhz (even more, because of (MS-)Windows not using the taskswitching mechanism of the CPU - so it is not especially optimized by most x86 producers). That's why I'm not sure wether to implement at least the KFD in a seperate task or not, yet (some performance tests have to show that).
XJava will be a Java clone, that will be slightly adjusted to be able to formulate the abilities of the XOS (see 1.2) and to fully be able to compile to native code. I decided to create XJava, because C isn't OO and C++ is ugly (and of course because I'd like to write my own compiler =) ! It certainly is possible to program XOS processes in any language including Assembly, but I'd like to have a language, that exactly mirrors the system's OO principles - XJava - as the commonly used HLL for application and software server programming. XJava is a project, that I will face, when the first implementation of XOS is running and the main system components do their main jobs, respectively. That'll be the time for first applications to test and debug the interfaces etc. .