Assignment 1 - Operating System
Assignment 1 - Operating System
Categories of Scheduling
Scheduling falls into one of two categories:
Pause
Non-preemptive: In this case, a process’s resource cannot be taken before the process
has finished running. When a running process finishes and transitions to a waiting
state, resources are switched.
Preemptive: In this case, the OS assigns resources to a process for a predetermined
period. The process switches from running state to ready state or from waiting for
state to ready state during resource allocation. This switching happens because the
CPU may give other processes priority and substitute the currently active process for
the higher priority process.
Types of Process Schedulers
There are three types of process schedulers:
1. Long Term or Job Scheduler
It brings the new process to the ‘Ready State’. It controls the Degree of Multi-
programming, i.e., the number of processes present in a ready state at any point in time. It
is important that the long-term scheduler make a careful selection of both I/O and CPU-
bound processes. I/O-bound tasks are which use much of their time in input and output
operations while CPU-bound processes are which spend their time on the CPU. The job
scheduler increases efficiency by maintaining a balance between the two. They operate at
a high level and are typically used in batch-processing systems.
2. Short-Term or CPU Scheduler
It is responsible for selecting one process from the ready state for scheduling it on the
running state. Note: Short-term scheduler only selects the process to schedule it doesn’t
load the process on running. Here is when all the scheduling algorithms are used. The
CPU scheduler is responsible for ensuring no starvation due to high burst time processes.
Short Term Scheduler
The dispatcher is responsible for loading the process selected by the Short-term scheduler
on the CPU (Ready to Running State) Context switching is done by the dispatcher only.
A dispatcher does the following:
Switching context.
Switching to user mode.
Jumping to the proper location in the newly loaded program.
3. Medium-Term Scheduler
It is responsible for suspending and resuming the process. It mainly does swapping
(moving processes from main memory to disk and vice versa). Swapping may be
necessary to improve the process mix or because a change in memory requirements has
overcommitted available memory, requiring memory to be freed up. It is helpful in
maintaining a perfect balance between the I/O bound and the CPU bound. It reduces the
degree of multiprogramming.
Medium Term Scheduler
It is a process-swapping
It is a job scheduler It is a CPU scheduler
scheduler.
It controls the degree of It gives less control over It reduces the degree of
multiprogramming how much multiprogramming.
Long Term Scheduler Short term schedular Medium Term Scheduler
multiprogramming is done.
It is barely present or
It is a minimal time-sharing It is a component of
nonexistent in the time-
system. systems for time sharing.
sharing system.
In order for a process execution to be continued from the same point at a later time,
context switching is a mechanism to store and restore the state or context of a CPU in
the Process Control block. A context switcher makes it possible for multiple processes to
share a single CPU using this method. A multitasking operating system must include
context switching among its features.
Program Counter
Scheduling information
The base and limit register value
Currently used register
Changed State
I/O State information
Accounting information
OR
Operating System - Process Scheduling
Previous
Next
Definition
The process scheduling is the activity of the process manager that
handles the removal of the running process from the CPU and the
selection of another process on the basis of a particular strategy.
Job queue − This queue keeps all the processes in the system.
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.
Device queues − The processes which are blocked due to
unavailability of an I/O device constitute this queue.
The OS can use different policies to manage each queue (FIFO,
Round Robin, Priority, etc.). The OS scheduler determines how to
move processes between the ready and run queues which can only
have one entry per processor core on the system; in the above
diagram, it has been merged with the CPU.
Running
1
When a new process is created, it enters into the system as in the running state.
Not Running
Processes that are not running are kept in queue, waiting for their turn to
execute. Each entry in the queue is a pointer to a particular process. Queue is
2 implemented by using linked list. Use of dispatcher is as follows. When a
process is interrupted, that process is transferred in the waiting queue. If the
process has completed or aborted, the process is discarded. In either case, the
dispatcher then selects a process from the queue to execute.
Schedulers
Schedulers are special system software which handle process
scheduling in various ways. Their main task is to select the jobs to
be submitted into the system and to decide which process to run.
Schedulers are of three types −
Long-Term Scheduler
Short-Term Scheduler
Medium-Term Scheduler
It is a process swapping
1 It is a job scheduler It is a CPU scheduler
scheduler.
It provides lesser
It controls the degree of It reduces the degree of
3 control over degree of
multiprogramming multiprogramming.
multiprogramming
It is almost absent or
It is also minimal in It is a part of Time
4 minimal in time sharing
time sharing system sharing systems.
system
Context Switching
A context switching is the mechanism to store and restore the state
or context of a CPU in Process Control block so that a process
execution can be resumed from the same point at a later time.
Using this technique, a context switcher enables multiple processes
to share a single CPU. Context switching is an essential part of a
multitasking operating system features.
When the scheduler switches the CPU from executing one process to
execute another, the state from the current running process is
stored into the process control block. After this, the state for the
process to run next is loaded from its own PCB and used to set the
PC, registers, etc. At that point, the second process can start
executing.
Context switches are computationally intensive since register and
memory state must be saved and restored. To avoid the amount of
context switching time, some hardware systems employ two or
more sets of processor registers. When the process is switched, the
following information is stored for later use.
Program Counter
Scheduling information
Base and limit register value
Currently used register
Changed State
I/O State information
Accounting information
Pause
Unmute
×
Preemptive scheduling has a number of advantages and disadvantages. The following are
preemptive scheduling’s benefits and drawbacks:
Advantages
1. Because a process may not monopolize the processor, it is a more reliable method.
2. Each occurrence prevents the completion of ongoing tasks.
3. The average response time is improved.
4. Utilizing this method in a multi-programming environment is more advantageous.
5. The operating system makes sure that every process using the CPU is using the same
amount of CPU time.
Disadvantages
1. Limited computational resources must be used.
2. Suspending the running process, change the context, and dispatch the new incoming
process all take more time.
3. The low-priority process would have to wait if multiple high-priority processes
arrived at the same time.
Non-Preemptive Scheduling
Non-preemptive Scheduling is used when a process terminates, or a process switches
from running to the waiting state. In this scheduling, once the resources (CPU cycles) are
allocated to a process, the process holds the CPU till it gets terminated or reaches a
waiting state. In the case of non-preemptive scheduling does not interrupt a process
running CPU in the middle of the execution. Instead, it waits till the process completes its
CPU burst time, and then it can allocate the CPU to another process.
Algorithms based on non-preemptive scheduling are: Shortest Job First (SJF basically
non preemptive) and Priority (nonpreemptive version), etc.
Non-preemptive scheduling has both advantages and disadvantages. The following are
non-preemptive scheduling’s benefits and drawbacks:
Advantages
1. It has a minimal scheduling burden.
2. It is a very easy procedure.
3. Less computational resources are used.
4. It has a high throughput rate.
Disadvantages
1. Its response time to the process is super.
2. Bugs can cause a computer to freeze up.
Conclusion
Preemptive scheduling is not better than non-preemptive scheduling, and vice versa. It all
depends on how a scheduling algorithm increases CPU utilization while decreasing
average process waiting time.
When a high-priority process comes in the ready queue, it doesn't have to wait for the
running process to finish its burst time. However, the running process is interrupted in
the middle of its execution and placed in the ready queue until the high-priority process
uses the resources. As a result, each process gets some CPU time in the ready queue. It
improves the overhead of switching a process from running to ready state and vice
versa, increasing preemptive scheduling flexibility. It may or may not include SJF and
Priority scheduling.
For example:
ADVERTISEMENT
Let us take the example of Preemptive Scheduling. We have taken P0, P1,
P2, and P3 are the four processes.
Process Arrival Time CPU Burst time (in millisec.)
P0 3 2
P1 2 4
P2 0 6
P3 1 4
ADVERTISEMENT
ADVERTISEMENT
o Firstly, the process P2 comes at time 0. So, the CPU is assigned to process P2.
o When process P2 was running, process P3 arrived at time 1, and the remaining
time for process P2 (5 ms) is greater than the time needed by process P3 (4 ms).
So, the processor is assigned to P3.
o When process P3 was running, process P1 came at time 2, and the remaining
time for process P3 (3 ms) is less than the time needed by processes P1 (4
ms) and P2 (5 ms). As a result, P3 continues the execution.
o When process P3 continues the process, process P0 arrives at time 3. P3's
remaining time (2 ms) is equal to P0's necessary time (2 ms). So,
process P3 continues the execution.
o When process P3 finishes, the CPU is assigned to P0, which has a shorter burst
time than the other processes.
o After process P0 completes, the CPU is assigned to process P1 and then to
process P2.
Advantages and disadvantages of Preemptive
Scheduling
There are various advantages and disadvantages of Preemptive scheduling. The
advantages and disadvantages of non-preemptive scheduling are as follows:
Advantages
1. It is a more robust method because a process may not monopolize the processor.
2. Each event causes an interruption in the execution of ongoing tasks.
3. It improves the average response time.
4. It is more beneficial when you use this method in a multi-programming
environment.
5. The operating system ensures that all running processes use the same amount of
CPU.
Disadvantages
When a non-preemptive process with a high CPU burst time is running, the other
process would have to wait for a long time, and that increases the process average
waiting time in the ready queue. However, there is no overhead in transferring processes
from the ready queue to the CPU under non-preemptive scheduling. The scheduling is
strict because the execution process is not even preempted for a higher priority process.
ADVERTISEMENT
ADVERTISEMENT
For example:
Let's take the above preemptive scheduling example and solve it in a non-preemptive
manner.
Advantages
Disadvantages
1. It has a poor response time for the process.
2. A machine can freeze up due to bugs.
ADVERTISEMENT
ADVERTISEMENT
The resources are assigned to a Once resources are assigned to a process, they are
process for a long time period. held until it completes its burst period or changes
to the waiting state.
Its process may be paused in the When the processor starts the process execution, it
middle of the execution. must complete it before executing the other
process, and it may not be interrupted in the
middle.
When a high-priority process When a high burst time process uses a CPU,
continuously comes in the ready another process with a shorter burst time can
queue, a low-priority process can starve.
starve.
It is flexible. It is rigid.
It affects the design of the operating It doesn't affect the design of the OS kernel.
system kernel.
Its CPU utilization is very high. Its CPU utilization is very low.
Examples: Round Robin and Shortest FCFS and SJF are examples of non-preemptive
Remaining Time First scheduling.
ADVERTISEMENT
Conclusion
It's not a case of preemptive scheduling being superior to non-preemptive scheduling
or vice versa. It all depends on how a scheduling algorithm reduces average process
waiting time while increasing CPU utilization
OR
In Short
Difference between Preemptive and Non-
Preemptive Scheduling
Preemptive vs Non-Preemptive Scheduling: Difference between
Preemptive and Non-Preemptive Scheduling
2 Here, interruption can occur between the Here, the process cannot be interrupted.
processes.
OR
Preemptive and Non-Preemptive
Scheduling
Key Differences between Preemptive and Non-
Preemptive Scheduling
In Preemptive Scheduling, the CPU is allocated to the processes
for a specific time period, and the non-preemptive scheduling
CPU is allocated to the process until it terminates.
In Preemptive Scheduling, tasks are switched based on priority,
while in non-preemptive Scheduling, no switching takes place.
The preemptive algorithm has the overhead of switching the
process from the ready state to the running state, while Non-
preemptive Scheduling has no such overhead of switching.
Preemptive Scheduling is flexible, while Non-preemptive
Scheduling is rigid.
Pr
eemptive vs Non-Preemptive Scheduling
At that time, the lower priority task holds for some time and resumes
when the higher priority task finishes its execution.
It is the only method that can be used for various hardware platforms.
That’s because it doesn’t need specialized hardware (for example, a
timer) like preemptive Scheduling.
Consider the following five processes each having its own unique
burst time and arrival time.
Process Queue Burst time Arrival time
P1 6 2
P2 2 5
P3 8 1
P4 3 0
P5 4 4
Step 0) At time=0, P4 arrives and starts execution.
Step 3) At time = 3, process P4 will finish its execution. The burst time
of P3 and P1 is compared. Process P1 is executed because its burst
time is less compared to P3.
Step 6) At time = 9, process P1 will finish its execution. The burst time
of P3, P5, and P2 is compared. Process P2 is executed because its
burst time is the lowest.
Step 7) At time=10, P2 is executing, and P3 and P5 are in the waiting
queue.
Step 8) At time = 11, process P2 will finish its execution. The burst
time of P3 and P5 is compared. Process P5 is executed because its
burst time is lower.
Step 11) Let’s calculate the average waiting time for above example.
Wait time
P4= 0-0=0
P1= 3-2=1
P2= 9-5=4
P5= 11-4=7
P3= 15-1=14
Average Waiting Time= 0+1+4+7+14/5 = 26/5 = 5.2
Step 7) Let’s calculate the average waiting time for above example.
Wait time
P1= 0+ 4= 4
P2= 2+4= 6
P3= 4+3= 7
Pause
In real-time, the first boundary of thread scheduling is beyond specifying the scheduling
policy and the priority. It requires two controls to be specified for the User level threads:
Contention scope, and Allocation domain. These are explained as following below.
Contention Scope
The word contention here refers to the competition or fight among the User level threads
to access the kernel resources. Thus, this control defines the extent to which contention
takes place. It is defined by the application developer using the thread library.
Depending upon the extent of contention it is classified as-
Process Contention Scope (PCS) :
The contention takes place among threads within a same process. The thread library
schedules the high-prioritized PCS thread to access the resources via available LWPs
(priority as specified by the application developer during thread creation).
System Contention Scope (SCS) :
The contention takes place among all threads in the system. In this case, every SCS
thread is associated to each LWP by the thread library and are scheduled by the
system scheduler to access the kernel resources.
In LINUX and UNIX operating systems, the POSIX Pthread library provides a
function Pthread_attr_setscope to define the type of contention scope for a thread
during its creation.
int Pthread_attr_setscope(pthread_attr_t *attr, int scope)
The first parameter denotes to which thread within the process the scope is defined.
The second parameter defines the scope of contention for the thread pointed. It takes two
values.
PTHREAD_SCOPE_SYSTEM
PTHREAD_SCOPE_PROCESS
If the scope value specified is not supported by the system, then the function
returns ENOTSUP.
Allocation Domain
The allocation domain is a set of one or more resources for which a thread is
competing. In a multicore system, there may be one or more allocation domains where
each consists of one or more cores. One ULT can be a part of one or more allocation
domain. Due to this high complexity in dealing with hardware and software architectural
interfaces, this control is not specified. But by default, the multicore system will have an
interface that affects the allocation domain of a thread.
Consider a scenario, an operating system with three process P1, P2, P3 and 10 user level
threads (T1 to T10) with a single allocation domain. 100% of CPU resources will be
distributed among all the three processes. The amount of CPU resources allocated to each
process and to each thread depends on the contention scope, scheduling policy and
priority of each thread defined by the application developer using thread library and also
depends on the system scheduler. These User level threads are of a different contention
scope.
In this case, the contention for allocation domain takes place as follows:
Process P1
All PCS threads T1, T2, T3 of Process P1 will compete among themselves. The PCS
threads of the same process can share one or more LWP. T1 and T2 share an LWP and
T3 are allocated to a separate LWP. Between T1 and T2 allocation of kernel resources via
LWP is based on preemptive priority scheduling by the thread library. A Thread with a
high priority will preempt low priority threads. Whereas, thread T1 of process p1 cannot
preempt thread T3 of process p3 even if the priority of T1 is greater than the priority of
T3. If the priority is equal, then the allocation of ULT to available LWPs is based on the
scheduling policy of threads by the system scheduler(not by thread library, in this case).
Process P2
Both SCS threads T4 and T5 of process P2 will compete with processes P1 as a whole
and with SCS threads T8, T9, T10 of process P3. The system scheduler will schedule the
kernel resources among P1, T4, T5, T8, T9, T10, and PCS threads (T6, T7) of process P3
considering each as a separate process. Here, the Thread library has no control of
scheduling the ULT to the kernel resources.
Process P3
Combination of PCS and SCS threads. Consider if the system scheduler allocates 50% of
CPU resources to process P3, then 25% of resources is for process scoped threads and the
remaining 25% for system scoped threads. The PCS threads T6 and T7 will be allocated
to access the 25% resources based on the priority by the thread library. The SCS threads
T8, T9, T10 will divide the 25% resources among themselves and access the kernel
resources via separate LWP and KLT. The SCS scheduling is by the system scheduler.
Note:
For every system call to access the kernel resources, a Kernel Level
thread is created
and associated to separate LWP by the system scheduler.
Number of Kernel Level Threads = Total Number of LWP
Total Number of LWP = Number of LWP for SCS + Number of LWP for PCS
Number of LWP for SCS = Number of SCS threads
Number of LWP for PCS = Depends on application developer
Here,
Number of SCS threads = 5
Number of LWP for PCS = 3
Number of SCS threads = 5
Number of LWP for SCS = 5
Total Number of LWP = 8 (=5+3)
Number of Kernel Level Threads = 8
Advantages of PCS over SCS
If all threads are PCS, then context switching, synchronization, scheduling everything
takes place within the userspace. This reduces system calls and achieves better
performance.
PCS is cheaper than SCS.
PCS threads share one or more available LWPs. For every SCS thread, a separate
LWP is associated.For every system call, a separate KLT is created.
The number of KLT and LWPs created highly depends on the number of SCS threads
created. This increases the kernel complexity of handling scheduling and
synchronization. Thereby, results in a limitation over SCS thread creation, stating
that, the number of SCS threads to be smaller than the number of PCS threads.
If the system has more than one allocation domain, then scheduling and
synchronization of resources becomes more tedious. Issues arise when an SCS thread
is a part of more than one allocation domain, the system has to handle n number of
interfaces.
The second boundary of thread scheduling involves CPU scheduling by the system
scheduler. The scheduler considers each kernel-level thread as a separate process and
provides access to the kernel resources.
OR
Scheduling threads
Laatst bijgewerkt: 2023-03-24
Threads can be scheduled, and the threads library provides several facilities to handle and control
the scheduling of threads.
It also provides facilities to control the scheduling of threads during synchronization operations
such as locking a mutex. Each thread has its own set of scheduling parameters. These parameters
can be set using the thread attributes object before the thread is created. The parameters can also
be dynamically set during the thread's execution.
Controlling the scheduling of a thread can be a complicated task. Because the scheduler handles
all threads system wide, the scheduling parameters of a thread interact with those of all other
threads in the process and in the other processes. The following facilities are the first to be used
if you want to control the scheduling of a thread.
The threads library allows the programmer to control the execution scheduling of the threads in
the following ways:
Scheduling parameters
Paramete
Description
r
The contention scope of a thread is defined by the thread model used in the threads
scope
library.
The scheduling policy of a thread defines how the scheduler treats the thread after it
policy
gains control of the CPU.
The scheduling priority of a thread defines the relative importance of the work being
priority
done by each thread.
The scheduling parameters can be set before the thread's creation or during the thread's
execution. In general, controlling the scheduling parameters of threads is important only for
threads that are CPU-intensive. Thus, the threads library provides default values that are
sufficient for most cases.
OR
Scheduling threads
Article
09/15/2021
12 contributors
Feedback
Every thread has a thread priority assigned to it. Threads created within the common
language runtime are initially assigned the priority of ThreadPriority.Normal. Threads
created outside the runtime retain the priority they had before they entered the
managed environment. You can get or set the priority of any thread with
the Thread.Priority property.
Threads are scheduled for execution based on their priority. Even though threads are
executing within the runtime, all threads are assigned processor time slices by the
operating system. The details of the scheduling algorithm used to determine the order
in which threads are executed varies with each operating system. Under some operating
systems, the thread with the highest priority (of those threads that can be executed) is
always scheduled to run first. If multiple threads with the same priority are all available,
the scheduler cycles through the threads at that priority, giving each thread a fixed time
slice in which to execute. As long as a thread with a higher priority is available to run,
lower priority threads do not get to execute. When there are no more runnable threads
at a given priority, the scheduler moves to the next lower priority and schedules the
threads at that priority for execution. If a higher priority thread becomes runnable, the
lower priority thread is preempted and the higher priority thread is allowed to execute
once again. On top of all that, the operating system can also adjust thread priorities
dynamically as an application's user interface is moved between foreground and
background. Other operating systems might choose to use a different scheduling
algorithm.
Process: Processes are basically the programs that are dispatched from the ready state
and are scheduled in the CPU for execution. PCB(Process Control Block) holds the
concept of process. A process can create other processes which are known as Child
Processes. The process takes more time to terminate and it is isolated means it does not
share the memory with any other process.
The process can have the following states new, ready, running, waiting, terminated, and
suspended.
Thread: Thread is the segment of a process which means a process can have multiple
threads and these multiple threads are contained within a process. A thread has three
states: Running, Ready, and Blocked.
The thread takes less time to terminate as compared to the process but unlike the process,
threads do not isolate.
Process vs Thread
3. It takes more time for creation. It takes less time for creation.
S.NO Process Thread
14. The process does not share data Threads share data with each other.
S.NO Process Thread
Note: In some cases where the thread is processing a bigger workload compared to a
process’s workload then the thread may take more time to terminate. But this is an
extremely rare situation and has fewer chances to occur.
OR
Race conditions are most commonly associated with computer science and
programming. They occur when two computer program processes, or threads,
attempt to access the same resource at the same time and cause problems in
the system.
Peterson's Solution
By Hari Hara Sankar
7 mins read
Last updated: 14 Oct 2023
1.1k views
Overview
In operating systems, there may be a need for more than one process to access a shared resource
such as memory or CPU. In shared memory, if more than one process is accessing a variable,
then the value of that variable is determined by the last process to modify it, and the last
modified value overwrites the first modified value. This may result in losing important
information written during the first process. The location where these processes occur is called
the critical section. These critical sections prevent information loss by preventing two processes
from simultaneously being in the same critical region or updating the same variable
simultaneously. This problem is called the Critical-Section problem, and one of the solutions to
this problem is the Peterson's solution.
What is Peterson's Solution in OS?
Peterson's solution is a classic solution to the critical section problem. The critical section
problem ensures that no two processes change or modify a resource's value simultaneously.
For example, let int a=5, and there are two processes p1 and p2 that can modify the value of a. p1
adds 2 to a a=a+2 and p2 multiplies a with 2, a=a*2. If both processes modify the value of a at
the same time, then a value depends on the order of execution of the process. If p1 executes first,
a will be 14; if p2 executes first, a will be 12. This change of values due to access by two
processes at a time is the cause of the critical section problem.
The section in which the values are being modified is called the critical section. There are three
sections except for the critical sections: the entry section,exit section, and the reminder
section.
The process entering the critical region must pass the entry region in which they request
entry to the critical section.
The process exiting the critical section must pass the exit region.
The remaining code left after execution is in the remainder section.
It ensures that if a process is in the critical section, no other process must be allowed to
enter it. This property is termed mutual exclusion.
If more than one process wants to enter the critical section, the process that should enter
the critical region first must be established. This is termed progress.
There is a limit to the number of requests that processors can make to enter the critical
region, provided that a process has already requested to enter and is waiting. This is
termed bounding.
It provides platform neutrality as this solution is developed to run in user mode, which
doesn't require any permission from the kernel.
Pseudocode for Peterson's Algorithm
The algorithm used for implementing Peterson's algorithm can be written in pseudocode as
follows,
There are total N processes, each with a variable flag set to false on initialization. This
variable flag indicates if a process is ready to enter the critical region.
The turn variable denotes which process its turn is now to enter the critical region.
Then, every process will enter the entry section in which we define j, which denotes
another process that came before process i.
We allow process I to enter the critical section and indicate that j process is now turned to
enter the critical section using turn=j.
Then we check whether the process j is in the critical region using the
conditions flag[j]==true && turn=j. If process j is in the critical region, the while loop
runs continuously, and stalls process i from entering the region until process j exits out of
the critical region.
The process which has exited the critical region is marked by flag[i]=false;, where I
denote the process exiting from the critical region.
The identifier is associated with the shared memory segment, and the storage_space is the
amount of space needed for storage. The flags tell us the permissions and type for the shared
memory. The IPC_CREAT | 06660 flag creates a new shared memory if it doesn't exist and gives
read and write permissions.
syntax:
The id is the value returned from the shmget() function, and the address is used to identify an
address within the segment and a NULL value will give the first address in the shared memory.
The flags are for permission.
The srand() and rand() functions are used to get random numbers based on time.
The srand() function marks the start of numbers for generating random numbers using
the rand() function.
Syntax:
srand(unsigned start);
int rand() //generate a numbers above start
Then the seconds are obtained from the default variable present in the time
structure, time.tv_sec.
The implementation of Peterson's Algorithm using the C programming language to solve the
producer-consumer problem is as follows,
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> //for constants and data types
#include <time.h> // data types to represent time
#include <sys/types.h> //has data types
#include <sys/ipc.h> // data types for shared memory
#include <sys/shm.h> // for shmat() and shmdt()
#include <stdbool.h> // for using boolean
#include <sys/time.h> // getimeofday() function
#define buffer_size 8
int getrandom(int n)
{
time_t t;
// set the range of random number based on time.
srand((unsigned)time(&t));
// crate random number and return it
return (rand() % n + 1);
}
time_t gettime()
{
// timer to check for busy waiting
struct timeval t;
gettimeofday(&t, NULL);
time_t time= t.tv_sec;
return time;
}
int main()
{
// creating shared memory (critical section)
// shared memory for flag varible
shmid1 = shmget(5354, sizeof(bool) * 2, IPC_CREAT | 0660);
// shared memory for turn
shmid2 = shmget(1232, sizeof(int) * 1, IPC_CREAT | 0660);
// shared memory for buffer
shmid3 = shmget(4232, sizeof(int) * buffer_size, IPC_CREAT | 0660);
//shared memory used by the timer
shmid4 = shmget( 5633, sizeof(int) * 1, IPC_CREAT | 0660);
// checking if the critical section is created successfully
if (shmid1 < 0 || shmid2 < 0 || shmid3 < 0 || shmid4 < 0) {
error("Creation failed: ");
exit(1);
}
// gettting time
time_t t1, t2;
t1 =gettime();
// exiting section
flag[j] = false;
if (*current_state == 0)
break;
wait_time = getrandom(2);
printf("Producer will wait for %d seconds\n\n",
wait_time);
sleep(wait_time);
}
exit(0);
}
else // consumer process
{
// getting data from critical region
flag_memory = (bool*)shmat(shmid1, NULL, 0);
turn_memory = (int*)shmat(shmid2, NULL, 0);
buffer_memory = (int*)shmat(shmid3, NULL, 0);
if (flag_memory == (bool*)-1 || turn_memory == (int*)-1 ||
buffer_memory == (int*)-1) {
perror("Consumer shared memory error");
exit(1);
}
//exit section
flag[i] = false;
if (*current_state == 0)
break;
wait_time = getrandom(15);
// time consumer need to wait to get new products
printf("Consumer will nedd to wait for %d seconds to
get products to be consumed.\n\n", wait_time);
sleep(wait_time);
}
exit(0);
}
//busy waiting
while (1) {
t2 = gettime();
// set current_state to 0 if process waits longer than 10 seconds.
if (t2 - t1 > 10)
{
*current_state = 0;
break;
}
}
// exit if too much time on busy waiting.
wait();
wait();
printf("Too much time has passed.\n");
return 0;
}
pid_t fork();
It returns a positive number on successful creation and a negative number when failed to create
processes.
A buf array is used to hold all the products, and computations are performed when a new
product is produced or consumed.
Conclusion
Peterson's solution is one of the classical solutions to solve the critical-section problem in
OS.
It follows a simple algorithm and is limited to two processes simultaneously.
We can implement Peterson's solution in any programming language, and it can be used
to solve other problems like the producer-consumer problem and reader-writer problem.
OR
It provides shared access to memory between the co-operating processes. It ensures mutual
exclusion among process sharing resources. The Producer Consumer is a classical problem
which can be solved using the Peterson’s Solution. To synchronise any two processes,
Peterson’s solution uses two variables:
1) Flag: a boolean array of size 2
2) Turn: integer variable
Initially both the flags in the array are set to false. Peterson’s Solution is a software-based
solution to race conditions. Although it is not used in modern-day computing, it provides an
algorithm-level way to solve the critical section problem. Peterson’s Solution works well in the
case of two cooperating processes as well as multiple processes wanting to share the critical
section.
Then, the current process starts executing in its critical section and utilises the shared resources.
Once the current process has executed completely, it sets its own flag to false to indicate that it
does not want to execute any further. Therefore, the current process executes for a fixed amount
of time before exiting from the system.
OR
void producer(int j) {
do {
flag[j] = true; // Producer j is ready to produce
turn = 1 - j; // Allow consumer to consume
while (flag[1 - j] && turn == 1 - j) {
// Wait for consumer to finish
// Producer waits if consumer is ready and it's consumer's turn
}
void consumer(int i) {
do {
flag[i] = true; // Consumer i is ready to consume
turn = i; // Allow producer to produce
while (flag[1 - i] && turn == i) {
// Wait for producer to finish
// Consumer waits if producer is ready and it's producer's turn
}
int main() {
std::thread producerThread(producer, 0); // Create producer thread
std::thread consumerThread(consumer, 1); // Create consumer thread
return 0;
}
C
// code for producer (j)
// producer j is ready
// to produce an item
flag[j] = true;
//--------------------------------------------------------
// code for consumer i
// consumer i is ready
// to consume an item
flag[i] = true;
try {
Thread.sleep(1000); // Run for 1 second
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
producerThread.interrupt(); // Interrupt producer thread
consumerThread.interrupt(); // Interrupt consumer thread
}
}
}
C#
using System;
using System.Threading;
using System.Collections.Generic;
class GFG
{
const int N = 2; // Number of threads
static List<bool> flag = new List<bool>(new bool[N]);
static int turn = 0; // Variable to indicate turn
// Producer method
static void Producer(object obj)
{
int j = (int)obj;
do
{
flag[j] = true;
turn = 1 - j;
// Wait for consumer to finish
// Producer waits if consumer is ready and it's consumer's turn
while (flag[1 - j] && turn == 1 - j)
{
// Wait
}
// Critical Section: Producer produces an item and
// puts it into the buffer
Console.WriteLine($"Producer {j} produced an item");
flag[j] = false;
// Remainder Section: Additional actions after critical section
Thread.Sleep(1000);
} while (true);
}
// Consumer method
static void Consumer(object obj)
{
int i = (int)obj;
do
{
flag[i] = true;
turn = i;
// Wait for producer to finish
// Consumer waits if producer is ready and it's producer's turn
while (flag[1 - i] && turn == i)
{
// Wait
}
// Critical Section: Consumer consumes an item from buffer
Console.WriteLine($"Consumer {i} consumed an item");
flag[i] = false;
// Remainder Section: Additional actions after critical section
Thread.Sleep(1000);
} while (true);
}
static void Main(string[] args)
{
Thread producerThread = new Thread(Producer); // Create producer
thread
Thread consumerThread = new Thread(Consumer); // Create consumer
thread
producerThread.Start(0); // Start producer thread with index 0
consumerThread.Start(1); // Start consumer thread with index 1
producerThread.Join(); // Wait for producer thread to finish
consumerThread.Join(); // Wait for consumer thread to finish
}
}
Javascript
const N = 2; // Number of threads (producer and consumer)
const lockObject = {}; // Lock object for synchronization
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
Pause
Unmute
×
C++
#include <iostream>
#include <vector>
#ifdef _WIN32
#include <windows.h>
#else
#include <unistd.h>
#endif
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/time.h>
#define BSIZE 8
#define PWT 2
#define CWT 10
#define RT 10
bool* SHM1;
int* SHM2;
int* SHM3;
int myrand(int n) {
static int initialized = 0;
if (!initialized) {
srand(static_cast<unsigned>(time(nullptr)));
initialized = 1;
}
return (rand() % n + 1);
}
void cleanup() {
shmdt(SHM1);
shmdt(SHM2);
shmdt(SHM3);
shmctl(shmid1, IPC_RMID, nullptr);
shmctl(shmid2, IPC_RMID, nullptr);
shmctl(shmid3, IPC_RMID, nullptr);
shmctl(shmid4, IPC_RMID, nullptr);
}
int main() {
shmid1 = shmget(k1, sizeof(bool) * 2, IPC_CREAT | 0660);
shmid2 = shmget(k2, sizeof(int) * 1, IPC_CREAT | 0660);
shmid3 = shmget(k3, sizeof(int) * BSIZE, IPC_CREAT | 0660);
shmid4 = shmget(k4, sizeof(int) * 1, IPC_CREAT | 0660);
struct timeval t;
gettimeofday(&t, nullptr);
time_t t1 = t.tv_sec;
int i = 0; // Consumer
int j = 1; // Producer
while (*state == 1) {
flag[j] = true;
printf("Producer is ready now.\n\n");
*turn = i;
if (index == BSIZE)
printf("Buffer is full, nothing can be produced!!!\n");
printf("Buffer: ");
index = 0;
while (index < BSIZE)
printf("%d ", buf[index++]);
printf("\n");
// Critical Section End
flag[j] = false;
if (*state == 0)
break;
wait_time = myrand(PWT);
printf("Producer will wait for %d seconds\n\n", wait_time);
#ifdef _WIN32
Sleep(wait_time * 1000);
#else
usleep(wait_time * 1000000);
#endif
}
exit(0);
}
while (*state == 1) {
flag[i] = true;
printf("Consumer is ready now.\n\n");
*turn = j;
printf("Buffer: ");
index = 0;
while (index < BSIZE)
printf("%d ", buf[index++]);
printf("\n");
// Critical Section End
flag[i] = false;
if (*state == 0)
break;
wait_time = myrand(CWT);
printf("Consumer will sleep for %d seconds\n\n", wait_time);
#ifdef _WIN32
Sleep(wait_time * 1000);
#else
usleep(wait_time * 1000000);
#endif
}
exit(0);
}
// Parent process will now wait for RT seconds before causing the child to
terminate
while (1) {
gettimeofday(&t, nullptr);
time_t t2 = t.tv_sec;
if (t2 - t1 > RT) {
*state = 0;
break;
}
}
cleanup();
printf("The clock ran out.\n");
return 0;
}
C
// C program to implement Peterson’s Algorithm
// for producer-consumer problem.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdbool.h>
#define _BSD_SOURCE
#include <sys/time.h>
int main()
{
shmid1 = shmget(k1, sizeof(bool) * 2, IPC_CREAT | 0660); // flag
shmid2 = shmget(k2, sizeof(int) * 1, IPC_CREAT | 0660); // turn
shmid3 = shmget(k3, sizeof(int) * BSIZE, IPC_CREAT | 0660); // buffer
shmid4 = shmget(k4, sizeof(int) * 1, IPC_CREAT | 0660); // time stamp
struct timeval t;
time_t t1, t2;
gettimeofday(&t, NULL);
t1 = t.tv_sec;
while (*state == 1) {
flag[j] = true;
printf("Producer is ready now.\n\n");
*turn = i;
while (flag[i] == true && *turn == i)
;
flag[j] = false;
if (*state == 0)
break;
wait_time = myrand(PWT);
printf("Producer will wait for %d seconds\n\n",
wait_time);
sleep(wait_time);
}
exit(0);
}
flag[i] = false;
if (*state == 0)
break;
wait_time = myrand(CWT);
printf("Consumer will sleep for %d seconds\n\n",
wait_time);
sleep(wait_time);
}
exit(0);
}
// Parent process will now for RT seconds before causing child to
terminate
while (1) {
gettimeofday(&t, NULL);
t2 = t.tv_sec;
if (t2 - t1 > RT) // Program will exit after RT seconds
{
*state = 0;
break;
}
}
// Waiting for both processes to exit
wait();
wait();
printf("The clock ran out.\n");
return 0;
}
C#
using System;
using System.Threading.Tasks;
class Program
{
const int BSIZE = 8;
const int PWT = 2;
const int CWT = 10;
const int RT = 10;
while (SHM2[0] == 1)
{
flag[1] = true;
Console.WriteLine("Producer is ready now.\n");
turn[0] = 0;
while (flag[0] && turn[0] == 0) ;
if (index == BSIZE)
Console.WriteLine("Buffer is full, nothing can be produced!!!\
n");
Console.Write("Buffer: ");
index = 0;
while (index < BSIZE)
Console.Write($"{buf[index++]} ");
Console.WriteLine("\n");
// Critical Section End
flag[1] = false;
if (SHM2[0] == 0)
break;
while (SHM2[0] == 1)
{
flag[0] = true;
Console.WriteLine("Consumer is ready now.\n");
turn[0] = 1;
while (flag[1] && turn[0] == 1) ;
Console.Write("Buffer: ");
index = 0;
while (index < BSIZE)
Console.Write($"{buf[index++]} ");
Console.WriteLine("\n");
// Critical Section End
flag[0] = false;
if (SHM2[0] == 0)
break;
// Parent process will now wait for RT seconds before causing the
child to terminate
while ((DateTime.Now - startTime).TotalSeconds <= RT) ;
SHM2[0] = 0;
int state = 1;
int myrand(int n) {
return rand() % n + 1;
}
void producer() {
while (state == 1) {
shmid1 = true;
std::cout << "Producer is ready now.\n";
std::this_thread::sleep_for(std::chrono::milliseconds(500));
shmid2 = 0;
while (shmid1 && shmid2 == 0) {}
void consumer() {
shmid1 = false;
std::this_thread::sleep_for(std::chrono::milliseconds(5000));
while (state == 1) {
shmid1 = true;
std::cout << "Consumer is ready now.\n";
std::this_thread::sleep_for(std::chrono::milliseconds(500));
shmid2 = 1;
while (shmid1 && shmid2 == 1) {}
shmid1 = false;
if (state == 0) break;
const int wait_time = myrand(CWT);
std::cout << "Consumer will sleep for " << wait_time / 1000.0 << "
seconds\n";
std::this_thread::sleep_for(std::chrono::milliseconds(wait_time));
}
}
int main() {
srand(time(nullptr));
// Join threads
producer_thread.join();
consumer_thread.join();
return 0;
}
Java
import java.util.Random;
turn = 0;
while (flag[0] && turn == 0) ;
synchronized (buf) {
index = 0;
while (index < BSIZE) {
if (buf[index] == 0) {
int tempo = myrand(BSIZE * 3);
System.out.println("Job " + tempo + " has been
produced");
buf[index] = tempo;
break;
}
index++;
}
if (index == BSIZE)
System.out.println("Buffer is full, nothing can be
produced!!!\n");
System.out.print("Buffer: ");
for (int value : buf) {
System.out.print(value + " ");
}
System.out.println("\n");
}
flag[1] = false;
if (!state) break;
int wait_time = myrand(PWT);
System.out.println("Producer will wait for " + wait_time + "
seconds\n");
try {
Thread.sleep(wait_time * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
turn = 1;
while (flag[1] && turn == 1) ;
synchronized (buf) {
if (buf[0] != 0) {
System.out.println("Job " + buf[0] + " has been
consumed");
buf[0] = 0;
index = 1;
while (index < BSIZE) {
buf[index - 1] = buf[index];
index++;
}
buf[index - 1] = 0;
} else
System.out.println("Buffer is empty, nothing can be
consumed!!!\n");
System.out.print("Buffer: ");
for (int value : buf) {
System.out.print(value + " ");
}
System.out.println("\n");
}
flag[0] = false;
if (!state) break;
int wait_time = myrand(CWT);
System.out.println("Consumer will sleep for " + wait_time + "
seconds\n");
try {
Thread.sleep(wait_time * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producer.start();
consumer.start();
try {
Thread.sleep(RT * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
state = false;
try {
producer.join();
consumer.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("The clock ran out.\n");
}
}
C#
using System;
using System.Threading;
using System.Collections.Generic;
lock (buf)
{
index = 0;
while (index < BSIZE)
{
if (buf[index] == 0)
{
int tempo = MyRand(BSIZE * 3);
Console.WriteLine($"Job {tempo} has been
produced");
buf[index] = tempo;
break;
}
index++;
}
if (index == BSIZE)
Console.WriteLine("Buffer is full, nothing can be
produced!!!\n");
Console.Write("Buffer: ");
foreach (int value in buf)
{
Console.Write($"{value} ");
}
Console.WriteLine("\n");
}
flag[1] = false;
if (!state) break;
int waitTime = MyRand(PWT);
Console.WriteLine($"Producer will wait for {waitTime} seconds\
n");
Thread.Sleep(waitTime * 1000);
}
});
while (state)
{
flag[0] = true;
Console.WriteLine("Consumer is ready now.\n");
turn = 1;
while (flag[1] && turn == 1) ;
lock (buf)
{
if (buf[0] != 0)
{
Console.WriteLine($"Job {buf[0]} has been consumed");
buf[0] = 0;
index = 1;
while (index < BSIZE)
{
buf[index - 1] = buf[index];
index++;
}
buf[index - 1] = 0;
}
else
Console.WriteLine("Buffer is empty, nothing can be
consumed!!!\n");
Console.Write("Buffer: ");
foreach (int value in buf)
{
Console.Write($"{value} ");
}
Console.WriteLine("\n");
}
flag[0] = false;
if (!state) break;
int waitTime = MyRand(CWT);
Console.WriteLine($"Consumer will sleep for {waitTime}
seconds\n");
Thread.Sleep(waitTime * 1000);
}
});
producerThread.Start();
consumerThread.Start();
try
{
Thread.Sleep(RT * 1000);
}
catch (ThreadInterruptedException e)
{
Console.WriteLine(e.StackTrace);
}
state = false;
try
{
producerThread.Join();
consumerThread.Join();
}
catch (ThreadInterruptedException e)
{
Console.WriteLine(e.StackTrace);
}
let state = 1;
function myrand(n) {
return Math.floor(Math.random() * n) + 1;
}
function producer() {
while (state === 1) {
shmid1 = true;
console.log("Producer is ready now.");
// Simulate some processing time
awaitTimeout(500);
shmid2 = 0;
while (shmid1 && shmid2 === 0) {}
shmid1 = false;
if (state === 0) break;
const wait_time = myrand(PWT);
console.log(`Producer will wait for ${wait_time / 1000} seconds`);
awaitTimeout(wait_time);
}
}
function consumer() {
shmid1 = false;
awaitTimeout(5000);
while (state === 1) {
shmid1 = true;
console.log("Consumer is ready now.");
// Simulate some processing time
awaitTimeout(500);
shmid2 = 1;
while (shmid1 && shmid2 === 1) {}
(async () => {
producer();
consumer();
await awaitTimeout(RT);
state = 0;
shmid1 = multiprocessing.Value('i', 0)
shmid2 = multiprocessing.Value('i', 0)
shmid3 = multiprocessing.Array('i', [0] * BSIZE)
shmid4 = multiprocessing.Value('i', 0)
state = multiprocessing.Value('i', 1)
def myrand(n):
return random.randint(1, n)
def producer():
global state
while state.value == 1:
shmid1.value = True
print("Producer is ready now.")
time.sleep(0.5) # Simulate some processing time
shmid2.value = 0
while shmid1.value == True and shmid2.value == 0:
pass
shmid1.value = False
if state.value == 0:
break
wait_time = myrand(PWT)
print(f"Producer will wait for {wait_time} seconds")
time.sleep(wait_time)
def consumer():
global state
shmid1.value = False
time.sleep(5)
while state.value == 1:
shmid1.value = True
print("Consumer is ready now.")
time.sleep(0.5) # Simulate some processing time
shmid2.value = 1
while shmid1.value == True and shmid2.value == 1:
pass
shmid1.value = False
if state.value == 0:
break
wait_time = myrand(CWT)
print(f"Consumer will sleep for {wait_time} seconds")
time.sleep(wait_time)
if __name__ == "__main__":
producer_process = multiprocessing.Process(target=producer)
consumer_process = multiprocessing.Process(target=consumer)
producer_process.start()
consumer_process.start()
time.sleep(RT)
state.value = 0
producer_process.join()
consumer_process.join()
Assume there is a producer (which produces goods) and a consumer (which consumes
goods). The producer, produces goods and places them in a fixed size buffer. The
consumer takes the goods from the buffer.
The buffer has a finite capacity so that if it is full, the producer must stop producing.
The type of situations we must cater for are when the buffer is full, so the producer
cannot place new items into it. Another potential problem is when the buffer is empty,
so the consumer cannot take from the buffer.
Five philosophers are sat around a circular table. In front of each of them is a bowl of
food. In between each bowl there is a fork. That is there are five forks in all.
Philosophers spend their time either eating or thinking. When they are thinking they
are not using a fork.
When they want to eat they need to use two forks. They must pick up one of the forks
to their right or left. Once they have acquired one fork they must acquire the other
one. They may acquire the forks in any order.
Once a philosopher has two forks they can eat. When finished eating they return both
forks to the table.
The question is, can a program be written, for each philosopher, that never gets stuck,
that is, a philosopher is waiting for a fork forever.
This problem was devised by (Courtois et al., 1971). It models a number of processes
requiring access to a database. Any number of processes may read the database but
only one can write to the database.
A barber shop consists of a waiting room with n chairs. There is another room that
contains the barbers chair. The following situations can happen.
The problem is to program the barber and the customers without getting into race
conditions.
OR
Reader Writer Problem in OS
There are multiple processes that can be either readers and writers with a shared
resource between them, let us suppose it as a file or a database. In case there are two
processes with both trying to access the resource simultaneously at the same instance.
Although it does not matter how many readers can access it simultaneously, it must be
kept in mind that only one writer can write it at a time.
There are several algorithms that are designed to curb this problem and among the
many algorithms available, we are going to use one to solve the reader-writer problem
in os.
Case One
Two processes cannot be allowed to write into shared data parallelly thus they must
wait to get access to write into it.
Case Two
Even if one process is writing on data and the other is reading then also they cannot be
allowed to have the access to shared resources for the reason that a reader will be
reading an incomplete value in this case.
Case Three
The other similar scenario where one process is reading from the data and another
writing on it, on the shared resource, it cannot be allowed. Because the writer updates
some data that is not available to the reader. The solution being that the writer
completes successfully and gives access.
Case Four
In the case that both processes are reading the data then sharing of resources among
both the processes will be allowed as it is a case free from any such anomaly because
reading does not modify the pre-existing data.
Mutex makes the process to release and acquire a lock when the readCount is being
updated. The lock is acquired when the readCount is updated and decremented back
after it has done performing the operation. The writer waits for the semaphore until it
is its turn to write and increments for other processes to write.
The idea remains to use semaphores when a reader enters the critical section up until
it exits the critical section such that no writer can enter in between as it can cause data
inconsistency. A semaphore is used to prevent writers from accessing the resource
while there are one or more readers accessing it.
Code:
The given code below is the code for the writer’s side.
while (TRUE) {
wait(w);
// ...
// Signal the "w" semaphore to allow other writer processes to access the
resource
signal(w);
}
Explanation:
The above code implements a simple solution for the writer process where the writer
waits for the "w" semaphore to become available and then performs the write
operation on the resource. The writer then signals the "w" semaphore to allow other
writer processes to access the resource. Note that the code is in an infinite loop, so the
writer process will continuously wait for the "w" semaphore to become available and
then perform the write operation.
// Acquire lock
if (readCount == 1) {
// Release lock
// Here we assume that the necessary code to read from the resource has been
added
// Acquire lock
if (readCount == 0) {
// Release lock
Explanation:
The code uses three variables: "mutex" to ensure mutual exclusion while updating the
"readCount" variable, "w" semaphore to ensure that no writer can access the critical
section when a reader is accessing it, and "readCount" to keep track of the number of
processes performing the read operation. In the code, each process first acquires the
"mutex" lock before updating the "readCount" variable. If this is the first reader
process to access the resource, it waits for the "w" semaphore to become available,
which means that no writer process is currently accessing the resource.
After performing the read operation, the process again acquires the "mutex" lock and
decrements the "readCount" variable. If this is the last reader process accessing the
resource, it signals the "w" semaphore to allow writer processes to access the critical
section.
Conclusion
Addressing the Reader-Writer Problem is crucial in ensuring efficient and safe
concurrent access to shared resources in operating systems. Understanding the
nuances and challenges posed by multiple readers and writers accessing the same data
concurrently is essential for designing robust and scalable systems. Employing
synchronization techniques and algorithms tailored to this problem can significantly
enhance system performance while preventing data corruption and inconsistencies.
OR
6 mins read
1.6k views
Overview
Producer-Consumer problem is a classical synchronization problem in the operating system.
With the presence of more than one process and limited resources in the system the
synchronization problem arises. If one resource is shared between more than one process at the
same time then it can lead to data inconsistency. In the producer-consumer problem, the producer
produces an item and the consumer consumes the item produced by the producer.
1. Producer Process should not produce any data when the shared buffer is full.
2. Consumer Process should not consume any data when the shared buffer is empty.
3. The access to the shared buffer should be mutually exclusive i.e at a time only one process
should be able to access the shared buffer and make changes to it.
For consistent data synchronization between Producer and Consumer, the above problem should
be resolved.
Semaphores are variables used to indicate the number of resources available in the system at a
particular time. semaphore variables are used to achieve `Process Synchronization.
Full
The full variable is used to track the space filled in the buffer by the Producer process. It is
initialized to 0 initially as initially no space is filled by the Producer process.
Empty
The Empty variable is used to track the empty space in the buffer. The Empty variable is initially
initialized to the BUFFER-SIZE as initially, the whole buffer is empty.
Mutex
Mutex is used to achieve mutual exclusion. mutex ensures that at any particular time only the
producer or the consumer is accessing the buffer.
Signal() - The signal function increases the semaphore value by 1. Wait() - The wait operation
decreases the semaphore value by 1.
void Producer(){
while(true){
// producer produces an item/data
wait(Empty);
wait(mutex);
add();
signal(mutex);
signal(Full);
}
}
wait(Empty) - Before producing items, the producer process checks for the empty space in the
buffer. If the buffer is full producer process waits for the consumer process to consume items
from the buffer. so, the producer process executes wait(Empty) before producing any item.
wait(mutex) - Only one process can access the buffer at a time. So, once the producer process
enters into the critical section of the code it decreases the value of mutex by executing
wait(mutex) so that no other process can access the buffer at the same time.
add() - This method adds the item to the buffer produced by the Producer process. once the
Producer process reaches add function in the code, it is guaranteed that no other process will be
able to access the shared buffer concurrently which helps in data consistency.
signal(mutex) - Now, once the Producer process added the item into the buffer it increases the
mutex value by 1 so that other processes which were in a busy-waiting state can access the
critical section.
signal(Full) - when the producer process adds an item into the buffer spaces is filled by one item
so it increases the Full semaphore so that it indicates the filled spaces in the buffer correctly.
void Consumer() {
while(true){
// consumer consumes an item
wait(Full);
wait(mutex);
consume();
signal(mutex);
signal(Empty);
}
}
Let's understand the above Consumer process code :
wait(Full) - Before the consumer process starts consuming any item from the buffer it checks if
the buffer is empty or has some item in it. So, the consumer process creates one more empty
space in the buffer and this is indicated by the full variable. The value of the full variable
decreases by one when the wait(Full) is executed. If the Full variable is already zero i.e the
buffer is empty then the consumer process cannot consume any item from the buffer and it
goes in the busy-waiting state.
wait(mutex) - It does the same as explained in the producer process. It decreases the mutex by
1 and restricts another process to enter the critical section until the consumer process increases
the value of mutex by 1.
consume() - This function consumes an item from the buffer. when code reaches the consuming
() function it will not allow any other process to access the critical section which maintains the
data consistency.
signal(mutex) - After consuming the item it increases the mutex value by 1 so that other
processes which are in a busy-waiting state can access the critical section now.
signal(Empty) - when a consumer process consumes an item it increases the value of the Empty
variable indicating that the empty space in the buffer is increased by 1.
wait(mutex);
wait(mutex) decreases the value of mutex by 1. so, suppose a process P1 tries to enter the critical
section when mutex value is 1. P1 executes wait(mutex) and decreases the value of mutex. Now,
the value of mutex becomes 0 when P1 enters the critical section of the code.
Now, suppose Process P2 tries to enter the critical section then it will again try to decrease the
value of mutex. But the mutex value is already 0. So, wait(mutex) will not execute, and P2 will
now keep waiting for P1 to come out of the critical section.
signal(mutex)
signal(mutex) increases the value of mutex by 1.mutex value again becomes 1. Now, the
process P2 which was in a busy-waiting state will be able to enter the critical section by
executing wait(mutex).
Dive into the world of operating systems with our free Operating System course. Join today
and acquire the skills from industry experts!
Conclusion
Producer Process produces data item and consumer process consumes data item.
Both producer and consumer processes share a common memory buffer.
Producer should not produce any item if the buffer is full.
Consumer should not consume any item if the buffer is empty.
Not more than one process should access the buffer at a time i.e mutual exclusion should be
there.
Full, Empty and mutex semaphore help to solve Producer-consumer problem.
Full semaphore checks for the number of filled space in the buffer by the producer process
Empty semaphore checks for the number of empty spaces in the buffer.
mutex checks for the mutual exclusion.
OR
Producer Consumer Problem in C
Pause
Unmute
×
Note: An inadequate solution could result in a deadlock where both processes are waiting
to be awakened.
Approach: The idea is to use the concept of parallel programming and Critical Section to
implement the Producer-Consumer problem in C language using OpenMP.
Below is the implementation of the above approach:
C
// C program for the above approach
#include <stdio.h>
#include <stdlib.h>
// Initialize a mutex to 1
int mutex = 1;
// Item produced
x++;
printf("\nProducer produces"
"item %d",
x);
// Switch Cases
switch (n) {
case 1:
case 2:
// Exit Condition
case 3:
exit(0);
break;
}
}
}
Output:
In the producer-consumer problem, there are two types of processes: producers, which produce
data, and consumers, which consume the data. The challenge is to ensure that the producers do
not produce data if the buffer is full, and that the consumers do not consume data if the buffer is
empty. This requires synchronization and coordination between the producers and consumers.
On the other hand, the reader-writer problem involves multiple processes accessing a shar
… (continue chat)
Absolutely not
Definitely yes
Upvote
Cristiano Cavo
·
Follow
The two problems may seem to be similar, but it no so. Both the problems are about
resource sharing.
The first, (the Producer and the Consumer problem or "PCP"), contemplate the sharing of
many resources between a single Producer and a single Consumer (this condition is not
mandatory) whose use a queue (circular buffer). The Producer fills the queue at the tail and
the Consumer empties it from the head. The inter process communication is made by two
semaphores: "emptyS" and "fullS" in which "S" stands for "semaphore".
Key concepts for the PCP:
- one Producer;
- one Consumer;
- many resources stored in a common queue.
I dare to say that the other problem: (the Writer and Reader Problem or "WRP") can be
considered the opposite problem, in fact: there is only a resource (that I call RES) shared by
one or more Writers and by one or more Readers. There are some little but fundamental
rules:
- two or more Readers can access to the RES concurrently (not in mutual exclusion) between
them, but absolutely a Reader couldn't accesses to the RES with a Writer (there is a mutual
exclusion between Readers and Writers);
- two or more Writers couldn't access concurrently to the RES. Only one at a time can
accesses to the RES and so there is a relation of total mutual exclusion when a Writers uses
a RES.
There are two ways to deal with the WRP:
- Way One. Give precedence to the Writers. This could put the Readers in starvation (but not
the System in deadlock!).
- Way Two. Give precedence to the Readers. Viceversa: this could put the Writers in
starvation.
However, exist many complex and elegant solutions than I proposed, but historically the
WRP and PCP are these.
OR
Producer-Consumer problem
The Producer-Consumer problem is a classical multi-process synchronization problem,
that is we are trying to achieve synchronization between more than one process.
The task of the Producer is to produce the item, put it into the memory buffer, and
again start producing items. Whereas the task of the Consumer is to consume the item
from the memory buffer.
ADVERTISEMENT
ADVERTISEMENT
o The producer should produce data only when the buffer is not full. In case it is found
that the buffer is full, the producer is not allowed to store any data into the memory
buffer.
o Data can only be consumed by the consumer if and only if the memory buffer is not
empty. In case it is found that the buffer is empty, the consumer is not allowed to use
any data from the memory buffer.
o Accessing memory buffer should not be allowed to producer and consumer at the same
time.
Producer Code
Producer Code
BUFFER
As we can see from Fig: Buffer has total 8 spaces out of which the first 5 are filled, in =
5(pointing next empty position) and out = 0(pointing first filled position).
Let's start with the producer who wanted to produce an element " F ", according to
code it will enter into the producer() function, while(1) will always be true, itemP = F will
be tried to insert into the buffer, before that while(count == n); will evaluate to be False.
ADVERTISEMENT
Note: Semicolon after while loop will not let the code to go ahead if it turns out to be
True(i.e. infinite loop/ Buffer is full)
ADVERTISEMENT
ADVERTISEMENT
ADVERTISEMENT
ADVERTISEMENT
Load Rp, m[count] → will copy count value which is 5 to register Rp.
Consumer Code:
Now starting consumer who wanted to consume the first element " A ", according to
code it will enter into the consumer() function, while(1) will always be true, while(count
== 0); will evaluate to be False( since the count is still 5, which is not equal to 0.
Note: Semicolon after while loop will not let the code to go ahead if it turns out to be
True( i.e. infinite loop/ no element in buffer)
ADVERTISEMENT
A is removed now
Load Rc, m[count] → will copy count value which is 5 to register Rp.
Suppose after this Context Switch occurs back to the leftover part of producer code. . .
Since context switch at producer code was occurred after Increment and before the
execution of the third line (store m[count], Rp)
ADVERTISEMENT
Now the current value of count is 6, which is wrong as Buffer has only 5 elements,
this condition is known as Race Condition and Problem is Producer-Consumer
Problem.
To solve the problem occurred above of race condition, we are going to use Binary
Semaphore and Counting Semaphore
Binary Semaphore: In Binary Semaphore, only two processes can compete to enter into
its CRITICAL SECTION at any point in time, apart from this the condition of mutual
exclusion is also preserved.
Counting Semaphore: In counting semaphore, more than two processes can compete
to enter into its CRITICAL SECTION at any point of time apart from this the condition of
mutual exclusion is also preserved.
1. 1. wait( S )
2. {
3. while( S <= 0) ;
4. S--;
5. }
1. 2. signal( S )
2. {
3. S++;
4. }
From the above definitions of wait, it is clear that if the value of S <= 0 then it will enter
into an infinite loop (because of the semicolon; after while loop). Whereas the job of the
signal is to increment the value of S.
Let's see the code as a solution of producer and consumer problem using semaphore
( Both Binary and Counting Semaphore):
1. void consumer(void)
2. {
3. wait ( empty );
4. wait(S);
5. itemC = buffer[ out ];
6. out = ( out + 1 ) mod n;
7. signal(S);
8. signal(empty);
9. }
6. wait(empty) will decrease the value of the counting semaphore variable empty by 1,
that is when the producer produces some element then the value of the space gets
automatically decreased by one in the buffer. In case the buffer is full, that is the value
of the counting semaphore variable "empty" is 0, then wait(empty); will trap the process
(as per definition of wait) and does not allow to go further.
8. signal(s) increases the binary semaphore variable S to 1 so that other processes who
are willing to enter into its critical section can now be allowed.
10.0wait(full) will decrease the value of the counting semaphore variable full by 1, that
is when the consumer consumes some element then the value of the full space gets
automatically decreased by one in the buffer. In case the buffer is empty, that is the
value of the counting semaphore variable full is 0, then wait(full); will trap the process(as
per definition of wait) and does not allow to go further.
11. wait(S) decreases the binary semaphore variable S to 0 so that no other process
which is willing to enter into its critical section is allowed.
12. signal(S) increases the binary semaphore variable S to 1 so that other processes
who are willing to enter into its critical section can now be allowed.
Producer Code:
Let's start with producer() who wanted to produce an element " F ", according to code it
will enter into the producer() function.
Suppose just after this context switch occurs and jumps to consumer code.
Consumer Code:
Now starting consumer who wanted to consume first element " A ", according to code it
will enter into consumer() function,
A is removed now
Suppose just after this context, switch occurs back to producer code
Since the next instruction of producer() is wait(S);, this will trap the producer process, as
the current value of S is 0, and wait(0); is an infinite loop: as per the definition of wait,
hence producer cannot move further.
Since the next instruction of producer() is wait(S); will successfully execute, as S is now 1
and it will decrease the value of S by 1, i.e. S = 0
1. Counting Semaphore
2. Binary Semaphore
Counting Semaphore
In Counting Semaphore, the value of semaphore S is initialized to the number of
resources in the system. When a process needs to access shared resources, it calls
the wait() method on the semaphore, decreasing its value by one. When the shared
resource is released, it calls the signal() method, increasing the value by 1.
When the semaphore count reaches 0, it implies that the processes have used all
resources. Suppose a process needs to utilize a resource when the semaphore count is
0. In that case, it performs the wait() method, and it is blocked until another process
using the shared resources releases it, and the value of the semaphore increases to 1.
Binary Semaphore
Semaphore has a value between 0 and 1 in binary semaphore. It's comparable to mutex
lock, except that mutex is a locking method while the semaphore is a signalling method.
When a process needs to access a binary semaphore resource, it uses the wait() method
to decrement the semaphore's value from 1 to 0.
When the process releases the resource, it uses the signal() method to increase the
semaphore value to 1. When the semaphore value is 0, and a process needs to use the
resource, it uses the wait() method to block until the current process that is using the
resource releases it.
Syntax:
The syntax of the semaphore may be used as:
1. // Wait Operation
2. wait(Semaphore S) {
3. while (S<=0);
4. S--;
5. }
6. // Signal Operation
7. signal(Semaphore S) {
8. S++;
9. }
Advantages;
1. They don't allow multiple processes to enter the critical part simultaneously.
Mutual exclusion is achieved in this manner, making it much more efficient than
other synchronization techniques.
2. There is no waste of process time or resources as a result of the busy waiting in
semaphore. It is because processes are only allowed to access the critical section
if a certain condition is satisfied.
3. They enable resource management that is flexible.
4. They are machine-independent because they execute in the microkernel's
machine-independent code.
Disadvantages
1. There could be a situation of priority inversion where the processes with low
priority get access to the critical section than those with higher priority.
2. Semaphore programming is complex, and there is a risk that mutual exclusion
will not be achieved.
3. The wait() and signal() methods must be conducted correctly to avoid
deadlocks.
What is Monitor?
It is a synchronization technique that enables threads to mutual exclusion and
the wait() for a given condition to become true. It is an abstract data type. It has a
shared variable and a collection of procedures executing on the shared variable. A
process may not directly access the shared data variables, and procedures are required
to allow several processes to access the shared data variables simultaneously.
At any particular time, only one process may be active in a monitor. Other processes
that require access to the shared variables must queue and are only granted access after
the previous process releases the shared variables.
Syntax:
The syntax of the monitor may be used as:
1. monitor {
2.
3. //shared variable declarations
4. data variables;
5. Procedure P1() { ... }
6. Procedure P2() { ... }
7. .
8. .
9. .
10. Procedure Pn() { ... }
11. Initialization Code() { ... }
12. }
Advantages
1. Mutual exclusion is automatic in monitors.
2. Monitors are less difficult to implement than semaphores.
3. Monitors may overcome the timing errors that occur when semaphores are used.
4. Monitors are a collection of procedures and condition variables that are
combined in a special type of module.
Disadvantages
Access When a process uses shared resources, When a process uses shared
it calls the wait() method on S, and resources in the monitor, it has to
when it releases them, it uses the access them via procedures.
signal() method on S.
Action The semaphore's value shows the The Monitor type includes shared
number of shared resources available variables as well as a set of
in the system. procedures that operate on them.
Conclusion
In summary, semaphore and monitor are two synchronization mechanisms. A
semaphore is an integer variable that performs the wait() and signal() methods. In
contrast, the monitor is an abstract data type that enables only a process to use a
shared resource at a time. Monitors are simpler to implement than semaphores, and
there are fewer chances of making a mistake in monitors than with semaphores.
OR
Difference Between Semaphore and Monitor
in OS
DifferencesComputersOperating System
Read this article to find out more about semaphores and monitors and how they
are different from each other.
What is Semaphore?
A semaphore is a process synchronizing tool. It is basically an integer variable,
denoted by "S". The initialization of this variable "S" is done by assigning a
number equal to the number of resources present in the system.
There are two functions, wait() and signal(), which are used to modify the value of
semaphore "S". The wait() and signal() functions indivisibly change the value of
the semaphore "S". That means, when one process is changing the value of the
semaphore "S", another process cannot change the value of the semaphore at
the same time.
In operating systems, semaphores are grouped into two categories− counting
semaphore and binary semaphore. In a counting semaphore, the value of the
semaphore is initialized to the number of resources present in the system. On
the other hand, in a binary semaphore, the semaphore "S" has the value "0" or
"1".
What is Monitor?
Monitor is also a process synchronization tool. It is an abstract data type that is
used for highlevel synchronization of processes. It has been developed to
overcome the timing errors that occur while using the semaphore for the
process synchronization. Since the monitor is an abstract data type, it contains
the shared data variables. These data variables are to be shared by all the
processes. Hence, this allows the processes to execute in mutual exclusion.
There can be only one process active at a time within a monitor. If any other
process tries to access the shared variable in the monitor, it will be blocked and
lined up in the queue to get the access to the data. This is done by a
"conditional variable" in the monitor. The conditional variable is used for
providing additional synchronization mechanism.
S. Semaphore Monitor
No.
Conclusion
Both semaphores and monitors are types process synchronization tools in
operating systems, however they are quite different from each other, as
descried in the above table. The most significant difference that you should note
here is that a semaphore is an integer variable that indicates the number of
resources available in the system, whereas a monitor is an abstract data type
that allows only one process to be executed at a time.
OR
Implementing Monitor using Semaphore
When multiple processes run at the same time and share system resources, the results
may be different. This is called a critical section problem. To overcome this problem we
can implement many solutions. The monitor mechanism is the compiler-type solution to
avoid critical section problems. In this section, we will see how to implement a monitor
using semaphore.
Primary Terminologies
Operating System: The operating system acts as an interface or intermediary
between the user and the computer hardware.
Process: The program in the execution state is called a process.
Process synchronization: Process synchronization is a mechanism that controls the
execution of processes running concurrently to ensure that consistent results are
produced.
Semaphore: A semaphore is an operating system-type solution to the critical
section problem. It is a variable that is used to provide synchronization among
multiple processes running concurrently.
Critical Section: It is the section of the program where a process accesses the shared
resource during its execution.
critical section
Implementing Monitor using Semaphore
Let’s implement a a monitor mechanism using semaphores.
Following is a step-by-step implementation:
Step 1: Initialize a semaphore mutex to 1.
Step 2: Provide a semaphore mutex for each monitor.
Pause
Unmute
×
Step 3: A process must execute wait (mutex) before entering the monitor and must
execute signal (mutex) after leaving the monitor.
Step 4: Since a signaling process must wait until the resumed process either leaves or
waits, introduce an additional semaphore, S, and initialize it to 0.
Step 5: The signaling processes can use S to suspend themselves. An integer variable
S_count is also provided to count the number of processes suspended next. Thus, each
external function Fun is replaced by
wait (mutex);
body of Fun
if (S_count > 0)
signal (S);
else
signal (mutex);
Mutual exclusion within a monitor is ensured.
Let’s see how condition variables are implemented.
Step 1: x is condition.
Step 2: Introduce a semaphore x_num and an integer variable x_count.
Step 3: Initialize both semaphores to 0.
x.wait() is now implemented as: x.wait()
x_count++;
if (S_count > 0)
signal (S);
else
signal (mutex);
wait (x_num);
x_count–;
The operation x.signal () can be implemented as
if (x_count > 0) {
S_count++;
signal (x_num);
wait (S);
S_count–;
}
Conclusion
In conclusion, we can implement a monitor mechanism using semaphore mutex, and
condition variable is implemented using wait and signal operations to allow the process
to access critical sections in a synchronized manner to avoid inconsistent results,
and mutual exclusion is ensured within monitors.
FAQs on Implementing Monitor Using Semaphore
Q.1: How monitor is different from a semaphore?
Answer:
Monitor and semaphore both are solutions to the critical section problem. Monitor
mechanism is a type solution for a compiler. Monitor is a higher level synchronization
construct that makes process synchronization easier by offering a high level abstraction
for accessing and synchronizing data and semaphore is operating system type solution to
the critical section problem. It is variable which is used to provide synchronization
among multiple processes running concurrently.
Q.2: Why do we use condition variables?
Answer:
Condition variable helps to allow a process to wait within monitor. It includes two
operations i.e., wait and signal.
OR
Monitors vs Semaphores
Computer ScienceMCAOperating System
Monitors and semaphores are used for process synchronization and allow
processes to access the shared resources using mutual exclusion. However,
monitors and semaphores contain many differences. Details about both of these
are given as follows −
Monitors
Monitors are a synchronization construct that were created to overcome the
problems caused by semaphores such as timing errors.
Monitors are abstract data types and contain shared data variables and
procedures. The shared data variables cannot be directly accessed by a process
and procedures are required to allow a single process to access the shared data
variables at a time.
monitor monitorName
{
data variables;
Procedure P1(....)
{
Procedure P2(....)
{
Procedure Pn(....)
{
Initialization Code(....)
{
}
}
Only one process can be active in a monitor at a time. Other processes that
need to access the shared variables in a monitor have to line up in a queue and
are only provided access when the previous process release the shared
variables.
Semaphores
A semaphore is a signalling mechanism and a thread that is waiting on a
semaphore can be signalled by another thread. This is different than a mutex as
the mutex can be signalled only by the thread that called the wait function.
A semaphore uses two atomic operations, wait and signal for process
synchronization.
wait(S)
{
while (S<=0);
S--;
}
signal(S)
{
S++;
}
There are mainly two types of semaphores i.e. counting semaphores and binary
semaphores.
The binary semaphores are like counting semaphores but their value is
restricted to 0 and 1. The wait operation only works when the semaphore is 1
and the signal operation succeeds when semaphore is 0.