Sei sulla pagina 1di 11

1.

Semaphores

When two or more processes or threads share some common


data, the final result of the shared data may depend on which process
executes first and precisely when. This is commonly referred as the
race condition problem. For example, two processes both increment
a shared counter. At the assembly language level, each process would
have code for counter increment like the following:

LOAD R1, M(counter); // load the counter value to register R1


INC R1; // increment the register R1
STORE R1, M(counter); // store the result back to counter

Assume the current value of the counter is 8. Then after both


processes complete their counter increment operations, we expect the
counter has value 10, no matter which process is executed first. But
when the two processes are executed concurrently the resultant value
of the counter may be 9, instead of correct value 10. Here is how it
may happen. Let’s say the first process (P1) executes the LOAD
instruction and so R1 is equal to 8, then P1 proceeds to increment R1
to 9. Before P1 executes the STORE instruction to store the result
back to the counter, it is preempted by the other process (P2)
(because P1’s time quantum is used up, for example). P2 executes its
LOAD instruction, since the modified value by P1 has not been stored
back to the counter yet, so R1 is 8. (Note: P1 and P2 have their own
contexts, when P2 starts its execution it has its own R1 through
context switch.) P2 proceeds to increment R1 by 1 and stores its
value, which is 9, to the counter. Later when P1 gets the CPU it
executes the STORE instruction which stores its R1 value, which is 9,
to the counter. Thus, the counter has incorrect value 9, instead of
correct value 10.

In this section we will introduce a process synchronization


mechanism called semaphores that may be used to solve the above-

5/18/2019 -- 11:06 AM 1/11


mentioned problem and we will discuss different applications of
semaphores.

1.1 The Definition of Semaphores

E. W. Dijstra proposed semaphores in 1965 as a mechanism for


process synchronization. A semaphore is a non-negative integer with
two access primitives, called P and V operations, which are the only
way for processes or threads to access the semaphore. The V
operation is to increment the value of the semaphore by 1 and the P
operation is to decrement the value of the semaphore by 1 as soon as
the resulting value would be non-negative. The two operations are
indivisible or atomic. For the V operation to be indivisible its
execution must not be interrupted by any P or V operations executed
by other processes on the same semaphore. For the P operation, there
are two separate execution branches based on the condition to be
checked. Logically it can be modeled as follows:

yes no
value >= 1

value = nothing
value -1

C
B

Figure 1 The execution branches of the P operation.

The indivisibility of the P operation means that the execution


from A to B or from A to C cannot be interrupted by any other P or V
operations performed on the same semaphore. The indivisibility
property of the P and V operations is necessary to guarantee the
correctness for the intended purpose of semaphores. If they were not
indivisible, then the value of the semaphore would a variable shared
by processes that perform P and V operation on it. Then the same
exact race condition problem described above may occur on this
semaphore.

5/18/2019 -- 11:06 AM 2/11


We want to note here that the indivisibility of the P and V
operations are only applicable to P and V operations that access the
same semaphore. A P operation (or a V operation) on semaphore S1
may be interrupted by a P or V operation performed on a different
semaphore S2.

It also be noted that the P operation may be interrupted when it


goes from point C to A, i.e., interruption is allowed to occur before the
operation tests the value of the semaphore again after a failed test.

Often people use wait and signal for P and V, respectively.


Another pair of names for P and V is down and up, respectively.

Semaphores often are further divided into binary semaphores


and general semaphores. A binary semaphore is one whose initial
value is 1, and a general semaphore is one whose initial value can
be any nonnegative integer. Binary semaphores are sometime referred
as mutual exclusion locks, or simply, mutex.

1.2 Implementation of Semaphores

In the above definition the P operation uses busy waiting to wait


for the semaphore value to be changed (A to C and back to A may be
considered as a while loop.). Busy waiting is not desirable in any real
system since it wastes CPU time. Most implementations of
semaphores in today’s operating systems put the calling process to a
waiting queue designated to the semaphore if the P operation cannot
be completed. When a V operation is executed, a waiting process, if
any, is awaken up and moved to ready state. This type implementation
may be modeled as follows for the P and V operations.

yes no
value >= 1

value = Go to sleep
value -1

B C

5/18/2019 -- 11:06 AM 3/11


Figure 2 Implementation of the P operation.

yes no
process
waiting?

Wakeup a value =
waiting value +1

B C

Figure 3 Implementation of the V operation.

For both operations the execution sequences from A to B or


from A to C must be indivisible for P and V operations performed on
the same semaphore.

1.3 The POSIX Semaphore API

IEEE Standard for Information Technology: Portable Operating


System Interface (POSIX) defines a standard operating system
interface and environment to support application portability at the
source code level. One of its amendments is Real Time Extension,
which defines the interface and environment for real-time
applications. One of the services specified in the extension is
semaphore. POSIX Rea-time Extension defines the following
semaphore functions in the syntax of the C programming language
and this set of function has been implemented by many real-time
operating systems including BrickOS.

