CSE 451, Spring 2000 Solutions to homework 2 1) When a physical device needs attention from the system it generates an interrupt. An interrupt in this case is handled like a mini-context switch. It only saves those registers used by the interrupt handling code rather than swapping out the entire register set as would occur in a normal context switch. Think about why this is true. List and explain briefly one advantage and one limitation of using mini-context switches over full-blown context switches to handle hardware interrupts. Some people suggested that a limitation would be that programmer error could cause overwriting of a user process' register set. If there is a programmer error in the kernel, there is more to worry about than a malfunctioning user program. Subset of advantages: * Much faster to handle the interrupt (good if interrupts are frequent) * Necessary if response time is crucial. Subset of limitations: * Limits the implementation languages that can be used to write an interrupt handler since compilers are rarely written to be optimal for only a couple registers. * Expanding on the previous limitation, it could possibly be inefficient if we have only a couple registers and need to do many calculations. We will be forced to go to memory often. 2) Describe the similarities and differences of doing a context switch between two processes as compared to doing a context switch between two threads in the same process. The process context switch needs to save and restore all of the process state, including program counter, registers, memory mapping, accounting, and other resource information. The thread context switch needs to save the program counter and registers. The memory mapping, accounting, and other resource information stays the same. 3) If you were given the task of writing a program using either multiple processes, each single-threaded, or a single process with multiple threads, what would be three important design considerations you should take into account when making your decision? Subset of answers: * Does the OS provide kernel support for threads? * Do you plan to context switch frequently? (If so, threads may be better since there is less overhead) * Do we want our separate threads of execution to be able to muck with the other threads' data? (If not, processes will be more secure.) * Do we have a multiprocessor system and a program that has parallelism in it? * Is the synchronization overhead worth the efficiency of threads instead of processes? Notes about wrong answers: Some said, "Does the OS provide support for user-level threads?" By definition, the OS has nothing to do with user-level threads. Also, any pair of answers that were opposite from each other got credit only for one answer. 4) Thread::Yield() trace. Many of you left out some crucial details on this problem. I was mainly looking to see that you got some of the important interrupt enable and disable calls, as well as understood what was happening in the scheduler and the actual context switch. I was also looking to see that you understood what happened upon executing a new thread. So, your answer should have been something like this: When Thread::Yield() is called, the interrupts are first disabled by the running thread. That thread tells the scheduler to find the next thread to run. The scheduler does this by pulling an item off the ready list. The currently running thread sets itself to be "ready to run" (via the scheduler's ready to run function) which puts the currently running thread's state to "READY" and puts it on the ready list. The currently running thread then tells the scheduler to run the next thread (which was found as described earlier). In the scheduler's Run() function, we do a "sanity check" and make sure the old thread hasn't overflowed its stack. The scheduler sets the current thread to be the one that we are preparing to run. We set the now current thread's status to "RUNNING" and call the SWITCH function, which is the heart of the context switch. In SWITCH, we save the old registers on the TCB of the old thread and load the new registers from the TCB of the new thread. Some particular registers of interest are the return address, which points to ThreadRoot, esi, which points to the function that the new thread will be calling, edx, which points to the argument to that function, edi, which points to the ThreadFinish function, and ecx which points to the ThreadBegin function. When we "return," we are now in the context of the new thread, running the ThreadRoot function in switch.s. The first thing we do is jump to the "StartupPC" function which is the function pointed to by ecx, or ThreadBegin. This in turn calls Thread::Begin() which will check if the previous thread is done executing and needs to be destroyed. Then it will reenable interrupts. After returning from Thread::Begin(), we are back in switch.s in the ThreadRoot function. We now call the InitialPC function which is the function pointed to by esi, or the function the new thread is to execute. This is the end of the problem as stated.