The Not Especially System Call Lab
The next lab is, in some sections, known as the "System Call Lab". You'll notice that I've renamed it "The Mini-UNIX Shell". I also toyed with the name "The Process Lab". So, what's in a name?
Well, the lab does make use of system calls. The library calls that are new to this lab such as, fork(), wait(), and waitpid() have system calls underneath the hood, to be sure. But, this is not new. We've used system calls before. Any time we've made use of, for example, I/O, the C library has made use of system calls to get the job done.
Who's In Charge Here, Anyway
So, what is a system call, anyway? Well, to answer that question, we need to first take a better look at the operating system. Most people operate under the assumption that the operating system is something akin to air traffic control or a paramilitary police force. The idea is that the operating system controls everything that is going on, that it controls who gets what and when, and that it can kill any process that is offensive.
And, this model is useful in that it does offer an explanation for a lot of what we see when we look casually at our computers and their operating systems. But, it does fail a more careful look. You guys have written a machine simulator. You know the basic life of a processor -- they just charge through memory and execute instructions. So, when the processor is in the context of a particular process chewing along following the instructions of that program, where is the operating system, this great controlling ruler, running? Answer: It is not. If the processor is running a user program, it is not simultaneously running the operating system. Yep, you got that right. Barring anything else, a program can run forever, starving the operating system, itself, from getting to the processor.
The Timer and Interrupts
Well, obviously that system won't do. It doesn't explain reality -- that the OS does seem to be in charge. Here's how the game is played. There is a piece of hardware known as the timer. At periodic intervals, this timer counts doen to zero and interrupts the processor. It pokes the processor directly, or via some arbitration hardware, and let's it know that it time to let the operating system run again.
The processor makes the operating system run again via what is, in effect, an array of function pointers known as the Interrupt Vector Table (IVT). Every device on the system has a handler, a function that services it. These handlers are known as Interrupt Service Routines (ISRs). Each interrupt has a number, for example 0 for the timer. When the hardware device wants attention, it uses its wire to signal the processor. The processor then executes the appropriate function fron the IVT.
So, if we ask the disk to get some data for us, when that data is ready, it can signal the processor, which will then run the disk's ISR to copy the data. When data shows up on the network, the network adapter can signal the processor via its own ISR. This mechanism is also used for the processor to signal the operating system that there is a problem with the process that is currently running, for example a divide-by-zero or an attempt to make an invalid memory access. This type of situation often results in the OS ending the current process as you have seen with SIGSEGV.
Regardless, periodically, the timer will signal the processor that "time is up" for the current processes. When this timer interrupt goes off, the processor runs the timer handler, which is in-effect a part of the operating system's scheduler. The OS can then look around and decide if that process should continue running or if it should take a break to allow another process to run for a while.
This interrupt system enables the operating system to schedule multiple processes to take turns so quickly that they all seem to be running at the same time. So, for example, your web page can update at the same time that you type an email and move the mouse.
How does all of this get set up, you ask? When a computer starts up, it goes throug ha boots strap process. The boot sector is read off of the disk. This boot sector contains the instructions about how to initialize, a.k.a. boot strap, the operating system. Part of this process is initializing the IVT and storing its address in a register so that the processor can find it. Another part of it is telling the timer how often to interrupt the processor.
Protection, Supervisor Mode and Interrupts
Well, as fun a story as this is, what does it have to do with so-called system calls? Well, a little more background first. A quick story about protection, to be specific. If a process, in effect, owns the processor while it is running, what prevents it from reading another process's memory? The OS's memory? Or from accessing a file on disk that contains a user's private information?
The hardware limits the access to hardware devices as well as to certain parts of memory that are specially tagged so that user processes can't, under normal circumstances, access them. So, for example, a process is normally not able to access the disk or to read protected memory.
Instead accessing these protected resources can only be done when in a special mode often known as supervisor mode. And, there is exactly one way to get into supervisor mode -- by executing one of the functions via the IVT. In this way, supervisor mode can only be entered in a very controlled way -- only through the functions that the operating system, itself, set up in the IVT.
So, for example, when the disk needs attention, and the disk handler runs, it can access the disk, the operating system's data structures, and the destination process's memory, because it is running via an ISR. When the function ends, privilege is lost.
And, again, I want to observe that the protected resources include not only hardware things like the disk, the network, and memory, but also more abstract resources, such as the data structures that represent processes, communication channels, &c.
See how nice and pretty? We now see how the OS can periodically get back in the saddle via the timer, as well as how it can gain access to resources not normally accesible via ISRs.
So, What's a System Call, Anyway?
Enough already! What is a so-called system call? Well, we know how the hardware can rquest's the OS's attention and invokes the OS's special privileged functions via the ISRs. But, what happens when a program wants to invoke the OS to do something that requires privilege, such as request data from the network or the disk? Or to do something that otherwise requries the OS, such as create a new process?
Well, for it to access the privileged resources, we know that it has to go through the IVT -- if it doesn't it can't get into the special mode needed to access the protected resources. And, this is exactly what it does. There is a special instruction often knwon as TRAP. This instruction causes the trap-handler to run from the IVT. This handler looks on the stack for a single argument -- a number which indicates what the user wants to do. This number, whcih is #defined to a useful name, suhc as OPEN, CLOSE, READ, WRITE, &c is then fed to a big switch statement which calls the appropriate function. But, since this function is invoked via the ISR, it has privilege.
So, what is a so-called system call? It is a function that is invoked indirectly via a TRAP instruction such that it passes through the IVT and has privilege.
We've seen thing that are, at least traditionally, system calls: read() and write(), for example. And, today we'll see some more, such as fork(), wait(), waitpid(), and getpid(). No magic here. They are just C library calls that, deep down inside, require privs.
Notice that I said, "at least traditionally". This is because the so-called system call interface has evolved over the years and is no longer the same as it once was -- and isn't the same across all flavors of UNIX. But, the details there are really the domain of 15-410 -- not 15-123!
The Life Cycle of a Process
Okay. So. Now that we've got a better understanding of the role of the OS and the nature of a system call, let's move in the direction of this week's lab. It involves the management of processes. So, let's begin that discussion by considering the lifecycle of a process:
A newly created process is said to be ready or runnable. It has everything it needs to run, but until the operating system's schedule dispatches it onto a processor, it is just waiting. So, it is put onto a list of runnable processes. Eventually, the OS selects it, places it onto the processor, and it is actually running.
If the timer interrupts its execution and the OS decides that it is time for another process to run, the other process is said to preempt it. The preempted process returns to the ready/runnable list until it gets the opportunity to run again.
Sometimes, a running process asks the operating system to do something that can take a long time, such as read from the disk or the network. When that happens, the operating system doesn't want to force the processor to idle while the process is waiting for the slow action. Instead, it blocks the process. It moves the proces to a wait list associated with the slow resource. It then chooses another process from the ready/runnabel list to run.
Eventually, the resource, via an interrupt, will let the OS know that the process can again be made ready to run. The OS will do what it needs to do, and ready, a.k.a., amke runnable, the previosly blocked process by moving it to the ready/runnable list.
Eventually a program may die. It might call exit under the programmer's control, in which case it is said to exit or it might end via some exception, in which case the more general term, terminated might be more descriptive. When this happens, the process doesn't immediately go away. Instead, it is said to be a zombie. The process remains a zombie until its parent uses wait() or waitpid() to collect its status -- and set it free.
If the parent died before the child, or if it died before waiting for the child, the child becomes an orphan. Shoudl this happen, the OS will reparent the orphan process to a special process called init. Init, by convention, has pid 1, and is used at boot time to start up other processes. But, it also has the special role of waiting() for all of the orphans that are reparented to it. In this way, all processes can eventually be cleaned up. When a process is set free by a wait()/waitpid(), it is said to be reaped.