The AcadOS logo under construction!

The ACADOS Kernel Interface

Douglas W. Jones
University of Iowa Department of Computer Science

General

The ACADOS kernel recognizes the following object classes Each of these object classes is discussed below. The kernel does not do many of the things you would expect from, say, a UNIX kernel. It does not contain device drivers and it does not handle page faults. These functions are expected to be performed by processes outside the kernel but essential to the operation of the system.

Many interactions with server processes involve sequences of kernel calls. It is expected that few applications will directly interact with servers using kernel calls; instead, the author of each server is expected to provide library routines that package the protocol which users are expected to use.

All code running under the AcadOS kernel runs under the authority of some thread running in some address space. Unless stated otherwise, pointers passed as parameters to the kernel are pointers to objects or data structures in that address space. Pointers to conventional data objects such as integers are pointers to data within page objects in the address space. Pointers to entire AcadOS objects are always page-aligned, and all AcadOS objects appear to occupy an entire page of the address space.

Many kernel calls and user level interface routines require the passing of array parameters. In all cases, these are described by three parameters

Address
The address of the first array element.

Length
The number of array intended to be manipulated.

Count
The address of a variable into which a count of the actual number of transferred array elements is placed.
Unfortunately, the kernel interface cannot assume language features such as array slices and implicit array bound parameters, nor can it assume static type checking. Thus, the caller must pass both the base and size of each buffer, and the user must receive, for each buffer passed, a specification of the number of elements actually manipulated.

Pages

Page objects provide the conventional memory resources for an ACADOS application. Most operations on page objects are performed by the CPU as it loads and stores data or fetches instructions. Page objects may be freely shared.

Page Allocation

register_to_receive( box return_box,
byte* pages_allocated, integer_size, null
cap* first_page, int pages_desired, null
);
send( box page_manager_box,
byte* pages_desired, integer_size, null
cap* return_box, 1, null
);
await_receipt()
The above sequence of kernel calls will attempt to allocate pages_desired pages, returning pages_allocated which will normally be equal to pages_desired but may be less, and returning a consecutive block of pages into the caller's address space starting at first_page and containing pages_allocated.

To alloate pages, the user's address space must contain a mailbox page_manager_box that references the memory manager, and a mailbox return_box used to receive the reply containing the allocated page(s). This communication through mailboxes allows memory management functions to be virtualized. In fact, the kernel may directly interpret messages sent to page_manager_box as kernel calls, or alternately, the page allocation function of the kernel may be implemented as a predefined thread running in the kernel address space.

The standard ACADOS programming environment provides a reserved location page_manager_box in the address space, and it provides a simplified interface procedure that uses this:

page_allocate(
cap* page_address,
unsigned int pages_desired,
unsigned int* pages_allocated
);
Newly allocated pages are always created with all access rights set.

Page Access Rights

The basic access rights are: Not all combinations of access rights are guaranteed to be enforced on all machines! The possible combinations are as follows:
Mandatory Rights Combinations
All ACADOS systems must fully support these combinations:
read_write_execute
read_execute
no_access
Other Rights Combinations
Some ACADOS systems may support some of these combinations:
execute_only; if unsupported, read_execute will be substituted.
read_write; if unsupported, read_write_execute will be substitued.
read_only; if unsupported, read_execute will be substitued.
Few ACADOS systems will support these:
write_execute; if unsupported, read_write_execute will be substitued.
write_only; if unsupported, read_write will be substitued.
On systems that enforce the read_only, read_write and execute_only rights combinations, their use can substantially improve early fault detection by catching inappropriate execution of data or fetching of instructions as soon as they occur instead of letting execution continue to some later failure. On systems that do not support these rights combinations, their use in source code can provide useful commentary, but this must be done with extreme care; failures may result when code that makes careless use of these combinations is ported to machines that enforce them correctly.

For all combinations of access rights that may not be supported on some or most architectures, the guiding principle in making substitutions is that code that requests these rights should continue to operate on machines where they cannot be enforced. Thus, if a machine cannot enforce some rights restriction, the nearest enforcable rights combination that allows the same operations is used.

Semaphores

Semaphores are used to synchronize the execution of threads, using the general semaphore model formalized by Dijkstra.

Semaphore Allocation

register_to_receive( box return_box,
byte* sem_allocated, integer_size, null
cap* first_sem, int sems_desired, null
);
send( box sem_manager_box,
byte* sems_desired, count, 2*integer_size, null
cap* return_box, 1, null
);
await_receipt()
The above sequence of kernel calls will attempt to allocate sems_desired semaphores, each with the specified initial count, returning sems_allocated which will normally be equal to sems_desired but may be less, and returning a consecutive block of semaphores into the caller's address space starting at first_sem and containing sems_allocated.

