NEXT UP previous
Next: Starting New Threads

Context Switching

The first piece of threads library code to look at is the release() function, which is called by a thread when it wants to pass its control of the CPU on to the next thread in the scheduling loop. When you examine this code, keep in mind that the variable current is a pointer to the struct context associated with the currently running thread:

	release (void)
	{
		/* 1 */
		if (threadcount<=1) 
			return 0;

		/* 2 */
		current = current->next; 
		switch_context(current->prev, current); 
		return 1;
	}



	static switch_context(struct context *from, struct context *to)
	{
		/* 3 */
		__asm__
	{
		"movl 8(%ebp),%eax\n\t" 
		"movl %ebp, (%eax)\n\t"
		"movl 12(%ebp),%eax\n\t"
		"movl (%eax),%ebp\n\t"
	}

Notice that the release() function calls another function (switch_context()) which is declared to be static so that it cannot be called by any code outside the threads library file. Notice also that the switch_comtext() function consists of a few lines of embedded assembly code, which are used to allow direct access to specific processor registers a level of control not directly available in standard C. This does mean that the library will not be portable to other processor architectures as it uses specific knowledge about the internal layout of the CPU. The numbered comments are:

  1. If there is only one thread in the scheduling loop then there is no point in doing a context switch at all. In this case, the release() function just returns straight away and hence the current thread resumes execution until the next release() call is encountered.

  2. Move current to point to the next thread in the scheduling loop. Then call switch_context(), passing the old value of current and its new value as parameters.

  3. Inside the switch_context() function there are four instructions in 386 assembly code. All these instructions do is to take the CPU ebp register contents and store them in the ebp field of the from structure, and then load the CPU ebp register from the ebp field of the to structure instead. The switch_comtext() function then returns.

So, how does a thread context switch take place just by changing the ebp register around? To answer this question we will need to take a look at stack use again.

Imagine that a thread which has been running for a while calls the release() function. What happens is that this function call and the subsequent call to switch_context() both take place using the stack of this thread. Figure 1 shows part of the stack layout for this thread just before it starts executing the assembly code section of the switch_comtext() function.

The assembly code section saves the current value of the CPU ebp register into the ebp field of this thread's struct context, then loads the CPU ebp register from the ebp field of the struct context belonging to the next thread to run, and then the switch_context() function returns.

Eventually, the CPU will have executed every thread in the scheduling loop up to its next release() call, so that the thread we are following gets its next turn to run. When this happens, the thread before ours in the scheduling loop will execute a release() call, and, by the time the assembly code section of switch_context() has run, the CPU ebp register will have been reloaded with our thread's saved ebp register value. Notice, however, that the stack pointer will still be pointing to the previous thread's stack and that the program counter will be pointing to the instructions at the end of the switch_context() function, and will be about to execute them.

What happens at the end of every function is that the stack pointer is set equal to the value in ebp, to discard any local variables. In this case, the switch_context function has no local variables but the assignment is done anyway. This has the beneficial side effect of moving the stack pointer so that it points back to the correct place in our thread's stack. The other actions which take place at the end of a function are popping the ebp register from the current stack (our thread's stack) and then using the return address on the stack to get back to the calling function.

This means that our thread's code resumes execution by an ordinary return from the release() function. In fact, as far as any of the threads is concerned, they just call release() at intervals, which then returns, allowing them to continue. The threads themselves are unaware of anything unusual happening during the release0 call.


NEXT UP previous
Next: Starting New Threads