Function Signature Description


int sem_init (sem_t *sem, int pshared, Initialize a semaphore.
unsigned int value)
int sem_wait (sem_t *sem) Wait for semaphore
(blocking).
int sem_trywait (sem_t *sem) Wait for semaphore (non-

5/18/2019 -- 11:06 AM 4/11


blocking).
int sem_post (sem_t *sem) Post a semaphore.
int sem_getvalue (sem_t *sem, int *sval) Get the semaphore value.
int sem_destroy (sem_t *sem) Destroy a semaphore.

The header <semaphore.h>, which must be provided by the


operating system if it conforms to the POSIX Real-time Extension,
defines the data type sem_t to represent semaphores. sem_init()
initializes the semaphore of sem with the initial value. The pshared
parameter is ignored in BrickOS1. sem_wait() peforms the wait
operation on semaphore sem and if the value of sem is less than one,
the calling thread is blocked. sem_trywait() does the same as
sem_wait() except it returns with error EAGAIN when the value of
the semaphore is less than one. The former is commonly called
blocking wait and the latter non-blocking wait. sem_post() performs
the signal operation of semaphore. sem_getValue() returns the
current value of the semaphore sem. sem_destroy() destroys the
semaphore sem.

1.4 Applications of Semaphores

Semaphores are most commonly used for mutual exclusion to


solve the race condition problem. They have also been used in other
applications. One such application is to enforce certain execution
sequences among statements/functions in different processes.
Another application is to use semaphores as counters to represent the
number of available system resources. In this section we will take a
look of the three applications of semaphores.

1.4.1 Mutual Exclusion

The question is how to solve the race condition problem


introduced at the beginning part of this document. We can use a
semaphore (sem) with initial value 1 and modify the increment
operation part as follows:

sem_wait(sem);
LOAD R1, M(counter); // load the counter value to register R1
INC R1; // increment the register R1
STORE R1, M(counter); // store the result back to counter
sem_post(sem);

1
pshared is used for multiple processes to share the same semaphore. Since BrickOS does not support
processes, so this option is not applicable and is ignored.

5/18/2019 -- 11:06 AM 5/11


Since the semaphore’s initial value is 1, the first process (P1)
will decrement the semaphore’s value by 1 and proceed to carry out
its increment counter operation. If P2 tries to increment the counter it
must execute its sem_wait on the same semaphore. Since the
semaphore’s value is 0 now, P2 will have to wait. When P1 completes
its increment operation, it executes the sem_post, which would
awaken P2. When P2 continues its execution at a later time, it will
increment the semaphore value based on the modified value by P1.

1.4.2 Enforcing Execution Sequences

Semaphores may be used for process synchronization in two


different ways. One is to enforce execution sequence among
processes. For example, if we want the statements among the
following three processes to be executed in the order depicted in
Figure 4 An example of enforced execution sequence.

T1 T2 T3

S1 S2
S3

S4

Figure 4 An example of enforced execution sequence.

We may use three semaphores (sem1, sem2, and sem3) with


initial value 0 for all to enforce the execution sequence as follows:

T1 T2 T3
.

S1 S2
sem_post(sem1); sem_wait(sem1); sem_post(sem2);
sem_wait(sem2);
S3
sem_post(sem3);
sem_wait(sem3);
S4

If T1 executes S1 and T3 executes S2 before T2’s S2, sem1 and


sem2 would have both value 1 when T2 tries to execute S3. Thus T2
will pass the two wait operations on sem1 and sem2 and then
proceeds to execute S3. However, if T2 tries to execute S3 before

5/18/2019 -- 11:06 AM 6/11


either T1 or T3 executes its post operation on sem1 or sem2, T2 would
have to wait on the semaphore sem1 or sem2, respectively. The same
is true for the execution order between S3 and S4. Thus the
execution sequence is enforced by the three semaphores.

1.4.3 Resource Counters

Semaphores may also be used for system resource control. For


example, suppose there are three printers to be shared by multiple
threads. To ensure a printer is only being used by one thread, we can
use a semaphore to control the allocation of the printers. The printer
semaphore is initialized three, the number of available shared system
resources, in this case, printers.

sem_t pntr_sem;
sem_t mutex;

int main() {
sem_init(&pntr_sem, 0, 3);
sem_init(&mutex, 0, 1);

execi(thread_fun, …);
execi(thread_fun, …);

}
void thread_fun() {
printer = allocate_pntr();
// use printer
deallocate(printer);
}

int allocate() {
sem_wait(&pntr_sem);
sem_wait(&mutext);
int printer = get_printer();
sem_post(&mutex);
return printer;
}
void deallocate(int printer) {
sem_wait(&mutex);
release_printer(printer);
sem_post(&mutex);
sem_post(&pntr_sem);
}

Question: why semaphore mutex is needed in the above example?

5/18/2019 -- 11:06 AM 7/11


1.5 Object-Oriented Implementation of Semaphores