To alloate semaphores, the user's address space must contain a mailbox sem_manager_box that references the semaphore manager, and a mailbox return_box used to receive the reply containing the allocated semaphore(s). This communication through mailboxes allows semaphore management functions to be virtualized. In fact, the kernel may directly interpret messages sent to sem_manager_box as kernel calls, or alternately, the semaphore allocation function of the kernel may be implemented as a predefined thread running in the kernel address space.

The standard ACADOS programming environment provides a reserved location box_manager_box in the address space, and it provides a simplified interface procedure that uses this:

sem_allocate(
cap* first_sem,
unsigned int sems_desired, count,
unsigned int* sems_allocated
);
Newly allocated semaphores are always created with all access rights set.

Semaphore Access Rights

The basic access rights are: Most semaphores held in an address space will be reusable. The possiblity of restricting reuse of semaphores may be important in order to prevent untrusted correspondents from sending multiple signals.

Semaphore Operations

signal( sem s
);

wait( sem s
);

inspect( sem s,
int* count
);
When one thread executes a signal on some semaphore, this permits some process to continue after a wait operation on the same semaphore. If a process executes a wait operation before the corresponding signal, is received, the process that executed the wait is blocked. If the wait is executed after the corresponding signal, the process that executed the wait continues executing without pause. If multiple processes execute wait operations, each signal will unblock exactly one waiting process.

If processes have no priority structure, waiting processes will be unblocked in FIFO order. If processes have priority, waiting processes will be unblocked in priority order.

The inspect operation allows a process to read the count from a semaphore. If this is greater than zero, it indicates the number of wait operations that may be performed before a process is blocked. If this is negative, it indicates the number of blocked processes waiting for that semaphore to be signalled. Inspecting a newly created semaphore should return the same count it was created with.

Mailboxes

Mailboxs are used to pass messages between threads. These messages may contain data, bits and bytes copied from the pages of the sender's address space to the pages of the receiver's address space, and they may also contain capabilities copied from the sender's address space to the receiver's address space. Mailboxes provide for nonbuffered interthread communication. As such, from a synchronization viewpoint, the exchange of a message through a mailbox constitutes a rendezvous between the sending and receiving threads.

Rendezvous semantics is desirable in some contexts, but dangerous in others, for example, when an untrustworthy partners in a transaction leaves a critical process hanging awaiting a rendezvous. For example, consider a typical client server interaction, consisting of two message exchanges, corresponding to a call to the server and the return of the result. If the client does not accept the return message, the server is left hanging. For this reason, nonblocking (but still unbuffered) versions of the basic message exchange primitives are also provided.

Mailbox Allocation

register_to_receive( box return_box,
byte* boxes_allocated, integer_size, null
cap* first_box, int boxes_desired, null
);
send( box box_manager_box,
byte* boxes_desired, integer_size, null
cap* return_box, 1, null
);
await_receipt()
The above sequence of kernel calls will attempt to allocate boxes_desired mailboxes, returning boxes_allocated which will normally be equal to boxes_desired but may be less, and returning a consecutive block of mailboxes into the caller's address space starting at first_box and containing boxes_allocated.

To alloate boxes, the user's address space must contain a mailbox box_manager_box that references the mailbox manager, and a mailbox return_box used to receive the reply containing the allocated mailboxes(s). This communication through mailboxes allows mailbox management functions to be virtualized. In fact, the kernel may directly interpret messages sent to box_manager_box as kernel calls, or alternately, the mailbox allocation function of the kernel may be implemented as a predefined thread running in the kernel address space.

The standard ACADOS programming environment provides a reserved location box_manager_box in the address space, and it provides a simplified interface procedure that uses this:

box_allocate(
cap* first_box,
unsigned int* boxes_desired,
unsigned int* boxes_allocated
);
Newly allocated mailboxes are always created with all access rights set.

Mailbox Access Rights

The basic access rights are: Most mailboxes held in an address space will be reusable. The possiblity of restricting reuse of mailboxes is important in order to prevent untrusted servers from sending multiple replies to a single request.

Mailbox Operations

send( box channel,
byte* data, int data_length, int* data_moved,
cap* first_cap, int num_caps, int* caps_moved
);

non_blocking_send( box channel,
byte* data, int data_length, int* data_moved,
cap* first_cap, int num_caps, int* caps_moved
);

receive( box channel,
byte* data, int data_length, int* data_moved,
cap* first_cap, int num_caps, int* caps_moved
);

register_to_receive( box channel,
byte* data, int data_length, int* data_moved,
cap* first_cap, int num_caps, int* caps_moved
);

await_receipt()
When one thread executes a send and another thread executes a receive on the same mailbox, channel, data and capabilities are copied from the sender's to the receiver's address space.

The basic send and receive primitives will block until the data transfer is completed. The non_blocking_send primitive will only transfer data if a recipient is already waiting to receive the data, and will do nothing in the event there is no waiting recipient. The register_receive primitive registers a process's willingness to receive data from some mailbox without blocking that process.

A thread may only register for one nonblocking transfer at a time. If a process wishes to receive or to register for a second transfer, the process will block until the first transfer is complete. The await_receipt primitive blocks the caller until a previously registered transfer is complete. Calling await_receipt when no transfer is pending is a no-op. Note that the normal receive primitive is exactly equivalent to register_receive immediately followed by await_receipt.

The address data is the address of the first byte of the data buffer to use, in the address space of the caller's thread, and data_length gives the length of the data buffer, in terms of the smallest directly addressable memory units, usually bytes. After the transfer of data, both sender and receiver are informed of the number of bytes transferred in data_moved; after a transfer, this will be equal to the minimum data_length given by the sender or receiver.

The address first_cap is the address of the first capability in the list of capabilities to copy, in the address space of the caller's thread, and num_caps gives the number of capabilities to copy. After the transfer of capabilities, both sender and receiver are informed of the number of capabilities transferred in caps_moved; after a normal transfer, this will be equal to the minimum num_caps given by the sender or receiver.

If the caller is dealing with a trusted correspondant and the caller is certain that the caller's data and object lists are fully legal, the caller may pass null pointers to data_moved and caps_moved. In this case, the kernel will not attempt to report the indicated result of the copying operations.

The sender must have write access to channel, the box being used, and the receiver must have read access. If a thread attempts to use a box for an operation for which it does not have appropriate rights, a fault will be raised. If a user's data buffer is not entirely addressable for the fetch and store operations required to copy the buffer, a fault will be raised. If a user's object string extends beyond the end of the user's address space, a fault will be raised. In the event there is a fault, the state of the user's thread will be suspended immediately before the offending kernel call, and no partial copying of data or objects will occur.

The act of copying a capability with the reuse right causes that capability to be duplicated. The act of copying a capability with this right missing causes the capability to be moved and replaced, in the sender's address space, with a capability of type void and name none. This is only possible if the sender's address space is writable; if the sender's address space is not writable, a trap will be raised for the sender and the copy operation will not be undertaken.

Note that both received data and capabilities overwrite whatever was in the locations into which they are received, without regard to what was previously there. Thus, the receipt of capabilities may result in the loss of access to objects that were previously accessible from an address space.

Address Spaces

Address spaces, also known as capability lists, are used to hold arrays of capabilities. The conventional concept of a process in most operating systems modelled in AcadOS as a thread executing in an address space, where that address space contains page objects that hold the data and program of the process. The kernel imposes no restrictions on the type and placement of objects in an address space, but kernel users will generally establish conventions about, for example, the placement of the mailboxes used for standard input and standard output, as well as the server mailboxes for commonly used resources.

Address Space Allocation

register_to_receive( box return_box,
byte* space_size, integer_size, null
cap* space, 1, null
);
send( box space_manager_box,
byte* desired_size, integer_size, null
cap* {return_box, handler} 2, null
);
await_receipt()
The above sequence of kernel calls will attempt to allocate an address space, returning space_size which will normally be greater than or equal to desired_size but may be less if there is a problem. The allocated size may be greater than the desired size because of granularity in the allocation of address spaces. If the size returned is zero, the returned capability will be of type void and name none. If there is no error, capability for the allocated space will be saved in the caller's address space at space.

To alloate a new address space, the user's address space must contain a mailbox space_manager_box that references the mailbox manager, a mailbox return_box used to receive the reply containing the allocated mailboxes(s), and a mailbox handler used to receive messages reporting the occurance of exceptions in the newly created address space. The last two, return_box and handler must be occupy consecutive pages of the caller's address space.

communication through mailboxes allows address space management functions to be virtualized; the kernel may directly interpret messages sent to space_manager_box as kernel calls, or alternately, the mailbox allocation function of the kernel may be implemented as a predefined thread running in the kernel address space.

