CS452 - Real-Time Programming - Spring 2015
Lecture 7 - Create
Public Service Annoucements
-
Due date for kernel 1: 25 May, 2015
-
Please remember that some aspects of the system configuration
you get depends on the state in which it was left by the previous
group. I have seen very hard to find bugs occur at the last
minute because a group unknowingly relied on state provided by
the group before it.
-
What
swi
and movs pc, lr
do.
Static/Global Variables.
Global variables
TD tdarray[64];
void main( ) {
....
}
int Create( int priority, ( *code( ) ) ) {
....
}
tdarray
is equally accessible to main and Create. With
suitable use of extern
it can be made available to
code in other files as well. Using your linker script you can have
it put anywhere in memory you like.
This kind of global variable is universally recognized as bad
programming practice.
Static variables You are used to lines of code like
printf( "Hello, world.\n" );
How does printf
get its argument? Near the end --
usually -- of the function containing printf
are 14
bytes containing the ASCII representation of the string. A pointer
to it is put into the code by the link editor.
Variables stored like this are called static
in C.
They preserve their values across function calls. There are three
reasons why you might prefer not to make them writable.
-
You must make the part of memory containing the instructions
writable, which leaves you open to a whole lot of widely known
exploits.
-
If you reuse code in different tasks you may find static variables
changing value unexpectedly and unpredictably.
-
If you do something like this
static TD* tdarray;
then read and write the array, strange and amazing things
might happen. (Use lint or equivalent.)
Scheduling
Implementation
Array of ready queues, one for each priority.
Each ready queue is a list with a head pointer (for extraction)and a tail
pointer (for insertion).
Implementing lists without memory allocation
You are probably used to implementing lists (and similar data
structures) like this
struct element { struct element *next;
struct whatever *content;
}
struct list { struct element *head;
struct element *tail;
}
void insert( list *l, content *c ) {
struct element *e;
e = malloc( element );
e->content = c;
e->next = null;
l->tail->next = e;
l->tail = e;
}
We don't like this because it requires allocating and freeing memory.
Here's the most common way to do this without allocating memory
typedef struct task-descr { ...
struct task-desc *rdy-next;
...
} TD;
All the allocation is done when the task descriptors are declared.
Of course, because allocating and freeing constant sized pieces
of data can be done in constant time, you could allocate a pool
of list elements when you initialize, and manage it using a free
list.
Implementation decisions
- How many priorities
- Which task should have which priority
- What to do when there is no ready task
Making the Stub that Wraps swi
For each kernel primitive there must be a function available in usr code:
the kernel's API.
- e.g.
int Create( int priority, void ( *code ) ( ) );
What gcc does for you
Described in lecture 6.
What the code you write does
- Moves the arguments from gcc's locations to whatever convention you
choose for passing arguments to your kernel
- Does swi n, where n is the code for Create.
- Moves the return value from your kernel's conventional location to
r0.
Creating a Task
In creating a task you have to do two things
- Get and initialize resources needed by the task
- Make the task look as if it had just entered the kernel
- it's ready to execute when it's scheduled
Things you need to do
Get an unused TD and memory for its stack
- memory could be associated with TD during initialization
- actually a form of constant time memory allocation
- unless you implement Destroy
Mostly filling in fields in the TD.
-
task id
-
task id must be unique; Destroy introduces complications.
-
must be easy to find TD from task id.
-
stack pointer
-
See below. Before you caninitialize the stack pointer you must have
decided how your kernel will allocate stack space amount tasks.
-
SPSR
-
link register
-
parent tid
-
the task that made the request, which is usualy the active
task
-
this assumes that the active task pointer remains valid
until scheduling is performed.
-
return value
-
different return values for the active task and for the
next to be scheduled task.
-
the one for the active task -- the one receiving service
-- goes in its TD or on its stack.
-
the one for the next active task should already be in its
TD or on its stack.
-
state, which is READY
-
install in its ready queue
Must also initialize the stack
-
stuff you left out of the TD must be part of stack initialization.
-
exactly as if the task had just done a kernel entry
-
look carefully at what your kernel exit code will do
-
At the end stack pointer must correspond to stack contents
-
I initialize the stack pointer to the top of allocated memory
then change it as I push stuff onto the stack
- imitating the context switch code
The following implementation decisions are up to you.
-
What should be saved on the stack; what should be saved in
the task descriptor.
-
How memory is laid out.
-
The maximum number of tasks in your system.
-
The argument and return value passing conventions for system
calls.
The Create Function
You also need a int Create( int priority, void (*code) ( ) )
function to call from user tasks.
Although it's no more than a wrapper there are a few problems to
solve.
-
Passing arguments
-
On entry to the kernel the arguments are somewhere, usually r0
& r1
-
You have to put them where the kernel can find them.
-
gcc's function extry code immediately puts them on the stack.
-
In assembly you can find them using the frame pointer.
-
Jumping into the kernel
-
Getting the return value from the kernel and returning it.
-
You find it where the kernel put it
-
gcc's function exit code expects it to be indexed off the frame
pointer
-
from where it does into r0
Initializing the Kernel
Set up the Hardware
- busy-wait io
- low memory
- Where is the kernel entry?
-
Turn off interrupts in the ICU
-
This should be unnecessary, but what if the previous kernel turned
them on?
-
Later you will initialize the ICU differently.
Prepare the Kernel Data Structures
Where is the kernel's stack pointer, right now? What does the stack look
like?
- Do you want it there? Would you rather have it somewhere else?
- This is your last chance to change it. (If you decide to change it you
might want to keep what you are replacing around. Why?)
The kernel data structures. At the very least you need
- an array of empty ready queues
- a pointer to the TD of the active task
- an array of TDs
- a free list of pointers to free TDs
Prepare the Memory to be Used by Tasks
- task memory
Create the First User Task
Can run with hardware interrupts turned off for now. But when
hardware interrupts are turned on in kernel 3 interrupts in user
tasks must be turned on, though they stay off in the kernel.
Reminder. The place where the kernel starts executing has the
global name main, which cannot be re-used.
Other Primitives
These primitives exist mostly so that we, which includes you, can ensure
that task creation and scheduling are working when there is not much else
implemented.
Tid MyTid( )
Self-explanatory
- Doesn't block, but does reschedule.
A question, to which there is a correct answer, or more specifically, a
correct (answer, reason) pair.
- Should the Tid be stored in user space?
Tid MyParentTid( )
Self-explanatory
- Doesn't block, but does reschedule.
Where is the parent Tid, and how does the kernel find it?
void Pass( )
Doesn't block: task calling Pass( )
makes a state
transition from ACTIVE
to READY
.
Does reschedule.
When is Pass( )
a NOP
?
void Exit( )
Calling task is removed from all queues, but its resources are
not reclaimed or reused.
That is, the task goes into a zombie state, and will never become
active or ready, but continues to own all its resources.
Return to: