mirror of
https://github.com/genodelabs/genode.git
synced 2024-12-29 10:08:53 +00:00
1038 lines
47 KiB
Plaintext
1038 lines
47 KiB
Plaintext
|
|
====================================
|
|
Design of the Genode OS Architecture
|
|
====================================
|
|
|
|
|
|
Norman Feske and Christian Helmuth
|
|
|
|
Abstract
|
|
########
|
|
|
|
In the software world, high complexity of a problem solution comes along with a
|
|
high risk for bugs and vulnerabilities.
|
|
This correlation is particularly perturbing for todays commodity operating
|
|
systems with their tremendous complexity.
|
|
The numerous approaches to increase the user's confidence in the correct
|
|
functioning of software comprise exhaustive tests, code auditing, static code
|
|
analysis, and formal verification.
|
|
Such quality-assurance measures are either rather shallow or they scale badly
|
|
with increasing complexity.
|
|
|
|
The operating-system design presented in this document focuses on the root of the
|
|
problem by providing means to minimize the underlying system complexity for
|
|
each security-sensitive application individually.
|
|
On the other hand, we want to enable multiple applications to execute on the
|
|
system at the same time whereas each application may have different functional
|
|
requirements from the operating system.
|
|
Todays operating systems provide a functional superset of the requirements of
|
|
all applications and thus, violate the principle of minimalism for each single
|
|
application.
|
|
We resolve the conflict between the principle of minimalism and the versatility
|
|
of the operating system by decomposing the operating system into small
|
|
components and by providing a way to execute those components isolated and
|
|
independent from each other.
|
|
Components can be device drivers, protocol stacks such as file systems and
|
|
network stacks, native applications, and containers for executing legacy
|
|
software.
|
|
Each application depends only on the functionality of a bounded set of
|
|
components that we call _application-specific_trusted_computing_base_(TCB)_.
|
|
If the TCBs of two applications are executed completely _isolated_ and
|
|
_independent_ from each other, we consider both TCBs as minimal.
|
|
|
|
In practice however, we want to share physical resources between multiple applications
|
|
without sacrificing their independence.
|
|
Therefore, the operating-system design has to enable the assignment of physical
|
|
resources to each application and its TCB to maintain independence from other
|
|
applications.
|
|
Furthermore, rather than living in complete isolation, components require to
|
|
communicate with each other to cooperate.
|
|
The operating-system design must enable components to create other components
|
|
and get them to know each other while maintaining isolation from uninvolved
|
|
parts of the system.
|
|
|
|
First, we narrow our goals and pose our mayor challenges in Section [Goals and Challenges].
|
|
Section [Interfaces and Mechanisms] introduces our fundamental concepts and
|
|
protocols that apply to each component in the system.
|
|
In Section [Core - the root of the process tree], we present the one component
|
|
that is mandatory part of each TCB, enables the bootstrapping of the system,
|
|
and provides abstractions for the lowest-level resources.
|
|
We exercise the composition of the presented mechanisms by the means of process
|
|
creation in Section [Process creation].
|
|
;Section [Framework infrastructure]
|
|
|
|
|
|
Goals and Challenges
|
|
####################
|
|
|
|
The Genode architecture is designed to accommodate the following types
|
|
of components in a secure manner concurrently on one machine:
|
|
|
|
:Device drivers:
|
|
|
|
Device drivers translate the facilities of raw physical devices to
|
|
device-class-specific interfaces to be used by other components.
|
|
They contain no security policies and provide their services
|
|
to only one client component per device.
|
|
|
|
:Services that multiplex resources:
|
|
|
|
To make one physical resource (e.g., a device) usable by multiple
|
|
components at the same time, the physical resource must be translated
|
|
to multiple virtual resources. For example, a
|
|
frame buffer provided by a device driver can only be used by one
|
|
client at the same time. A window system multiplexes this physical
|
|
resource to make it available to multiple clients. Other examples
|
|
are an audio mixer or a virtual network hub.
|
|
In contrast to a device driver, a _resource multiplexer_ deals with multiple
|
|
clients and therefore, plays a crucial role for maintaining the independence
|
|
and isolation of its clients from each other.
|
|
|
|
:Protocol stacks:
|
|
|
|
Protocol stacks translate low-level protocols to a higher and more applicable
|
|
level.
|
|
For example, a file system translates a block-device protocol to a file
|
|
abstraction, a TCP/IP stack translates network packets to a socket
|
|
abstraction, or a widget set maps high-level GUI elements to pixels.
|
|
Compared to resource multiplexers, protocol stacks are typically an
|
|
order of magnitude more complex.
|
|
Protocol stacks may also act as resource multiplexers. In this case however,
|
|
high complexity puts the independence and isolation of multiple
|
|
clients at a high risk.
|
|
Therefore, our design should enable the instantiation of protocol stacks per
|
|
application.
|
|
For example, instead of letting a security-sensitive application share one
|
|
TCP/IP stack with multiple other (untrusted) applications, it could use a
|
|
dedicated instance of a TCP/IP stack to increase its independence and
|
|
isolation from the other applications.
|
|
|
|
:Containers for executing legacy software:
|
|
|
|
A _legacy container_ provides an environment for the execution of existing
|
|
legacy software. This can be achieved by the means of a virtual machine
|
|
(e.g., a Java VM, a virtual PC), a compatible programming API (e.g., POSIX,
|
|
Qt), a language environment (e.g., LISP), or a script interpreter.
|
|
In the majority of cases, we regard legacy software as an untrusted black box.
|
|
One particular example for legacy software are untrusted legacy device drivers.
|
|
In this case, the container has to protect the physical hardware from
|
|
potentially malicious device accesses by the untrusted driver.
|
|
Legacy software may be extremely complex and resource demanding, for example
|
|
the Firefox web browser executed on top of the X window system and the Linux
|
|
kernel inside a virtualized PC.
|
|
In this case, the legacy container may locally implement sophisticated
|
|
resource-management techniques such as virtual memory.
|
|
|
|
:Small custom security-sensitive applications:
|
|
|
|
Alongside legacy software, small custom applications implement crucial
|
|
security-sensitive functionality.
|
|
In contrast to legacy software, which we mostly regard as untrusted anyway,
|
|
a low TCB complexity for custom applications is of extreme importance.
|
|
Given the special liability of such an application, it is very carefully
|
|
designed to have low complexity and require as little infrastructure as
|
|
possible.
|
|
A typical example is a cryptographic component that protects credentials
|
|
of the user.
|
|
Such an application does not require swapping (virtual memory), a POSIX API,
|
|
or a complete C library.
|
|
Instead, the main objectives of such an application are to avoid as much as
|
|
possible code from being included in its TCB and to keep its requirements at
|
|
a minimum.
|
|
|
|
Our design must be able to create and destroy subsystems that are composed of
|
|
multiple such components.
|
|
The _isolation_ requirement as stated in the introduction raises the
|
|
question of how to organize the locality of name spaces and how to distribute
|
|
access from components to other components within the system.
|
|
The _independence_ requirement demands the assignment of physical resources
|
|
to components such that different applications do not interfere.
|
|
Instead of managing access control and physical resources from a central
|
|
place, we desire a distributed way for applying policy for trading and revocating
|
|
resources and for delegating rights.
|
|
|
|
|
|
|
|
Interfaces and Mechanisms
|
|
#########################
|
|
|
|
The system is structured as a tree.
|
|
The nodes of the tree are processes.
|
|
A node, for which sub-nodes exist, is called the _parent_ of these sub-nodes
|
|
(_children_).
|
|
The parent creates children out of its own resources and defines
|
|
their execution environment.
|
|
Each process can announce services to its parent.
|
|
The parent, in turn, can mediate such a service to its other children.
|
|
When a child is created, its parent provides the initial contact to the
|
|
outer world via the following interface:
|
|
|
|
! void exit(int exit_value);
|
|
!
|
|
! Session_capability session(String service_name,
|
|
! String args);
|
|
!
|
|
! void close(Session_capability session_cap);
|
|
!
|
|
! int announce(String service_name,
|
|
! Root_capability service_root_cap);
|
|
!
|
|
! int transfer_quota(Session_capability to_session_cap,
|
|
! String amount);
|
|
|
|
|
|
:'exit': is called by a child to request its own termination.
|
|
|
|
:'session': is called by a child to request a connection to the specified
|
|
service as known by its parent whereas 'service_name' is the name
|
|
of the desired service _interface_.
|
|
The way of resolving or even denying a 'session' request depends on
|
|
the policy of the parent.
|
|
The 'args' parameter contains construction arguments for the session
|
|
to be created.
|
|
In particular, 'args' contains a specification of resources that the
|
|
process is willing to donate to the server during the session lifetime.
|
|
|
|
:'close': is called by a child to inform its parent that the specified
|
|
session is no longer needed.
|
|
The parent should close the session and hand back donated
|
|
resources to the child.
|
|
|
|
:'announce': is called by a child to register a locally implemented
|
|
service at its parent. Hence, this child is a server.
|
|
|
|
:'transfer_quota': enables a child to extend its resource donation
|
|
to the server that provides the specified session.
|
|
|
|
We provide a detailed description and motivation for the different functions
|
|
in Sections [Servers] and [Quota].
|
|
|
|
Servers
|
|
=======
|
|
|
|
Each process may implement services and announce them via the 'announce'
|
|
function of the parent interface.
|
|
When announcing a service, the server specifies a _root_ capability for
|
|
the implemented service.
|
|
The interface of the root capability enables the parent to create, configure,
|
|
and close sessions of the service:
|
|
|
|
! Session_capability session(String args);
|
|
!
|
|
! int transfer_quota(Session_capability to_session_cap,
|
|
! String amount);
|
|
!
|
|
! void close(Session_capability session_cap);
|
|
|
|
|
|
[image announce 60%]
|
|
Announcement of a service by a child (server).
|
|
Colored circles at the edge of a component represent remotely accessible
|
|
objects. Small circles inside a component represent a reference (capability)
|
|
to a remote object. A cross-component reference to a remote object is
|
|
illustrated by a dashed arrow. An opaque arrow symbolizes a RPC call/return.
|
|
|
|
Figure [announce] illustrates an announcement of a service.
|
|
Initially, each child has a capability to its parent.
|
|
After Child1 announces its service "Service", its parent knows the
|
|
root capability of this service under the local name 'srv1_r' and stores
|
|
the root capability with the announced service name in its _root_list_.
|
|
The root capability is intended to be used and kept by the parent only.
|
|
|
|
[image request 60%]
|
|
Service request by a client.
|
|
|
|
When a parent calls the 'session' function of the root interface of a server
|
|
child, the server creates a new client session and returns the corresponding
|
|
'client_session' capability.
|
|
This session capability provides the actual service-specific interface.
|
|
The parent can use it directly or it may pass it to other processes, in
|
|
particular to another child that requested the session.
|
|
In Figure [request], Child2 initiates the creation of a "Service" session
|
|
by a 'session' call at its parent capability (1).
|
|
The parent uses its root list to look up the root capability that matches the
|
|
service name "Service" (2) and calls the 'session' function at the
|
|
server (3).
|
|
Child1 being the server creates a new session ('session1') and returns the
|
|
session capability as result of the 'session' call (4).
|
|
The parent now knows the new session under the local name 'srv1_s1' (5) and
|
|
passes the session capability as return value of Child2's initial 'session'
|
|
call (6).
|
|
The parent maintains a _session_list_, which stores the interrelation between
|
|
children and their created sessions.
|
|
Now, Child2 has a direct communication channel to 'session1' provided by
|
|
the server (Child1) (7).
|
|
|
|
The 'close' function of the root interface instructs the server to
|
|
destroy the specified session and to release all session-specific resources.
|
|
|
|
; Mittels 'set_quota' kann der Parent einen Dienst anweisen, die Ressourcennutzung
|
|
; für eine angegebene 'client_session' zu begrenzen. Eine nähere Beschreibung des
|
|
; Ressourcen-Accountings erfolgt in Kapitel [Quota].
|
|
|
|
[image twolevels 80%]
|
|
Announcement and request of a service in a subsystem.
|
|
For simplicity, parent capabilities are not displayed.
|
|
|
|
Even though the prior examples involved only one parent,
|
|
the announce-request mechanism can be used recursively for tree
|
|
structures of any depth and thus allow for partitioning
|
|
the system into subsystems that can cooperate with each other whereas
|
|
parents are always in complete control over the communication
|
|
and resource usage of their children (and their subsystems).
|
|
|
|
Figure [twolevels] depicts a nested subsystem on the left.
|
|
Child1 announces its service named "Service" at its parent that, in turn,
|
|
announces a service named "Service" at the Grandparent.
|
|
The service names do not need to be identical.
|
|
Their meaning spans to their immediate parent only and there
|
|
may be a name remapping on each hierarchy level.
|
|
Each parent can decide itself whether to further announce
|
|
services of their children to the outer world or not.
|
|
The parent can announce Child1's service to the grandparent
|
|
by creating a new root capability to a local service that forwards
|
|
session-creation and closing requests to Child1.
|
|
Both Parent and Grandparent keep their local root lists.
|
|
In a second step, Parent2 initiates the creation of a session to
|
|
the service by issuing a 'session' request at the Grandparent (1).
|
|
Grandparent uses its root list to look up the service-providing child (from
|
|
Grandparent's local view) Parent1 (2).
|
|
Parent1 in turn, implements the service not by itself but delegates
|
|
the 'session' request to Child1 by calling the 'session' function
|
|
of the actual "Service" root interface (3).
|
|
The session capability, created by Child1 (4), can now be passed to Parent2
|
|
as return value of nested 'session' calls (5, 6).
|
|
Each involved node keeps the local knowledge about the created session
|
|
such that later, the session can be closed in the same nested fashion.
|
|
|
|
Quota
|
|
=====
|
|
|
|
Each process that provides services to other processes consumes resources on
|
|
behalf of it clients.
|
|
Such a server requires memory to maintain session-specific state, processing
|
|
time to perform the actual service function, and eventually further system
|
|
resources (e.g., bus bandwidth) dependent on client requests.
|
|
To avoid denial-of-service problems, a server must not allocate such
|
|
resources from its own budget but let the client pay.
|
|
Therefore, a mechanism for donating resource quotas from the client to the
|
|
server is required.
|
|
Both client and server may be arbitrary nodes in the process tree.
|
|
In the following, we examine the trading of resource quotas within
|
|
the recursive system structure using memory as an example.
|
|
|
|
When creating a child, the parent assigns a part of its own memory quota
|
|
to the new child.
|
|
During the lifetime of the child, the parent can further transfer
|
|
quota back and forth between the child's and its own account.
|
|
Because the parent creates its children out of its own resources,
|
|
it has a natural interest to correctly manage child quotas.
|
|
When a child requests a session to a service, it can bind a part
|
|
of its quota to the new session by specifying a resource donation
|
|
as an argument.
|
|
When receiving a session request, the parent has to distinct
|
|
three different cases, dependent on where the corresponding server
|
|
resides:
|
|
|
|
:Parent provides service:
|
|
|
|
If the parent provides the requested services by itself,
|
|
it transfers the donated amount of memory quota from the
|
|
requesting child's account to its own account to compensate
|
|
the session-specific memory allocation on behalf of its own
|
|
child.
|
|
|
|
:Server is another child:
|
|
|
|
If there exists a matching entry in the parent's root list,
|
|
the requested service is provided by another child (or a
|
|
node within the child subsystem). In this case, the parent
|
|
transfers the donated memory quota from the requesting child
|
|
to the service-providing child.
|
|
|
|
:Delegation to grandparent:
|
|
|
|
The parent may decide to delegate the session request to
|
|
its own parent because the requested service is provided by
|
|
a lower node of the process tree.
|
|
Thus, the parent will request a session on behalf of its child.
|
|
The grandparent neither knows nor cares about the actual
|
|
origin of the request and will simply decrease the memory
|
|
quota of the parent.
|
|
For this reason, the parent transfers the donated memory
|
|
quota from the requesting child to its own account before
|
|
calling the grandparent.
|
|
|
|
This algorithm works recursively.
|
|
Once, the server receives the session request, it checks if
|
|
the donated memory quota suffices for storing the session-specific
|
|
data and, on success, creates the session.
|
|
If the initial quota donation turns out to be too scarce during
|
|
the lifetime of a session, the client may make further donations
|
|
via the 'transfer_quota' function of the parent interface that
|
|
works analogously.
|
|
|
|
If a child requests to close a session, the parent must distinguish
|
|
the three cases as above.
|
|
Once, the server receives the session-close request from its parent,
|
|
it is responsible to release all resources that were used for this session.
|
|
After the server releases the session-specific resources, the
|
|
server's quota can be decreased to the prior state.
|
|
However, an ill-behaving server may fail to release those resources by malice
|
|
or caused by a bug.
|
|
|
|
If the misbehaving service was provided by the parent himself,
|
|
it has the full authority to not hand back session-quota to
|
|
its child.
|
|
If the misbehaving service was provided by the grandparent,
|
|
the parent (and its whole subsystem) has to subordinate.
|
|
If, however, the service was provided by another child and the
|
|
child refuses to release resources, decreasing its quota after
|
|
closing the session will fail.
|
|
It is up to the policy of the parent to handle such a failure either by
|
|
punishing it (e.g., killing the misbehaving server) or by granting more of its
|
|
own quota.
|
|
Generally, misbehavior is against the server's own interests and
|
|
each server would obey the parent's 'close' request to avoid intervention.
|
|
|
|
|
|
Successive policy management
|
|
============================
|
|
|
|
For supporting a high variety of security policies for access control, we
|
|
require a way to bind properties and restrictions to sessions. For example,
|
|
a file service may want to restrict the access to files according to an
|
|
access-control policy that is specific for each client session.
|
|
On session creation, the 'session' call takes an 'args' argument that can be
|
|
used for that purpose. It is a list of tag-value pairs describing the session
|
|
properties. By convention, the list is ordered by attribute priority starting
|
|
with the most important property.
|
|
The server uses these 'args' as construction arguments for the new
|
|
session and enforces the security policy as expressed by 'args' accordingly.
|
|
Whereas the client defines its desired session-construction arguments, each
|
|
node that is incorporated in the session creation can alter these arguments in
|
|
any way and may add further properties.
|
|
This effectively enables each parent to impose any desired restrictions to
|
|
sessions created by its children.
|
|
This concept works recursively and enables each node in the process hierarchy
|
|
to control exactly the properties that it knows and cares about. As a side
|
|
note, the specification of resource donations as described in the Section
|
|
[Quota] is performed with the same mechanism. A resource donation is a property
|
|
of a session.
|
|
|
|
[image incremental_restrictions]
|
|
Successive application of policies at the creation time of a new session.
|
|
|
|
Figure [incremental_restrictions] shows an example scenario. A user
|
|
application issues the creation of a new session to the 'GUI' server and
|
|
specifies its wish for reading user input and using the string "Terminal" as
|
|
window label (1).
|
|
The parent of the user application is the user manager that introduces
|
|
user identities into the system and wants to ensure that each displayed window
|
|
gets tagged with the user and the executed program. Therefore, it overrides the
|
|
'label' attribute with more accurate information (2). Note that the modified
|
|
argument is now the head of the argument list.
|
|
The parent of the user manager, in turn, implements further policies. In the
|
|
example, Init's policy prohibits the user-manager subtree from reading
|
|
input (for example to disable access to the system beyond official working hours)
|
|
by redefining the 'input' attribute and leaving all other attributes unchanged (3).
|
|
The actual GUI server observes the final result of the successively changed
|
|
session-construction arguments (4) and it is responsible for enforcing the specified
|
|
policy for the lifetime of the session.
|
|
Once a session has been established, its properties are fixed and cannot be changed.
|
|
|
|
|
|
Core - the root of the process tree
|
|
###################################
|
|
|
|
Core is the first user-level program that takes control when starting up the
|
|
system. It has access to the raw physical resources and converts them to
|
|
abstractions that enable multiple programs to use these resources.
|
|
In particular, core converts the physical address space to higher-level
|
|
containers called _dataspaces_.
|
|
A dataspace represents a contiguous physical address space region with an
|
|
arbitrary size (at page-size granularity).
|
|
Multiple processes can make the same dataspace accessible in their
|
|
local address spaces.
|
|
The system on top of core never deals with physical memory pages but
|
|
uses this uniform abstraction to work with memory, memory-mapped I/O
|
|
regions, and ROM areas.
|
|
|
|
*Note:* _Using only contiguous dataspaces may lead to fragmentation of the_
|
|
_physical address space. This property is, however, only required by_
|
|
_a few rare cases (e.g., DMA transfers). Therefore, later versions of the_
|
|
_design will support non-contiguous dataspaces._
|
|
|
|
Furthermore, core provides all prerequisites to bootstrap the process tree.
|
|
These prerequisites comprise services for creating processes and threads,
|
|
for allocating memory, for accessing boot-time-present files, and for managing
|
|
address-space layouts.
|
|
Core is almost free from policy. There are no configuration options.
|
|
The only policy of core is the startup of the init process to which core
|
|
grants all available resources.
|
|
|
|
In the following, we explain the session interfaces of core's services in
|
|
detail.
|
|
|
|
|
|
RAM - allocator for physical memory
|
|
===================================
|
|
|
|
A RAM session is a quota-bounded allocator of blocks from physical memory.
|
|
There are no RAM-specific session-construction arguments.
|
|
Immediately after the creation of a RAM session, its quota is zero.
|
|
To make the RAM session functional, it must be loaded with quota from
|
|
another already existing RAM session, which we call the _reference account_.
|
|
The reference account of a RAM session can be defined initially via:
|
|
!int ref_account(Ram_session_capability ram_session_cap);
|
|
Once the reference account is defined, quota can be transferred back and
|
|
forth between the reference account and the new RAM session with:
|
|
!int transfer_quota(Ram_session_capability ram_session_cap,
|
|
! size_t amount);
|
|
Provided, the RAM session has enough quota, a dataspace of a given size
|
|
can be allocated with:
|
|
!Ram_dataspace_capability alloc(size_t size);
|
|
The result value of 'alloc' is a capability to the RAM-dataspace
|
|
object implemented in core. This capability can be communicated to other
|
|
processes and can be used to make the dataspace's physical-memory region
|
|
accessible from these processes.
|
|
An allocated dataspace can be released with:
|
|
!void free(Ram_dataspace_capability ds_cap);
|
|
The 'alloc' and 'free' calls track the used-quota information of the RAM
|
|
session accordingly.
|
|
Current statistical information about the quota limit and the
|
|
used quota can be retrieved by:
|
|
!size_t quota();
|
|
!size_t used();
|
|
Closing a RAM session implicitly destroys all allocated dataspaces.
|
|
|
|
|
|
ROM - boot-time-file access
|
|
===========================
|
|
|
|
A ROM session represents a boot-time-present read-only file. This may be a
|
|
module provided by the boot loader or a part of a static ROM image. On session
|
|
construction, a file identifier must be specified as a session argument using the
|
|
tag 'filename'. The available filenames are not fixed but depend on the actual
|
|
deployment. On some platforms, core may provide logical files for special memory
|
|
objects such as the GRUB multiboot info structure or a kernel info page. The
|
|
ROM session enables the actual read access to the file by exporting the file as
|
|
dataspace:
|
|
!Rom_dataspace_capability dataspace();
|
|
|
|
|
|
IO_MEM - memory mapped I/O access
|
|
=================================
|
|
|
|
With IO_MEM, core provides a dataspace abstraction for non-memory parts of the
|
|
physical address space such as memory-mapped I/O regions or BIOS areas. In
|
|
contrast to a memory block that is used for storing information of which the
|
|
physical location in memory is of no matter, a non-memory object has a special
|
|
semantics attached to its location within the physical address space. Its
|
|
location is either fixed (by standard) or can be determined at runtime, for
|
|
example by scanning the PCI bus for PCI resources. If the physical location of
|
|
such a non-memory object is known, an IO_MEM session can be created by
|
|
specifying 'base' and 'size' as session-construction arguments.
|
|
The IO_MEM session then provides the specified physical memory area as
|
|
dataspace:
|
|
!Io_mem_dataspace_capability dataspace();
|
|
|
|
|
|
IO_PORT - access to I/O ports
|
|
=============================
|
|
|
|
For platforms that rely on I/O ports for device access, core's IO_PORT service
|
|
enables fine-grained assignment of port ranges to individual processes.
|
|
Each IO_PORT session corresponds to the exclusive access right to a
|
|
port range as specified with the 'io_port_base' and 'io_port_size'
|
|
session-construction arguments. Core creates the new IO_PORT session
|
|
only if the specified port range does not overlap with an already existing
|
|
session. This ensures that each I/O port is driven by only one
|
|
process at a time. The IO_PORT session interface resembles the
|
|
physical I/O port access instructions. Reading from an I/O port
|
|
can be performed via an 8bit, 16bit, or 32bit access:
|
|
!unsigned char inb(unsigned short address);
|
|
!unsigned short inw(unsigned short address);
|
|
!unsigned inl(unsigned short address);
|
|
Vice versa, there exist functions for writing to an I/O port via
|
|
an 8bit, 16bit, or 32bit access:
|
|
!void outb(unsigned short address, unsigned char value);
|
|
!void outw(unsigned short address, unsigned short value);
|
|
!void outl(unsigned short address, unsigned value);
|
|
The address argument of I/O-port access functions are absolute
|
|
port addresses that must be within the port range of the session.
|
|
|
|
|
|
IRQ - handling device interrupts
|
|
================================
|
|
|
|
The IRQ service of core provides processes with an interface to
|
|
device interrupts. Each IRQ session corresponds to an attached
|
|
interrupt. The physical interrupt number is specified via the
|
|
'irq_number' session-construction argument. A physical interrupt
|
|
number can be attached to only one session. The IRQ session
|
|
interface provides a blocking function to wait for the next
|
|
interrupt:
|
|
!void wait_for_irq();
|
|
While the 'wait_for_irq' function blocks, core unmasks the
|
|
interrupt corresponding to the IRQ session.
|
|
On function return, the corresponding interrupt line is masked
|
|
and acknowledged.
|
|
|
|
;*Note:* _The interface of the IRQ service is going to be changed_
|
|
;_with the planed addition of signals to the framework._
|
|
|
|
|
|
RM - managing address space layouts
|
|
===================================
|
|
|
|
RM is a _region manager_ service that allows for constructing address space
|
|
layouts (_region map_) from dataspaces and that provides support for assigning
|
|
region maps to processes by paging the process' threads.
|
|
Each RM session corresponds to one region map. After creating a new RM session,
|
|
dataspaces can be attached to the region map via:
|
|
!void *attach(Dataspace_capability ds_cap,
|
|
! size_t size=0, off_t offset=0,
|
|
! bool use_local_addr = false,
|
|
! addr_t local_addr = 0);
|
|
The 'attach' function inserts the specified dataspace into the region map and
|
|
returns the actually used start position within the region map.
|
|
By using the default arguments, the region manager chooses an appropriate
|
|
position that is large enough to hold the whole dataspace.
|
|
Alternatively, the caller of 'attach' can attach any sub-range of the dataspace
|
|
at a specified target position to the region map by enabling 'use_local_addr'
|
|
and specifying an argument for 'local_addr'. Note that the interface allows for the
|
|
same dataspace to be attached not only to multiple region maps but also multiple
|
|
times to the same region map.
|
|
As the counterpart to 'attach', 'detach' removes dataspaces from the region map:
|
|
!void detach(void *local_addr);
|
|
The region manager determines the dataspace at the specified 'local_addr' (not
|
|
necessarily the start address) and removes the whole dataspace from the region
|
|
map.
|
|
To enable the use of a RM session by a process, we must associate it with
|
|
each thread running in the process. The function
|
|
!Thread_capability add_client(Thread_capability thread);
|
|
returns a thread capability for a _pager_ that handles the page faults of the
|
|
specified 'thread' according to the region map.
|
|
With subsequent page faults caused by the thread, the address-space layout
|
|
described by the region map becomes valid for the process that is executing the
|
|
thread.
|
|
|
|
|
|
CPU - allocator for processing time
|
|
===================================
|
|
|
|
A CPU session is an allocator for processing time that allows for the creation,
|
|
the control, and the destruction of threads of execution.
|
|
There are no session arguments used.
|
|
The functionality of starting and killing threads is provided by two functions:
|
|
!Thread_capability create_thread(const char* name);
|
|
!void kill_thread(Thread_capability thread_cap);
|
|
The 'create_thread' function takes a symbolic thread name (that is only used
|
|
for debugging purposes) and returns a capability to the new thread.
|
|
Furthermore, the CPU session provides the following functions for operating
|
|
on threads:
|
|
!int set_pager(Thread_capability thread_cap,
|
|
! Thread_capability pager_cap);
|
|
|
|
!int cancel_blocking(Thread_capability thread_cap);
|
|
|
|
!int start(Thread_capability thread_cap,
|
|
! addr_t ip, addr_t sp);
|
|
|
|
!int state(Thread_capability thread,
|
|
! Thread_state *out_state);
|
|
The 'set_pager' function registers the thread's pager whereas 'pager_cap'
|
|
(obtained by calling 'add_client' at a RM session) refers to the RM session to
|
|
be used as the address-space layout.
|
|
For starting the actual execution of the thread, its initial instruction
|
|
pointer ('ip') and stack pointer ('sp') must be specified for the 'start'
|
|
operation.
|
|
In turn, the 'state' function provides the current thread state including
|
|
the current instruction pointer and stack pointer.
|
|
The 'cancel_blocking' function causes the specified thread to cancel a
|
|
currently executed blocking operation such as waiting for an incoming message
|
|
or acquiring a lock. This function is used by the framework for gracefully
|
|
destructing threads.
|
|
|
|
*Note:* _Future versions of the CPU service will provide means to further control the_
|
|
_thread during execution (e.g., pause, execution of only one instruction),_
|
|
_acquiring more comprehensive thread state (current registers), and configuring_
|
|
_scheduling parameters._
|
|
|
|
|
|
PD - providing protection domains
|
|
=================================
|
|
|
|
A PD session corresponds to a memory protection domain. Together
|
|
with one or more threads and an address-space layout (RM session), it forms a
|
|
process.
|
|
There are no session arguments. After session creation, the PD contains no
|
|
threads. Once a new thread has been created from a CPU session, it can be assigned
|
|
to the PD by calling:
|
|
! int bind_thread(Thread_capability thread);
|
|
|
|
|
|
CAP - allocator for capabilities
|
|
================================
|
|
|
|
A capability is a system-wide unique object identity that typically refers to a
|
|
remote object implemented by a service. For each object to be made remotely
|
|
accessible, the service creates a new capability associated with the local
|
|
object. CAP is a service to allocate and free capabilities:
|
|
! Capability alloc(Capability ep_cap);
|
|
! void free(Capability cap);
|
|
The 'alloc' function takes an entrypoint capability as argument, which is the
|
|
communication receiver for invocations of the new capability's RPC interface.
|
|
|
|
|
|
LOG - debug output facility
|
|
===========================
|
|
|
|
The LOG service is used by the lowest-level system components such as the init
|
|
process for printing debug output.
|
|
Each LOG session takes a 'label' string as session argument,
|
|
which is used to prefix the debug output of this session.
|
|
This enables developers to distinguish multiple producers of debug output.
|
|
The function
|
|
! size_t write(const char *string);
|
|
outputs the specified 'string' to the debug-output backend of core.
|
|
|
|
|
|
Process creation
|
|
################
|
|
|
|
The previous section presented the services implemented by core.
|
|
In this section, we show how to combine these basic mechanisms to create and
|
|
execute a process.
|
|
Process creation serves as a prime example for our general approach to first
|
|
provide very simple functional primitives and then solve complex problems using
|
|
a composition of these primitives.
|
|
We use slightly simplified pseudo code to illustrate this procedure.
|
|
The 'env()' object refers to the environment of the creating process, which
|
|
contains its RM session and RAM session.
|
|
|
|
:Obtaining the executable ELF binary:
|
|
|
|
If the binary is available as ROM object, we can access its data by creating
|
|
a ROM session with the binary's name as argument and attaching its dataspace
|
|
to our local address space:
|
|
!Rom_session_capability file_cap;
|
|
!file_cap = session("ROM", "filename=init");
|
|
!Rom_dataspace_capability ds_cap;
|
|
!ds_cap = Rom_session_client(file_cap).dataspace();
|
|
!
|
|
!void *elf_addr = env()->rm_session()->attach(ds_cap);
|
|
|
|
The variable 'elf_addr' now points to the start of the binary data.
|
|
|
|
:ELF binary decoding and creation of the new region map:
|
|
|
|
We create a new region map using the RM service:
|
|
!Rm_session_capability rm_cap;
|
|
!rm_cap = session("RM");
|
|
!Rm_session_client rsc(rm_cap);
|
|
Initially, this region map is empty.
|
|
The ELF binary contains CODE, DATA, and BSS sections.
|
|
For each section, we add a dataspace to the region map.
|
|
For read-only CODE and DATA sections, we attach the corresponding ranges of
|
|
the original ELF dataspace ('ds_cap'):
|
|
!rsc.attach(ds_cap, size, offset, true, addr);
|
|
The 'size' and 'offset' arguments specify the location of the section within
|
|
the ELF image. The 'addr' argument defines the desired start position at the
|
|
region map.
|
|
For each BSS and DATA section, we allocate a read-and-writeable RAM dataspace
|
|
!Ram_dataspace_capability rw_cap;
|
|
!rw_cap = env()->ram_session()->alloc(section_size);
|
|
and assign its initial content (zero for BSS sections, copy of ELF DATA sections).
|
|
!void *sec_addr = env()->rm_session()->attach(rw_cap);
|
|
! ... /* write to buffer at sec_addr */
|
|
!env()->rm_session()->detach(sec_addr);
|
|
After iterating through all ELF sections, the region map of the new process
|
|
is completely initialized.
|
|
|
|
:Creating the first thread:
|
|
|
|
For creating the main thread of the new process, we create a
|
|
new CPU session from which we allocate the thread:
|
|
!CPU_session_capability cpu_cap = session("CPU");
|
|
!Cpu_session_client csc(cpu_cap);
|
|
!Thread_capability thread_cap = csc.create_thread();
|
|
When the thread starts its execution and fetches its first instruction, it
|
|
will immediately trigger a page fault. Therefore, we need to assign a
|
|
page-fault handler (pager) to the thread. With resolving subsequent page faults, the
|
|
pager will populate the address space in which the thread is executed with
|
|
memory mappings according to a region map:
|
|
!Thread_capability pager_cap = rsc.add_client(thread_cap);
|
|
!csc.set_pager(thread_cap, pager_cap);
|
|
|
|
:Creating a protection domain:
|
|
|
|
The new process' protection domain corresponds to a PD session:
|
|
!Pd_session_capability pd_cap = session("PD");
|
|
!Pd_session_client pdsc(pd_cap);
|
|
|
|
:Assigning the first thread to the protection domain:
|
|
|
|
!pdsc.bind_thread(thread_cap);
|
|
|
|
:Starting the execution:
|
|
|
|
Now that we defined the relationship of the process' region map, its main
|
|
thread, and its address space, we can start the process by specifying the
|
|
initial instruction pointer and stack pointer as obtained from the ELF
|
|
binary.
|
|
!csc.start(thread_cap, ip, sp);
|
|
|
|
; supplying the parent capability to the new process
|
|
|
|
|
|
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
|
|
================
|
|
|
|
[image 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 [server_framework].
|
|
|
|
:'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.
|
|
|
|
| For example, on the L4v2 platform, the parent writes the information about
|
|
| the parent capability to a defined position of the new process' address space
|
|
| after decoding the ELF image. On the Linux platform, the parent
|
|
| uses environment variables to communicate the parent capability to the
|
|
| child.
|
|
|
|
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._
|
|
|
|
;The 'Child' serves the parent interface for the new process by a distinct thread.
|
|
|
|
|
|
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.
|
|
|
|
|
|
Limitations and Outlook
|
|
#######################
|
|
|
|
In its current incarnation, the design is subject to a number of limitations.
|
|
As a prime example for managing resources, we focused our work on physical
|
|
memory and ignored other prominent resource types such as processing time, bus
|
|
bandwidth, and network bandwidth.
|
|
We intend to apply the same methodology that we developed for physical memory to
|
|
other resource types analogously in later design revisions.
|
|
We do not cover features such as virtual-memory or transparent
|
|
copy-on-write support, which we regard as non-essential at the current stage.
|
|
At this point, we also do not provide specifics about the device-driver
|
|
infrastructure and legacy-software containers.
|
|
Note that the presented design does not fundamentally prevent the support
|
|
of these features.
|
|
|