NOTE: These notes are adapted from those of
Allan Gottlieb, and are
reproduced here with his permission.
================ Start Lecture #5
A semaphore is an integer-valued variable that is manipulated through
the two operations DOWN(s) (also called P(s)) and UP(s) (also called V(s))
These are executed atomically (i.e. not interrupted in mid execution
by other user processes) as follows:
if (s > 0)
else block the calling process and put it on a queue waiting for s
if there are any processes waiting for s
then choose one such process and move it to the ready queue
semaphore s = 1
Readers and writers
- Two classes of processes.
- Readers, which can work concurrently.
- Writers, which need exclusive access.
- Must prevent 2 writers from being concurrent.
- Must prevent a reader and a writer from being concurrent.
- Must permit readers to be concurrent when no writer is active.
- Perhaps want fairness (i.e., freedom from starvation).
- Writer-priority readers/writers.
- Reader-priority readers/writers.
Quite useful in multiprocessor operating systems. The ``easy way
out'' is to treat all processes as writers in which case the problem
reduces to mutual exclusion (P and V). The disadvantage of the easy
way out is that you give up reader concurrency.
Semaphores: mutex = 1. db=1.
Shared variable: nreaders = 0.
nreaders++ *** WRITING DATA BASE ***
if (nreaders==1) DOWN(db) UP(db)
*** READING DATA BASE ***
if (nreaders==0) UP(db)
- Two classes of processes
- Producers, which produce times and insert them into a buffer.
- Consumers, which remove items and consume them.
- What if the producer encounters a full buffer?
Answer: Block it.
- What if the consumer encounters an empty buffer?
Answer: Block it.
- Also called the bounded buffer problem.
- Another example of active entities being replaced by a data
structure when viewed at a lower level (Finkel's level principle).
Initially e=k, f=0 (counting semaphore); b=open (binary semaphore)
loop forever loop forever
DOWN(e) DOWN(b); take item from buf; UP(b)
DOWN(b); add item to buf; UP(b) UP(e)
- k is the size of the buffer
- e represents the number of empty buffer slots
- f represents the number of full buffer slots
- We assume the buffer itself is only serially accessible. That is,
only one operation at a time.
- This explains the UP(b), DOWN (b) around buffer operations
- I use ; and put three statements on one line to suggest that
a buffer insertion or removal is viewed as one atomic operation.
- Of course this writing style is only a convention, the
enforcement of atomicity is done by the DOWN/UP
- The DOWN(e), UP(f) motif is used to force bounded alternation. If k=1
it gives strict alternation.
A classical problem from Dijkstra
What algorithm do you use for access to the shared resource (the
- 5 philosophers sitting at a round table
- Each has a plate of spaghetti
- There is a fork between each two
- Need two forks to eat
- The obvious solution (pick up right; pick up left) deadlocks.
- Big lock around everything serializes.
- Good code in the book.
The purpose of mentioning the Dining Philosophers problem without giving
the solution is to give a feel of what coordination problems are like.
The book gives others as well. We are skipping these (again this
material would be covered in a sequel course). If you are interested
look, for example,
|Per process items||Per thread items
|Address space||Program counter
|Global variables||Machine registers
|Signals and signal handlers
The idea is to have separate threads of control (hence the name)
running in the same address space. Each thread is somewhat like a
process (e.g., it is scheduled to run) but contains less state (the
address space belongs to the process in which the thread runs.
2.2.1: The Thread Model
A process contains a number of resources such as address space,
open files, accounting information, etc. In addition to these
resources, a process has a thread of control, e.g., program counter,
register contents, stack. The idea of threads is to permit multiple
threads of control to execute within one process. This is often
called multithreading and threads are often called
lightweight processes. Because the threads in a
process share so much state, switching between them is much less
expensive than switching between separate processes.
Individual threads within the same process are not completely
independent. For example there is no memory protection between them.
This is typically not a security problem as the threads are
cooperating and all are from the same user (indeed the same process).
However, the shared resources do make debugging harder. For example
one thread can easily overwrite data needed by another and if one thread
closes a file other threads can't read from it.
2.2.2: Thread Usage
Often, when a process A is blocked (say for I/O) there is still
computation that can be done. Another process can't B do this
computation since it doesn't have access to the A's memory. But two
threads in the same process do share the memory so there is no problem.
An important example is a multithreaded web server. Each thread is
responding to a single WWW connection. While one thread is blocked on
I/O, another thread can be processing another WWW connection. Why not
use separate processes, i.e., what is the shared memory?
Ans: The cache of frequently referenced pages.
A common organization is to have a dispatcher thread that fields
requests and then passes this request on to an idle thread.
Another example is a producer-consumer problem
in which we have 3 threads in a pipeline.
One reads data, the second processes the data read, and the third
outputs the processed data. Again, while one thread is blocked the
others can execute.
2.2.3: Implementing threads in user space
Write a (threads) library that acts as a mini-scheduler and
implements thread_create, thread_exit,
thread_wait, thread_yield, etc. The central data
structure maintained and used by this library is the thread
table the analogue of the process table in the operating system
- Requires no OS modification.
- Very fast since no context switching.
- Can customize the scheduler for each application.
- Re-doing the effort of writing a scheduler.
- Blocking systems can't be executed directly since that would block
the entire process.
- Similarly a page fault would block the entire process.
- A thread with infinite loop prevents all other threads in this
process from running.
2..2.4: Implementing Threads in the Kernel
Move the thread operations into the operating system itself. This
naturally requires that the operating system itself be (significantly)
modified and is thus not a trivial undertaking.
- Thread-create and friends are now system calls and hence much slower.
- A thread that blocks causes no particular problem. The kernel can
run another thread from this process or can run another process.
- Similarly a page fault in one thread does not automatically block
the other threads in the process.
2.2.5: Hybrid Implementations
One can write a (user-level) thread library even if the kernel also
has threads. Then each kernel thread can switch between user level
Making Single-threaded Code Multithreaded
Definitely NOT for the faint of heart.
- There often is state that should not be shared. A well-cite
example is the unix errno variable that contains the error
number (zero means no error) of the error encountered by the last
system call. Errno is hardly elegant, but its use is widespread.
If multiple threads issue faulty system calls the errno value of the
second overwrites the first and thus the first errno value may be lost.
- Much existing code, including many libraries, are not
- What should be done with a signal sent to a process. Does it go
to all or one thread?
- How should stack growth be managed. Normally the kernel grows the
(single) stack automatically when needed. What if there are
Chapter 3: Deadlocks
A deadlock occurs when a every member of a set of
processes is waiting for an event that can only be caused
by a member of the set.
Often the event waited for is the release of a resource.
In the automotive world deadlocks are called gridlocks.
- The processes are the cars.
- The resources are the spaces occupied by the cars
For a computer science example consider two processes A and B that
each want to print a file currently on tape.
- A has obtained ownership of the printer and will release it after
printing one file.
- B has obtained ownership of the tape drive and will release it after
reading one file.
- A tries to get ownership of the tape drive, but is told to wait
for B to release it.
- B tries to get ownership of the printer, but is told to wait for
A to release the printer.
The resource is the object granted to a process.
3.1.1: Preemptable and Nonpreemptable Resourses
- Resources come in two types
- Preemptable, meaning that the resource can be
taken away from its current owner (and given back later). An
example is memory.
- Non-preemptable, meaning that the resource
cannot be taken away. An example is a printer.
- The interesting issues arise with non-preemptable resources so
those are the ones we study.
- Life history of a resource is a sequence of
- Processes make requests, use the resourse, and release the
resourse. The allocate decisions are made by the system and we will
study policies used to make these decisions.
3.1.2: Resourse Acquisition
Simple example of the trouble you can get into.
- Two resources and two processes.
- Each process wants both resources.
- Use a semaphore for each. Call them S and T.
- If both processes execute P(S); P(T); --- V(T); V(S)
all is well.
- But if one executes instead P(T); P(S); -- V(S); V(T)
3.2: Introduction to Deadlocks
To repeat: A deadlock occurs when a every member of a set of
processes is waiting for an event that can only be caused
by a member of the set.
Often the event waited for is the release of
3.2.1: (Necessary) Conditions for Deadlock
The following four conditions (Coffman; Havender) are
necessary but not sufficient for deadlock. Repeat:
They are not sufficient.
- Mutual exclusion: A resource can be assigned to at most one
process at a time (no sharing).
- Hold and wait: A processing holding a resource is permitted to
- No preemption: A process must release its resources; they cannot
be taken away.
- Circular wait: There must be a chain of processes such that each
member of the chain is waiting for a resource held by the next member
of the chain.
3.2.2: Deadlock Modeling
On the right is the Resource Allocation Graph,
also called the Reusable Resource Graph.
- The processes are circles.
- The resources are squares.
- An arc (directed line) from a process P to a resource R signifies
that process P has requested (but not yet been allocated) resource R.
- An arc from a resource R to a process P indicates that process P
has been allocated resource R.