As a Chinese old saying goes: Review the old and know the new (温故而知新). I learnt a lot from the OS class, recently, despite that I have taken this class 5 years ago during my undergraduate study. The latest topic discussed on the class is concurrency and deadlock, I think it is a good time for me to review some concepts of concurrent programming.
Mutual Exclusion: There is a critical section (process can write shared data here) that allows only one process to enter. To ensure there is no other process enter this area, some measures should be applied here which called mutual exclusion.
Synchornization: A process can only execute when others finish their work. So we should take some actions to guarantee this, which are synchornization methods.
The crucial problem for busy waiting is how to ensure the critical region can not be entered by two different processes at a time. As we known, preemption happens with interrupts. If we disable interrupts temporarily when there is a process enter the critical section, this problem can be solved. But only on single processor system.
On multi-processor system this does not work, because interrupts can only be disabled on one of the processors. Processses running on other processor can still enter the critical section simultaneously.
It is a very simple solution to this problem. Check if the lock variable is 0, continuously. If it becomes 0, set it to 1, and enter the critical region. When it finishes, reset the lock variable to 0, so that other waiting process can enter the critical section.
But this can not work as expected, because you can’t perform the set lock operation atomically. There are changces for other processes modify the lock variable at the same time. The solution is not work either.
This is a hardware supported primitive. Using lock variable needs set and read lock variable atomically, which means no other operation can interrupt the lock setting procedure. Most of CPUs implement the TSL instruction to support this function. TSL means test and set lock. CPU will first lock the memory bus to prohibit other CPUs from accessing memory, then copy the lock value into register and store a non-zero value to lock variable. So this guarantees the operation is indivisable.
It is an alternative atomic instruction for TSL. Just like its name, XCHG will exchange the value in a resgister with value in memory (lock variable).
Busy waiting with TSL and XCHG can solve the problem, but they are not efficient enough. Busy waiting consumes a lot of CPU resources to check whether the lock varaible is available.
Semaphore is not like busy waiting, it does not run the checking procedure continuously. Instead, semaphore mechanism uses sleep and wakeup to make it work.
semaphore is a variable indicating how many processes can perform action. value of semaphore could be 0 or greater than 0. When a process want to perform action, it should decrease the semaphore by 1. When it finishes its work, it becomes ready and increases the semaphore by 1. When the semaphore is 0, which means nobody can perform action at this time, the process will be put into
sleep by scheduler. Util the semaphore becomes greater than 0, scheduler will randomly wakes up that number of sleeping processes doing its job.
The decrement and increament operations are named P, V (Proberen (try) and Verhogen (raise, make higher) in Dutch, because its inventor, Dijkstra, is from Netherlands) operations. It is essential for ensuring these operations are atomic. So usually implementations apply TSL and XCHG for these operations to promise atomicity.
eg: Producer and Consumer Problem
#define N 100 // N slots in the buffer
As we see in the code sniplet before, when the value of semaphore are only 0 or 1, it is called a binary-semaphore, or a mutex which could be used for mutual exclusion and synchornization.
Every lock and unlock operation will first trap into kernel by sycalls to check if the lock variable is owned by others. But, in reality, most of time the lock variable does not belongs to anyone.It is usually free for processes to enter the critical section, but the test lock procedure still cost a lot by doing a syscall. To make this more efficient, futex were created.
Futex stands for “Fast Userspace muTexes”, which, basically, does the lock test procedure in user space. Only when the lock is not free, it performs a syscall. That’s why futex improves performance a lot.
Condition Variables is a synchornization mechanism. Synchornization needs more than just mutual exclusion; also need a way to wait for another thread to do something (e.g., wait for a character to be added to the buffer). So, it provides a way to make process wait util the specific condition is fullfilled.
Which Lock should I use, busy waiting or semaphores? There are two simple principles:
If lock hold time is short or task is uninterruptable, busy waiting and lock variables are OK (Linux kernel always use it).
Else use semaphores.
These two concepts are usually confusing. Basically, Deadlock happens when processes are waiting on events or resources that will never come. It can be on only one process or system wide. And in this case, no process can go further.
Under Starvation, some process can move forward, but there does have some process(es) can not be excuted. Because, there is no resource available for them to access, due to scheduling reason.
For an instance, if the process scheduler always process short execution time process first (SRTF), the long run time process may have no changce to be executed, if there always comes short time tasks. Then, a starvation happened.
When a process A need a resource to move forward, but that resource is held by process B. At the meantime, process B is also waiting for the resource held by A.
If the resource is like memory, which can be preempted, deadlock will not happen. But, if it is a non-preemptable resource, like a Blu-ray recorder, deadlock will happen.
There are 4 essential conditions for deadlock:
- Each resource is either currently assigned to exactly one process or is available.
- Processes currently holding resources that were granted earlier can request new resources.
- Resources previously granted cannot be forcibly taken away from a process. They must be explicitly released by the process holding them.
- There must be a circular chain of two or more processes, each of which is waiting for a resource held by the next member of the chain.
Basically, the algorithm to detect deadlock is to detect cycles in the resource allocation graph.
For Each node N in the graph do:
- Initialize L to empty list and designate all arcs as unmarked
- Add the current node to end of L. If the node appears in L twice then we have a cycle and the algorithm terminates
- From the given node pick any unmarked outgoing arc. If none is available go to 5.
- Pick an outgoing arc at random and mark it. Then follow it to the new current node and go to 2.
- If the node is the initial node then no cycles and the algorithm terminates. Otherwise, we are in dead end. Remove that node and go back to the previous one. Go to 2.
If one of the 4 essential conditions can’t be reached, deadlock is impossible structurally.
- Modern Operating Systems 4th Edition–Andrew Tanenbaum.pdf Section 2.3 and 6