Program Context and Switching

CS 321 Lecture, Dr. Lawlor, 2006/01/30

So periodically the CPU is interrupted.  The OS takes control.  Let's say the program that was using the CPU has been running for a long time, and the OS would like to run some other program for a while.

We can't just kill the old program--then we'd have to start it up again from scratch.  We just want to gently suspend it, keeping its information around so we can eventually restore the program, starting it up again where it left off.

Program Context

So what kind of stuff do we need to save and restore?  In other words, what is a "program"?  A "program" can be thought of as including, in order of importance:
  1. The values in the processor registers.  If we suspended a program, and then restored it with a totally different value in eax, the program wouldn't do the same thing as if we'd just left it alone.  So if we want to swap during an interrupt, we've got to save and restore all the processor registers.  If we switch programs during a subroutine call, we've got a lot less to save and restore, since we don't need to bother saving and restoring scratch registers.
  2. The values on the stack.  The stack stores local variables and the stuff we're working on right now, so it's important that it get saved when we switch programs.  If we just put the different program's stacks at different places in memory, then we just need to save and restore the stack pointer when we switch programs.
  3. The values everywhere in program memory.  All of memory can actually be switched quite easily efficiently with the virtual memory hardware, but we won't talk about how that works for a few more weeks.  For now, you can imagine just choosing different places in memory for each program's values.
  4. All connections to the outside world, including open files and network connections.  We'll talk about how to save and restore these things when we talk about I/O.  For now, we'll just ignore open files and network connections.
Step (1) is just a little bit of assembly code to save and restore registers.  Since you've got to do this inside almost every subroutine, it should be old hat by now.  Check out my little "minithread" example code for an example (Directory, Zip, Tar-gzip).   The key routine is just the "mth_swap" subroutine, which saves an old set of registers and loads up a new one, and the core of this is just:
    mov [eax],esp  ; Save the old stack pointer
    mov esp, [ecx] ; Load up the new stack pointer

Kinds of Program Context

There are quite a few different levels of saving and restoring that you can design.  Existing levels are nesting, and from least-to-most context include:
  1. User-level threads, where you save and restore processor registers yourself, usually via an explicit "swap" subroutine call.  Also called "coroutines" or "user/userspace threads", or (by Sun Java folks) "green threads".  Typical examples include "minithread" (above), Netscape Threads, or any of a thousand tiny projects mostly relying on setjmp/longjmp or "makecontext/swapcontext".  Each thread has its own stack, usually just allocated with "malloc", but shares the rest of memory with other user-level threads.
  2. Kernel-level threads, or just "threads", are where the OS saves and restores processor registers to switch threads at interrupt time.  Also called "native threads". The most common example is pthreads.  There's actually a hybrid of kernel and user-level threads where you save and restore processor registers yourself (not in the OS), but you do it inside a signal handler during an interrupt.
  3. Processes, where the OS swaps out all of memory and all I/O connections in addition to the registers.  This is what you usually think of as a "program".
  4. Virtual Machines, where the OS itself and everything in it is swapped out. QEMU is a pretty typical example.  A virtual machine is usually treated as an ordinary process inside the outer ("host") OS.
All these levels actually nest.  So a virtual machine might contain several processes, with the OS switching between them.  Each process might contain several kernel threads, with the OS again switching between them.  Each kernel thread might contain several user-level threads, which user code explicitly switches between.