Here is a state diagram of these three states with their transitions
This state diagram is perhaps the most important in the entire study
of Operating Systems, so it is important to understand what is
happening in each state and how a process transitions from
one state to another.
Here is what is happening at each transition.
- Ready to Running This event is triggered by
a kernel operation called the process scheduler or dispatcher, which selects
a process in the Ready state and starts running it.
- Running to Blocked This event would be triggered
when a running process attempts to execute an IO operation
such as a read or write or otherwise needs
a resource which is not currently available. Accessing
data from a disk takes several orders of magnitude longer
than executing statements in the CPU, and so it makes sense
for a process which is waiting for data from a disk to
give up the CPU to another process while it is waiting.
- Blocked to Ready A process is in the waiting state
when it is waiting for an event to occur, so when this event
occurs, it moves from Waiting to Ready. For example, a process
which was waiting for an IO operation would be unblocked when
the IO operation was completed and the data had been copied
to or from user space.
- Running to Ready A process moves from Running to
Ready when the Process Scheduler determines that there
is a process with a higher priority in the Ready queue.
In many operating systems, if there are several runnable processes,
each process is allocated a time slice or time quantum, and when
this time slice ends, the running process is replaced by
another process which is also ready to run, and the formerly
running process is returned to the ready queue.
- New to Ready When a new process is created, it is moved to the
ready state.
- Running to terminated There are two ways that a process can
terminate. A normal termination occurs when the process reaches the
end of main or calls the exit routine. An abnormal termination occurs
when a process encounters an abnormal condition from which it cannot
recover, such as a segmentation fault (memory access error) or when
the user manually terminates the process.
The process of switching from one process to another is
called Context Switching. If there are several runnable
processes, all of them are loaded into memory. To execute a
context switch, the
kernel has to store the contents of all of the registers
for the process which was formerly running, copy the
register values for the new process into the
registers, and start executing the next instruction of the
new process. Thus, context switching can be done quickly.
This scenario is an oversimplification.
The process scheduler is itself a process and so has to be swapped
into the running state, although it is in the kernel, and is
always in memory. Also, other kinds of interrupts may
occur. In particular, clock interrupts happen many times
a second, and so the running process has to be preempted to
handle the new interrupt.
Here is a table from the text showing
everything that happens when an interrupt occurs.
It should be obvious that multiprogramming can improve CUP utilization.
Here is a graph showing theoretical CPU utilization as a function of the number of
processes available to run (degree of multiprogramming), and the amount of time
that each process spends performing I/O operations (20%, 50%, or 80%).
If processes are I/O bound, that is, spend most of their time performing I/O operations,
many processes can be in memory at the same time.
The layout of a process in memory
Historically, a C program consists of the following pieces.
- text segment also known as the code segment
- the machine instructions executed by the CPU
- initialized data segment
- global variables which are initialized automatically
- uninitialized data segment
- (call the bss) global variables which are
not initialized (e.g. long sum[1000])
- stack
- storage for automatic variables along with other info
about a function call (i.e. return address, register values)
- heap
- memory for dynamic memory allocation. When a running program asks for
more memory by calling malloc in a C program or new in
a C++ program, the memory is found in the heap.
- user area
- maintains housekeeping info about the process (running time,
open file descriptors, how it responds to signals)
- page tables
- used by the memory management system
The Run Time Stack
The concept of the run time stack is important. The normal state of
affairs when a process is running is for a function to run for a while,
then this function calls another function. Execution stops in the calling
function and starts running in the called function. The called function may
in turn call another function. Each time a new function is called, a
new entity, called a stack frame is created and placed on
the run time stack. A stack frame contains the following:
- memory for arguments which are passed to the function
- memory for any automatic variables (variables which are
local to that function)
- the return address (the address of the first statement to
be executed after the function terminates. This will be in
the calling function, which will be the stack frame directly
below the current stack frame).
- the register values to be restored to the old function
when the current function terminates
Stack frames are stored on a stack (duh!), which is a
last-in-first-out data structure. A stack frame is created
whenever a new function is called and a stack frame is
destroyed (popped off the stack) when the currently running
function terminates, either because it executes a return
statement or it runs to the end of a void function.
Notice that both the stack and the heap vary in size during the
execution of a program. As the run time stack increases, it grows
upward, and as the heap increases, it grows downward.
File formats
The image of an executable which is stored on a disk usually contains
header information which tells the operating system how to load and
start the process. This is needed because when the executable image
is created, the exact memory address where it will be loaded is not
known. When the process is actually loaded, the system has to perform
some address relocation. This usually involves setting values
in various registers. To facilitate this, the header will contain
information on the various segments. It may also have a symbol
table.
There are a number of different formats for this. Many Unix systems
use the coff (Common Object File Format), but most implementations of
Linux use ELF(Executable and Linkable Format).
Windows also supports several file formats. The usual is the
.exe format. You may have noticed that even a very
simple program, which could not be more than a few lines of
executable code, results in a executable file of tens of thousands
of bytes. This is because of the extensive header information.
In the very early days of DOS (and CP/M), executables were often
in the .com format. This was very small, with essentially
no header information, but the size of the file was restricted to
one segment (64K).
Device drivers and other kernel code are often in the .sys
format. This is raw binary code with no relocation information.
The Process Control Blocks in the Kernel
The kernel needs a way to store housekeeping information about each current
process. This information is stored in a structure which is often called
a process control block. Typically when a new process is created, a new process
control block is created and the appropriate values are filled in. A typical
process control block might contain such fields as the following:
- Data for restarting the process
- Register values, including stack pointers, program status word,
program counter etc.
- Pointer to the page table or tables
- Process identification information
- Process Id
- Owner
- Parent process id
- command string
- Process group
- Housekeeping information
- start time
- total cpu time to date
- open file descriptors or handles
- current working directory
- root directory
- scheduling priority
- List of unhandled signals
- Thread information
Typically there is a finite number of process control blocks, and
so it is possible for a system to fill up its process control block
table. When this occurs, it will be impossible for new processes to
be created, and the system will not function properly.
Process creation and termination
All operating systems need a mechanism for creating new processes and
terminating processes. We will discuss system calls to do this in the next
section. When a new process is created, the system must allocate a new
process control block, fill in the initial values for it, allocate memory
for the new process, and copy at least some of the code for the new
process into memory.
A newly created process is put in the ready queue. The process scheduler
may decided that the newly created process has a higher priority and
thus it will preempt the currently running process, running the new
process immediately and placing the formerly running process on the ready
queue.
We can distinguish two types of process termination, normal and
abnormal. A normal termination of a process occurs either when
the program reaches the end of main or when it executes the exit
system call. Normal termination can only occur when the program
is in the running state. An abnormal termination occurs when the
program encounters an error condition from which it cannot
recover, such as a memory exception fault (a segmentation fault in
Unix jargon), or it is killed by another process. (You can generally kill
a currently running process by hitting Control-c).
Regardless of how a process is terminated, the operating system
will free up the memory used by the process, destroy the process control
block for that process, and, in some systems, notify the parent process.