In this section we will show how to implement semaphores as


objects in BrickOS and C++ and also in the LeJOS environment.

1.5.1 BrickOS and C++

Here is the source code of Semaphore.H. It encapsulates the


low level details of semaphore operation and provides an object-
oriented interface.

//file name: Semaphore.H


#include <semaphore.h>

class Semaphore {
private:
sem_t sem;
public:
Semaphore() { Semaphore(1); }
Semaphore(int val) {
sem_init(&sem, 0, val);
}
int wait() {
return sem_wait(&sem);
}
int trywait() {
return sem_trywait(&sem);
}
int signal() {
return sem_post(&sem);
}
int getValue() {
int value;
sem_getvalue(&sem, &value);
return value;
}
void destroy() {
sem_destroy(&sem);
}
~Semaphore() {
sem_destroy(&sem);
}
};

The following program, using the Semaphore.H file defined


above, creates two threads, which populate the same 10-element
array with a given value. When the semaphore operations are
commented out, the resultant array contains 3, 3, 3, 3, 3, 3, 5, 5, 5.
That implies that two threads were accessing the array at the same
time. When the semaphore operations are kept, the resultant array

5/18/2019 -- 11:06 AM 8/11


contains either all 3’s, by the first thread, or all 5’s by the second
thread.

// filename: critical-sec.C
#include <config.h>
#include <conio.h>
#include <unistd.h>
#include <stdlib.h>
#include <dlcd.h>
#include <sys/tm.h>
#include "Semaphore.H"

int thread1(int argc, char** argv);


int thread2(int argc, char** argv);

class Shared {
private:
int shared[10];
public:
Shared(int x) {
for (int i=0; i<10; i++) shared[i] = x;
}
void set(int idx, int y) {
shared[idx] = y;
}
void print() {
for (int i=0; i<10; i++) {
lcd_int(shared[i]);
sleep(2);
lcd_clear();
sleep(1);
}
}
};
Shared shared(10);
Semaphore *mutex;
int value1 = 3;
int value2 = 5;
int main(int argc, char **argv)
{
tid_t pid1, pid2;
mutex = new Semaphore(1);
pid1 = execi(&thread1, 0, 0, 15, DEFAULT_STACK_SIZE);
lcd_int(pid1);
pid2 = execi(&thread2, 0, 0, 15, DEFAULT_STACK_SIZE);
lcd_int(pid1);
sleep(1);
cputs("before");
sleep(5);
shared.print();
cputs("after");
}
int thread1(int val, char** ptr) {
lcd_int(mutex->getValue());
mutex->wait();
lcd_int(mutex->getValue());

5/18/2019 -- 11:06 AM 9/11


for (int i=0; i<10; i++) {
shared.set(i, value1);
msleep(10-i);
lcd_int(value1);
}
mutex->signal();
lcd_int(mutex->getValue());
sleep(2);
return 0;
}

int thread2(int val, char** ptr) {


lcd_int(mutex->getValue());
mutex->wait();
lcd_int(mutex->getValue());
for (int i=0; i<10; i++) {
shared.set(i, value2);
msleep(5+i);
lcd_int(value2);
}
mutex->signal();
lcd_int(mutex->getValue());
sleep(2);
return 0;
}

The above example uses the semaphore class, which is defined


in Section 1.5. It first defines a semaphore object named mutex with
initial value 1. To perform a wait or signal operation on mutex, it uses
the C++ member function syntax: mutex->wait() or mutex-
>signal().

1.5.1 LeJOS and Java

The Java programming language does not support semaphore;


instead it supports a higher-level abstraction mechanism for solving
the race condition problem. The mechanism is called monitors, or
synchronized objects in Java terminology. Java also provides wait()
and notify() and nottifyAll() methods for thread synchronization.
The wait() method of Java puts the calling thread to waiting state
until a notify() or notifyAll() is executed on the same synchronized
object in which the wait() was called. The following shows how to
implement semaphores in Java. Since word wait is a reserved word in
Java, we choose another pair of names down and up for semaphore’s
wait and signal, respectively. In this implementation, semaphores are
synchronized objects with a private integer for semaphore’s value.
The down() operation checks the value. If the value is less than one,
the calling thread is put to waiting state, otherwise the value is
decremented by 1 and the down() operation returns. The up()
operation increments the semaphore value by 1 and then notifyAll all

5/18/2019 -- 11:06 AM 10/11


waiting threads, if any. Because the down() and up() operations are
synchronized methods, so the Java Virtual Machine guarantees their
mutual exclusion.

class Semaphore {
private int value;

Semaphore(int value) {
if (value < 0) throws
new IllegalArgumentException(value + “<0”);
this.value = value;
}
public synchronized down() throws InterruptedException {
while (value < 1) wait();
value = value –1;
}
public synchronized void up() {
value = value + 1;
notifyAll();
}
};

References

5/18/2019 -- 11:06 AM 11/11

Potrebbero piacerti anche