Personality Layer Design

Provides some guidelines for the design of a personality layer for real time Applications for the Kernel.

Memory management

The personality layer assumes that the RTA will run in a single flat address space in which there is neither protection between different parts of the application nor protection of any hardware or CPU resource from any part of the application. For example, any part of the application code can access any I/O port, and can disable interrupts.

To get this behaviour under the Kernel Architecture 2, the RTA must run in supervisor mode in the kernel address space. The obvious way to do this is to make the RTA together with the personality layer a kernel extension. This also ensures that it is started automatically early on in the boot process.

In general the RTA will have its own memory management strategy and will not wish to use the standard Symbian platform memory management system. To achieve this, the personality layer will allocate a certain fixed amount of RAM for use by the real time application at boot time. For a telephony stack this will be around 128K - 256K. This can be done either by including it in the kernel extension's .bss section, or by making a one-time allocation on the kernel heap at boot time. Depending on the RTA requirements, the personality layer can manage this area of RAM (if the RTOS being emulated provides memory management primitives) or the RTA can manage it.

Threads and mapping thread priorities

A nanokernel thread will be used for each RTOS thread

A priority mapping scheme will be required to map RTOS priorities, of which there are typically 64 to 256 distinct values, to nanokernel priorities. As long as the RTA does not use more than 35 threads running simultaneously, which is usually the case, it should be possible to produce a mapping scheme that allows each thread to have a distinct priority, if needed. If this limit is exceeded, it will be necessary to fold some priorities together.

Note that any attempt to increase the number of priorities supported by both the nanokernel and the Symbian platform kernel would be prohibitively expensive in terms of RAM usage.

Communication between Symbian platform and the RTOS Environments

To allow the functionality of the RTA to be available to Symbian platform applications, it is necessary that a mechanism exist by which Symbian platform code and the RTA may communicate with each other. In practice this means:

  • it must be possible for a Symbian platform thread to cause an RTOS thread to be scheduled and vice-versa

  • it must be possible for data to be transferred between Symbian platform and RTOS threads in both directions.

It will usually be possible for a Symbian platform thread to make standard personality layer calls (the same calls that RTOS threads would make) in order to cause an RTOS thread to be scheduled. This is because the nanokernel underlies both types of thread and most 'signal' type operations (i.e. those that make threads ready rather than blocking them) can be implemented using operations which make no reference to the calling thread, and which are therefore not sensitive to which type of thread they are called from.

The standard personality layer calls will not work in the other direction, since it will not be possible for a Symbian platform thread to wait on a personality layer wait object. The most straightforward way for RTOS threads to trigger scheduling of a Symbian platform thread would be to enque a DFC on a queue operated by a Symbian platform thread. Another possibility would be for the Symbian platform thread to wait on a fast semaphore which could then be signalled by the RTOS thread. However the DFC method fits better with the way device drivers are generally written. A device driver will be necessary to mediate communication between Symbian platform user mode processes and the RTA since the latter runs kernel side.

All data transfer between the two environments must occur kernel side. It will not be possible for any RTOS thread to access normal user side memory since the functions provided for doing so access parts of the DThread structure representing the Symbian platform calling thread, for example to perform exception trapping. Some possibilities for the data transfer mechanism are:

  • A fairly common architecture for real time applications involves a fixed block size memory manager and message queues for inter-thread communication. The memory manager supports allocation and freeing of memory in constant time. The sending thread allocates a memory block, places data in it and posts it to the receiving thread's message queue. The receiving thread then processes the data and frees the memory block, or possibly passes the block to yet another thread. It would be a simple proposition to produce such a system in which the memory manager could be used by any thread. In that case a Symbian platform thread could pass messages to RTOS threads in the same way as other RTOS threads. Passing data back would involve a special type of message queue implemented in the personality layer. When a message was sent to a Symbian platform thread a DFC would be enqueued. That DFC would then process the message data and free the memory block as usual. This scheme combines the data transfer and scheduling aspects of communication.

  • Any standard buffering arrangement could be used between the RTA and the device driver (e.g. circular buffers). Contention between threads could be prevented using nanokernel fast mutexes, on which any thread can wait, or by the simpler means of disabling preemption or disabling interrupts. It will also be possible for RTOS threads to make use of shared I/O buffers for transfer direct to user mode clients, provided that these buffers are set up by the Symbian platform device driver thread. This may be useful as a way to reduce copying overhead if bulk data transfer is necessary between the two domains.

Synchronisation/communication primitives

The nanokernel does not support most of the synchronisation and communication primitives provided by a standard RTOS. Any such primitives required by the RTA will have to be implemented in the personality layer. This means that the personality layer needs to define new types of object on which threads can wait. This in turn means that new nanokernel thread states (N-state) must be defined to signify that a thread is waiting on an object of the new type. In general, each new type of wait object requires an accompanying new nanokernel thread state.

Blocking a thread on a wait object

To make a thread block on a new type of wait object, call the nanokernel function Kern::NanoBlock(), passing the maximum time for which the thread should block, the new N-state value, and a pointer to the wait object.

Use the TPriListLink base class of NThreadBase to attach the thread to a list of threads waiting on the object. Note that this must be done after calling Kern::NanoBlock(). As preemption is disabled before this function is called, a reschedule will not occur immediately, but will be deferred until preemption is reenabled.

State handler

Every thread that can use a new type of wait object, must have a nanokernel state handler installed to handle operations on that thread when it is waiting on that wait object. A nanokernel state handler is a function that has the following signature:

void StateHandler(NThread* aThread, TInt aOp, TInt aParam);
  • aThread is a pointer to the thread involved.

  • aOp is one of the NThreadBase::NThreadOperation values that indicates which operation is being performed on the thread.

  • aParam is a parameter that depends on aOp.

Note that the state handler is always called with preemption disabled.

The possible values of aOp are:

aOp value

Description

ESuspend

Called if the thread is suspended while not in a critical section and not holding a fast mutex. Called in whichever context NThreadBase::Suspend() is called from.

aParam contains the requested suspension count.

EResume

Called if the thread is resumed while actually suspended, and the last suspension has been removed. Called in whichever context NThreadBase::Resume() is called from.

aParam contains no additional information.

EForceResume

Called if the thread has all suspensions cancelled while actually suspended. Called in whichever context NThreadBase::ForceResume() is called from.

aParam contains no additional information.

ERelease

Called if the thread is released from its wait. This call should make the thread ready if necessary. Called in whichever context NThreadBase::Release() was called from.

aParam is the value passed into NThreadBase::Release() to be used as a return code.

If aParam is non-negative, this indicates normal termination of the wait condition.

If aParam is negative, it indicates early or abnormal termination of the wait; in this case the wait object should be rolled back as if the wait had never occurred. For example, a semaphore's count needs to be incremented if aParam is negative, since in that case the waiting thread never acquired the semaphore.

EChangePriority

Called if the thread's priority is changed. Called in whichever context NThreadBase::SetPriority() is called from. This function should set the iPriority field of the thread, after doing any necessary priority queue manipulations.

aParam contains the new priority.

ELeaveCS

Called in the context of the thread concerned if the thread calls NKern::ThreadLeaveCS() with an unknown iCsFunction that is negative but not equal to ECsExitPending.

aParam contains the value of iCsFunction.

ETimeout

Called if the thread's wait time-out expires and no time-out handler is defined for that thread. Called in the context of the nanokernel timer thread (DfcThread1). This should cancel the wait and arrange for an appropriate error code to be returned. The handler for this condition will usually do the same thing as the handler for ERelease with a parameter of KErrTimedOut.

aParam contains no additional information.

See the code in ...\e32\personality\example\... for practical examples.

Releasing the thread

When a thread's wait condition is resolved, the nanokernel function NThreadBase::Release() should be called. This takes a single TInt parameter.

The parameter is usually KErrNone, if the wait condition is resolved normally. A typical example is where the semaphore on which the thread is waiting, is signalled. A negative parameter value is used for an abnormal termination, and in this case, the wait object may need to be rolled back.

NThreadBase::Release() should be called with preemption disabled. It performs the following actions:

  • sets the NThreadBase::iWaitObj field to NULL

  • cancels the wait timer if it is still running

  • stores the supplied return code in NThreadBase::iReturnCode

  • calls the state handler passing ERelease and the return code. If the return code is negative this should remove the thread from any wait queues and roll back the state of the wait object. In any case it should call NThreadBase::CheckSuspendThenReady() to make the thread ready again if necessary.

Thread scheduling following a hardware interrupt

Most RTOS allow interrupt service routines (ISRs) to perform operations such as semaphore signal, queue post, set event flag directly, usually using the same API as would be used in a thread context. The Kernel Architecture 2 nanokernel does not allow this; ISRs can only queue an IDFC or DFC.

The way to get round this limitation is to incorporate an IDFC into each personality layer wait object. The personality layer API involved then needs to check whether it is being invoked from an ISR or a thread and, in the first case, it queues the IDFC. It may need to save some other information for use by the IDFC, for example, it may need to maintain a list of messages queued from ISRs, a count of semaphore signals from ISRs or a bit mask of event flags set by ISRs. Checking for invocation from an ISR can be done either by using NKern::CurrentContext(), or by checking the CPSR directly; if doing the latter note that any mode other than USR or SVC on the ARM counts as interrupt context.

Hardware interrupts serviced by the RTA will need to conform to the same pattern as those serviced by Symbian platform extensions or device drivers. This means that the standard preamble must run before the actual service routine and the nanokernel interrupt postamble must run after the service routine to enable reschedules to occur if necessary. This can be done by calling Interrupt::Bind() as provided by the base port during RTA initialisation (possibly via a personality layer call if it must be called from C code). See also Interrupt Dispatcher Tutorial.