CS452 - Real-Time Programming - Fall 2011
Lecture 8 - Create
Public Service Annoucements
- Reference handin for a0.
Kernel Work for Create
Allocate Resources
Task ID
- 32-bit word to match architecture
- Possibly broken into TD index and generation.
Task Descriptor
- During initialization create an array of task descriptors
- How many should there be?
- Without task destruction
With task destruction
Notice that because all task descriptors are the same same size allocation
and re-allocation are constant time.
Memory
- During initialization break the spare memory into equal sized chunks
- How many should there be?
- Without task destruction
With task destruction
You could create a more intimate relationship between TD and 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 memory
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.
Initialize Memory
The principle is, `Make the memory (the stack) exactly ready to be
processed by your kernel exit code.'
In practice,
- After running the kernel entry code the stack is exactly ready for the
kernel exit code to be run.
- Make the stack look as though the kernel entry code has just been
run.
- After runing the kernel exit code you must have
- the PC pointing at the first instruction of the created task
- the SP pointing to the top of an empty stack
- Anything else?
Fill in the Task Descriptor
For some state there is a choice between stack and task descriptor. The
minimum is
- run state: READY to start with
- priority
- parent Tid
- SP
- Pointers for queues.
There could be more
Enter the Task into its Ready Queue
Implementation of Ready Queues
- One ready Q for each priority
- To link them up without needing memory allocation
Making a Stub to Wrap 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
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
Receive
s
- 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
Receive
d 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: