Locking is important in multi-processor or pre-emptable systems. Data cannot be written and read at the same time by different tasks, or bizarre, random errors will occur, which may even damage the whole system.
Atomic Operations
These are operations that cannot be interrupted by any means. They are not technically locking, but they don’t need locking because they work as if they were locked already.
To use these operations, you declare a atomic_t variable. Then, you pass that variable to proper functions that perform simple arithmetic or bit-by-bit atomic operations: some functions can add an integer value atomically, others can increment the value, others can set, unset or flip a single bit, and many other functions exist.
Spinlocks
Spinlocks are the most simple and cost effective locking mechanism we have available.
If the task attempts to acquire the lock and the lock is available, the lock is acquired and the task can proceed. If the lock is unavailable, the task keeps trying to acquire the lock until it succeeds. That’s the spin part.
There are many pitfalls and consequences to the use if this lock: the lock must be held for the minimum time frame possible, because any time spinning is time lost. If your process takes a lock to protect a device from being used, and that device issues an interrupt, the interrupt handler will try to acquire the lock but it will be unavailable. The processor will spin forever. The code should not sleep with a lock acquired, as it may cause another thread to try to acquire a lock acquired by code that is not even running, causing it to spin until it loses the processor (more time is lost).
More complex methods can entirely disable interrupts, or just software interrupts. Sleep can be avoided by being careful about function calls.
There’s also a non-blocking version of this lock, using specific functions for that.
Reader/Writer Spinlocks
Usually, data be can simultaneously read by many tasks concurrently, but writing can only be done by one task at a time, to avoid overwriting and generating incorrect data.
As a consequence, there is also the reader/writer spinlock, that is analogous to the standard spinlock, except that it allows any number of readers access the critical section, but writers must have exclusive access.
The reader/writer spinlock should be used only if writer access is rarely required. For writers have exclusive access, they have priority. Therefore, once a writer gets access, no reader will get it until all writers are done. Therefore, this can cause reader starvation, denying reader access for a long time.
There are also functions to disable interrupts, and the same pitfalls that apply for the standard spinlock applies here as well.
Semaphores
Semaphores are the sleeping locks. On contention, instead of spinning, they cause the task to sleep. They also allow for a certain number of tasks to acquire access to the critical section (or resource, in the contect of semaphores), instead of only one.
Semaphores count the number of resources available. That’s their initial value. When a thread attempts to acquire access to the critical section, it decrements that counter. If the new counter is zero or more, the access is granted. If the new counter is a negative value, the access is denied and the thread is sent to sleep. When a task comes out of the critical section, it increases the counter by one. Consequently, the counter can be seen as counting the number of resources available if it’s positive or the number of tasks waiting to enter the critical section if it’s a negative number. If it’s zero, means there are no resources available, but there are no tasks waiting either.
Semaphores are more expensive than spinlocks, but they can held for longer periods of time, since they sleep when access is denied.
When the initial value of the counter of a semaphore is 1, the semaphore is called a mutex (stands for mutual exclusion), because only one task can be in the critical section at any given instant.
Reader/writer Semaphores
Analogous to the reader/writer spinlocks, there are the reader/writer semaphores. There can be as many readers as resources available, but there can only be one writer at any given moment.
RCU
RCU (stands for read-copy-update) is a synchronization method that allows as many readers and writers in the critical section at any given time.
The method uses a publish-subscribe framework that keeps things straight while tasks read and concurrently modify the contents of data. Usually, there’s a list that holds the current valid or in use data structures. Every time a task creates a new data structure, it publishes it (adds it to that list). Every time a task needs to read that data structure, it uses a pointer to subscribe to the list. The pointer is pointed to the latest published data structure and can be used by that task. Therefore, even if the data structure is modified by another task, the current task will work with the one it has now until it’s done. When it subscribes again, it will receive the latest data structure.
Those accesses are monitored, and every time an outdated data structure is not being used by any task anymore, it is reclaimed.
The Big Kernel Lock
The last lock discussed is the Big Kernel Lock (BKL). It’s discussed just out of curiosity, since it’s been extinct for quite a while now, and shouldn’t be used anymore, since it’s dangerous and kills the purpose of pre-emption and concurrency of the whole system.
This lock is a global lock. Every thread that tries to acquire this lock is competing with every other thread in the whole system that tries to acquire it, not only in the process. It was introduced in the kernel 2.0 as the first kind of locking mechanism in the system, and it’s been gradually replaced by more meaningful, efficient and safe locking mechanisms (the ones discussed previously) until its extinction.
Conclusion
For each situation in which a lock is needed, there are many alternatives. Careful thinking is required to decide the best way to handle the problem, taking into consideration efficiency and security. Locking issues can severely slow down the system, completely freeze it in a deadlock, open security holes or generate completely bizarre, random errors when data is simultaneously written by diverse tasks. It’s been a topic of intense research in the computing industry and academia and there’s a lot yet to be found.