The standard ACADOS programming environment provides a reserved location space_manager_box in the address space, and it provides a simplified interface procedure that uses this:

space_allocate(
cap* space,
cap* handler,
unsigned int* desired_size,
unsigned int* space_size
);
Newly allocated address spaces are always created with the read and write access rights set.

Address Space Access Rights

The basic access rights are:

Address Space Operations

move_cap( cap* src, src_cap;
cap* dst, dst_cap;
int rights_mask; int* rights
);
The basic operation on an address space or capability list is to move a capability from one place to another, either within or between address spaces. The source and destination address spaces are specified by src and dst, capability offsets within the source and destination address space are given by src_cap and dst_cap. If either of the former are self the address space of the caller is used.

When a capability is moved from one place to another, the set of access rights for that capability may be restricted by rights_mask. This is anded with the rights field of the capability being moved, so rights not present in the mask are deleted. The actual set of rights delivered with the destination capability are returned in rights; if this is empty (zero), the destination capability will be of type void and name none.

To move a capability from an address space, the user must have read access to that address space. To move capabilities into an address space, the user must have write access to that address space. If a capability is moved that does not have the reuse right, it is replaced in the source address space with a capability of type void and name none; this operation requires the write access right to the source address space, if this right is missing, rights will be set to zero and void will be moved.

test( cap* space, object;
int* type, rights, name
);
The text operation allows a thread to probe the capability for for some object in the indicated space (possibly self), in order to learn the type, the access rights and the name (unique identifier) of the object. This may be used to determine what operations are legal on the object.

If two capabilities refer to objects of the same type, the name returned from testing one will be numerically equal to the name returned by testing the other only if the two capabilities reference the exact same object. It is possible for the names of objects of different types to be coincidentally equal.

make_void( cap* space, object;
int name
);
The make_void operation replaces the capability for some object in the indicated space (possibly self) with a capability of type void and the given name. Names of void objects may be assigned arbitrarily by the user. This is the usual way for a program to indicate to the system that it no longer has a need for some capability, but it should be noted that overwriting one capability with any other capability will have the same effect of making the first capability unreachable.
make_executable( cap* space;
int start
);
The make_executable operation sets the starting address for threads spawned from space to start. This operation requires write access to space and it makes space executable. The only part of a thread's initial state specified is the starting address of the code in that space. All other components of the thread state must usually be initialized by the code at that address.

Secondary Address Space Operations

The following operations on address spaces may be implemented either by the kernel or by standard library routines. Implementing these in the kernel may reduce the overhead of long strings of kernel calls, while implementing them in a standard library reduces the complexity of the kernel.
move_caps( cap* src, src_cap;
cap* dst, dst_cap; int num_caps; int* caps_moved
);
The operation move_caps moves a block of num_caps consecutive capabilities from the source to the destination, reporting, in the return argument caps_moved is used to report the number of non-void capabilities delivered to the destination. This is equivalent to a sequence of calls to move_cap.
move_data( cap* src, src_byte;
cap* dst, dst_byte; int num_bytes; int* bytes_moved
);
The operation move_data is analogous to move_caps, but it moves a block of consecutive data bytes between the pages of the indicated address space or spaces. It is equivalent to using using movecaps to get the capabilities for the required pages and then copying the bytes relative to the calling thread's address space.
restrict( cap* space, object;
int mask
);
The restrict operation manipulates the rights field of the capability for some object in the indicated space (possibly self). The rights associated with that capability are anded with the rights_mask. The indicated space must have read-write access. Restrict is equivalent to move_cap with the source and destination addresses the same.

Threads

Threads allow code to be executed in the context of some address space. The conventional concept of a process in most operating systems modelled in AcadOS as a thread executing in an address space, where that address space contains page objects that hold the data and program of the process. More than one thread may execute in an address space; if this is the case, the threads that share an address space have exactly the same access rights to all objects in that space.

Thread Allocation

register_to_receive( box return_box,
null, 0, null
cap* new_thread; 1; int* success
);
send( box thread_manager_box,
null, 0, null
cap* {return_box, space}; 3, null
);
await_receipt()
The above sequence of kernel calls will attempt to allocate a new thread attached to the indicated space. If there is no error, success will be one and a capability for the allocated thread will be saved in the caller's address space at new_thread. The newly created thread comes up running state. This implies that the pages containing the code to be executed must already be present in space.

The two capabilities return_box and space must occupy consecutive pages of the caller's address space. The capability return_box must be a mailbox and space must be an address space.

