CS452 - Real-Time Programming - Spring 2011
Lecture 8 - Wrappers; Send-Receive-Reply
Pubilic Service Announcement
- Five minute network outage at 08.00 on Friday.
- Reference solution for a0.
Kernel Work for Create
1. Allocate Resources
Task ID
Task Descriptor
Memory
Comment on Memory Allocation
At the beginning of the course we said, `No memory allocation, ' and now
we are doing it, right in the middle of a kernel primitive, which is supposed
to be real-time. What's going on?
Really there are two qualitatively different cases of resource
allocation:
- the malloc/new type of resource allocation:
- allocation of chunks of random size,
- allocation without recycling can be done in constant time, but
what's the point?
- allocation with recycling cannot be done in constant time because
it requires code to coalesce memory;
- allocation of big same-size chunks of memory,
- or small multiples of constant size allocation,
- allocation can be done in constant time using a free-list,
- obvious trade-off between chunk size and free-list size.
2. Initialize Memory
3. Fill in the Task Descriptor
4. Enter the Task into its Ready Queue
5. Set up the Task's Return Value
Remember that the task that provided the request may not be the next task
to be activated.
Scheduling
When to schedule
Every time we are in the kernel, so the issue is `When do we enter the
kernel?'
Enter the kernel
- when a task makes a system call
- when hardware indicates that it needs service
Who to Schedule
- Of the tasks in the ready state, the task with the highest
priority.
- If there is more than one ready task at the highest priority, the one
at the head of the ready queue.
- Tasks are inserted at the end of the appropriate queue.
Two possible implementations
Constant time, without memory allocation
1. Conceptually simple
- Array of ready queues, each element of the array containing a head
pointer and a tail pointer.
- A highest priority index.
- Insertion is constant time
- Index into the array using the task's priority.
- Use the tail pointer to insert, updating the tail pointer.
- If priority of inserted task is greater than the highest priority
index, update.
- Extraction is (almost) constant time.
- Index into the array using the highest priority index
- Return the task at the head of the queue, and update the head
pointer.
- If the queue is now empty, update the highest index pointer. (This
is at least logarithmic in the number of priorities, but the number
of priorities is bounded above by a small number -- log_2<=6 -- so
in practice this is bounded above.)
2. Only a little more complex
- One sorted list of all ready tasks.
- An array of pointers, one for each priority, pointing to the tail of
each priority.
- Insertion is (almost) constant time
- Index into the array of tail pointers using the task's priority
- Use the tail pointer to insert, updating the tail pointer.
- If there is more than one tail pointer pointing with the updated
tail pointer, then they also may have to be updated.
- Extraction is constant time
- Return the task at the head of the queue, updating the head
pointer.
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.
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 *message, int mslen )
- 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 *reply, int rplen )
- does not block
- unblocks task that called Send
- Send and Reply become READY at the same time.
Sequence of States
Sender
- Active -> Receive_Blocked
- Receive_Blocked -> Reply_Blocked
- May happen right away
- When Receive was called first AND
- the Receiver's SendQ is 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
Send
...
Receive
...
Reply
... ...
Message copying occurs inside Receive and Reply.
Receive needs to have a list of current senders, the ReceiveQ
Receive before Send
Receive
...
Send
...
Reply
... ...
Message copying occurs inside Send and Reply.
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
- Typed as strings
- which require formatting and parsing
- Normally cast from pointers to structs
- sizeof( ) is useful
- struct expected by receiver/sender must agree with struct
provided by sender/receiver
- Type-checking by compiler not possible
- Dynamic type-checking possible
- First element of struct is its type
- Even polymorphism is possible
- Message types should be handled in a more modern way
- type extension
- would provide a more uniform handling of error returns
- Task states
- DEFUNCT is terminal
- READY -> ACTIVE
- ACTIVE -> SEND_BLOCKED, RECEIVE_BLOCKED, READY, DEFUNCT
- SEND_BLOCKED -> READY
- RECEIVE_BLOCKED -> REPLY_BLOCKED
- REPLY_BLOCKED -> READY
- You can add extra return values beyond those specified
int Send( Tid tid, char *message, int mslen, char *reply, int rplen )
These are pretty self explanatory, except
- The return value is the number of characters actually placed in the
reply-buffer
- including the terminal character ( \000 ) if the contents of the
reply buffer is a string
- If something goes wrong, the return value is negative, coded to
indicate what went wrong
What can go wrong
- Illegal
tid
tid not an existing task
It's up to Send to check that the reply-buffer was big
enough by looking at its return value
It's not an error if the task to which we Send never
Receives
- Should it be?
- Hint. Finding out if a task "never Receives" is equivalent to what
problem?
- Parsing
argument and reply-buffer is
potentially costly and error-prone
- A type system might be nice
- But then you would feel compelled to implement run-time type
checking
Implementing Send
What's in user space is just stubs.
- checking arguments
- putting arguments in the right place
- Note that there are five arguments
What the kernel must do
- Check arguments
- Change state of sender to RECEIVE_BLOCKED
- Put sender on the end of the receiver's sendQ
- If receiver is SEND_BLOCKED, do #5 in Receive, immediately below.
int Receive( Tid *tid, char *message, int msglen )
These are pretty self explanatory, except
- How is the task id copied form kernel to receiver?
- That is, where does the pointer point to?
- What if the buffer wasn't big enough?
- If several tasks have done
Send, which one gets
Received first?
- return value is number of bytes in message, including terminal
character (\000) if the message is really a string..
- It seems as though the return value should be the tid. Something is
not right.
- If something goes wrong, the return value is negative, coded to
indicate what went wrong
What can go wrong?
- Only part of the message was copied
It's up to Receive to check that the message-buffer was
big enough by looking at its return value
Implementing Receive
What the kernel must do
- Check arguments
- Change receiver's state to SEND_BLOCKED
- Check the
sendQ
- If SENDQ_EMPTY
- Exit from kernel after scheduling
sendQ is not empty
- extract head of the send queue, called the sender below
- copy message from sender to receiver, after checking buffer
sizes
- change sender's state to REPLY_BLOCKED
- change receiver's state to READY
- put sender's tid into receiver's argument
- put receiver on its
readyQ
- set up receiver's return value
int Reply( Tid tid, char *reply, int rplen )
These are pretty self explanatory, except
- The Replyer need not be the Receiver, but must be in contact with the
Receiver
- When all goes well Reply leaves two tasks READY when it completes
Implementing Reply
- Check arguments
- sender (tid) must be REPLY_BLOCKED
- Copy message from replier to sender, checking buffer sizes
- Put sender on readyQ
- Set up sender's return value
- Change sender's state to READY
- Put replier on readyQ
- Set up replier's return value
Change replier's state to READY.
Return to: