Let's face it, this is in direct contrast to the desires of the programmer's and the processes they start. Processes can accomplish their goals much quicker if they are given access to other processes and the OS. Instead of going through the hassle of asking the OS for a piece of information and waiting for it to respond, it could just read the information directly out of OS memory in much less time. Processes could try to modify the OS's memory in order to boost their own priority or allow access to restricted resources. Also, a process with high disk demands will be able to get information faster if it does not need to share the disk with other processes. So, in a sense, the relationship between processes and the OS is adversarial... although the OS's goal is to provide a fair and safe environment, the process want to execute as quickly as possible.
So, lets first think about the most restricted space an OS can
provide that will allow a process to run. Lets say a process consists
of only two elements: a memory space (a subsection of the OS memory space)
and a set of registers (including PC and
stack pointer). The memory space is one continuous chunk of memory with
x bytes and addresses ranging from 0 to x - 1. Loaded into the memory
space is a set of assembly instructions that describe a program, and the
instruction to be executed next is pointed to by the PC. There is also a
stack so the program can call functions, and the top of this stack is
pointed to by the SP. We restrict the SP and FP so that they must at all
times lie between 0 and x. We will also disallow any memory
accesses for space outside this range. What we have is a fully functional
program, capable of doing any type of computations as long as it is
self-contained.The question is, is this process that we have defined capable of doing anything interesting? No, its not, and the reason is that for a program to have any dynamism is must have access to some other resources. A program is not just a set of instructions and an execution state... it is really a collection of resources cooperating to accomplish some goal. Without access to external information, a program has no dynamism. It can only do the same thing over and over again. To perform meaningful computation, it must be able to draw on different sets of data from files, user input, special devices, other processes, and the OS itself. In order to accomplish this, we provide one and only one method to request that the operating system fetch us some resource: the system call.
To the process, a system call looks just like a function. In the DEC OS (like the one we use in KOS), all system calls get sent to a special set of instructions that are compiled in to all programs. First, these instructions saves the name of the system call that was made in a register (r4). It then save the the arguments in registers 5, 6, and 7. Lastly, it generates a software interrupt, also called a trap, to let the OS know that there is something it needs to deal with.
Interrupts are a specific method of communication that are used to let the OS know that there is something it needs to deal with. When the OS gets an interrupt, it stops whatever it was doing and deals with it immediately. Usually, when it receives an interrupt it is running a program, which is certainly the case when a system call is made. If this is the case, the OS has to save the state of the running program somewhere because it is about to start using the CPU, memory, and registers to handle the interrupt. If it didn't save the program, it would potentially lose its register values any time an interrupt occurs. So what it does is it associates a record with each process, called a PCB, where it can save the register values and the program's memory space after an interrupt. When it gets an interrupt from a system call, it does this and saves the PCB somewhere.
What the OS does next is it checks the register values to see what the program wants. Usually, this request takes the form of accessing some device, like the disk or the keyboard. Devices are basically machines of their own, independent from the OS and the CPU, that speak their own language. The OS, which knows how to speak devicese, formulates a request and passes it along to the device. Now that the buck has been passed, the OS may have nothing to do and will have to wait until the device is done. So, it initiates a noop, which puts the OS into an idle state until something occurs that it needs to deal with.
Again, this event that it is waiting for is an interrupt. Devices, like processes, communicate with the OS through interrupts, so when a device wants attention, it generates one. In the case of the system call we were discussing, the device interrupts to say, "I am done with your request, here are the results." The OS, when it sees a hardware interrupt, looks for the PCB that describes the process waiting for that device. It then returns the result to the process, either by writing the result directly into the program's memory or through the return value, which is passed back to the process through register 2 on DEC. Next, it bumps the program counter up to the next instruction to signify the system call is complete, restores the registers and restarts the program.
As an example, lets discuss an imaginary system call called clock().
Here is the way clock() works: when you issue the clock system call, the
OS sends a get_clock_time instruction to the clock device. The OS,
having nothing else to do until the clock device replies with the answer, noops.
The clock device, receiving a get_clock_time request, causes
a red light in a room down the hall to flash, signaling a human operator
to look at the wall clock in the room and type the time into a keypad. Once
the clock operator inputs the time, he hits a button and the clock device
generates an interrupt for the OS. The OS wakes up, finds the process
making the clock call, sets the register 2 value in the pcb to the time
returned by the device, and restarts the program. To the program, all that
appears to happen is it makes a clock() function call, and it returns the
wall clock time.

This example gives us a nice illustration of a couple of points about Operating Systems and processes. First, it shows us how the process is able to escape its own memory bounds and make requests for external information through system calls. Second, it shows us that processes and devices alike declare their needs using interrupts, which causes the OS to halt its behavior and deal with some important event. It also shows us how the OS is able to deal with the fact that devices do not take a predetermined amount of time to fulfil the OS's requests, and how the OS deals with this.
Lets go through the details of how this would work. Assume we have two processes, P1 and P2. P1 is currently running, and will shortly make an operating system request through the clock() system call discussed earlier. P2 is a new process that wants to count to one million, and will not be making any system requests. Be aware that both processes are described by their own PCB's and have their own unique state. P1 occupies the CPU, and runs for some time until it makes the clock() call and generates an interrupt. The OS catches an interrupt and saves P1's state in its PCB. It then determines that the interrupt was caused by a system call from the running process, P1, and looks in P1's registers to see that it wants to make a clock call. It forms a request to the clock device and initiates it, setting aside P1's PCB. It then loads P2's state onto the CPU, setting it's registers and declaring that it will be using P2's memory space. P2 runs for a while, but is stooped and saved when an interrupt occurs. The OS determines this interrupt comes from the clock device, and finds P1's PCB. It saves the result of the clock in P1's registers, and then restarts P1 on the CPU. This time, instead of sitting idle while P1 was blocked, we run P2 while the CPU is free.
The OS keeps track of the processes under its control by classifying them
in terms of their state. We see processes in the previous example in three
different states: running on the CPU (P1, P2, and P1 again), ready to run
(P2 while P1 is running), and waiting (P1 waiting on the clock). The OS
keeps a list associated with each of these states, and when a process
transitions from one state to another, it's PCB is removed from the first list and
put in the second. For instance, when an interrupt occurs, the OS saves
the current processes. If that process wants to make a request from a device,
the request is made, the running process's PCB is added to the blocked list, and
a process from the ready list moves to the running state. If the interrupt
was from a device, the running process is saved and put in the ready list
and the OS finds the process in the blocked list that was waiting on the
interrupt, writes the device results to the process, and puts that process
in the ready list as well. Then, a process is pulled of the ready list
and added to the running list.
This description is a general idea of what happens. For administrative purposes,
it is actually easier to divide the blocked list up into multiple lists... one
per device. Also, each device waiting list has an OS thread associated with it
that manages events on the list. When a process makes a request from a device,
the OS adds it to the waiting list for that device and signals the associated
thread that there is a process needing service. This thread wakes up, sees
there is a waiting request, and formulates a command to the device to service it.
Once the request is complete, the interrupt pops and the OS wakes up that thread
to indicate that the device has replies. The thread wakes up, turns the results
over to the process, and adds the process back to the ready list. If there
are more pending requests to that device, it starts to serve them as well. If not,
the thread goes to sleep until another request is made. This helps to assure fair
access to each device and simplifies making multiple requests to one device.
Here is an example, again with the clock system call. We have P1 and P2, who both intend to call the clock, and P3, who is going to count. P1 runs first and generates a system call interrupt. The OS sees that P1 made a system call request to the clock and adds P1 to the clock wait list and signals the clock device thread. The thread forms a request to the device and issues it. The OS then takes P2 out of the ready list and runs it until it generates another clock system call interrupt. P2 is passed to the clock wait list and the clock thread is signaled, but it knows there is already a pending request so it does nothing for now. P3 is run next, and runs until the clock device generates an interrupt. When the OS sees the interrupt, it tells the clock device thread that the clock has a result, and the clock thread wakes up, fulfils P1's request, and puts it in the ready list. It then sees the list is not empty and issues P2's request to the clock. Another processes is run until the clock produces another interrupt, at which point P2 is complete and returns to the ready list and another program is run.
In this way, multiple requests for a device can be outstanding, and they are taken care of as the device becomes free. Processes are not run before their requests are complete, and all requests will eventually be complete. And as long as there are runnable processes, the CPU is never idle. The one final thing we would like to do is have runnable processes share the CPU. To do this, we periodically stop the running processes and replace it with another ready processes. This is pretty easy with the help of the timer, a special piece of hardware that generates periodic interrupts. When the OS gets a timer interrupt it saves the running processes and determines the type of interrupt. If it is the timer, it just takes the running process and puts it in the ready list and makes one of the ready processes the new running process.
We have discussed the PCB in terms of the bare minimum details
that need to be kept around to save a program's state. These things are the
registers and the processes memory. However, there are a significant number
of other things that the OS needs to keep track of for each process. Most of
them are intuitive:
Of course, we all understand why the registers and memory have to be in the PCB. Also, some of the other fields are intuitively useful. The owner field can be used to determine the process's permissions... which files and devices it has access to, what processes it has control over, etc. Scheduling information seems like it could be important for the OS in determining when a program gets run. Accounting information is just an administrative tool for keeping track for how a processes has been behaving, and is mostly useful for charging users for resource usage and helping administrators configure the OS for the processes' usage patterns. I/O information is there so the OS can tell at a glance if the processes has any open files or devices, and is also used to make sure that when a processes exits, if it hasn't closed them the OS will do it.
However, the reason the state is kept in the PCB is less clear. We have already described a very nice mechanism for sorting PCB's into ready, waiting, and running lists. Why would we want to duplicate this information in the PCB? The reason is that sometimes processes or the OS wants to check on the status of a processes, but may not know where it is. It doesn't want to have to search through all the lists because that would be slow and because there is a chance the OS will miss it if the processes transitions while the OS is looking for it. One example that helps motivate this is the kill command, which allows one processes with the proper permission to cause another to exit. The kill command specifies a processes by process id, so the OS can find it without consulting the lists. However, if it just exits the processes and frees it, there may be problems if the processes was waiting on a response from a device. If the device interrupts and expects a processes to be there but it is not, there can be problems. By keeping track of state in the PCB, these problems can be avoided.
Basically, the PCB is used to keep track of any administrative details that the OS will want to know at a glance. This includes details that must be associated with a processes, as well as details that are difficult to figure out through other means. This allows the OS to manage processes smoothly and easily.
The last important detail we need to discuss is how the OS deals with memory...
its own and for the processes. A computer's memory space is composed of a
collection of chips designed to provide fast, volatile storage. To the computer,
this memory appears as an array of storage space, and each byte can be addressed
directly. The operating system is a program, and like any other it has a stack,
a heap, and a data segment that contains the OS code. Data structures used by
the OS like the PCB and state queues are kept in this memory space.
Usually, the OS is loaded
into the end of this memory array. The rest of the memory, from the beginning of
the space down to the OS's data segment, is used for processes memory.Processes know where their exact memory space lies through the use of base and limit values. The base refers to the beginning of the address space for that processes. The limit tells the processes how much space is available to it. When the processes runs on the CPU, it addresses memory that is in the range of 0 to limit. The CPU translates this to an actual memory address using the base register. So, when a processes modifies memory location x, the CPU modifies memory at base + x. The processes is not allowed to reference a memory address greater than limit, and if it tries to the CPU generates an interrupt to tell the OS what has happened.
Lets consider an OS with 8MB of data allocated for processes at the beginning of the memory space (from address 0x0 to 0x7FFFFF). If we want to run a single processes in this space, we would set the base value in the process's PCB to 0x0, and the limit to 0x800000. This is pretty straightforward, since any of the process's memory references are going to translate directly into physical memory (base (0x0) + x = x). The processes is not allowed to access memory addresses above 0x7FFFFF.
Now, what would we do if we instead wanted to run 4 processes in this OS. Each processes is going to have one 2MB slice of the total address space. Lets call these processes P1, P2, P3, and P4, and lets give them base values of 0x0, 0x200000, 0x400000, and 0x600000 respectively. All four processes have a limit of 2MB. When processes P3 is on the CPU, it is believes that it is accessing memory values between 0x0 and 0x1FFFFF. In reality, it is using addresses 0x400000 to 0x5FFFFF. When it stores a value at memory address 0x110204, it really is inserted at 0x510204. Now if P3 is descheduled and P2 is run, the CPU is now using memory addresses 0x200000 through 0x3FFFFF.
This is how the OS make sure each processes only uses its own memory space and no other. This system allows a descent amount of flexibility, as well. It may not always be the case the processes memory starts at 0x0, the OS may well be loaded in the front of the memory space instead. In KOS, as you know, processes memory is somewhere in the middle of the OS's addresses space starting at a variable called main_memory. However, using base and limit values makes this difference immaterial, since we can set them to arbitrary values and run anywhere in the allowed memory space.