Framework infrastructure
Apart from the very fundamental mechanisms implemented by core, all higher-level services have to be implemented as part of the process tree on top of core. There are a number of frameworks at hand that provide convenient interfaces to be used by such components. In this section, we outline the most important frameworks.
Communication
The basic mode of operation of our RPC framework is based on C++ streams. It uses four different stream classes: Ipc_ostream for sending messages, Ipc_istream for receiving messages, Ipc_client for performing RPC calls, and Ipc_server for dispatching RPC calls. In the following, we use illustrative examples.
- Sending a message
-
Ipc_ostream sender(dst, &snd_buf); sender << a << b << IPC_SEND;
The object sender is an output stream that is initialized with a communication endpoint (dst) and a message buffer (snd_buf). For sending the message, we sequentially insert both arguments into the stream to transform the arguments to a message and finally invoke the IPC mechanism of the kernel by inserting the special object IPC_SEND.
- Receiving a message
-
int a, b; Ipc_istream receiver(&rcv_buf); receiver >> IPC_WAIT >> a >> b;
For creating the receiver input stream object, we specify a receive message buffer as argument that can hold one incoming message. By extracting the special object IPC_WAIT from the receiver, we block for a new message to be stored into rcv_buf. After returning from the blocking receive operation, we use the extraction operator to unmarshal the message argument by argument.
- Performing a RPC call
-
Ipc_client client(dst, &snd_buf, &rcv_buf); int result; client << OPCODE_FUNC1 << 1 << 2 << IPC_CALL >> result;The first argument is a constant that references one among many server functions. It is followed by the actual server-function arguments. All arguments are marshalled into the snd_buf. When inserting the special object IPC_CALL into the client stream, the client blocks for the result of the RPC. After receiving the result message in rcv_buf, the RPC results can be sequentially unmarshalled via the extraction operator. Note that rcv_buf and snd_buf may use the same backing store as both buffers are used interleaved.
- Dispatching a RPC call
-
Ipc_server server(&snd_buf, &rcv_buf); while (1) { int opcode; server >> IPC_REPLY_WAIT >> opcode; switch (opcode) { case OPCODE_FUNC1: { int a, b, ret; server >> a >> b; server << func1(a, b); break; } .. } }The special object IPC_REPLY_WAIT replies to the request of the previous server-loop iteration with the message stored in snd_buf (ignored for the first iteration) and then waits for an incoming RPC request to be received in rcv_buf. By convention, the first message argument contains the opcode to identify the server function to handle the request. After extracting the opcode from the server stream, we branch into a server-function-specific wrapper that reads the function arguments, calls the actual server function, and inserts the function result into the server stream. The result message is to be delivered at the beginning of the next server-loop iteration. The two-stage argument-message parsing (the opcode to select the server function, reading the server-function arguments) is simply done by subsequent extraction operations.
Server framework
|
|
Relationships between the classes of the server object framework
|
Each component that makes local objects remotely accessible to other components has to provide means to dispatch RPC requests that refer to different objects. This procedure highly depends on the mechanisms provided by the underlying kernel. The primary motivation of the server framework is to hide actual kernel paradigms for communication, control flow, and the implementation of local names (capabilities) behind a generic interface. The server framework unifies the control flow of RPC dispatching and the mapping between capabilities and local objects using the classes depicted in Figure 5.
- Object_pool
-
is an associative array that maps capabilities from/to local objects. Because capabilities are protected kernel objects, the object pool's functionality is supported by the kernel.
Note: On L4v2 and Linux, capabilities are not protected by the kernel but are implemented as unique IDs. On these base platforms, the object pool performs the simple mapping of such unique IDs to object pointers in the local address space.
- Server_object
-
is an object-pool entry that contains a dispatch function. To make a local object type available to remote components, the local object type must inherit from Server_object and provide the implementation of the dispatch function as described in Section Communication.
- Server_entrypoint
-
is an object pool that acts as a logical communication entrypoint. It can manage any number of server objects. When registering a server object to be managed by a server entrypoint (manage method), a capability for this object gets created. This capability can be communicated to other processes, which can then use the server object's RPC interface.
- Server_activation
-
is the stack (or thread) to be used for handling RPC requests of an entrypoint.
Note: On L4v2 and Linux, exactly one server activation must be attached to a server entrypoint. This implicates that RPC requests are handled in a strictly serialized manner and one blocking server function delays all other pending RPC requests referring the same server entrypoint. Concurrent handling of RPC requests should be realized with multiple (completely independent) server entrypoints.
Process environment
As described in Section Interfaces and Mechanisms, a newly created process can only communicate to its immediate parent via its parent capability. This parent capability gets created "magically" dependent on the actual platform.
Before entering the main function of the new process, the process' startup code crt0 is executed and initializes the environment framework. The environment contains RPC communication stubs for communicating with the parent and the process' RM session, CPU session, PD session, and RAM session. Furthermore, the environment contains a heap that uses the process' RAM session as backing store. The environment can be used from the actual program by dereferencing the pointer returned by the global function:
Env *env();
Child management
The class Child provides a generic and extensible abstraction to unify the creation of child processes, serve parent-interface requests, and to perform the book keeping of open sessions. Different access-control and resource-trading policies can be realized by inheriting from this class and supplementing suitable parent-interface server functions.
A child process can be created by instantiating a Child object:
Child(const char *name,
Dataspace_capability elf_ds_cap,
Ram_session_capability ram_session_cap,
Cpu_session_capability cpu_session_cap,
Cap_session *cap_session,
char *args[])
NOTE: The name parameter is only used for debugging. The args parameter is not yet supported.
Heap partitioning
In Section Goals and Challenges where we introduced the different types of components composing our system, we highlighted resource multiplexers as being critical for maintaining the isolation and independence of applications from each other. If a flawed resource multiplexer serves multiple clients at a time, information may leak from one client to another (corrupting isolation) or different clients may interfere in sharing limited physical resources (corrupting independence). One particular limited resource that is typically shared among all clients is the heap of the server. If the server performs heap allocations on behalf of one client, this resource may exhaust and renders the service unavailable to all other clients (denial of service). The resource-trading concept as presented in Section Quota enables clients to donate memory quota to a server during the use of a session. If the server's parent closes the session on request of the client, the donated resources must be released by the server. In order to comply with the request to avoid intervention by its parent, the server must store the state of each session on dedicated dataspaces that can be released independently from other sessions. Instead of using one heap to hold anonymous memory allocations, the server creates a heap partition for each client and performs client-specific allocations exclusively on the corresponding heap partition. There exist two different classes to assist developers in partitioning the heap:
- Heap
-
is an allocator that allocates chunks of memory as dataspaces from a RAM session. Each chunk may hold multiple allocations. This kind of heap corresponds loosely to a classical heap and can be used to allocate a high number of small memory objects. The used backing store gets released on the destruction of the heap.
- Sliced_heap
-
is an allocator that uses a dedicated dataspace for each allocation. Therefore, each allocated block can be released independently from all other allocations.
The Sliced_heap must be used to obtain the actual session objects and store them in independent dataspaces. Dynamic memory allocations during the lifetime of a session must be performed by a Heap as member of the session object. When closing a session, the session object including the heap partition gets destroyed and all backing-store dataspaces can be released without interfering other clients.

