Lecture 8 Code example on csserver in directory /home/hwang/cs470/lecture08 Questions? Friday, January 27 CS 470 Operating Systems - Lecture 8 1 Outline Peterson's solution Semaphores Friday, January 27 CS 470 Operating Systems - Lecture 8 2 Recall: Generalized Process A process is generalized to: 1. Loop 1.1 Entry Section - code to attempt to gain entry 1.2 Critical Section (CS) - code to be protected 1.3 Exit Section - code to clean up 1.4 Remainder Section (RS) - non-CS code Protocol is the implementation of Entry and Exit sections. Friday, January 27 CS 470 Operating Systems - Lecture 8 3 Recall: Protocol Requirements A solution to the CS problem must meet three requirements Mutual exclusion (ME): when a process is in its CS, no other process can be in their CS Progress: if no process is in its CS and there is some process attempting to get in (i.e., executing 1.1), only waiting processes (i.e., other processes executing 1.1) can be involved in deciding which one will be next to enter its CS, and the choice cannot be postponed indefinitely. Friday, January 27 CS 470 Operating Systems - Lecture 8 4 Recall: Protocol Requirements Bounded wait: there must be an upper bound on the number of times other processes are allowed to enter their CS's after a process makes a request. Previous attempts failed the progress requirement, but other attempts can fail the other requirements. Friday, January 27 CS 470 Operating Systems - Lecture 8 5 Peterson's Solution Peterson's solution combines the ideas from Attempt 1 and Attempt 2. It uses the flags to allow immediate access when the other process is not trying to enter and uses the turn variable to determine who goes first when both processes are trying to enter at the same time. shared bool flag[2] = {false, false} // neither ready shared int turn = 0 // arbitrary initialization Friday, January 27 CS 470 Operating Systems - Lecture 8 6 Peterson's Solution 1. Loop 1.1. Set flag[ i ] to true // signal readiness to enter Set turn to j // assert other's turn While flag[ j ] AND turn = j do no-op // wait while the other is ready and it is // the other's turn 1.2. CS 1.3. Set flag[ i ] to false // no longer ready 1.4. RS Friday, January 27 CS 470 Operating Systems - Lecture 8 7 Peterson's Solution Check the requirements. Need to show that the criteria always holds. Do so by arguing that the wrong thing never happens. Friday, January 27 CS 470 Operating Systems - Lecture 8 8 Peterson's Solution ME: when Pi is ready (in 1.1), it sets flag[ i ] to true and turn to j. Consider three cases: Pj is not ready (in RS), so flag[ j ] is false. Pi enters its CS, and Pj then cannot enter until Pi leaves since flag[i] is true. Pj is in its CS, so flag[ j ] is true. Pi sees turn = j and is prevented from entering its CS until Pj leaves and sets flag[j] to false. Pj is ready (in 1.1). Each sees the other's flag is true, but turn can only be 0 or 1, so only one of the processes can enter, while the other must wait. Friday, January 27 CS 470 Operating Systems - Lecture 8 9 Peterson's Solution Bounded wait: there is only one place a process can get stuck, the while loop in 1.1 when flag[ j ] is true AND turn is j. Three cases again when Pi is ready. Pj is not ready (in RS), so flag[ j ] is false. Pi enters its CS. Pj is ready, flag[ j ] is true, but turn = i, so P i enters. Pj is ready, flag[ j ] is true, but turn = j, so P i waits. But when Pj finishes, it sets flag[ j ] to false. Even if Pj keeps the CPU and cycles back to 1.1., it sets turn = i, so will wait at 1.1. Eventually, it will be pre-empted and Pi will see turn = i and enter. So at most, Pi waits for one turn by Pj. Friday, January 27 CS 470 Operating Systems - Lecture 8 10 Peterson's Solution Progress: The same argument also shows progress since Pj does not affect Pi when it is not ready and there is a decision in all cases. Friday, January 27 CS 470 Operating Systems - Lecture 8 11 Peterson's Solution Peterson's solution primarily is a theoretical one that shows that CS's can be created using only load and store operations. Unfortunately, in many modern architectures, even load and store are not atomic operations. Also, it would be very tedious and error prone if the application programmer had to set up the solution each time there was a need for a CS. Friday, January 27 CS 470 Operating Systems - Lecture 8 12 Hardware Synchronization Hardware techniques for synchronization are covered in Section 6.4. These include masking interrupts during the CS, and atomic test-andset or swap instructions. This is still tedious and error prone. As with all software engineering problems, we abstract up a level and come up with higher-level ideas that are implemented using these techniques. Then we solve problems using the higher-level constructs as primitives. Friday, January 27 CS 470 Operating Systems - Lecture 8 13 Semaphores One of the first such ideas is the semaphore. Logically, the semaphore is an integer initialized to some value with two atomic operations. Classically, the initial value is 1 and the operations are defined as: void wait (semaphore s) { while (s <= 0) ; // do nothing s­­; } Friday, January 27 void signal (semaphore s) { s++; } CS 470 Operating Systems - Lecture 8 14 Semaphores Semaphores can be used to solve any ME problem. A shared resource has a semaphore initialized to 1 associated with it (often called a mutex, short for mutual exclusion) and used in the following way: shared semaphore mutex = 1 1. Loop 1.1 wait (mutex) 1.2 CS 1.3 signal(mutex) 1.4 RS Friday, January 27 CS 470 Operating Systems - Lecture 8 15 Semaphores If we assume some fairness to the Wait Queue, then this solution would meet the three criteria for a correct solution. In addition, semaphores can be used to synchronize the order in which processes execute. This is often done using a semaphore initialized to 0. shared semaphore synch = 0 P0: 1. Statement1 P1: 1. wait(synch) // waits for signal 2. signal(synch) 2. Statement2 Friday, January 27 CS 470 Operating Systems - Lecture 8 16 Semaphores The classical definition of semaphores has only values of 0 and 1 (also called a binary semaphore). Most systems generalize to counting semaphores where initial value is some n > 0. Can think of n as the number of processes that can pass through until a wait call causes an actual wait. It is also nice if wait( ) always decrements, then for a semaphore s, when s < 0, |s| is the number of waiting processes. Friday, January 27 CS 470 Operating Systems - Lecture 8 17 Semaphores An actual implementation would have the waiting processes block rather than busy wait. Such an implementation might look like: struct Semaphore { int value; list<pid_t> l; }; void wait(Semaphore &s) { s.value­­; if (s.value < 0) { s.l.push_back(PID); block(PID); } } Friday, January 27 void signal (Semaphore &s) { s.value++; if (s.value <= 0) { PID = s.l.front(); s.l.pop_front(); wake(PID); } } CS 470 Operating Systems - Lecture 8 18 Semaphores The implementations of signal and wait must be atomic. Usually use various hardware techniques. This does not eliminate busy waiting entirely, but reduces the size of the CS to just the bodies of signal and wait. Friday, January 27 CS 470 Operating Systems - Lecture 8 19 Producer/Consumer Problem Look at the Producer/Consumer problem again, which is one of the classical problems in synchronization. This problem requires both ME access synchronization (access to numItems) and order synchronization (to make sure the producer does not overwrite and the consumer does not reread). Think of the buffer as BUFSIZE slots all initially empty. Semaphores keep track of empty/full. Friday, January 27 CS 470 Operating Systems - Lecture 8 20 Producer/Consumer Processes shared semaphore mutex = 1, // ME to numItems empty = BUFSIZE, // # of empty full = 0; // # of full // Producer Item nextP; while (true) { nextP = MakeItem(); wait(empty); buffer[in] = nextP; in=(in+1)%BUFSIZE; wait(mutex); numItems++; signal(mutex); signal(full); } Friday, January 27 // Consumer Item nextC; while (true) { wait(full); nextC = buffer[out]; out=(out+1)%BUFSIZE; wait(mutex); numItems­­; signal(mutex); signal(empty); UseItem(nextC); } CS 470 Operating Systems - Lecture 8 21 Producer/Consumer Processes To see how this works, suppose the producer starts first without the consumer. The waits on empty decrement it. The signals on full increment it. Eventually, empty becomes 0, so the producer stops. (And full will be BUFSIZE.) When the consumer starts, the waits on full, decrement it. The signals on empty increment it and cause the producer to wake up. Vice versa, if the consumer starts first. The wait on full blocks, since it is 0, until the producer starts. Friday, January 27 CS 470 Operating Systems - Lecture 8 22 System V Semaphores An example implementation using System V (SysV) semaphores is shown in file shm-semprod-cons.cpp System V semaphore routines are defined in library <sys/sem.h>. This is the same program that uses shared memory with synchronization by semaphores added. In addition to the mutex, empty, and full semaphores, there also is a semphore (init) to make sure the shared memory is initialized. Friday, January 27 CS 470 Operating Systems - Lecture 8 23