To allocate a new thread, the user's address space must contain a mailbox thread_manager_box that references the thread manager. Communication through this mailbox allows thread management functions to be virtualized. In fact, the kernel may directly interpret messages sent to thread_manager_box as kernel calls, or alternately, the thread allocation function of the kernel may be implemented as a predefined thread running in the kernel address space.

To create a new thread, the caller's address space must have execute access to space. If the caller does not have this right, or if something else prevents the system from allocating a new thread, success will be zero.

Even if the caller has no other rights to space, The created thread always gains read-write-execute access to its own address space, no matter what access was available to the caller of thread_allocate. In launching a new thread, the caller's capability to the new thread's address space will be deleted if the caller does not have reuse rights. Thus, it is possible to give out the capability to launch exactly one thread in an address space.

The standard ACADOS programming environment provides a reserved location thread_manager_box in the address space, and it provides a simplified interface procedure that uses this:

thread_allocate(
cap* space;
cap* new_thread;
int* success;
);
If the caller had read-write access to space, the new thread capability will be returned with all access rights set. If the caller did not have read or write access to space, the corresponding rights will be omitted on the new thread capability.

Thread Access Rights

The basic access rights for a thread are:

Thread Operations

start( cap* thread
);

stop( cap* thread
);
The start and stop operations are used to control whether a CPU may be assigned to the thread. Threads that were awaiting a signal on a semaphore or a message in a mailbox before a stop operation will resume only after both a start operation and the indicated event.

The start and stop operations require that the user have execute access to the thread.

get_state( cap* thread;
state* s
status* s
);

set_state( cap* thread;
state* s
);
The operations, get_state and set_state allow the the state of a running thread to be inspected or changed; these may be necessary to implement exception handlers, debuggers and trace utilities.

The operation, get_state requires read access to the thread, while set_state requires write access to the thread.

Thread State Manipulation

The thread status record is a read-only attribute of the thread; the status of a thread indicates whether or not it is running and if not, why not.
int status
This gives the status of the thread with regard to the start and stop operations and reports whether there has been an exception: Additional values of this field are hardware defined; these are likely to include:

int address
In the event of an address fault, this field indicates the virtual address that was referenced to cause the fault.

int state
This indicates the state of the thread from the point of view of the kernel's scheduler: The arrival of a signal or message may change the state of a thread from waiting to ready even when that thread is not executable, but the thread will not enter the running state until it is made executable.
The thread state record is a read-write attribute of the thread, reflecting the contents of the CPU registers of the thread. The structure of this record depends on the machine architecture, but a standard set of macros or functions is provided to extract and manipulate certain fields of this state. These are semantically equivalent to record fields, but they are presented with a procedural interface because of the possibility that some architectures may have native formats for presenting these fields that are not compatable with the record field allocation schemes used by programming languages. These functions must include the following:
int get_pc( state* s)
set_pc( state* s; int pc)
The program counter of the thread.

int get_cc( state* s)
set_cc( state* s; int pc)
The condition codes of the thread. In the event that the host machine does not have condition codes, get_cc returns zero and set_cc is a no-op.

int get_sp( state* s)
set_sp( state* s; int pc)
The stack pointer of the thread. In the event that the host machine does not have a stack pointer, get_sp returns zero and set_sp is a no-op.

int get_gpr( state* s; int r)
set_gpr( state* s; int r, pc)
General purpose register r of the thread. The integer parameter r, specifying the register number, must be in the range min_gpr to max_gpr (inclusive). If there are no general purpose registers in the saved state, min_gpr will be one greater than max_gpr.

On some machines, general purpose registers may be used to hold the stack pointer and condition codes. If this is the case, programmers should be aware that, for example, set_sp may change the value of a general purpose register.

On some machines, the register set has a very irregular structure, with some registers devoted to one function and some to another; this irregular structure may be masked by the apparent presentation of all of these registers as if they were general purpose registers.

int get_fpr( state* s; int r)
set_fpr( state* s; int r, pc)
Floating point register r of the thread. The integer parameter r, specifying the register number, must be in the range min_fpr to max_fpr (inclusive). If there are no floating point registers in the saved state, min_gpr will be one greater than max_gpr.

Exceptions

When a thread encounters an exception, for example, if it attempts a fetch or store operation on an object that is not a page, the thread is stopped, exactly as if some other thread had performed a stop operation on it. After stopping the thread, the kernel sends a message to that thread's handler mailbox. An exception handler thread under AcadOS is a thread that is blocked awaiting messages on a mailbox used for the handler of some other thread.

The message to the exception handler contains the following informatio: