CS452 - Real-Time Programming - Fall 2011
Lecture 7 - Create
Pubilc Service Annoucements
Kernel Structure
main( ) {
Initalize_kernel( );
Create_First_User_Task( );
FOREVER {
active = Schedule( );
request = Activate( active );
Handle( request );
}
Comments
- After Schedule( ), active is a pointer to the TD of the next task to
run
- Inside Activate( ), active is run.
- Activate( ) returns when an interrupt occurs,
- Handle( ) returns when manipulation of kernel data structures is
complete
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 issue is `When do we enter the
kernel?'
Three possibilities
- Tasks run to completion, which means until they make a request for
kernel services
- Event-driven pre-emption, which means when hardware makes a request for
service
- Time-slicing
We do 1 & 2, but not 3, because our tasks co-operate. Time-slicing is
needed when tasks are adversarial.
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.
Inexpensive (=constant time)ways to schedule
Least expensive first
- active task decides = co-routines
- round robin
- everybody gets the same chance
- but usually long running time = unimportant
- priorities
- fixed at compile time
- fixed when task is created
- re-fixed every time task is scheduled
- Do you have a good algorithm?
The number of priorities should be small, but not too small.
Tasks at the same priority should have the same precedence.
Scheduling algorithm
- Find the highest priority non-empty ready queue.
- Schedule the first task in the queue.
The state of the most recently scheduled (running) task is ACTIVE, not
READY.
The kernel maintains a pointer to the TD of the active task so it
knows which task is making the current request.
- When a task is made ready it is put at the end of its ready queue.
Implementation
Array of ready queues, one for each priority.
Each ready queue is a list with a fornt pointer (for extraction)and a back
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
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.
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
To do this we need to know how scheduling works.
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
TD
s and
PQ
s
- 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.
Return to: