CS452 - Real-Time Programming - Spring 2013
Lecture 7 - Tasks & Kernels
Pubilc Service Annoucements
- Due date for kernel 1: 27 May.
- Remember. The task going into the kernel may not be the next task to
run.
After the Software Interrupt
In the kernel
The order matters
kernel entry:
- State on entry
- supervisor mode
- interrupts off
- spsr_svc = cpsr_usr
- arguments in somewhere you chose
- caller context in registers r4-r12
- caller local variables indexed off fp
- kernel stack pointer (sp_svc) in r13
- address of instruction following swi in r14, i.e., lr_svc = return
address = pc_usr
- Thus,
swi n is accessed as [lr,
#-4]
- kernel entry in r15
- Change to system state
- Save the user state
- on its stack
- This might include scratch registers (if the contain something
useful like arguments), which you may or may not need later.
- Then you can put
sp_usr in a scratch register, say
r2
- Return to supervisor mode
- Get the request into a scratch register
ldr r3, [lr, #-4]
- Retrieve the kernel state, which should not include the scratch
registers. Why?
- You now have the kernel frame pointer
- You can use it to put stuff in kernel memory
- Change the active task's TD appropriately
active is indexed off the kernel's frame pointer
active is a pointer to the TD of the requester
- Somewhere above you may have picked up the arguments. If you didn't, do
it now.
- must be done after 5. Why?
- must be done before 9. Why?
- Return from
getNextRequest( active ) and get to work
- Don't forget to store the return value when you're finished
handling the request and before scheduling.
There is more than one way to do almost everything in this list, and I
have chosen this way of describing what is to be done because it's simplest
to describe, not because it's necessarily best!.
Self-test question. Why is it ridiculous to put saving
state into a function?
Handling the Request
What needs to be done
- Check for errors
- Manipulating TDs, including
- Sometimes, copying bytes from one address space to another.
Saving the return value
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 requester is scheduled.
- One solution is to put it in the TD.
- It's also possible to put it where it will be needed on activation
(such as r0) immediately.
States of a task
Ready
- task is not running (active)
- everything is ready for running the task.
- task is linked into a ready queue (readyQ)
Active
- Task is running with the pc in its code and its state in the
registers.
- If kernel is running the active task is either about to run or just
finished running.
These are the only two states that you will use when your kernel is
complete. To make it possible to test task creation and scheduling we have
added one other state.
Zombie
- Task is removed from all queues, and will never run again.
- Task retains any resources allocated to it.
The change of state from active to ready, or active to zombie, occurs when
the scheduling function returns.
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?'
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.
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 head pointer (for extraction)and a tail
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
The queues of typical running system
- Highest priority:
- tasks waiting on interrupts
- almost always blocked
- do minimal processing, then release tasks blocked on them
- Medium priority
- receive blocked tasks
- almost always blocked
- provide service to application tasks
- Low priority
- send-blocked tasks
- blocked more often than not
- make decisions about what should be done next
- Lowest priority
- one task that runs without blocking
- the idle task
- uses power without doing anything
Before the Software Interrupt
After a while it's time to leave the kernel
- Schedule the next task to run
- i.e. get the value of
active
- Call
GetNextRequest( active )
Inside GetNextRequest
- From TD, or the user stack
- get sp_usr
- set spsr_svc = cpsr_usr
- You should understand how this takes us back to user mode.
- set lr_svc = pc for return to user mode
- Save kernel state on kernel stack
- Combined with 6, above this should be a NOP
- Set return value by overwriting r0 on user stack
- Switch to system mode
- Load registers from user stack
- Combined with 3 above this should be a NOP
- 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 could 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.
After Create has returned
- gcc stores register r0, the return value, in the variable to which the
result of Create is assigned.
- Registers R4-14 have been returned to their previous values
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.
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
This is the only memory allocation that ever occurs when a program is
running. It is benign because
- All pieces of memory allocated or deallocated are the same size.
- Defragmenting memory (garbage collection) is never needed.
- Allocation can be done using a free list.
Mostly filling in fields in the TD.
- task id
- must be unique
- index of task id plus generation.
- stack pointer
- pointers for readQ and other queues you don't need yet.
- SPSR
- two link registers
- parent tid
- return value
- dummy
- different return value for the active task, which goes in its
TD
- state
Only the first two must be on the stack, but many of the others benefit
from being in the TD.
Must also initialize the stack
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
- an array of empty ready queues
- a poimter 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 interrupts turned off for now (belt and braces) but will need
to be turned on later.
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 or ACTIVE.
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 are no tasks left on the ready queues, it goes back to
RedBoot.
This behaviour changes when hardware interrupts are implemented.
Return to: