CS452 - Real-Time Programming - Spring 2016
Lecture 6 - Context Switch Details
Public Service Annoucements
-
Due date for kernel 1: 27 May, 2016
-
Changed Winter to Spring.
ARM 920T core
Specific documentation from ARM, which covers
- ARM v4T instruction set
- 32 bit word, 32 bit address/data bus
- all instructions one word
- except thumb instructions (T suffix), which you don't use
- in-order instruction issue
- some instructions privileged, which means
-
not executable in user mode.
-
Only one instruction that lets you from the unprivileged mode
(user) into a privileged mode: SWI -- software interrupt
- Core includes
- CPU
- MMU
- L1 cache
- Co-processor
interface
- System control co-processor
Features
-
Sixteen 32-bit registers
- r15, pc, special in the architecture
- r14, lr, special in the architecture
- r13, sp, special in the architecture
- r12, ip, used by gcc as a scratch register
- r11, fp, used by gcc as the frame pointer
- r10, sl
-
r4 to r9, used by gcc to store data over function calls,
if used, must be saved by the called function.
-
r0 to r3, used by gcc as scratch registers and function
arguments.
-
r0, used by gcc for the value returned by a function.
Note that there are partially separate register sets in some modes.
They are essential for writing a kernel.
-
There are seven processor modes, of which four are used in the course.
M[4:0] |
Mode |
Registers accessible |
Used for |
10000 |
User |
r0-r15 cpsr
|
OS servers, application code |
10010 |
IRQ (Interrupt processing) |
r0-r12, r15
r13_irq, r14_irq
cpsr, spsr_irq
|
Responding to an ordinary interrupt |
10011 |
Supervisor |
r0-r12, r15
r13_svc, r14_svc
cpsr, sprs_svc
|
Kernel runs in this mode |
11111 |
System |
r0-r15
cpsr
|
Execute privileged instructions while seeing all user mode
registers. |
-
There are seven exceptions, of which three are used in our
kernels.
Exception
Type
|
Modes
Called from
|
Mode at
Completion
|
Instruction
Address
|
Reset |
hardware |
supervisor |
0x00 |
Software interrupt |
any |
supervisor |
0x08 |
Ordinary interrupt |
any |
IRQ |
0x18 |
-
We are concerned right now with Reset and Software Interrupt.
-
The first instruction executed by the CPU after reset is
the one at location
0x00000000
. Usually it is
ldr pc, [pc, #0x18]
; 0xe590f018 is the binary encoding
which you will normally find in addresses 0x00
to 0x1c
. You see #0x18
where you might expect to see #0x20
because
the evaluation of the offset from pc
occurs
in the third stage of the pipeline so that the pc
has been incremented twice.
-
RedBoot puts entry points of RedBoot into all the
addresses
0x20
to 0x3c.
This makes it possible to jump anywhere in the 32
bit address space.
Context Switch
From the point-of-view of the task
What gcc does
Function call
-
Save r0-r3 into local variables.
-
Put arguments into r0-r3.
-
bl #entry; lr gets pc, pc gets #entry
-
mov ip, sp; allows multiple store
-
push {fp, ip, lr} onto stack; possibly other registers
-
application code gets arguments from the registers
-
kernel does something somehow
-
application code puts the return value into r0.
-
pop {fp, sp, pc} from stack; possibly others. This returns from the
function.
-
put r0 (return value) into local variable
Steps 1, 2, 10 are in user code; step 3 is the calling of the function;
steps 4, 5, 9 are in the function preamble and postamble; steps 6,8 set
up arguments for the kernel and retrieve the return value; step 7 is
done by the kernel.
It is pretty clear that the whole sequence above is a NOP, except
for changing the arguments into the return value, only if step 7
is a similar NOP. Step 7, however, can't be a function call because
we have to change from user to supervisor mode. We need a more
exotic NOP.
Software Interrupt SWI
The software interrupt instruction ( SWI{cond}
<immed_24>
). What happens when it is executed?
-
lr_svc
gets address of the following instruction.
This is where the kernel will return to.
-
SPSR_svc
gets CPSR
. This saves the
mode, condition codes, etc.
-
CPSR[0:4]
gets 0b10011
. Supervisor mode.
-
CPSR[5] gets 0. ARM (not Thumb) state.
-
CPSR[6] gets 1. Privileged interrupts disabled.
-
CPSR[7] gets 1. Normal interrupts disabled.
-
PC gets 0x08
The software interrupt has an inverse instruction which, executed
immediately following it, undoes its effect. Its most common form
is
movs pc, lr
which has the following effect.
-
The
pc
gets the contents of the
lr_<mode>
.
-
The
cpsr
gets the contents of the
spsr_<mode>
.
Software Interrupt, SWI.
- Does what bl does, plus
- saves the cpsr in the kernel's spsr
- This must be done atomically. Why?
-
24 bits unused in the swi instruction; you can read them. How?
-
Executes the instruction in 0x08
-
which is mov pc, pc+0x18.
To make the software interrupt work you must set low memory
correctly. You know what to put in 0x08:
-
You need to put the entry point of the kernel into 0x28.
The CPU ignores the 24-bit immediate value, which can be used by
the programmer as another argument, usually identifying the system
call.
; In calling code
; Store r0-r3 ; Put arguments into r0-r3
; 0x28 holds the kernel entry point
swi n ; n identifies the system call
; retrieve return value from r0 ; r1-r3 have useless junk
; In kernel
kernel entry:
; Change to system mode
; Save user state on user stack
; Return to supervisor mode
ldr r4, [lr, #-4] ; gets the request type
; at this point you can get the arguments
; Where are they?
; Retrieve kernel state from kernel stack
; Do kernel work
The sequence
swi n
.
.
.
kernel entry:
movs pc, lr
is a NOP
. The 's' in movs puts the SPSR into CPSR.
Leaving the kernel.
When the previous request has been handled completely, it's time
to leave the kernel.
-
You are in svc mode, executing kernel instructions
-
Schedule to discover which task runs next.
- i.e. get the value of
active
-
Enter
activate( active )
-
active
is a pointer to a task descriptor
(TD*
). From it, or the from the user stack get
sp_usr
-
set spsr_svc = cpsr_usr
-
You should understand how this takes us back to user mode.
-
Store kernel state on kernel stack
-
During kernel entry you load the kernel registers from
the kernel stack. Taken together loading and storing
should be a NOP.
-
Storing the kernel state before overwriting its registers is
essential because there are two different link registers.
-
get the address of the next instruction of
active
to be run, its pc
-
set lr_svc = pc
-
At this point
movs pc, lr
would start executing
the code of active at the correct instruction, but the register
values in the CPU are the kernel's.
Switch to system mode
-
Load registers from user stack
- Combined with 3 above this should be a NOP
-
Return to supervisor mode
-
Set return value by overwriting r0
- What about registers r1-3?
-
Let it go
movs pc, lr
Somewhere after movs
is the kernel entry.
- There might be something like an assertion before the
entry.
After the kernel entry is the inverse of this sequence.
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 question is `When do we enter
the kernel?'
The answer is: whever user code executes SWI.
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. A ready queue
can be as simple as a linked list of pointers to task
descriptors.
-
The task found is removed from its queue and becomes the
active task. (Until this point active has pointed to the TD
of the previously active task.)
-
When a task is made ready it is put at the end of its ready
queue.Thus, all tasks oat the same priority get equal chances
of running.
Implementation Comments
The main data structure is usually an 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, event-blocked tasks
- 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
Making the Stub that Wraps swi
For each kernel primitive there must be a function available in
user 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.
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.
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
Fill in the fields of the TD.
- task id
- stack pointer
- SPSR
- link register
- parent tid
- return value
- dummy
- different return value for the active task
- state
- install in the ready queues
Must also initialize the stack
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
.
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.
Try reading this.
Return to: