Os Unit-II Notes
Os Unit-II Notes
Process
A process is a program in execution. For example, when we write a program in C or C++ and
compile it, the compiler creates binary code. The original code and binary code are both
programs. When we actually run the binary code, it becomes a process.
A process is an ‘active’ entity instead of a program, which is considered a ‘passive’ entity. A
single program can create many processes when run multiple times; for example, when we open
a .exe or binary file multiple times, multiple instances begin (multiple processes are created).
Process vs Program
The Text section is made up of the compiled program code, read in from non-volatile
storage when the program is launched.
The Data section is made up of the global and static variables, allocated and initialized
prior to executing the main.
The Heap is used for the dynamic memory allocation and is managed via calls to new,
delete, malloc, free, etc.
The Stack is used for local variables. Space on the stack is reserved for local variables
when they are declared.
Process States
Process states in operating system are a way to manage resources efficiently by keeping track
of the current state of each process. There are several process states in operating system, they
are:
New State: When a process is first created or is initialized by the operating system, it is in the
new state. In this state, the process is being prepared to enter the ready state.
Ready State: When a process is ready to execute, it is in the ready state. In this state, the
process is waiting for the CPU to be allocated to it so that it can start executing its
instructions. A process can remain in the ready state for an indeterminate period, waiting for
the CPU to become available.
Running State: When the CPU is allocated to a process, it enters the running state. In this
state, the process executes its instructions and uses system resources such as memory, CPU,
and I/O devices. Only one process can be in the running state at a time, and the operating
system determines which process gets access to the CPU using scheduling algorithms.
Waiting/Blocked State: Sometimes, a process needs to wait for a particular event, such as
user input or data from a disk drive. In such cases, the process enters the blocked state. In this
state, the process is not using the CPU, but it is not ready to run either. The process remains in
the blocked state until the event it is waiting for occurs.
Terminated State: The terminated state is reached when a process completes its execution or
terminates by the operating system. In this state, the process no longer uses any system
resources, and its memory space is deallocated.
Process State
Throughout its existence, each process goes through various phases. The present state of the
process is defined by the process state.
Process Number
When a new process is created by the user, the operating system assigns a unique ID i.e.
a process-ID to that process. This ID helps the process to be distinguished from other
processes existing in the system. The operating system has a limit on the maximum number of
processes it is capable of dealing with, let's say the OS can handle most N processes at a time.
So, process-ID will get the values from 0 to N-1.
Program Counter
The address of the next instruction to be executed is specified by the program counter. The
address of the program’s first instruction is used to initialize the program counter before it is
executed.
The value of the program counter is incremented automatically to refer to the next instruction
when each instruction is executed. This process continues till the program ends.
Registers
The registers vary in number and type, depending on the computer architecture. They include
accumulators, index registers, stack pointers, and general-purpose registers, plus any
condition- code information. Along with the program counter, this state information must be
saved when an interrupt occurs, to allow the process to be continued correctly afterward.
List of open files
This contains information about those files that are used by that process. This helps the
operating system close all the opened files at the termination state of the process.
Context Switching
A context switching is a mechanism that involves switching of the CPU from one process or
task to another. While performing switching the operating system saves the state of a running
process so it can be resumed later, and then loads the state of another process. A context
switching allows a single CPU to handle multiple process requests simultaneously without the
need for any additional processors.
Following are the reasons that describe the need for context switching in the Operating system.
1. If a high priority process falls into the ready queue, the currently running process will be shut
down or stopped by a high priority process to complete its tasks in the system.
2. If any running process requires I/O resources in the system, the current process will be
switched by another process to use the CPUs. And when the I/O requirement is met, the old
process goes into a ready state to wait for its execution in the CPU. Context switching stores
the state of the process to resume its tasks in an operating system. Otherwise, the process
needs to restart its execution from the initials level.
3. If any interrupts occur while running a process in the operating system, the process status is
saved as registers using context switching. After resolving the interrupts, the process switches
from a wait state to a ready state to resume its execution at the same point later, where the
operating system interrupted occurs.
.
The following steps describe the basic sequence of events when moving between processes:
1. The CPU executes Process 0.
2. A triggering event occurs, such as an interrupt or system call.
3. The system pauses Process 0 and saves its state (context) to PCB 0, the process control block
that was created for that process.
4. The system selects Process 1 from the queue and loads the process's state from PCB 1.
5. The CPU executes Process 1, picking up from where it left off (if the process had already been
running).
6. When the next triggering event occurs, the system pauses Process 1 and saves its state to PCB
1.
7. The Process 0 state is reloaded, and the CPU executes the process, once again picking up from
where it left off. Process 1 remains in an idle state until it is called again.
Advantage of Context Switching
Context switching is used to achieve multitasking. Multitasking gives an illusion to the users
that more than one process are being executed at the same time. But in reality, only one
task is being executed at a particular instant of time by a processor. Here, the context
Switching is so fast that the user feels that the CPU is executing more than one task at the same time.
The disadvantage of Context Switching
The disadvantage of context switching is that it requires some time for context switching i.e. the context
switching time. Time is required to save the context of one process that is in the running state and then
getting the context of another process that is about to come in the running state. During that time, there is
no useful work done by the CPU from the user perspective. So, context switching is pure overhead in this
condition.
Process Operations
The operations of process carried out by an operating system are primarily of two types:
1. Process creation
2. Process termination
Process creation
Process creation in operating systems (OS) is a fundamental operation that involves the initiation of
a new process. The creating process is called the parent while the created process is called the child.
Each child process may in turn create new child process. Every process in the system is identified
with a process identifier(PID) which is unique for each process. A process uses a System call to create
a child process.
Figure Below illustrates a typical process tree for the Solaris operating system, showing the name of
each process (daemon) and its pid.
In Linux and other Unix-like operating systems, a daemon is a background process that runs
independently of any interactive user session. Daemons typically start at system boot and run
continuously until the system is shut down. They perform various system-level tasks, such as
managing hardware devices, handling network requests, scheduling jobs, or any other kind of service
that needs to run in the background without direct user intervention
Daemon Operation
The init process is the first process started by the Linux/Unix kernel and
init
holds the process ID (PID) 1.
In Solaris, the process at the top of the tree is the sched process, with pid of 0. The sched process
creates several children processes-including pageout and fsflush. The sched process also creates the
init process, which serves as the root parent process for all user processes. we see two children of
init- inetd and dtlogin. When a user logs in, dtlogin creates an X-windows session (Xsession), which
in turns creates the sdt_shel process. (sdt_shel, part of the CDE (Common Desktop Environment), is
a graphical user interface tool designed to provide users with an easy way to access various system
functions and applications). Below sdt_shel, a user's command-line shell-the C-shell or csh-is created.
In this command line interface, the user can then invoke various child processes, such as the ls and
cat commands. We also see a csh process with pid of 7778 representing a user who has logged onto
the system using telnet. This user has started the Netscape browser (pid of 7785) and the emacs editor
(pid of 8105). (Emacs is a text editor which provides a robust platform for executing complex editing
tasks, writing code in various programming languages, managing files, reading emails, browsing the
web, and even playing games.)
The “exec()” system call is often used after a “fork()” call in order to replace the child
process’s code with a different program. This allows the child process to execute a
different program while preserving the parent process’s execution.
Together, the “fork()” and “exec()” system calls enable processes to create child processes and
replace their own execution with different programs, facilitating process creation,
concurrency, and program execution in operating systems.
A process begins its life when it is created. A process goes through different states before it gets
terminated.
The first state that any process goes through is the creation of itself. Process creation happens
through the use of fork() system call, (A system call is a function that a user program uses to ask the operating
system for a particular service .System calls serve as the interface between an operating system and a process )
which creates a new process(child process) by duplicating an existing one(parent process). The process that
calls fork() is the parent, whereas the new process is the child.
In most cases, we may want to execute a different program in child process than the parent. The
exec() family of function calls creates a new address space and loads a new program into it.
Finally, a process exits or terminates using the exit() system call. This function frees all the
resources held by the process(except for pcb).
A parent process can enquire about the status of a terminated child using wait() system call.
When the parent process uses wait() system call, the parent process is blocked till the child on which it is
waiting terminates.
System call interface for process management
A system call is a function that a user program uses to ask the operating system for a particular
service which cannot be carried out by normal function. System calls provide the interface between
the process and the operating system.
Example:
It makes a parent process stop its execution till the termination of the
wait
child process.
It makes a parent process stop its execution till the termination of the
waitpid
specified child process.(Multiple child process)
Parent and child process execute Parent process remains suspended till child
simultaneously. process completes its execution.
If the child process alters any page in the If child process alters any page in the address
address space, it is invisible to the parent space, it is visible to the parent process as they
process as the address space are separate. share the same address space.
Syntax:
pid=vfork();
Sample Program
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
/* fork a process */
pid_t p;
p = vfork();
/* the child and parent will execute every line of code after the fork (each separately) */
if(p == 0)
{
// Child process
printf("This is the child process. PID: %d\n", getpid());
printf("Child process is exiting with exit() \n");
exit(0);
}
else if(p > 0)
{
// Parent process might wait for child process to terminate
printf("This is the parent process. PID: %d\n", getpid());
}
else
printf("Error: fork() failed\n");
return 0;
}
output
This is the child process. PID: 91
Child process is exiting with exit()
This is the parent process. PID: 90
Wait()
This system call is used in processes that have a parent-child relationship. It makes a parent process
stop its execution till the termination of the child process. The program creates a child process via
the fork() system call and then calls the wait() system call to wait for the child process to finish its
execution.
Syntax
Below, we can see the syntax for the wait() system call. To use this in our C programs, we will have
to include the Standard C library sys/wait.h.
pid_t wait(int *status);
Parameters and return value
The system call takes one argument named status. This argument represents a pointer to an integer
that will store the exit status of the terminated child program. we can even replace status with NULL
parameter.
When the system call completes, it can return the following.
Process ID: The process ID of the child process that is terminated, which has the object
type pid_t.
Error value: If there is any error during system call execution, it will return -1, which can be
used for error handling.
Sample program
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{p
id-t p,childpid;
p = fork();
// code for the child process
if(p== 0){
printf("Child: I am running!!\n\n");
printf("Child: I have PID: %d\n\n", getpid());
exit();
}
// code for the parent process
else{
// print parent running message
printf("Parent: I am running and waiting for child to finish!!\n\n");
// call wait system call
childpid = wait(NULL);
// print the details of the child process
printf("Parent: Child finished execution!, It had the PID: %d,\n", childpid);
}
return 0;
}
Output:
Parent: I am running and waiting for child to finish!!
Child: I am running!!
Child: I have PID: 102
Parent: Child finished execution!, It had the PID: 102
waitpid()
This system call waits for a specific process to finish its execution. This system call can be
accessed using our C programs' library sys/wait.h.
Let's understand how the waitpid() function works with the help of a diagram.
The diagram shows that we have a parent process with 3 child processes, namely, Child1, Child2,
and Child3. If we want the parent to wait for a specific child, we could use the waitpid() function.
In the diagram above, we made the parent process wait for Child1 to finish its execution using the
function. When the parent process is called, the waitpid() function gets blocked by the operating
system and can't continue its execution till the specified child process Child1 is terminated.
If we were to replace the process in the waitpid() function with Child3, then the parent would only
continue when Child3 had terminated.
Syntax
pid_t waitpid(pid_t pid, int *status_ptr, int options);
Let's discuss the three arguments that we provide to the system call.
pid: Here, we provide the process ID of the process we want to wait for. If the provided pid is
0, it will wait for any arbitrary child to finish.
status_ptr: This is an integer pointer used to access the child's exit value. If we want to ignore
the exit value, we can use NULL here.
options: Here, we can add additional flags to modify the function's behavior. The various flags
are discussed below:
o WCONTINUED: It is used to report the status of any child process that has been
terminated and those that have resumed their execution after being stopped.
o WNOHANG: It is used when we want to retrieve the status information immediately
when the system call is executed. If the status information is not available, it returns
an error.
WUNTRACED: It is used to report any child process that has stopped or terminated.
Return value
The system call will return the process ID of the child process that was terminated. If there is any error while
waiting for the child process via the waitpid() system call, it will return-1, which corresponds to an error.
Sample Program
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
pid_t pids[2], wpid;
// Fork first child process
pids[0] = fork();
if (pids[0] == 0) {
// First child process
printf("First child process: PID = %d\n", getpid());
printf("First child process exiting\n");
exit(0); // First child exits
}
// Fork second child process
pids[1] = fork();
if (pids[1] == 0)
{
// Second child process
printf("Second child process: PID = %d\n", getpid());
printf("Second child process exiting\n");
exit(0); // Second child exits
}
if(pid[0]>0 && pid[1]>0)
{
// Parent process: wait specifically for the first child
wpid = waitpid(pids[1], NULL,0);
printf("Parent: Proceeding after the second child with pid =%d has finished.\n",pid[1]);
}
return 0;
}
Output
First child process: PID = 119
First child process exiting
Second child process: PID = 120
Second child process exiting
Parent: Proceeding after the second child with pid =120 has finished
exec()
The “exec()” system call is used to replace the current process with a new program or executable. It
loads a new program into the current process’s memory, overwriting the existing program’s code, data,
and stack.
When “exec()” is called, the operating system performs the following steps:
a. The current process’s memory is cleared, removing the old program’s instructions and data.
b. The new program is loaded into memory.
c. The program’s entry point is set as the starting point of execution.
d. The new program’s code begins execution.
The “exec()” system call is often used after a “fork()” call in order to replace the child process’s code
with a different program. This allows the child process to execute a different program while preserving
the parent process’s execution.
Exec system call is a collection of functions and in C programming language, the standard names for
These functions are as follows:
(l-represents list, v represents vector)
execl(): Executes a program specified by a pathname, and arguments are passed as a variable-length list of
strings terminated by a NULL pointer.
Syntax: int execl(const char *path, const char *arg, ..., NULL);
Parameters:
path: Specifies the path to the executable file which the process will run.
arg: Represents the list of arguments to be passed to the executable. The first argument
typically is the name of the program being executed. The list of arguments must be
terminated by NULL to indicate the end of the argument list.
Return Value:
On success, execl() does not return; the new program image takes over the process.
If an error occurs, it returns -1, and errno is set to indicate the error.
execv(): Executes a program specified by a pathname, and arguments are passed as an array of
strings terminated by a NULL pointer.
Syntax: int execv(const char *path, char *const argv[]);
Parameters:
path: Specifies the path to the executable file which the process will run.
argv: An array of pointers to null-terminated strings that represent the argument list available
to the new program. The first argument, by convention, should be the name of the executed
program. The array of pointers must be terminated by a NULL pointer.
Return Value:
On success, execv() does not return because the current process image is replaced by the new
program image.
On failure, it returns -1, and errno is set to indicate the error.
execlp(): Similar to execl(), but the program is searched for in the directories specified by the PATH
environment variable.
Syntax : int execlp(const char *file, const char *arg, ..., NULL);
Parameters:
file: The name of the executable file to run. If this name contains a slash (/), then execlp() will
treat it as a path and will not search the PATH environment variable.
arg: Represents the list of arguments to be passed to the executable. The first argument, by
convention, should be the name of the program being executed. The list of arguments must be
terminated by NULL to indicate the end of the argument list.
Return Value:
On success, execlp() does not return; the new program image takes over the process.
On failure, it returns -1, and errno is set to indicate the error.
execvp(): Similar to execv(), but the program is searched for in the directories specified by the
PATH environment variable.
Syntax: int execvp(const char *file, char *const argv[]);
Parameters:
file: The name of the executable file to run. If this name contains a slash (/), then execvp()
will treat it as a path and will not search the PATH environment variable.
argv: An array of pointers to null-terminated strings that represent the argument list available
to the new program. The first argument, by convention, should be the name of the executed
program. The array of pointers must be terminated by a NULL pointer to indicate the end of the
argument list.
Return Value:
On success, execvp() does not return because the current process image is replaced by the
new program image.
On failure, it returns -1, and errno is set to indicate the error.
execle(): Similar to execl(), but allows specifying the environment of the executed program as an array of
strings.
Syntax: int execle(const char *path, const char *arg, ..., char *const envp[]);
Parameters:
path: Specifies the path to the executable file which the process will run.
arg: Represents the list of arguments to be passed to the executable. The first argument, by
convention, should be the name of the program being executed. The list of arguments must be
terminated by NULL to indicate the end of the argument list.
...: A variable number of arguments representing the arguments to be passed to the executable,
terminated by a NULL pointer.
envp[]: An array of strings, conventionally of the form key=value, which are passed as the
environment of the new program. The array must be terminated by a NULL pointer.
Return Value:
On success, execle() does not return because the current process image is replaced by the new
program image.
On failure, it returns -1, and errno is set to indicate the error.
execve(): The most general form, which executes a program specified by a pathname, takes an
array of arguments and an array of environment variables.
Syntax: int execve(const char *pathname, char *const argv[], char *const envp[]);
Parameters:
pathname: Specifies the file path of the executable file which the process will run. This file
must be a binary executable or a script with a shebang (#!) line.
argv[]: An array of pointers to null-terminated strings that represent the argument list to be
passed to the new program. The first argument, by convention, should be the name of the
executed program. The array of pointers must be terminated by a NULL pointer.
envp[]: An array of strings, conventionally of the form key=value, which are passed as the
environment of the new program. The array must be terminated by a NULL pointer.
Return Value:
On success, execve() does not return because the current process image is replaced by the new
program image.
On failure, it returns -1, and errno is set to indicate the error.
Sample Program
include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// Child process
printf("Child process (PID %d): executing '/bin/ls'\n", getpid());
// Replace the child process with '/bin/ls'
// execl() takes the path to the program, the name of the program,
// and any command-line arguments, ending with a NULL pointer.
execl("/bin/ls", "ls", NULL);
}
}
else
{ //Parent process
printf("Parentprocess(PID%d):waitingforchildprocess\n",getpid());
}
}
output
Parent process(PID142):waiting for child process Child process
(PID 143): executing '/bin/ls'
a.out exec.c fork.c vfork.c wait.c waitpid waitpid.c
exit( )
● A process terminates when it finishes executing its final statement and asks the operating system to delete it
by using the exit( )system call.
Syntax:
exit();
Orphan Process:
A child process that remains running even after its parent process is terminated or completed without waiting for the
child process execution is called an orphan. A process becomes an orphan unintentionally. Some time intentionally
becomes orphans due to long-running time to complete the assigned task without user attention. The orphan process
has controlling terminals.
Zombie Process:
A Zombie is a process that has completed its task but still, it shows an entry in a process table. The zombie process
usually occurred in the child process. Very short time the process is a zombie. After the process has completed all of
its tasks it reports the parent process that it has about to terminate.
Process Scheduling
To increase CPU utilization, multiple processes are loaded into the memory of the CPU and a process
is selected from these processes. Loading multiple processes into the main memory is called multiprogramming
and the act of determining which process is in the ready state, and should
be moved to the running state is known as Process Scheduling.
The goal of process scheduling is to achieve efficient utilization of the CPU, fast response time and fair allocation of
resources among all processes.
Process Scheduling Queues
There are multiple states a process has to go through during execution. The OS maintains a separate
queue for each state along with the process control blocks (PCB) of all processes. The PCB moves to a
new state queue, after being unlinked from its current queue, when the state of a process changes.
These process scheduling queues are:
1. Job Queue – In starting, all the processes get stored in the job queue. It is maintained in the
secondary memory.
2. Ready Queue – This queue keeps a set of all processes residing in main memory, ready and
waiting to execute. A new process is always put in this queue. Ready queue is maintained in
primary memory..
3. Device Queue – When the process needs some IO operation in order to complete its execution,
OS changes the state of the process from running to waiting. The context (PCB) associated with
the process gets stored on the waiting queue which will be used by the Processor when the
process finishes the IO. Each IO Device will have a separate device Queue
A queuing diagram as shown in the figure below represents the scheduling process. There are Two
queues – ready queue and device queue. A new process is first admitted into the ready queue. It waits until it is
dispatched (allocated to the CPU for execution).
Jobqueue
request .
If the current process terminates.
When the scheduler needs to move a process from running to ready state as it has already
to the ready state. So, the scheduler can decide to replace the currently-running process
with a newly-ready one.
There are 3 kinds of schedulers-
1. Long-term Scheduler
It selects a balanced mix of I/O bound and CPU bound processes from the secondary memory
(new state).
CPU Bound Jobs: CPU-bound jobs are tasks or processes that necessitate a significant amount
of CPU processing time and resources (Central Processing Unit). These jobs can put a significant strain
on the CPU, affecting system performance and responsiveness.
I/O Bound Jobs: I/O bound jobs are tasks or processes that necessitate a large number of
input/output (I/O) operations, such as reading and writing to discs or networks. These jobs are less
dependent on the CPU and can put a greater strain on the system’s I/O subsystem.
Then, it loads the selected processes into the main memory (ready state) for execution.
2. Short-term Scheduler
It decides which process to execute next from the ready queue.
After short-term scheduler decides the process, Dispatcher assigns the decided process to the
3. Medium-term Scheduler
Medium-term scheduler swaps-out the processes from main memory to secondary memory to
When a running process makes an I/O request it becomes suspended i.e., it cannot be
completed. Thus, in order to remove the process from the memory and make space for others, the
suspended process is sent to the secondary storage. This is known as swapping, and the process that
goes through swapping is said to be swapped out or rolled out.
After some time when main memory becomes available, medium-term scheduler swaps-in the
swapped-out process to the main memory and its execution is resumed from where it left off.
The major differences between long term, medium term and short term scheduler are as follows -
Long term scheduler Medium term scheduler Short term scheduler
Short term scheduler is
Long term scheduler is a job Medium term is a process of
called a CPU
scheduler. swapping schedulers.
scheduler.
The speed of medium term
The speed of long term is is in The speed of short term is
lesser than between short and long fastest
the short term. term among the other two.
scheduler.
The short term provides
Long term controls the Medium term reduces the
lesser
degree of degree of
control over the degree of
multiprogramming. multiprogramming.
multiprogramming.
The long term is almost nil The medium term is a part Short term is also a minimal
or minimal of the time
in the time sharing system. time sharing system. sharing system.
The long term selects the Medium term can
processes reintroduce the Short term selects those
from the pool and loads process into memory and processes
them into execution that are ready to execute.
memory for execution. can be continued.
CPU Scheduling
CPU Scheduling is a process that allows one process to use the CPU while another process is delayed
due to unavailability of any resources such as I / O etc, thus making full use of the CPU. Whenever
the CPU becomes idle, the operating system must select one of the processes in the ready queue to be executed.
The selection process is carried out by the short-term scheduler (or CPU scheduler). The
scheduler selects a process from the processes in memory that are ready to execute and allocates the
CPU to that process.
(depending on the algorithm), the running process is interrupted, and it switches to the ready
state and joins the ready queue.
Also, whenever a process requires an I/O operation or some blocked resource, it switches to
the waiting state. On completion of I/O or receipt of resources, the process switches to the
ready state and joins the ready queue.
Preemptive scheduling has a lot of advantages:
It ensures no process can monopolize the CPU
Gives room for reconsideration of choice after every interruption, so if priorities change, a
Throughput
Turnaround time
Waiting time
Response time
CPU utilization: The main objective of any CPU scheduling algorithm is to keep the CPU as busy as
possible. Theoretically, CPU utilization can range from 0 to 100 but in a real-time system, it varies
from 40 to 90 percent depending on the load upon the system.
Throughput: A measure of the work done by the CPU is the number of processes being executed and
completed per unit of time. This is called throughput. The throughput may vary depending on the
length or duration of the processes.
Arrival time: The arrival time of a process is when a process is ready to be executed, which means it is
finally in a ready state and is waiting in a queue for its turn to be executed.
Burst Time: The burst time of a process is the number of time units it requires to be executed by the
CPU
Completion Time: The completion time is the time when the process stops executing, which means
that the process has completed its burst time and is completely executed.
Turnaround time: For a particular process, an important criterion is how long it takes to execute that
process. The time elapsed from the time of submission of a process to the time of completion is
known as the turnaround time. Turn-around time is the sum of times spent waiting to get into
memory, waiting in the ready queue, executing in CPU, and waiting for I/O.
Turn Around Time = Completion Time – Arrival Time.
Waiting time-:A scheduling algorithm does not affect the time required to complete the process once
it starts execution. It only affects the waiting time of a process i.e. time spent by a process waiting in
the ready queue.
Waiting Time = Turnaround Time – Burst Time.
Response time :In an interactive system, turn-around time is not the best criterion. A process may
produce some output fairly early and continue computing new results while previous results are
being output to the user. Thus another criterion is the time taken from submission of the process of
the request until the first response is produced. This measure is called response time.
Response Time = CPU Allocation Time(when the CPU was allocated for the first) – Arrival Time
The aim of the scheduling algorithm is to maximize and minimize the following:
Maximize:
CPU utilization - It makes sure that the CPU is operating at its peak and is busy.
Throughput - It is the number of processes that complete their execution per unit of time.
Minimize:
Waiting time- It is the amount of waiting time in the queue.
Response time- Time retired for generating the first request after submission.
Advantages of FCFS:
The following are some benefits of using the FCFS scheduling algorithm:
1. The job that comes first is served first.
2. It is the CPU scheduling algorithm’s simplest form.
3. It is quite easy to program.
Disadvantages
1. It is non-pre-emptive algorithm, which means the process priority doesn't matter.
If a process with very least priority is being executed, more like daily routine backup process,
which takes more time, and all of a sudden, some other high priority process arrives, like interrupt
to avoid system crash, the high priority process will have to wait, and hence in this case, the
system will crash, just because of improper process scheduling.
2. In FCFS, the Average Waiting Time is comparatively high.
3. Resources utilization in parallel is not possible, which leads to Convoy Effect, (Convoy Effect
is a situation where many processes, who need to use a resource for short time are blocked by one
process holding that resource for a long time and hence poor resource(CPU, I/O etc) utilization.
Example 1 -FCFS Scheduling:(Without Arrival Times)
TAT=Completion Time-Arrival Time
Waiting Time=Turn Around Time-Burst
Time
Response Time =First Response - Arrival Time
Process Burst time(milliseconds)
P1 5
P2 24
P3 16
P4 10
P5 3
Gantt Chart for FCFS: (Generalized Activity Normalization Time Table (GANTT))
A Gantt chart is a horizontal bar chart used to represent operating systems CPU scheduling in
graphical view that help to plan, coordinate and track specific CPU utilization factor like
throughput, waiting time, turnaround time etc.
0 = 55
Average Response Time => (0+5+29+45+55)/5 =>25ms
Process Burst Completion Turnaround Waiting Response
time(milliseconds) Time Time Time time
P1 5 5 5 0 0
P2 24 29 29 5 5
P3 16 45 45 29 29
P4 10 55 55 45 45
P5 3 58 58 55 55
First Come FirstServe:(With Arrival Times)
TAT=Completion Time-Arrival Time
Waiting Time=Turn Around Time-Burst
Time First Response - Arrival Time
P1 3 0
P2 6 2
P3 4 4
P4 5 6
P5 2 8
Gantt Chart
P2 6 2 9 7 1 1
P3 4 4 13 9 5 5
P4 5 6 18 12 7 7
P5 2 8 20 12 10 10
Non-Premptive Mode(Example)
P1 3 1
P2 1 4
P3 4 2
P4 0 6
P5 2 3
Exit
Process Id time/Finishing/Completio Turn Around time Waiting time
n Time
P1 7 7–3=4 4–1=3
P2 16 16 – 1 = 15 15 – 4 = 11
P3 9 9–4=5 5–2=3
P4 6 6–0=6 6–6=0
P5 12 12 – 2 = 10 10 – 3 = 7
Now,
Average Turn Around time = (4 + 15 + 5 + 6 + 10) / 5 = 40 / 5 = 8 unit
Average waiting time = (3 + 11 + 3 + 0 + 7) / 5 = 24 / 5 = 4.8 unit
Advantages
According to the definition, short processes are executed first and then followed by longer processes.
The throughput is increased because more processes can be executed in less amount of time.
Disadvantages:
The time taken by a process must be known by the CPU beforehand, which is not possible.
Longer processes will have more waiting time, eventually they'll suffer starvation.
P1 0 7
P2 1 5
P3 2 3
P4 3 1
P5 4 2
P6 5 1
Gantt Chart-
Now, we know-
Turn Around time = Exit time – Arrival time
Waiting time = Turn Around time – Burst time
P1 19 19 – 0 = 19 19 – 7 = 12
P2 13 13 – 1 = 12 12 – 5 = 7
P3 6 6–2=4 4–3=1
P4 4 4–3=1 1–1=0
P5 9 9–4=5 5–2=3
P6 7 7–5=2 2–1=1
Advantages
Processes are executed faster than SJF, being the preemptive version of it.
Disadvantages
Context switching is done a lot more times and adds to the overhead time.
Like SJF, it may still lead to starvation and requires the knowledge of process time
beforehand.
Impossible to implement in interactive systems where the required CPU time is unknown.
Priority scheduling in OS is the scheduling algorithm that schedules processes according to the
priority assigned to each of the processes. Higher priority processes are executed before lower
priority process
Priority of processes depends on some factors such as:
Time limit
Memory requirements of the process
Ratio of average I/O to average CPU burst time
There can be more factors on the basis of which the priority of a process/job is determined.
This priority is assigned to the processes by the scheduler. These priorities of processes are
represented as simple integers in a fixed range such as 0 to 7, or maybe 0 to 4095. These
numbers depend on the type of system.
Note : Generally, the process with the Larger integer number will have low priority, and the process
with the smaller integer value will have high priority.
1 2 0 3
2 6 2 5
3 3 1 4
4 5 4 2
5 7 6 9
6 4 5 4
7 10 7 10
1 2 0 3 3 3 0 0
2 6 2 5 18 16 11 13
3 3 1 4 7 6 2 3
4 5 4 2 13 9 7 11
5 7 6 9 27 21 12 18
6 4 5 4 11 6 2 7
7 10 7 10 37 30 18 27
1 2(L) 0 1
2 6 1 7
3 3 2 3
4 5 3 6
5 4 4 5
6 10(H) 5 15
7 9 15 8
At time 1, P2 arrives. P1 has completed its execution and no other process is available at this
time hence the Operating system has to schedule it regardless of the priority assigned to it.
The Next process P3 arrives at time unit 2, the priority of P3 is higher to P2. Hence the execution
of P2 will be stopped and P3 will be scheduled on the CPU.
During the execution of P3, three more processes P4, P5 and P6 becomes available. Since, all
these three have the priority lower to the process in execution so P3 can't preempt the process. P3
will complete its execution and then P5 will be scheduled with the priority highest among the
available processes.
Meanwhile the execution of P5, all the processes got available in the ready queue. At this point,
the algorithm will start behaving as Non Preemptive Priority Scheduling. Hence now, once all the
processes get available in the ready queue, the OS just took the process with the highest priority
and execute that process till completion. In this case, P4 will be scheduled and will be executed
till the completion.
Since P4 is completed, the other process with the highest priority available in the ready queue is
P2. Hence P2 will be scheduled next.
P2 is given the CPU till the completion. Since its remaining burst time is 6 units hence P7 will be
scheduled after this.
The only remaining process is P6 with the least priority, the Operating System has no choice unless of
executing it. This will be executed at the last.
The Completion Time of each process is determined with the help of GANTT chart. The
turnaround time and the waiting time can be calculated by the following formula.
1. Turnaround Time = Completion Time - Arrival Time
2. Waiting Time = Turn Around Time - Burst Time
1 2 0 1 1 1 0
2 6 1 7 22 21 14
3 3 2 3 5 3 0
4 5 3 6 16 13 7
5 4 4 5 10 6 1
6 10 5 15 45 40 25
7 9 6 8 30 24 16
In the above example, we have taken 4 processes P1, P2, P3, and P4 with an arrival time of 0,1,2,
and 4 respectively. They also have burst times 5, 4, 2, and 1 respectively. Now, we need to create
two queues the ready queue and the running queue which is also known as the Gantt chart.
Step 1: first, we will push all the processes in the ready queue with an arrival time of 0. In this
example, we have only P1 with an arrival time of 0.
This is how queues will look after the completion of the first step.
Step 2: Now, we will check in the ready queue and if any process is available in the queue then we
will remove the first process from the queue and push it into the running queue. Let’s see how the
queue will be after this step.
In the above image, we can see that we have pushed process P1 from the ready queue to the running
queue. We have also decreased the burst time of process P1 by 2 units as we already executed 2 units
of P1.
Step 3: Now we will push all the processes arrival time within 2 whose burst time is not 0.
In the above image, we can see that we have two processes with an arrival time within 2 P2 and P3 so,
we will push both processes into the ready queue. Now, we can see that process P1 also has remaining
burst time so we will also push process P1 into the ready queue again.
Step 4: Now we will see if there are any processes in the ready queue waiting for execution. If there is
any process then we will add it to the running queue.
In the above image, we can see that we have pushed process P2 from the ready queue to the running
queue. We also decreased the burst time of the process P2 as it already executed 2 units.
Step 5: Now we will push all the processes arrival time within 4 whose burst time is not 0.
In the above image, we can see that we have one process with an arrival time within 4 P4 so, we
will push that process into the ready queue. Now, we can see that process P2 also has remaining
burst time so we will also push process P2 into the ready queue again.
Step 6: Now we will see if there are any processes in the ready queue waiting for execution. If there
is any process then we will add it to the running queue.
In the above image, we can see that we have pushed process P3 from the ready queue to the running
queue. We also decreased the burst time of the process P3 as it already executed 2 units. Now,
process P3’s burst time becomes 0 so we will not consider it further.
Step 7: Now we will see if there are any processes in the ready queue waiting for execution. If there
is any process then we will add it to the running queue.
In the above image, we can see that we have pushed process P1 from the ready queue to the running
queue. We also decreased the burst time of the process P1 as it already executed 2 units.
Step 8: Now we will push all the processes arrival time within 8 whose burst time is not 0.
In the above image, we can see that process P1 also has a remaining burst time so we will also push
process P1 into the ready queue again.
Step 9: Now we will see if there are any processes in the ready queue waiting for execution. If there
is any process then we will add it to the running queue.
In the above image, we can see that we have pushed process P4 from the ready queue to the running
queue. We also decreased the burst time of the process P4 as it already executed 1 unit. Now,
process P4’s burst time becomes 0 so we will not consider it further.
Step 10: Now we will see if there are any processes in the ready queue waiting for execution. If
there is any process then we will add it to the running queue.
In the above image, we can see that we have pushed process P2 from the ready queue to the running
queue. We also decreased the burst time of the process P2 as it already executed 2 units. Now,
process P2’s burst time becomes 0 so we will not consider it further.
Step 11: Now we will see if there are any processes in the ready queue waiting for execution. If
there is any process then we will add it to the running queue.
In the above image, we can see that we have pushed process P1 from the ready queue to the running
queue. We also decreased the burst time of the process P1 as it already executed 1 unit. Now,
process P1’s burst time becomes 0 so we will not consider it further. Now our ready queue is empty
so we will not perform any task now.
After performing all the operations, our running queue also known as the Gantt chart will look like
the below.
Let’s calculate the other terms like Completion time, Turn Around Time (TAT), Waiting Time
(WT), and Response Time (RT). Below are the equations to calculate the above terms.
Turn Around Time = Completion Time – Arrival
Time
Waiting Time = Turn Around Time – Burst
Time
Response Time = CPU first time – Arrival Time
Let’s calculate all the details for the above example.
Advantages-
It gives the best performance in terms of average response time.
It is best suited for time sharing system, client server architecture and interactive system.
It doesn’t face the issues of starvation or convoy effect.
All the jobs get a fair allocation of CPU.
It deals with all process without any priority
Disadvantages-
It leads to starvation for processes with larger burst time as they have to repeat the cycle
many times.
Its performance heavily depends on time quantum.
Lower time quantum results in higher the context switching overhead in the system and
Higher time quantum makes it as FCFS
Finding a correct time quantum is a quite difficult task in this system.
Priorities can not be set for the processes.
Multilevel queue scheduling
Multilevel queue scheduling is used when processes in the ready queue can be divided into different
Example
Suppose we have five following processes of different nature. Let's schedule them using a multilevel
queue scheduling:
Process IDProcess Type Description
Solution
Let's handle this process scheduling using multilevel queue scheduling. To understand this better, look
at the figure below:
The above illustration shows the division of queues based on priority. Every single queue has an
absolute priority over the low-priority queue, and no other process is allowed to execute until the high-
priority queues are empty. For instance, if the interactive editing process enters the ready queue while
the batch process is underway, then the batch process will be preempted
Advantages
1. You can use multilevel queue scheduling to apply different scheduling methods to
distinct processes.
2. It will have low overhead in terms of scheduling.
Disadvantages
The steps involved in MLFQ scheduling when a process enters the system.
1. When a process enters the system, it is initially assigned to the highest priority queue.
2. The process can execute for a specific time quantum in its current queue.
3. If the process completes within the time quantum, it is removed from the system.
4. If the process does not complete within the time quantum, it is demoted to a lower priority
queue and given a shorter time quantum.
5. This promotion and demotion process continues based on the behavior of the processes.
6. The high-priority queues take precedence over low-priority queues, allowing the latter
processes to run only when high-priority queues are empty.
7. The feedback mechanism allows processes to move between queues based on their
execution behavior.
8. The process continues until all processes are executed or terminated.
Example of MLFQ
There are various scheduling algorithms that can be applied to each queue, including FCFS, shortest
remaining time, and round robin. Now, we will explore a multilevel feedback queue configuration
consisting of three queues.
Queue 1 (Q1) follows a round robin schedule with a time quantum of 8 milliseconds.
Queue 2 (Q2) also uses a round robin schedule with a time quantum of 15 milliseconds.
Finally, Queue 3 (Q3) utilizes a first come, first serve approach.
Additionally, after 110 milliseconds, all processes will be boosted to Queue 1 for high-priority
execution.
The multilevel feedback queue scheduler has the following parameters:
The number of queues in the system.
The scheduling algorithm for each queue in the system.
The method used to determine when the process is upgraded to a higher-priority queue.
The method used to determine when to demote a queue to a lower - priority queue.
The method used to determine which process will enter in queue and when that process needs
service.
Advantages of Multilevel Feedback Queue Scheduling:
It is more flexible.
It allows different processes to move between different queues.
It prevents starvation by moving a process that waits too long for the lower priority queue to the
higher priority queue.
Disadvantages of Multilevel Feedback Queue Scheduling:
The selection of the best scheduler, it requires some other means to select the values.
It produces more CPU overheads.
It is the most complex algorithm.
Introduction to Threads
Thread is the smallest executable unit of a process. A thread is often referred to as a lightweight process due to
its ability to run sequences of instructions independently while sharing the same memory space and resources of
a process. A process can have multiple threads. Each thread will have their own task and own path of execution in
a process. Threads are popularly used to improve the application through parallelism . Actually only one thread is
executed at a time by the CPU, but the CPU switches rapidly between the threads to give an illusion that the
threads are running parallelly.
For example, in a browser, multiple tabs can be different threads or While the movie plays on the
device, various threads control the audio and video in the background.
Another example is a web server - Multiple threads allow for multiple requests to be satisfied
simultaneously, without having to service requests sequentially or to fork off separate processes for
every incoming request.
Components of Threads in Operating System
The Threads in Operating System have the following three components.
Stack Space
Register Set
Program Counter
The given below figure shows the working of a single-threaded and a multithreaded process:
A single-threaded process is a process with a single thread.
A multi-threaded process is a process with multiple threads. As the diagram clearly shows that the multiple
threads in it have its own registers, stack, and counter but they share the code and data segment.
Process simply means any program in execution while the thread is a segment of a process. The main
differences between process and thread are mentioned below:
Process Thread
A Process simply means any program in Thread simply means a segment of a
execution. process.
The process takes more time to terminate The thread takes less time to terminate.
Benefits
The benefits of multithreaded programming can be broken down into four major categories:
Resource Sharing
Processes may share resources only through techniques such as-Message Passing, Shared Memory
Such techniques must be explicitly organized by programmer. However, threads share the memory and the
resources of the process to which they belong by default. A single application can have different threads within
the same address space using resource sharing.
Responsiveness
Program responsiveness allows a program to run even if part of it is blocked using multithreading. This can
also be done if the process is performing a lengthy operation. For example - A web browser with
multithreading can use one thread for user contact and another for image loading at the same time.
Utilization of Multiprocessor Architecture
In a multiprocessor architecture, each thread can run on a different processor in parallel using
multithreading. This increases concurrency of the system. This is in direct contrast to a single processor system,
where only one process or thread can run on a processor at a time.
Economy
It is more economical to use threads as they share the process resources. Comparatively, it is more
expensive and time-consuming to create processes as they require more memory and resources. The
overhead for process creation and management is much higher than thread creation and management.
Thread Types
Threads are of two types. These are described below.
User Level Thread
Because of the presence of only Program Counter, Register Set, and Stack Space, it has a
simple representation.
Disadvantages of User-Level Threads
There is a lack of coordination between Thread and Kernel.
Whenever any process requires more time to process, Kernel-Level Thread provides more
time to it.
Disadvantages of Kernel-Level threads
Kernel-Level Thread is slower than User-Level Thread.
Implementation of this type of thread is a little more complex than a user-level thread.
If one user level thread perform blocking If one kernel level thread perform
operation then entire process will be blocking operation then another thread
blocked can continue execution.
There are also hybrid models that combine elements of both user-level and kernel-level threads. For example,
some operating systems use a hybrid model called the “two-level model”, where each process has one or more
user-level threads, which are mapped to kernel-level threads by the operating system.
Advantages
Hybrid models combine the advantages of user-level and kernel-level threads, providing greater flexibility and
control while also improving performance.
Hybrid models can scale to larger numbers of threads and processors, which allows for better use of available
resources.
Disadvantages:
Hybrid models are more complex than either user-level or kernel-level threading, which can make them more
difficult to implement and maintain.
Hybrid models require more resources than either user-level or kernel-level threading, as they require both a
thread library and kernel-level support.
User threads are mapped to kernel threads by the threads library. The way this mapping is done is called the
thread model. Multi threading model are of three types.
Many to Many Model
In this model, we have multiple user threads multiplex to same or lesser number of kernel level threads. Number
of kernel level threads are specific to the machine, advantage of this model is if a user thread is blocked we can
schedule others user thread to other kernel thread. Thus, System doesn’t block if a particular thread is blocked. It
is the best multi threading model.
Threading Issues in OS
In a multithreading environment, there are many threading-related problems. Such as
System Call
Thread Cancellation
Signal Handling
Thread Pool
Scheduler Activation
Thread Pool
The server develops an independent thread every time an individual attempts to access a
page on it. However, the server also has certain challenges. Bear in mind that no limit in the number
of active threads in the system will lead to exhaustion of the available system resources because we
will create a new thread for each request.
The establishment of a fresh thread is another thing that worries us. The creation of a new
thread should not take more than the amount of time used up by the thread in dealing with the
request and quitting after because this will be wasted CPU resources.
Hence, thread pool could be the remedy for this challenge. The notion is that as many fewer
threads as possible are established during the beginning of the process. A group of threads that forms
this collection of threads is referred as a thread pool. There are always threads that stay on the
thread pool waiting for an assigned request to service.
A new thread is spawned from the pool every time an incoming request reaches the server,
which then takes care of the said request. Having performed its duty, it goes back to the pool and
awaits its second order.
Whenever the server receives the request, and fails to identify a specific thread at the ready
thread pool, it may only have to wait until some of the threads are available at the ready thread pool.
It is better than starting a new thread whenever a request arises because this system works well with
machines that cannot support multiple threads at once.
Scheduler Activation
A final issue to be considered with multithreaded programs concerns communication between
the kernel and the thread library, which may be required by the many-to-many and two-level models .Many
systems implementing either the many-to-many or the two-level model place an intermediate
data structure between the user and kernel threads. This data structure-typically known as a
lightweight process, or LWP-is shown in Figure.
To the user-thread library, the LWP appears to be a virtual processor on which the application
can schedule a user thread to run. Each LWP is attached to a kernel thread, and it is kernel threads that the
operating system schedules to run on physical processors.
If a kernel thread blocks (such as while waiting for an I/0 operation to complete), the LWP blocks
as well. Up the chain, the user-level thread attached to the LWP also blocks.
An application may require any number of LWPs to run efficiently. Consider a CPU-bound
application running on a single processor. In this scenario, only one thread can run at once, so one LWP is
sufficient. An application that is I/O intensive may require multiple LWPs to execute. Typically, an LWP is required
for each concurrent blocking system call. Suppose, for example, that five different file-read requests occur
simultaneously. Five LWPs are needed, because all could be waiting for I/0 completion in the kernel. If a process
has only four LWPs, then the fifth request must wait for one of the LWPs to return from the kernel.
Furthermore, the kernel must inform an application about certain events. This procedure
is known as an Upcall. Upcalls are handled by the thread library with an upcall handlers and upcall
handlers must run on a virtual processor. One event that triggers an upcall, occurs when an
application thread is about to block. In this scenario, the kernel makes an upcall to the application
informing it that a thread is about to block and identifying the specific thread.
The kernel then allocates a new virtual processor to the application. The application runs an
upcall handler on this new virtual processor, which saves the state of the blocking thread and
relinquishes the virtual processor on which the blocking thread is running. The upcall handler then
schedules another thread that is eligible to run on the new virtual processor.
When the event that the blocking thread was waiting for occurs, the kernel makes another
upcall to the thread library informing it that the previously blocked thread is now eligible to run. The
up call handler for this event also requires a virtual processor, and the kernel may allocate a new
virtual processor or preempt one of the user threads and run the upcall handler on its virtual
processor.
Process Synchronization
The shared buffer is implemented as a circular array with two logical pointers: in and out.
The variable―in‖points to the next free position in the buffer and―out‖points to the first full position in the
buffer.
An integer variable counter is initialized to 0. Counter is incremented every time we add a new item to
the buffer and is decremented every time we remove one item from the buffer.
The buffer is empty when counter== 0 and the buffer is full when counter==Buffer_size.
The producer process has a local variable next_produced in which the new item to be produced is stored.
The consumer process has a local variable next_consumed in which the item to be consumed is stored.
Race Condition
When more than one process is executing the same code or accessing the same memory or any shared
variable in that condition there is a possibility that the output or the value of the shared variable is wrong
so for that all the processes doing the race to say that my output is correct this condition known as a
race condition.
Example :
Suppose we have two process Producer and Consumer and counter variable is shared between these two.
Initial Value of Counter is 5.
Producer Routine
register1 = counter
register1 = register1 + 1
counter= register1
Consumer Routine
register2 = counter
register2 = register2 - 1
counter= register2
Although both the producer and consumer routines above are correct separately, they may not function
correctly when executed concurrently.
The concurrent execution of "counter++" and "counter--" is equivalent a sequential execution in which the
lower-level statements presented previously are interleaved in some arbitrary order (but the order within
each high-level statement is preserved). One such interleaving is To:
Notice that we have arrived at the incorrect state "counter == 4", indicating that four buffers are full,
when, in fact, five buffers are full. If we reversed the order of the statements at T4 and T5, we would arrive
at the incorrect state "counter== 6". We would arrive at this incorrect state because we allowed both
processes to manipulate the variable counter concurrently. The only correct result, though, is counter ==
5, which is generated correctly if the producer and consumer execute separately.
Critical Section Problem
The regions of a program that try to access shared resources and may cause race conditions are called critical
section. To avoid race condition among the processes, we need to assure that only one process at a time can
execute within the critical section.
Process Synchronization is the coordination of execution of multiple processes in a multi- process system to
ensure that they access shared resources in a controlled and predictable manner. On the basis of
synchronization, processes are categorized as one of the following two types:
Independent Process: The execution of one process does not affect the execution of
other processes.
Cooperative Process: A process that can affect or be affected by other processes
executing in the system.
Process synchronization problem arises in the case of Cooperative processes also because resources are
shared in Cooperative processes.
The critical-section problem is to design a protocol that the processes can use to cooperate. Each process
must request permission to enter its critical section. The section of code implementing this request is the
entry Section The critical section may be followed by an exit Section. The remaining code is the remainder
Section.
The general structure of a typical process Pi is shown in figure
Any solution to the critical section problem must satisfy three requirements:
Mutual Exclusion: If a process is executing in its critical section, then no other process is
allowed to execute in the critical section.
Progress: If no process is in the critical section and there exist some processes that wish to enter
their critical section, then the selection of the processes that will enter the critical section next
cannot be postponed indefinitely. Essentially, the system must guarantee that every process can
eventually enter its critical section.
Bounded Waiting: There should be a bound or a limit on the number of times a particular
process can enter the critical section. It should not happen that the same process is taking up
critical section every time resulting in starvation of other processes. In this case, other processes
would have to wait for an infinite amount of time, this should not happen.
Peterson’s Solution
is a classic Software-Based Solution to the critical-section problem. Peterson’s solution is restricted to two
processes that alternate execution between their critical sections and remainder sections. The processes are
numbered P0 and P1. Let Pi represents one process and Pj represents other processes (i.e. j =i-1)
do {
flag[i] = true;
turn = j;
while (flag[j] && turn = = j);
critical section
flag[i] = false;
remainder section
} while (true);
Peterson’s solution requires the two processes to share two data items:
int turn; Boolean flag[2];
The variable turn indicates whose turn it is to enter its critical section. At any point in time, the turn value
will be either 0 or 1 but not both.
● if turn == i, then process Pi is allowed to execute in its critical section.
● if turn == j, then process Pj is allowed to execute in its critical section
● The flag array is used to indicate if a process is ready to enter its critical section.
Example: if flag[i] is true, this value indicates that Pi is ready to enter its critical section.
To enter the critical section, process Pi first sets flag[i]=true and then sets turn=j, thereby Pi checks if
the other process wishes to enter the critical section, and it can do so.
If both processes try to enter at the same time, the turn will be set to both i and j at the same time. Only
one of these assignments will be taken. The other will occur but will be over written immediately.
The eventual value of turn determines which of the two processes is allowed to enter its critical section
first.
The above code must satisfy the following requirements:
1. Mutual exclusion
2. The progress
3. The bounded-waiting
Check for Mutual Exclusion
● Each Pi enters its critical section only if either flag[j] == false or turn ==i.
● If both processes can be executing in their critical sections at the same time,then flag[0]== flag[1] == true.
But the value of turn can be either 0 or 1 but cannot be both.
● Hence P0 and P1 could not have successfully executed their while statements at about the same time.
● If Pi executed ―turn== j‖and the process Pj executed flag[j]=true then Pj will have successfully executed
the while statement. Now Pj will enter into its Critical section.
● At this time, flag[j] == true and turn == j, and this condition will persist as long as Pj is in its critical
section. As a result, mutual exclusion is preserved.
Check for Progress and Bounded-waiting
● The while loop is the only possible way that a process Pi can be prevented from entering the critical
section only if it is stuck in the while loop with the condition flag[j] == true and turn ==j.
● If Pj is not ready to enter the critical section, then flag[j] == false, and Pi can enter its critical section.
● If Pj has set flag[j]==true and is also executing in its while statement, then either turn== i or turn ==
j.
● If turn == i, then Pi will enter the critical section. If turn == j, then Pj will enter the critical section.
● Once Pj exits its critical section, it will reset flag[j] to false, allowing Pi to enter its critical section.
● If Pj resets flag[j] to true, it must also set turn == i. Thus, since Pi does not change the value of the
variable turn while executing the while statement, Pi will enter the critical section (Progress) after at
most one entry by Pj(Bounded Waiting).
Problem with Peterson Solution
There are no guarantees that Peterson’s solution will work correctly on modern computer architectures that
perform basic machine-language instructions such as load and store.
● The critical-section problem could be solved simply in a single-processor environment if we could
prevent interrupts from occurring while a shared variable was being modified. This is a Non-
preemptive kernel approach.
● We could be sure that the current sequence of instructions would be allowed to execute in order
without preemption.
● No other instructions would be run, so no unexpected modifications could be made to the shared
variable.
Synchronization Hardware
Some times the problems of the Critical Section are also resolved by hardware. The hardware solution
is as follows:
1. Disabling Interrupts
2. Test and Set
3. Compare and swap
Disabling Interrupts
Synchronization Hardware:
Modern computer systems provide special hardware instructions that allow us either to test and modify the
content of a word or to swap the contents of two words atomically (i.e.) as one uninterruptible unit.
There are two approaches to hardware synchronization:
1. test_and_set function
2. compare_and_swap function
test_and_set function
The test_and_set instruction is executed atomically (i.e.) if two test_and_set( ) instructions are executed
simultaneously each on a different CPU, they will be executed sequentially in some arbitrary order.
If the machine supports the test_and_set( ) instruction, then we can implement mutual exclusion by
declaring a boolean variable lock. The lock is initialized to false.
The definition of test_and_set instruction for process Pi is given as:
Boolean test_and_set(boolean *target)
{
Boolean rv = *target;
*target = true; return rv;
}
The below algorithm satisfies all the requirements of the Critical Section problem for the process Pi
that uses two Boolean data structures: waiting[ ], lock.
boolean waiting[i] = false; boolean lock
= false;
do
{
waiting[i] = true; key = true;
while (waiting[i] && key) key = test and set(&lock); waiting[i] =
false;
/* critical section */
j = (i + 1) %n;
while ((j != i) && !waiting[j]) j = (j + 1) %n;
if (j == i)
lock = false; else
waiting[j] = false;
/* remainder section */
} while (true);
Process Pi can enter its critical section only if either waiting[i] == false or key ==false.
The value of the key can become false only if the test_and_set( ) is executed.
The first process to execute the test_and_set( ) will find key == false and all other processes must wait.
The variable waiting[i] can become false only if another process leaves its critical section. Only one
waiting[i] is set to false, maintaining the mutual-exclusion requirement.
Progress
A process exiting the critical section either sets lock==false or sets waiting[j]==false.
Both allow a process that is waiting to enter its critical section to proceed.
This requirement ensures progress property.
Bounded Waiting
When a process leaves its critical section, it scans the array waiting in the cyclic ordering (i+1, i+2, ...,
n−1, 0, ...,i−1).
It designates the first process in this ordering that is in the entry section (waiting[j] == true) as the next
one to enter the critical section.
Any process waiting to enter its critical section will thus do so within n−1turns.
This requirement ensures the bounded waiting property.
compare_and_swap function
compare_and_swap( ) is also executed atomically. The compare_and_swap( ) instruction operates on three
operands. The definition code is given below:
intcompare_and_swap(int *value, int expected, int new_value)
{
int temp = *value;
if (*value == expected)
*value = new_value; return temp;
}
Mutual-exclusion implementation with the compare and swap( ) instruction: do
{
while (compare_and_swap(&lock, 0, 1) != 0); /* do nothing */
/* critical section */
lock = 0;
/* remainder section */
} while (true);
The operand value is set to a new value only if the expression (*value == exected) is true. Regardless,
compare_and_swap( ) always returns the original value of the variable value.
Mutual Exclusion with compare_and_swap( )
A global variable lock is declared and is initialized to 0 (i.e. lock=0).
The first process that invokes compare_and_swap( ) will set lock=1. It will then enter its critical section
because the original value of the lock was equal to the expected value of 0.
Subsequent calls to compare_and_swap( ) will not succeed, because lock now is not equal to the
expected value of 0. (lock==1).
When a process exits its critical section, it sets the lock back to 0 (lock ==0), which allows another
process to enter its critical section.
Problem with Hardware Solution:
The hardware-based solutions to the critical-section problem are complicated and they are inaccessible to
application programmers.
Semaphores
In 1965, Dijkstra proposed a new and very significant technique for managing concurrent processes by
using the value of a simple non negative integer variable to synchronize the progress of interacting
processes. This non negative integer variable is called a semaphore . So, it is basically a synchronizing
tool and is accessed only through two low standard atomic operations, wait and signal
designated by P(S) and V(S) respectively.
Entry to the critical section is controlled by the wait operation and exit from a critical region is taken care
by signal operation.
Wait Operation (P):
Wait(S)
{
while (S<=0);// no operation
S--;
}
S++;
}
This operation indicates that the resource is now available for other processes/threads
waiting on it. If there are any blocked processes/threads waiting for the semaphore, one of
them is awakened and granted access to the resource.
Types of Semaphores-
1. Counting Semaphores
2. Binary Semaphores or Mutexes
do { wait
(S) ;
// critical section
signal(S);
//remainder section
} while (TRUE)
Counting Semaphores
1. Initialize the counting semaphore with a value that represents the maximum number
of resources that can be accessed simultaneously.
2. When a process attempts to access the shared resource, it first attempts to acquire the
semaphore using the `wait()` or `P()` function.
3. The semaphore value is checked. If it is greater than zero, the process is allowed to
proceed and the value of the semaphore is decremented by one. If it is zero, the process is
blocked and added to a queue of waiting processes.
4. When a process finishes accessing the shared resource, it releases the semaphore
using the `signal()` or `V()` function.
5. The value of the semaphore is incremented by one, and any waiting processes are
unblocked and allowed to proceed.
6. Multiple processes can access the shared resource simultaneously as long as the
value of the semaphore is greater than zero.
7. The counting semaphore provides a way to manage access to shared resources and
ensure that conflicts are avoided, while also allowing multiple processes to access the
resource at the same time.
Assuming Initial Value of Semaphore=3
Binary Semaphore
This is also known as a mutex lock. It can have only two values – 0 and 1. Its value is initialized to 1. It
is used to implement the solution of critical section problems with multiple processes.
typedef struct {
typedef struct { int semaphore_variable;
Structure int semaphore_variable; Queue
Implementation } list; //A queue to
binary_semaphore; store the list of task
}counting_semaphore;
The main disadvantage of the semaphore is that it requires busy waiting. Busy waiting
wastes CPU cycles that some other process might be able to use productively. This type of
semaphore is also called a spinlock because the process spins while waiting for the lock.
To overcome the need for busy waiting, we can modify the definition of wait () and
Signal () semaphore operations. When the process executes the wait () operation and finds
that the semaphore value is not positive it must wait. Rather that engaging in busy waiting the
process can block itself.
The block operation places a process into a waiting queue associated with the
semaphore and the state of the process is switched to the waiting state. The process that is
blocked waiting on a semaphore S should be restarted when some other process executes a
signal () operation, the process is restarted by a wakeup () operation.
Definition of semaphore as a C
struct typedef struct
{
int value;
struct process *list;
} semaphore;
Each semaphore has an integer value and a list of processes list.
When a process must wait on a semaphore, it is added to the list of processes.
A signal() operation remove one process from the list of waiting processes and
awakens that process.
Wait () operation can be defined as
Wait (semaphore *s)
{
S->value--;
If (S->Value<0)
{
Add this Process to s->value list;
block ();
}}
Signal operation can be defined as
Signal (semaphore *S)
{
S->value++;
If (S-> value <=0)
{
Remove a process P from S-> list;
Wakeup (P);
}}
The block () operation suspends the process that invokes it. The wakeup () operation resumes the execution
of a blocked process P.
Deadlock and Starvation:
The implementation of semaphore with a waiting queue may result in a situation where two more
processes are waiting indefinitely for an event that can be caused only by one of the waiting
processes. When such a state is reached that process are said to be deadlocked.
P0 P1
Wait(S); wait(Q);
Wait(Q); Wait(S);
......
Signal( S); Signal (Q);
Signal (Q); Signal (S);
P0 executes wait (S) and then P1 executes wait (Q). When p0 executes wait (Q), it must wait until
p1 executes Signal (Q) in the same way P1 must wait until P0 executes signal (S). So p0 and p1 are
deadlocked. The other problem related to deadlocks is indefinite blocking or starvation, a situation
where processes wait indefinitely within the semaphore
P0 executes wait (S) and then P1 executes wait (Q). When p0 executes wait (Q), it must wait until
p1 executes Signal (Q) in the same way P1 must wait until P0 executes signal (S). So p0 and p1 are
deadlocked. The other problem related to deadlocks is indefinite blocking or starvation, a situation
where processes wait indefinitely within the semaphore.
Priority inversion
Priority inversion is a phenomenon that can occur in an operating system where a higher- priority
task is blocked because it is waiting for a lower-priority task to release a resource it needs. This can
happen in a system where tasks or threads have different priorities, and a lower- priority task is
currently holding a resource that a higher-priority task needs to execute.
At any particular time, the current value of empty denotes the number of vacant slots in the buffer,
while full denotes the number of occupied slots.
do
{
// process will wait until the empty > 0 and further decrement of 'empty' wait(empty);
// To acquire the lock
wait(mutex);
o When we look at the above code for a producer, we can see that it first waits until at
least one slot is vacant.
o wait(empty) decreases the value of the semaphore variable "empty" by one,
indicating that when the producer produces anything, the value of the empty space in the
buffer decreases. If the buffer is full, or the value of the semaphore variable "empty" is 0,
the program will stop and no production will take place.
o wait(mutex) sets the semaphore variable "mutex" to zero, preventing any other
process from entering the critical section.
o The buffer is then locked, preventing the consumer from accessing it until the
producer completes its function.
o signal(mutex) is being used to mark the semaphore variable "mutex" to "1" so that
other processes can arrive into the critical section though because the production is finished
and the insert operation is also done.
o So, After the producer has filled a slot in the buffer, the lock is released.
o signal(full) is utilized to increase the semaphore variable "full" by one because after
inserting the data into the buffer, one slot is filled in the buffer and the variable "full" must
be updated.
do
{
// need to wait until full > 0 and then decrement the 'full' wait(full);
// To acquire the lock
wait(mutex);
The consumer waits until the buffer has at least one full slot.
wait(full) is used to reduce the semaphore variable "full" by one since the variable
"full" must be reduced by one of the consumers consuming some data.
wait(mutex) sets the semaphore variable "mutex" to "0", preventing any other
processes from entering the critical section.
And soon after that, the consumer then acquires a lock on the buffer.
The consumer then completes the data removal operation by removing data from one
of the filled slots.
So because the consumption and remove operations are complete, signal(mutex) is
being used to set the semaphore variable "mutex" to "1" so that other processes can enter the
critical section now.
The lock is then released by the consumer.
Because one slot space in the buffer is released after extracting the data from the
buffer, signal(empty) is used to raise the variable "empty" by one.
The readers-writers problem relates to an object such as a file that is shared between multiple
processes. . The problem statement is, if a database or file is to be shared among several concurrent
process, there can be broadly 2 types of users
Readers – Reader are those processes/users which only read the data
Writers – Writers are those processes which also write, that is, they change the data .
It is allowed for 2 or more readers to access shared data, simultaneously as they are not making any
change and even after the reading the file format remains the same.
But if one writer(Say w1) is editing or writing the file then it should locked and no other
writer(Say w2) can make any changes until w1 has finished writing the file.
Writers are given to be exclusive access to shared database while writing to database. This is called
Reader’s writer problem.
Variables used –
Mutex – mutex (used for mutual exclusion, when readcount is changed) initialised as 1
Semaphore – wrt (used by both readers and writers) initialised as 1
readers_count – Counter of number of people reading the filei initialised as 0
Functions –
Reader Process
while (TRUE)
{
// Acquire lock
wait(m);
readCount++;
if (readCount == 1)
{
wait(w);
}
// Release lock
signal(m);
wait(m);
readCount--;
if (readCount == 0)
{
signal(w);
}
// Release lock
signal(m);
}
Writer Process
do {
wait(wrt);
// writing is performed
signal(wrt);
} while (TRUE);
The dining philosopher's problem is the classical problem of synchronization which says that Five
philosophers are sitting around a circular table and their job is to think and eat alternatively. A bowl of
noodles is placed at the center of the table along with five chopsticks for each of the philosophers. To eat
a philosopher needs both their right and a left chopstick. A philosopher can only eat if both immediate left
and right chopsticks of the philosopher is available.
do {
wait( chopstick[i] );
wait( chopstick[ (i+1) % 5] );
..
. EATING THE RICE
.
signal( chopstick[i] );
signal( chopstick[ (i+1) % 5] );
.
. THINKING
.
} while(1);
In the above structure, first wait operation is performed on chopstick[i] and chopstick[ (i+1)
% 5]. This means that the philosopher i has picked up the chopsticks on his sides. Then the eating
function is performed.
After that, signal operation is performed on chopstick[i] and chopstick[ (i+1) % 5]. This means that the
philosopher i has eaten and put down the chopsticks on his sides. Then the philosopher goes back to
thinking.
Difficulty with the solution
The above solution makes sure that no two neighboring philosophers can eat at the same time. But
if all five philosophers are hungry simultaneously, and each of them pickup one chopstick, then a
deadlock situation occurs because they will be waiting for another chopstick forever. Then none of
them can eat and deadlock occurs.
Some of the ways to avoid deadlock are as follows −
There should be at most four philosophers on the table.
An even philosopher should pick the right chopstick and then the left chopstick
while an odd philosopher should pick the left chopstick and then the right
chopstick.
A philosopher should only be allowed to pick their chopstick if both are available
at the same time.
Monitors
A monitor is essentially a class, in which all data is private, and with the special restriction that only
one method within any given monitor object may be active at the same time. An additional restriction is
that monitor methods may only access the shared data within the monitor and any data passed to them
as parameters. I.e. they cannot access any data external to the monitor.
In order to fully realize the potential of monitors, we need to introduce one additional new data type,
known as a condition. A variable of type condition has only two legal operations, wait and signal. I.e. if
X was defined as type condition, then legal operations would be X.wait( ) and X.signal( ).
The wait operation blocks a process until some other process calls signal, and adds the blocked process
onto a list associated with that condition.
The signal process does nothing if there are no processes waiting on that condition. Otherwise it wakes
up exactly one process from the condition's list of waiting processes.
Signal and wait - When process P issues the signal to wake up process Q, P then waits, either for Q
to leave the monitor or on some other condition.
Signal and continue - When P issues the signal, Q waits, either for P to exit the monitor or for some
other condition.
Dining Philosophers Solution using Monitors
Monitors are used because they give a deadlock free solution to the Dining Philosophers problem. It
is used to gain access over all the state variables and condition variables. After implying monitors, it
imposes a restriction that a philosopher may pickup his chopsticks only if both of them are available
at the same time.
To code the solution, we need to distinguish among three states in which may find a philosopher.
THINKING
HUNGRY
EATING
Example
Here is implementation of the Dining Philosophers problem using Monitors –
monitor DiningPhilosophers {
enum {THINKING, HUNGRY, EATING} state[5];
condition self[5];
void pickup(int i) {
state[i] =
HUNGRY; test(i);
if (state[i] != EATING)
{ self[i].wait();
}
}
void putdown(int i) {
state[i] =
THINKING; test((i +
4) % 5);
test((i + 1) % 5);
}
void test(int i) {
if (state[(i + 4) % 5] != EATING
&& state[i] == HUNGRY &&
state[(i + 1) % 5] != EATING) {
state[i] = EATING;
self[i].signal();
}
}
initialization code() { for(int
i=0;i<5;i++) state[i] =
THINKING;
}
}
Before start eating, each philosopher must invoke the pickup() operation. It indicates that the philosopher is
hungry, means that the process wants to use the resource. It also set the state to EATING in test() only if the
philosopher’s left and right neighbors are not eating. If the philosopher is unable to eat, then wait() operation
is invoked. After the successful completion of the operation, the philosopher may now eat.
Keeping that in mind, the philosopher now invokes the putdown() operation. After leaving forks, it checks on
his neighbors. If they are HUNGRY and both of its neighbors are not EATING, then invoke signal() and offer
them to eat.
Thus a philosopher must invokes the pickup() and putdown() operations simultaneously which ensures that
no two neighbors are eating at the same time, thus achieving mutual exclusion. Thus, it prevents the
deadlock. But there is a possibility that one of the philosopher may starve to death.
1. A semaphore is an integer variable that allows many processes in a parallel system to manage access to a
common resource like a multitasking OS. On the other hand, a monitor is a synchronization technique that
enables threads to mutual exclusion and the wait() for a given condition to become true.
2. When a process uses shared resources in semaphore, it calls the wait() method and blocks the resources.
When it wants to release the resources, it executes the signal() In contrast, when a process uses shared
resources in the monitor, it has to access them via procedures.
3. Semaphore is an integer variable, whereas monitor is an abstract data type.
4. In semaphore, an integer variable shows the number of resources available in the system. In contrast, a
monitor is an abstract data type that permits only a process to execute in the crucial section at a time.
5. Semaphores have no concept of condition variables, while monitor has condition variables.
6. A semaphore's value can only be changed using the wait() and signal() In contrast, the monitor has the
shared variables and the tool that enables the processes to access them.