CS452 - Real-Time Programming - Spring 2013
Lecture 8 - Initialization: Create & Kernel
Pubilc Service Annoucements
- Due date for kernel 1: 27 May.
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
- interrupts disabled; by Kernel 3 this will be changed to
enabled
- two link registers
- parent tid
- return value
- dummy
- different return value for the active task, which goes into its TD
or onto its stack
- state
Only the first two must be in the TD, 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 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 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.
Inter-task Communication
Overview
Message passing combines synchronization and communication in one set of
primitives.
int Send( Tid tid, char *message, int mslen, char *reply, int rplen )
- blocks until Reply occurs
int Receive( Tid *tid, char *msg, int msglen )
- blocks until message is available
- Only one waiting sender is processed per Receive
- Why?
- Hint. There might be tasks that are higher priority than the
Receiver.
int Reply( Tid tid, char *rply, int rplylen )
- does not block
- unblocks task that called Send
Send and Reply become READY at the same time.
How are They Used?
The Producer/Consumer Problem
Middle (double) arrow shows the direction that information flows.
+--------------+ +--------------+
| | ---> | |
| Producer | ===> | Consumer |
| | <--- | |
+--------------+ +--------------+
Producer Sends
Upper arrow
- Producer sends and blocks: "I have some XXX for you."
- Consumer receives: "Give me some XXX."
- Consumer accepts XXX.
- Consumer replies: "I got the XXX."
- Producer & consumer are simultaneously READY.
Note. 1 & 2 could be run in the opposite order.
Consumer Sends
Lower arrow
- Consumer sends and blocks: "I am ready for some XXX from you."
- Producer receives: "I have some XXX."
- Producer replies: "Here is the XXX."
- Consumer accepts XXX.
- Producer and consumer are simultaneously READY.
Note. 1 & 2 could be run in the opposite order.
Multiple Producers
Producers send; consumer receives in the order that producers send.
Notes.
- Critical races can occur, which the application programmer must
resolve.
- There are two types of critical race.
- Critical races internal to the application. For example, order of
production changes in one producer because you add or remove a printf
statement. These ones you can program out of existence by changing
priorities, communication patterns, etc.
- Ones external to the application. For example, one producer is
forwarding bytes from the keyboard, the other from the train
controller, and the order of production changes because the user
types a little faster. These ones you cannot program out of
existence, but must program so that the right thing happens
regardless of the order of production.
Multiple Consumers
Consumers send; producer receives in the order that consumers send.
Note. Critical races can occur, which the application programmer must
resolve.
Multiple Consumers AND Multiple Producers
Consumers send and producers send: who receives?
- A third task: you call it a FIFO or buffer; I call it a warehouse
+---------------+ +------------+ +---------------+
| | ---> | | <--- | |
| Producers | ===> | Buffer | ===> | Consumers |
| | | | | |
+---------------+ +------------+ +---------------+
Buffer receives two types of request
- Producer: Here is some XXX
Send( warehouse, accept some XXX, ... )
- Warehouse receives
Receive( *producer, XXX )
- Warehouse stores XXX, replies
insert( XXX, shelf );
Reply( producer, ack 1 XXX, )
- If warehouse is full of XXX two strategies are possible
- Warehouse queues producer, who remains Reply_Blocked until
there is space created in the warehouse
- Warehouse replies with refusal
Reply( producer, can't take XXX, )
The second strategy is considered to be not so good. Why?
- Consumer: I want some YYY
Send( warehouse, want some YYY, ... )
- Warehouse receives, gets YYY
Receive( &consumer, YYY )
- Warehouse replies, providing YYY
extract( YYY, shelf);
Reply( consumer, YYY )
- If warehouse is empty of YYY, two strategies are possible
- Warehouse queues sender, who remains Reply_Blocked
- Warehouse replies with refusal
Reply( consumer, all out of YYY, )
Notes.
- Only a receiver can accept two types of requests at once. The buffer,
which only receives is the simplest example of a server.
- Messages in Send/Receive/Reply must be the same 'type'.
Sequence of States
Sender
- Active -> Receive_Blocked
- Receive_Blocked -> Reply_Blocked
- May happen right away
- When Receive is called with the Receiver's SendQ empty
- Otherwise, when Receive is called
- Reply_Blocked -> Ready
Receiver
- Active -> Send_Blocked
- Send_Blocked -> Ready
- May happen right away
- if the sendQ is not empty
- Ready -> Active
- ...
- Active -> Ready
There are two cases
Send before Receive
| Sender
Action
|
Sender
State
|
Receiver
Action
|
Receiver
State
|
Comments |
|
active |
|
|
|
| Send |
RCV_BL |
|
|
sender added to receiver's sendQ |
|
|
|
active |
|
|
RPL_BL |
Receive |
ready |
request copied
sender deleted from receiver's sendQ
|
|
|
|
active |
service performed |
|
ready |
Reply |
ready |
reply copied |
Receive before Send
| Sender
Action
|
Sender
State
|
Receiver
Action
|
Receiver
State
|
Comments |
|
|
|
active |
|
|
|
Receive |
SND_BL |
receiver's sendQ empty |
|
active |
|
|
|
| Send |
RPL_BL |
|
ready |
request copied |
|
|
|
active |
service perfomed |
|
ready |
Reply |
ready |
reply copied |
Practical Details
- Need to keep around request
- For Send_Blocked receivers in the SendQ
- The same as Receive_Blocked senders
- For Reply_Blocked senders.
- Messages
- Task states
- You can add extra return values beyond those specified
Example of a Difficult Bug // Must figure this out!
- You notice that a Receiver is never on a readyQ when it is
Send_Blocked
- When a task becomes Send-Blocked on the Receiver,
- If the Receiver's sendQ is empty
- Set the Receiver's sendQ head pointer to the task's TD
- Set the Receiver's next pointer to the task's TD.
- If the Receiver's sendQ is not empty
- and the next pointer in the TD to NIL
- probably to fit two TDs into a single cache line
- You test and test and test and nothing ever goes wrong
- One week before the demo, your kernel crashes under your
application
What two things might have gone wrong?
You might have caught one while testing, not likely the other.
Return to: