Assignment #2

Due: October 29 before 16:00
To be done individually.


Grading


Change Log

October 26

October 25

October 21
(courtesy of your friendly TA)

October 12

October 9

October 8


If you encounter any issues that have not been specified, you may make use of reasonable assumptions provided that these are clearly and explicitly stated in your report and are not in conflict with any of the assignment specifications. You may submit a partial solution to any of the components of this assignment. Grading will be balanced according to the difficulty of each component and the fraction completed.

Introduction

This assignment will give you experience with context switching and scheduling techniques, and you will emerge from the ordeal with a solid understanding of the role of the scheduler in modern operating systems.

Your task is to implement a threads library that provides the following functions to other programs wishing to run lightweight processes. More information about creating a library is provided below.

Thread Descriptor data structure

typedef int ThreadId;

typedef struct {
	unsigned pc;
	unsigned sp;
	} Registers;

typedef struct Thread {
	struct Thread *next;	/* points to next TD in this queue */
	ThreadId id;		/* unique identifier assigned to this thread */
	Registers regs;		/* stores PC and SP for later resumption */
	int priority;		/* lower values indicate higher priority */
	Queue *inqueue;		/* identifies the queue to which we belong */
	} TD;

You will need to maintain two queues for the thread descriptors: ReadyQueue and BlockedQueue, as well as a linked list of free thread descriptors, FreeTDs.

Helper code

Sample (but incomplete) code is given below. You are free to use this in your assignment but make sure that you fully understand the code before you begin.

#define MAX_THREAD      32
#define STACK_ADJUST    200 * sizeof(unsigned) /* err on the large side */

/* 
 * Save all registers on the stack. This must be done by the
 * operating system, since the user process does not have 
 * access to the register sets other than the currently active ones.
 */
#define SAVE_REGS() \
	asm( "ta 3" )

/*
 * The 'in' and 'local' registers of the current register window  
 * are loaded from the stack.
 */
#define RESTORE_REGS() \
	asm( "ld [%sp+0],%l0" ); \
	asm( "ld [%sp+4],%l1" ); \
	asm( "ld [%sp+8],%l2" ); \
	asm( "ld [%sp+12],%l3" ); \
	asm( "ld [%sp+16],%l4" ); \
	asm( "ld [%sp+20],%l5" ); \
	asm( "ld [%sp+24],%l6" ); \
	asm( "ld [%sp+28],%l7" ); \
	asm( "ld [%sp+32],%i0" ); \
	asm( "ld [%sp+36],%i1" ); \
	asm( "ld [%sp+40],%i2" ); \
	asm( "ld [%sp+44],%i3" ); \
	asm( "ld [%sp+48],%i4" ); \
	asm( "ld [%sp+52],%i5" ); \
	asm( "ld [%sp+56],%i6" ); \
	asm( "ld [%sp+60],%i7" )

/*
 * Save the PC and SP of the invoking procedure in the Active thread
 * descriptor.  Note that the PC saved is actually the return address
 * of the callee, which appears here in register %i7, and the SP saved
 * is actually the FP (that is, the SP of the callee prior to the
 * last subroutine call).
 */
#define SAVE_CONTEXT() \
	asm("st %%fp,%0" : "=m"(Active->regs.sp));      /* save sp */ \
	asm("st %%i7,%0" : "=m"(Active->regs.pc))       /* save pc */

/*
 * Loads PC (really the subroutine return address) and SP (of the
 * calling function, presently stored here as the FP) from newly 
 * selected Active thread descriptor. The FP will become the SP 
 * of the invoking procedure on return.
 */
#define RESTORE_CONTEXT() \
	asm("ld %0,%%fp"::"g"(Active->regs.sp));      /* restore sp */ \
	asm("ld %0,%%i7"::"g"(Active->regs.pc))       /* restore pc */

/*
 * Prior to removing the active thread from the CPU, it is necessary 
 * to save its context so that it may resume execution from where it
 * left off.  The context switch is performed by placing the Active 
 * thread onto the ReadyQueue and selecting the first element (the
 * thread of highest priority) from the ReadyQueue as the new Active
 * thread.  The context for the new active thread is then restored.   
 * When ContextSwitch returns, execution will resume in the new Active 
 * thread.
 */
void ContextSwitch () {

	SAVE_CONTEXT();

	/*
	 * At this point, it is safe to perform a context switch 
	 * (change the Active thread) as all pertinent state information 
	 * for this thread has been saved on the stack (SAVE_REGS) and 
	 * the thread descriptor (SAVE_CONTEXT).
	 */
	QueueInsert(Active, ReadyQueue);
	Active = QueueDelete( ReadyQueue ) ;
	Active->inqueue = NULL ;

	/*
 	 * The subroutine return address has now been set to the newly
	 * selected active thread and the FP (which will become the SP 
	 * on return) has been set to that of the selected thread.
 	 */
	RESTORE_CONTEXT();

	/*
	 * If the active thread has been changed, we will now return 
	 * to a different thread.
	 */
	return ;
}

/*
 * Creates a new thread by grabbing the next descriptor from
 * the FreeTDs list. TD is initialized and placed on the ReadyQueue.
 * Returns: id of created thread if successful
 *	ERROR if there are no free TDs.
 * 	STACK_ERROR if stack_size is smaller than MIN_STACK_SIZE
 *	PRIORITY_ERROR if priority is outside of [MIN_PRIORITY, MAX_PRIORITY]
 */
ThreadId ThreadCreate( unsigned pc, unsigned stack_size, int priority ) {
	void * stack ;
	unsigned response ;
	TD *td;
	ThreadId id;

	/*
	 * For you to write:
	 *
	 * 1. make sure input parameters are legal 
	 */

	/* create a stack */
	stack = (void *) malloc( stack_size ) ;
	stack += stack_size ;

	/* reserve room for saving registers */
	stack -= STACK_ADJUST ;

	/*
	 * For you to write:
	 *
	 * 2. make sure stack is on a word boundary (must be multiple of 4) 
	 * 3. allocate a TD from the freelist
	 * 4. get the next ThreadId 
	 */

	/*
	 * Fill in thread descriptor:
	 * NOTE: in code below, use pc-8 as indicated because SPARC
	 * processor will add 8 to this saved value when restoring
	 * the PC on a return statement.
 	 */
	FillTD( td, pc-8, stack, priority, id);
	
	/*
	 * For you to write:
	 *
	 * 5. add TD to ReadyQueue 
	 * 6: decide whether to invoke a context switch
	 */

	if( /* context switch appropriate */ ) {
		SAVE_REGS();
		ContextSwitch();
		RESTORE_REGS();
	}

	return (ThreadId) response;
}

Explanation of the context switch

The explanation refers to the following excerpts of labelled code:

	ThreadId ThreadCreate( unsigned pc, unsigned stack_size, int priority ) {
		...

		if( /* context switch appropriate */ ) {
L1:			SAVE_REGS();
L2:                	ContextSwitch();
L3:                	RESTORE_REGS();
        	}                                                 
		...
	}

	void ContextSwitch () {

L4:	        SAVE_CONTEXT();

	        /*
         	 * At this point, it is safe to perform a context switch
         	 * (change the Active thread) as all pertinent state information
         	 * for this thread has been saved on the stack (SAVE_REGS) and
         	 * the thread descriptor (SAVE_CONTEXT).
         	 */
        	QueueInsert(Active, ReadyQueue);
L5:	        Active = QueueDelete( ReadyQueue ) ;
        	Active->inqueue = NULL ;

        	/*
         	 * The subroutine return address has now been set to the newly
         	 * selected active thread and the FP (which will become the SP
         	 * on return) has been set to that of the selected thread.
         	 */
L6:	        RESTORE_CONTEXT();

		/*
	 	 * If the active thread has been changed, we will now return
         	 * to a different thread.
         	 */                               
L7:		return;
	}
