CS452 - Real-Time Programming - Winter 2017

Lecture 6 - Context Switch Details

Public Service Annoucements

  1. Due date for kernel 1: 27 January, 2017

ARM 920T core

Features

  1. Processor status register
  2. Sixteen 32-bit registers
  3. There are seven processor modes, of which four are used in the course.
  4. There are seven exceptions, of which three are used explicitly 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

    1. We are concerned right now with Reset and Software Interrupt.
    2. 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.

    3. 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

We start by modelling what happens in a context switch as a function call to a kernel function. After all, that's what it looks like to the user code.

    ...
    tid = Create( priority, code );
    ...
  

Function call

  1. Save r0-r3 into local variables.
  2. Put arguments into r0-r3.
  3. bl #entry; lr gets pc, pc gets #entry
  4. mov ip, sp; allows multiple store
  5. push {fp, ip, lr} onto stack; possibly other registers
  6. function code gets arguments from the registers
  7. kernel does something somehow
  8. function code puts the return value into r0.
  9. pop {fp, sp, pc} from stack; and any others. This returns from the function.
  10. 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.

Using a function call to get into the kernel is blatently incorrect.

Software Interrupt SWI

The software interrupt instruction ( SWI{cond} <immed_24> ). What happens when it is executed?

  1. lr_svc gets address of the following instruction. This is where the kernel will return to.
  2. SPSR_svc gets CPSR. This saves the mode, condition codes, etc.
  3. CPSR[0:4] gets 0b10011. Supervisor mode.
  4. CPSR[5] gets 0. ARM (not Thumb) state.
  5. CPSR[6] gets 1. Fast interrupts disabled.
  6. CPSR[7] gets 1. Normal interrupts disabled.
  7. PC gets 0x08
The problem is that immediately after SWI you have Before the kernel can do any work it must get its own state into the CPU. Before it overwrites the user state it must be saved. Why and where?

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.
  1. The pc gets the contents of the lr_<mode>.
  2. The cpsr gets the contents of the spsr_<mode>.
The inverse depends on the link register and the stack pointer in the calling program being undisturbed.

Software Interrupt, SWI.

To make the software interrupt work you must set low memory correctly. You know what to put in 0x08:

The SWI instruction is encoded in eight ( = four + four ) bits. 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.

  1. You are in svc mode, executing kernel instructions
  2. Schedule to discover which task runs next.
  3. Enter activate( active )
  4. active is a pointer to a task descriptor (TD*). From it, or the from the user stack get sp_usr
  5. set spsr_svc = cpsr_usr
  6. Store kernel state on kernel stack
  7. get the address of the next instruction of active to be run, its pc
  8. set lr_svc = pc
  9. 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.
  10. Switch to system mode
  11. Load registers from user stack
  12. Return to supervisor mode
  13. Set return value by overwriting r0
  14. Let it go
    movs   pc, lr

Somewhere after movs is the kernel entry.

After the kernel entry is the inverse of this sequence.

At this point you should be able to insert the following sequence

    // Store r0-r4 in memory
    // Insert arguments in r0-r4
    SWI
    // Store r0 in memory
    // Re-load r0-r3
  
anywhere in user code and execute is with nothing happening!


Scheduling

There are two important issues for scheduling

  1. When do we reschedule?
  2. 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

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

  1. Find the highest priority non-empty ready queue. A ready queue can be as simple as a linked list of pointers to task descriptors.
  2. 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.)
  3. 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

  1. How many priorities
  2. Which task should have which priority
  3. What to do when there is no ready task

The queues of typical running system

  1. Highest priority:
  2. Medium priority
  3. Low priority
  4. Lowest priority

Making the Stub that Wraps swi

For each kernel primitive there must be a function available in user code: the kernel's API.

What gcc does for you

Before calling Create

  1. gcc saves the scratch registers to memory.
  2. gcc puts the arguments into the scratch registers, and possibly on the stack.

While calling Create

  1. bl to the entry point of Create

While executing Create

  1. gcc saves the registers that it thinks could be altered during execution of the function.
  2. your code gets executed
  3. gcc restores the registers it saved, and only those registers.

Exiting from Create

  1. mov pc, lr, or equivalent, is executed, returning the execution to the instruction following bl

After calling Create

  1. gcc stores register r0, the return value, in the variable to which the result of Create is assigned.

What the code you write does

  1. Moves the arguments from gcc's locations to whatever convention you choose for your kernel
  2. Does swi n, where n is the code for Create.
  3. 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

  1. Get and initialize resources needed by the task
  2. Make the task look as if it had just entered the kernel

Things you need to do

Get an unused TD and memory for its stack

Fill in the fields of the TD.

  1. task id
  2. stack pointer
  3. SPSR
  4. link register
  5. parent tid
  6. return value
  7. state
  8. install in the ready queues

Must also initialize the stack


Initializing the Kernel

Set up the Hardware

  1. busy-wait io
  2. low memory
  3. Turn off interrupts in the ICU

Prepare the Kernel Data Structures

Where is the kernel's stack pointer, right now? What does the stack look like?

The kernel data structures

  1. an array of empty ready queues
  2. a poimter to the TD of the active task
  3. an array of TDs
  4. a free list of pointers to free TDs

Prepare the Memory to be Used by Tasks

  1. 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

A question, to which there is a correct answer, or more specifically, a correct (answer, reason) pair.

Tid MyParentTid( )

Self-explanatory

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: