NEXT UP previous
Next: Rendezvous

Starting New Threads

Starting a mew thread just means creating a struct context for the thread and allocating a block of memory for its stack. These data structures can then be initialized and added to the scheduling loop to get the opportunity to run. It all sounds straightforward when you say it fast but in fact it is a little bit tricky and requires some attention to detail. The tricky bit is initializing the thread's stack, as several values need to be hand crafted into the empty stack before it can be used:

	new_thread(int (*start_addr) (void), int stack_size)
	{
		struct context *ptr;
		int esp;

		/* 1 */
		if (!(ptr = (struct context *)malloc(sizeof(struct context))))
			return 0;

		/* 2 */

		if (!(ptr->stack = (char *)malloc(stack_size))) 
			return 0;

		/* 3 */
		eap = (int) (ptr->stack+(stack_size-4));
		*(int *)esp = (int)exit_thread;
		*(int *)(esp-4) = (int)start_addr;
		*(int *)(esp-8) = esp-4;
		ptr->ebp = esp-8;

		/* 4 */
		if (thread_count++)
		{
			/* 5 */ 
			ptr->next = current->next; 
			ptr->prev = current; 
			current->next->prev = ptr; 
			current->next = ptr;
		}
		else
		{
			/* 6 */
			current = ptr->next = ptr->prev = ptr; 
			switch_context(&main_thread, current);
		}

		return 1;
	}

The new_thread() function takes two parameters, the first is the address of the function where execution of this thread will begin and the second is the size of the memory block (in bytes) to allocate for stack space to the new thread. The numbered comments in the code are as follows:

  1. Use malloc() to create a struct context for this thread. Check that the memory allocation was successful and return a zero value on error.

  2. Use malloc() again to allocate a block of memory of the specified size for the thread's stack. The stack pointer in the new struct context is set to point to the stack memory. Again, a memory allocation error will cause a zero to be returned by new_thread().

  3. Initialize the stack to the state shown in Figure 1. Remember that the pointer to the stack memory in the struct context points to the lowest numbered address of the block, whereas the action on the stack is taking place at the high address end of the block.

  4. The next part depends on whether or not this is the first new thread within the current process. If it is, execution of the main program is suspended and a context switch to the new thread is performed; otherwise the new thread is just added to the scheduling loop to await its turn to run.

  5. Adding a new thread to an existing scheduler loop is made a little more complex by the fact that the loop is doubly linked so that a forward and backward set of pointers need to be set up. A new thread is inserted into the scheduling loop in such a way that it will automatically be the next thread to run when the current thread performs a release().

  6. Here, no threads are currently running so the scheduling loop needs to be created and started by a first call to switch_context(). In order to make sure that the main program is suspended, the switch_context() call stores its ebp register contents in a static struct context called main_thread. This structure is not part of the scheduling loop and so the main program will not be executed again, except by special arrangement, when there are no more threads left to run.

Figure 1 shows that the ebp element of the struct context associated with the new thread is set to point into the thread's stack. When a context switch is made to the new thread, this value is loaded into the CPU ebp register. The return from the switch_context() function then causes the stack pointer to point to the same stack location. Performing a stack pop into ebp loads this register appropriately and also makes the stack pointer point to the thread start function. Finally, performing a return from subroutine will pop the start address into the program counter, thus performing a jump to the beginning of the thread start function.

An extra twist is required when a thread's start function terminates in order to remove the thread from the scheduling loop and free its malloc()ed memory back to the system. These actions are performed by the exit_thread() function. So that you don't need to remember to call this function at the end of each thread's start function, a jump to the function is automatically crafted into the thread's stack. This means that the function is automatically executed when the thread's start function terminates, using a similar trick to the one which executed the start function in the first place.

The code for the exit_thread() function appears as follows:

	static exit_thread(void)
	{
		struct context dump, *ptr;

		/* 1 */
		if (--thread_count)
		{
			/* 2 */
			ptr = current; 
			current->prev->next = current->next; 
			current->next->prev = current->prev; 
			current = current->next; 
			free (ptr->stack); 
			free (ptr); 
			switch_context(&dump, current);
		}
		else
		{
			/* 3 */ 
			free(current->stack); 
			free(current); 
			switch_context (&dunp, &main_thread);
		}
	}

The exit_thread() function terminates the thread pointed to by current. The fact that the exit_thread() function is declared to be static prevents it from being called from outside the library. The numbered comments are:

  1. If current is not the last thread in the scheduling loop, then unlink it from the scheduler and switch context to the next thread; otherwise, as there are no more threads to execute, resume execution of the main thread.

  2. Take a copy of the pointer to the current thread. Unlink the current thread from the scheduling loop, then reassign current to point to the next thread to execute. Now free() the malloc()ed memory associated with the terminating thread and finally switch_context() from this thread to the new current thread.

  3. Here, it is only necessary to free() the malloc()ed memory associated with the final thread in the scheduling loop and then switch_context() back to the main program, to resume its execution.


NEXT UP previous
Next: Rendezvous