In ThreadCreate(), the statement at L1 causes all of the current register windows to be saved onto the stack. This must be done by the operating system, since the user process does not have access to the register sets other than the currently active ones.

At L2, the subroutine ContextSwitch() is called. In doing so, the parameters are placed into the out registers of the current register window, and control is passed to ContextSwitch(). The old PC, still pointing to L2 is stored into %o7.

ContextSwitch() first executes a save instruction (not visible in the code, but generated by the compiler), which first shifts the register window and then decrements the stack pointer, effectively allocating more space on the stack. Note that the old stack pointer is now the frame pointer, and that %o7 becomes %i7. ContextSwitch() at L4 then stores the values of the frame pointer in the SP field of the active thread descriptor and the call return address (in %i7) into the PC field of the thread descriptor.

At L5, Active may now be set to point to a new thread. Then, in L6, the frame pointer (%i6) is set to the SP value last stored in the thread descriptor, and %i7 is set to the PC value of the descriptor, before returning. The return at L7 causes a restore instruction to be executed, which shifts the register window back by one set, so all the in registers become out registers again. In particular, this sets the stack pointer to the value of the frame pointer.

The processor then continues executing at the instruction pointed to by %i7+8 (taken before the restore), which happens to be L3. At that point, all the in and local registers of the current register window are loaded back from the stack (see the macro for RESTORE_REGS()). Note, that no out registers are loaded, so any return values are still valid. When ThreadCreate() returns, the register window will move back one set, and the appropriate registers for the calling function will be restored.

A very brief introduction to libraries

A library is a set of compiled functions that can be linked together with a user's program to provide the desired services. For example, the math library, libm.a, contains the functions that implement sin(), exp(), log(), etc. The library does not contain a main() function, as that is the responsibility of the user program to provide.

Libraries may be created as either static or dynamic (also known as shared). For this assignment, we will only consider static libraries. To create such a library on the SPARC machines, consisting of the code in files file.c and file2.c, you would use:

gcc -c file1.c file2.c
ar -rcv libname.a file1.o file2.o
where name is the name of your library, in this case, threads.

Note that while the generated library file will thus be called libthreads.a, by UNIX naming conventions, we refer to this library simply as threads. In the following, when the linker sees the parameter -lthreads, it knows to search for a library named libthreads.a. Thus, to link the code from myprog.c (containing the main() function as well as any code that is not part of the threads library), with the library, you would use the following:

gcc -c myprog.c
gcc myprog.o -L. -lthreads -o myprog
The -L. tells the linker to look in the current directory for any libraries you specify, in this case, libthreads.a.

Testing

To test your threads library, write a main() function that calls ThreadsInit(), then creates a thread and yields the processor to it. This thread should run in an infinite loop, printing out a message every second to indicate that it is still alive, and yielding the processor every iteration of the loop in case other threads are ready. In order to ensure that the thread continues running, you must prevent main() from terminating. One way to accomplish this is to sleep for a very long time (see sleep(3)). However, you can also take advantage of thread priorities to make sure that main() doesn't get control of the processor unless no other threads are on the ReadyQueue.

At this point, you should add additional threads to your test program. Try creating children threads within threads, and also figure out a way to create multiple threads from the main() function. Once you have done so, create a special idle thread that will always be available to run (at a low priority).

Sample code for this thread is given below:

/*
 * A sample (and simple) idle thread.  The code loops forever,
 * printing information about the list of allocated threads.
 * In a real system, Idle would look more like:
 *	void Idle() { while (1) ThreadYield(); }
 */
