CS452 - Real-Time Programming - Spring 2011
Lecture 7 - Context Switch, Create
Pubilic Service Announcements
- Groups have been created
getent group | grep 'cs452_[0-9]' | sort
Context Switch on the ARM
Software Interrupt
swi n ; n identifies which system call you are calling
; In kernel
kernel entry:
; Save user state in user space
; Restore kernel state from kernel space
; Return request to do kernel work
Kernel Work for Create
Allocate Resources
Task ID
- 32-bit word to match architecture
- Possibly broken into TD index and generation.
Task Descriptor
- During initialization create an array of task descriptors
- How many should there be?
- Without task destruction
With task destruction
Notice that because all task descriptors are the same same size allocation
and re-allocation are constant time.
Memory
- During initialization break the spare memory into equal sized chunks
- How many should there be?
- Without task destruction
With task destruction
You could create a more intimate relationship between TD and memory.
Initialize Memory
The principle is, `Make the memory (the stack) exactly ready to be
processed by your kernel exit code.'
In practice,
- After running the kernel entry code the stack is exactly ready for the
kernel exit code to be run.
- Make the stack look as though the kernel entry code has just been
run.
- After runing the kernel exit code you must have
- the PC pointing at the first instruction of the created task
- the SP pointing to the top of an empty stack
- Anything else?
Fill in the Task Descriptor
For some state there is a choice between stack and task descriptor. The
minimum is
- run state: READY to start with
- priority
- parent Tid
- SP
- Pointers for queues.
There could be more
Enter the Task into its Ready Queue
To do this we need to know how scheduling works.
Scheduling
There are two important issues for scheduling
- When do we reschedule?
- Who do we activate when we schedule
When to schedule
Every time we are in the kernel, so the issue is `When do we enter the
kernel?'
Three possibilities
- Tasks run to completion, which means until they make a request for
kernel services
- Event-driven pre-emption, which means when hardware makes a request for
service
- Time-slicing
We do 1 & 2, but not 3, because our tasks co-operate. Time-slicing is
needed when tasks are adversarial.
Who to Schedule
Whoever is needed to meet all the deadlines
- or to optimize something.
Because this is not an easy problem, we don't want to solve it within the
kernel. What the kernel does should be fast (=constant time) and not resource
constrained.
Inexpensive (=constant time)ways to schedule
Least expensive first
- active task decides = co-routines
- round robin
- everybody gets the same chance
- but usually long running time = unimportant
- priorities
- fixed at compile time
- fixed when task is created
- re-fixed every time task is scheduled
- Do you have a good algorithm?
The number of priorities should be small, but not too small.
Tasks at the same priority should have the same precedence.
Scheduling algorithm
- Find the highest priority non-empty ready queue.
- Schedule the first task in the queue.
The state of the most recently scheduled (running) task is ACTIVE, not
READY.
The kernel maintains a pointer to the TD of the active task so it
knows which task is making the current request.
- When a task is made ready it is put at the end of its ready queue.
Implementation
Array of ready queues, one for each priority.
Each ready queue is a list with a fornt pointer (for extraction)and a back
pointer (for insertion).
Hint. The Art of Computer Programming (Donald Knuth) says that circular
queues are better. Why?
Implementation decisions
- How many priorities
- Which task should have which priority
- What to do when there is no ready task
Set up the Return Value
Because the task that made the request may not be the next one to run, the
kernel needs to save the request's return value until the next time the task
is scheduled.
Before the Software Interrupt
When kernel work is complete it's time to leave the kernel, which
requires activating the active task
- Schedule the next task to run
- i.e. get the value of
active
- Call
GetNextRequest( active )
Inside GetNextRequest
- Save kernel state on kernel stack
- From TD
- set spsr_svc = cpsr_usr
- set lr_svc = pc for execuation in user mode
- get sp_usr
- Set return value by setting r0
- Switch to system mode
- Load registers from user stack
- Return to supervisor mode
- Let it go
movs pc, lr
The instruction after this one is normally the kernel entry.
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
Before calling Create
- gcc saves the scratch registers to memory.
- gcc puts the arguments into the scratch registers, and possibly on the
stack.
While calling Create
bl to the entry point of Create
While executing Create
- gcc saves the registers that it thinks will be altered
during execution of the function.
- gcc thinks wrong, because only the assembler knows that swi is in
the instruction stream
- your code gets executed
- gcc restores the registers it saved, and only those registers.
Exiting from Create
- mov pc, lr, or equivalent, is executed, returning the execution to the
instruction following bl
After calling Create
- gcc stores register r0, the return value, in the variable, to which the
result of Create is assigned.
What the code you write does
- Moves the arguments from gcc's locations to whatever convention you
choose for your kernel
- Does swi n, where n is the code for Create.
- Moves the return value from your kernel's conventional location to
r0.
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 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
What follows just seems to say the same thing again. But we might as well
leave it here because some student might find it useful.
How do we implement the API for user code?
- This requires a bit of assembly language.
- In assembly language all arguments & return values are words.
- usually pointers
- stored in registers
- What happens when there are too many arguments?
- What happens when
Create is called?
- Two arguments
- stored in
r0 and r1
- which is which?
- Immediately placed on the stack, referred to by
fp
- Create puts them in the special place you choose
swi switches context into the kernel.
What happens when Create returns?
- When the kernel is finished manipulating
TDs and
PQs
- it knows the tid to be returned
- it places the tid in the caller's return value in its
TD
When the caller is next activated,
- as part of activation the return value is put in your special place,
usually
r0
user code has to get it and put it in the compiler's special place, r0 for
gcc.
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( ) remains ready to
execute.
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, in which it cannot be active
or ready, but continues to own all its resources.
How Should Execution Terminate?
Nicely.
When there ar no tasks left on the ready queues, it goes back to
RedBoot.
Return to: