15 Synchronization
15 Synchronization
My formulation
OS = data structurs + synchronization
Synchronization problems make writing OS
code challenging
Demand exceptional coding skills
Race problem
long c = 0, c1 = 0, c2 = 0, run = 1; int main() {
void *thread1(void *arg) { pthread_t th1, th2;
while(run == 1) { pthread_create(&th1, NULL, thread1,
c++; NULL);
c1++; pthread_create(&th2, NULL, thread2,
} NULL);
} //fprintf(stdout, "Ending main\n");
void *thread2(void *arg) { sleep(2);
while(run == 1) { run = 0;
c++; fprintf(stdout, "c = %ld c1+c2 = %ld
c2++; c1 = %ld c2 = %ld \n", c, c1+c2, c1, c2);
} fflush(stdout);
} }
Race problem
On earlier slide
Value of c should be equal to c1 + c2, but it is not!
Why?
There is a “race” between thread1 and thread2 for
updating the variable c
thread1 and thread2 may get scheduled in any
order and interrupted any point in time
The changes to c are not atomic!
What does that mean?
Race problem
C++, when converted to assembly code, could be
mov c, r1
add r1, 1
mov r1, c
Now following sequence of instructions is possible among
thread1 and thread2
thread1: mov c, r1
thread2: mov c, r1
thread1: add r1, 1
thread1: mov r1, c
thread2: add r1, 1
thread2: mov r1, c
What will be value in c, if initially c was, say 5?
It will be 6, when it is expected to be 7. Other variations also possible.
Races: reasons
Interruptible kernel
If entry to kernel code does not disable interrupts, then modifications
to any kernel data structure can be left incomplete
This introduces concurrency
Multiprocessor systems
On SMP systems: memory is shared, kernel and process code run on
all processors
Same variable can be updated parallely (not concurrently)
What about non-interruptible kernel on multiprocessor
systems?
What about non-interruptible kernel on uniprocessor systems?
Critical Section Problem
Consider system of n processes {p0, p1, … pn-1}
Each process has critical section segment of code
Process may be changing common variables, updating table,
writing file, etc
When one process in critical section, no other may be in its
critical section
Critical section problem is to design protocol to solve this
Each process must ask permission to enter critical section
in entry section, may follow critical section with exit
section, then remainder section
Especially challenging with preemptive kernels
Critical Section problem
Expected solution characteristics
1. Mutual Exclusion
If process Pi is executing in its critical section, then no other
processes can be executing in their critical sections
2. Progress
If no process is executing in its critical section and there exist
some processes that wish to enter their critical section, then the
selection of the processes that will enter the critical section next
cannot be postponed indefinitely
3. Bounded Waiting
A bound must exist on the number of times that other processes
are allowed to enter their critical sections after a process has
made a request to enter its critical section and before that
request is granted
Assume that each process executes at a nonzero speed
No assumption concerning relative speed of the n processes
suggested solution - 1
int flag = 1;
What’s wrong here?
void *thread1(void *arg) {
while(run == 1) {
Assumes that
while(flag == 0) while(flag ==) ; flag
; =0
flag = 0;
c++;
will be atomic
flag = 1;
c1++;
}
}
suggested solution - 2
int flag = 0; void *thread2(void *arg) {
void *thread1(void *arg) { while(run == 1) {
while(run == 1) { if(!flag)
if(flag)
c++;
c++;
else
else
continue;
continue;
c1++; c2++;
flag = 0; flag = 1;
} }
} }
Peterson’s solution
Two process solution
Assume that the LOAD and STORE instructions are atomic;
that is, cannot be interrupted
The two processes share two variables:
int turn;
Boolean flag[2]
The variable turn indicates whose turn it is to enter the
critical section
The flag array is used to indicate if a process is ready to
enter the critical section. flag[i] = true implies that process Pi
is ready!
Peterson’s solution
do {
flag[i] = TRUE;
turn = j;
while (flag[j] && turn == j)
;
critical section
flag[i] = FALSE;
remainder section
} while (TRUE);
Provable that
Mutual exclusion is preserved
Progress requirement is satisfied
Bounded-waiting requirement is met
Hardware solution – the one
actually implemented
Many systems provide hardware support for critical section
code
Uniprocessors – could disable interrupts
Currently running code would execute without preemption
Generally too inefficient on multiprocessor systems
Operating systems using this not broadly scalable
Modern machines provide special atomic hardware instructions
Atomic = non-interruptable
Either test memory word and set value
Or swap contents of two memory words
Basically two operations (read/write) done atomically in hardware
Solution using test-and-set
lock =- false; //global
Definition:
do {
while ( TestAndSet (&lock )) boolean TestAndSet (boolean
; // do nothing *target)
// critical section {
lock = FALSE; boolean rv = *target;
// remainder section *target = TRUE;
} while (TRUE); return rv:
}
Solution using swap
lock = false; //global
do {
key = true
while ( key == true))
swap(&lock, &key)
// critical section
lock = FALSE;
// remainder section
} while (TRUE);
Spinlock
A lock implemented to do ‘busy-wait’
Using instructions like T&S or Swap
As shown on earlier slides
spinlock(int *lock){
While(test-and-set(lock))
;
}
spinunlock(lock *lock) {
*lock = false;
}
do {
Bounded wait M.E. with T&S
waiting[i] = TRUE;
key = TRUE;
while (waiting[i] && key)
key = TestAndSet(&lock);
waiting[i] = FALSE;
// critical section
j = (i + 1) % n;
while ((j != i) && !waiting[j])
j = (j + 1) % n;
if (j == i)
lock = FALSE;
else
waiting[j] = FALSE;
// remainder section
} while (TRUE);
Some thumb-rules of spinlocks
Never block a process holding a spinlock !
Typical code:
while(condition)
{ Spin-unlock()
Schedule()
Spin-lock()
}
Hold a spin lock for only a short duration of time
Spinlocks are preferable on multiprocessor systems
Cost of context switch is a concern in case of sleep-wait locks
Short = < 2 context switches
sleep-locks
Spin locks result in busy-wait
CPU cycles wasted by waiting
processes/threads
Solution – threads keep waiting for the lock
to be available
Move therad to wait queue
The thread holding the lock will wake up one of
them
Sleep locks/mutexes
//ignore syntactical issues Block(mutex *m, spinlock *sl) {
typedef struct mutex { spinunlock(sl);
currprocess->state = WAITING
int islocked;
move current process to m->q
int spinlock;
Sched();
waitqueue q;
spinlock(sl);
}mutex;
}
wait(mutex *m) {
release(mutex *m) {
spinlock(m->spinlock);
spinlock(m->spinlock);
while(m->islocked)
m->islocked = 0;
Block(m, m->spinlock)
Some process in m->queue
lk->islocked = 1; =RUNNABLE;
spinunlock(m->spinlock); spinunlock(m->spinlock);
} }
Locks in xv6 code
struct spinlock
// Mutual exclusion lock.
struct spinlock {
uint locked; // Is the lock held?
// For debugging:
char *name; // Name of lock.
struct cpu *cpu; // The cpu holding the lock.
uint pcs[10]; // The call stack (an array of program counters)
// that locked the lock.
};
spinlocks in xv6 code
struct { static struct spinlock idelock;
struct spinlock lock; struct {
struct buf buf[NBUF]; struct spinlock lock;
// For debugging:
char *name; // Name of lock.
int pid; // Process holding lock
};
Sleeplock acquire and release
void void
acquiresleep(struct sleeplock *lk)
releasesleep(struct sleeplock
{
*lk)
acquire(&lk->lk);
while (lk->locked) { {
/* Abhijit: interrupts are not disabled in acquire(&lk->lk);
sleep !*/
sleep(lk, &lk->lk); lk->locked = 0;
} lk->pid = 0;
lk->locked = 1;
wakeup(lk);
lk->pid = myproc()->pid;
release(&lk->lk); release(&lk->lk);
} }
Where are sleeplocks used?
struct buf
Just two !
waiting for I/O on
this buffer
struct inode
waiting for I/o to this
inode
Sleeplocks issues
sleep-locks support yielding the processor during their critical
sections.
This property poses a design challenge:
if thread T1 holds lock L1 and has yielded the processor (waiting for some
other condition),
and thread T2 wishes to acquire L1,
we have to ensure that T1 can execute
while T2 is waiting so that T1 can release L1.
T2 can’t use the spin-lock acquire function here: it spins with interrupts
turned off, and that would prevent T1 from running.
To avoid this deadlock, the sleep-lock acquire routine (called
acquiresleep) yields the processor while waiting, and does not
disable interrupts.
Sleep-locks leave interrupts enabled, they cannot be used in
interrupt handlers.
More needs of synchronization
Not only critical section problems
Run processes in a particular order
Allow multiple processes read access, but
only one process write access
Etc.
Semaphore
Synchronization tool that
Can only be accessed via two
indivisible (atomic) operations
does not require busy
wait (S) {
waiting
while S <= 0
Semaphore S – integer ; // no-op
variable S--;
Two standard operations }
modify S: wait() and signal (S) {
signal() S++;
}
Originally called P() and V()
--> Note this is Signal() on a
Less complicated semaphore, different froms signal
system call
Semaphore for synchronization
Counting semaphore – integer value can range over an unrestricted domain
Binary semaphore – integer value can range only between 0
and 1; can be simpler to implement
Also known as mutex locks
Can implement a counting semaphore S as a binary semaphore
Provides mutual exclusion
Semaphore mutex; // initialized to 1
do {
wait (mutex);
// Critical Section
signal (mutex);
// remainder section
} while (TRUE)
Semaphore implementation
Wait(sem *s) {
Left side – expected
while(s <=0) behaviour
block(); // could be ";"
Both the wait and
s--; signal should be
} atomic.
signal(sem *s) {
This is the sematics
s++; of the semaphore.
}
Semaphore implementation? - 1
struct semaphore { signal(seamphore *s) {
int val; spinlock(*(s->sl));
spinlock lk; (s->val)++;
};
spinunlock(*(s->sl));
sem_init(semaphore *s, int initval) {
}
s->val = initval;
- suppose 2 processes trying wait.
s->sl = 0;
} val = 1;
wait(semaphore *s) { Th1: spinlock Th2: spinlock-waits
spinlock(&(s->sl)); Th1: while -> false, val-- => 0; spinulock;
while(s->val <=0) Th2: spinlock success; while() -> true, loops;
; Th1: is done with critical section, it calls
(s->val)--; signal. it calls spinlock() -> wait.
spinunlock(&(s->sl)); Who is holding spinlock-> Th2. Itis waiting
} for val > 0. Who can set value > 0 , ans: Th1,
and Th1 is waiting for spinlock which is held
by The2.
circular wait. Deadlock.
None of them will proceed.
Semaphore implementation? - 2
struct semaphore { wait(semaphore *s) {
int val;
spinlock(&(s->sl));
spinlock lk;
};
while(s->val <=0) {
sem_init(semaphore *s, int initval) { spinunlock(&(s->sl));
s->val = initval; spinlock(&(s->sl));
s->sl = 0;
}
}
(s->val)--;
signal(seamphore *s) {
spinlock(*(s->sl)); spinunlock(&(s->sl));
(s->val)++; }
spinunlock(*(s->sl));
Problem: race in spinlock of whille
} loop and signal's spinlock.
Bounded wait not guaranteed.
Spinlocks are not good for a long
wait.
Semaphore implementation? - 3,
idea
struct semaphore { wait(semaphore *s) {
int val; spinlock(&(s->sl));
spinlock lk;
while(s->val <=0) {
};
Block();
sem_init(semaphore *s, int initval) {
s->val = initval; }
s->sl = 0; (s->val)--;
} spinunlock(&(s->sl));
block() { }
put this current process on wait-q; signal(seamphore *s) {
schedule();
spinlock(*(s->sl));
}
(s->val)++;
spinunlock(*(s->sl));
}
Semaphore implementation? - 3a
struct semaphore { wait(semaphore *s) {
int val;
spinlock(&(s->sl));
spinlock lk;
while(s->val <=0) {
list l;
}; spinunlock(&(s->sl));
sem_init(semaphore *s, int initval) { block(s);
s->val = initval; }
s->sl = 0;
(s->val)--;
}
block(semaphore *s) {
spinunlock(&(s->sl));
listappend(s->l, current); }
schedule(); signal(seamphore *s) {
} spinlock(*(s->sl));
problem is that block() will be called
without holding the spinlock and the (s->val)++;
access to the list is not protected. spinunlock(*(s->sl));
Note that - so far we have ignored changes
to signal() }
Semaphore implementation? - 3b
struct semaphore { wait(semaphore *s) {
spinlock(&(s->sl));
int val;
while(s->val <=0) {
spinlock lk; block(s);
list l; }
}; (s->val)--;
raw_spinlock_t lock; {
unsigned long flags;
unsigned int count;
struct list_head wait_list;
raw_spin_lock_irqsave(&sem->lock, flags);
};
if (likely(sem->count > 0))
static noinline void __sched
sem->count--;
__down(struct semaphore *sem)
else
{
__down(sem);
__down_common(sem,
raw_spin_unlock_irqrestore(&sem->lock,
TASK_UNINTERRUPTIBLE, flags);
MAX_SCHEDULE_TIMEOUT);
}
}
Semaphore in Linux
static inline int __sched for (;;) {
__down_common(struct semaphore if (signal_pending_state(state, task))
*sem, long state, long timeout)
goto interrupted;
{
if (unlikely(timeout <= 0))
struct task_struct *task = current; goto timed_out;
struct semaphore_waiter waiter; __set_task_state(task, state);
list_add_tail(&waiter.list, &sem- raw_spin_unlock_irq(&sem->lock);
>wait_list);
timeout = schedule_timeout(timeout);
waiter.task = task; raw_spin_lock_irq(&sem->lock);
waiter.up = false; if (waiter.up)
return 0;
}
....
}
Deadlocks
Deadlock
two or more processes are waiting indefinitely for an event that can
be caused by only one of the waiting processes
Let S and Q be two semaphores initialized to 1
P0 P1
wait (S); wait (Q);
wait (Q); wait (S);
. .
. .
. .
signal (S); signal (Q);
signal (Q); signal (S);
Example of deadlock
Let’s see the pthreads program : deadlock.c
Same programe as on earlier slide, but with
pthread_mutex_lock();
Non-deadlock, but similar
situations
Starvation – indefinite blocking
A process may never be removed from the semaphore
queue in which it is suspended
Priority Inversion
Scheduling problem when lower-priority process holds a
lock needed by higher-priority process (so it can not pre-
empt lower priority process), and a medium priority
process (that does not need the lock) pre-empts lower
priority task, denying turn to higher priority task
Solved via priority-inheritance protocol : temporarily
enhance priority of lower priority task to highest
Livelock
Similar to deadlock, but processes keep doing
‘useless work’
E.g. two people meet in a corridor opposite each
other
Both move to left at same time
Then both move to right at same time
Keep Repeating!
No process able to progress, but each doing ‘some
work’ (not sleeping/waiting), state keeps changing
Livelock example
/* thread one runs in this function */ /* thread two runs in this function */
void *do work one(void *param) void *do work two(void *param)
{ {
int done = 0; int done = 0;
while (!done) { while (!done) {
pthread mutex lock(&first mutex); pthread mutex lock(&second mutex);
if ( pthread mutex trylock (&second mutex)) { if ( pthread mutex trylock (&first mutex)) {
/** /**
* Do some work * Do some work
*/ */
pthread mutex unlock(&second mutex); pthread mutex unlock(&first mutex);
/* On each use*/
P (&sem);
Use resource;
V (&sem);
Event-wait
/* During initialization */
semaphore event;
initsem (&event, 0); /* probably at boot time */
Readers-Writers if (readcount == 0)
signal (wrt) ;
problem signal (mutex) ;
} while (TRUE);
Readers-Writers Problem
Variations
First variation – no reader kept waiting unless
writer has permission to use shared object
Second variation – once writer is ready, it
performs write asap
Both may have starvation leading to even
more variations
Problem is solved on some systems by kernel
providing reader-writer locks
Reader-write lock
A lock with following operations on it
Lockshared()
Unlockshared()
LockExcl()
UnlockExcl()
Possible additions
Downgrade() -> from excl to shared
Upgrade() -> from shared to excl
Code for reader-writer locks
struct rwlock { void lockShared {struct rwlock *r)
(
int nActive; /* num of active spin_lock {&r->sl);
readers, or-1 if a writer is r->nPendingReads++;
active */ if (r->nPendingWrites > O)
if(lk != &ptable.lock){
acquire(&ptable.lock);
release(lk);
}
with
release(lk);
acquire(&ptable.lock);
Doing this would break sleep. How?
`
bget() problem
bget() panics if no free buffers!
Quite bad
Should sleep !
But that will introduce many deadlock
problems. Which ones ?
iget() and ilock()
iget() does no hold lock on inode
Ilock() does
Why this separation?
Performance? If you want only “read” the inode,
then why lock it?
What if iget() returned the inode locked?
Interesting cases in namex()
while((path = skipelem(path, name)) ! if((next = dirlookup(ip, name, 0)) == 0){
= 0){ iunlockput(ip);
ilock(ip); return 0;
if(ip->type != T_DIR){ }
iunlockput(ip); iunlockput(ip);
return 0; ip
} }
if(nameiparent && *path == '\0'){ --> only after obtaining next from
dirlookup() and iget() is the lock
// Stop one level early. released on ip;
iunlock(ip); -> lock on next obtained only after
return ip; releasing the lock on ip. Deadlock
possible if next was “.”
}
Xv6
Interesting case of holding and releasing
ptable.lock in scheduling