void Idle() {
	Queue *queue;
	int i;

	while( 1 ) {
		printf( "CPU is idle\n" );
		for( i = 0; i < MAX_THREAD; ++i ) {
			queue = TDArray[i].inqueue;
			if( queue != FreeTDs ) {
				printf( "id = %d, ", TDArray[i].id );
				printf( "pri = %d, ", TDArray[i].priority );
				if( queue == NULL ) printf( "ACTIVE\n");
				else if (queue == ReadyQueue) printf("READY\n");
				else printf( "BLOCKED\n");
			}
		}

		(void) sleep( (unsigned) 1 );
		ThreadYield() ;
	}
}
Style note: Since Idle() examines the thread queues and details of the thread descriptor, you may prefer to include this function in the library code. That way, you do not have to expose the implementation details of the thread data structure to the user program.

Scheduler

Once you have written and tested the threads library, implement a simple priority scheduler that uses internal priority computation (see the text, pp. 172-174) to prevent starvation. Your scheduler should give all of the ready threads a chance to run, but pays more attention to threads with a higher external (user assigned) priority. To take advantage of the scheduler, the user program must call a threads library function, ThreadsUseScheduler() after the call to ThreadsInit(). From that point on, any call to ThreadYield() will select the next ready thread according to the scheduler policy. You may wish to add additional fields to the thread descriptor data structure in order to maintain timing information (see gettimeofday(2)) and internal priority. Note that your scheduler will still be non-preemptive, so every thread must call ThreadYield() at some point to relinquish the processor.

Test your scheduler with various combinations of threads requiring light and heavy processor usage. Use the sleep(3) call to simulate extended thread execution. Does your scheduler adequately penalize CPU hogs? How long does it take to reach a steady state given a static pool of threads with a fixed distribution of processor requirements? Illustrate this by reproducing the CPU utilization of each thread in a graphical format.

Submitting your assignment

You must submit a hardcopy consisting of the following:

In addition to the hardcopy submission, you must provide an electronic version of your assignment in UNIX tar(8) format, encoded with uuencode(1). The electronic submission will be compiled and tested automatically on the Sparc systems in the ECE undergraduate lab. The electronic submission must contain:

by sending an email message to the instructor.

Your message must be sent by October 29, 16:00. Assignments will not be considered complete if they have not been submitted both in hardcopy and electronically or if the two versions of the source code differ. Incomplete submissions will not be marked. The hardcopy and electronic versions may be submitted at different times within the assigned deadline.

In order to allow for automatic processing, your submission message must be formatted as specified in the following paragraphs. An unproperly formatted message will be automatically bounced back to the sender and the submission will not be considered complete. You may then correct the errors and resubmit. However, you may not resubmit after a properly formatted message has been accepted: please make sure that your files contain exactly what you wish to submit.

The "Subject:" field of your email message must have the following format: 427-A2-YOUR_STUDENT_ID, with the digits of your student ID number in place of YOUR_STUDENT_ID; do not write anything else in the subject; do not write anything in the message other than the tar file.

The tar file must create, when opened, a directory named with your student ID, containing all the source files (there may be subdirectories, if needed) and the Makefile as described above. An optional file named README may be included, containing instructions for the correct compilation and use of the library. Do not include any non-text files (e.g. compiled executables, relocatable objects), since they may be handled improperly by the email software, causing corruption of the entire tar file. Make sure that all files are in UNIX text format (lines terminated by a line-feed character, ASCII 10): use a converter if you edit them on a PC under MS-DOS/Windows.

An example sequence of UNIX shell commands that creates and sends a tar file as specified follows [comments in square brackets]:

[Create the source directory for student with id 952334.]
   % mkdir ./952334

[Copy all the sources in it.]
   % cp foo.c bar.c baz.c flop.c README ./952334

[Create the tar file with the directory content.]
   % tar cvf 427-A2-952334.tar ./952334

[Uuencode the tar file]
   % uuencode 427-A2-952334.tar 427-A2-952334.tar > 427-A2-952334.tar.uu

[Send it with the proper subject field.]
   % mail -s "427-A2-952334"  < 427-A2-952334.tar.uu

If the submission is correct, you will receive an automatic acknowledgement in reply. Otherwise, the message will be refused, and an explanatory error message will be sent to you.