While having some daunting tasks within printing industry I’ve decided to have some relax (in some working hours break, obviously) grasping, and remembering about proper-locking techniques in Linux kernel as follows:
Facts and issues:
- It provides code path (critical sections) syncronization in context of concurrency, and re-entrancy. Any code that does not have re-entrancy, or concurrency protection will encounter the so called race conditions
- True concurrency is encountered in symmetric multiprocessing systems (SMP hardware with SMP kernel in this case), but pseudo concurency that may lead to race conditions are give by interrupt handlers, and preemptibile kernels also. In later case different code is not actually executed in the same time (having one CPU), but accesing shared data may cause race conditions.
- SMP and UP (uniprocessor) linux kernels are kept distinct. Some locks exists in one type, but not in the other. Though, developer should not care because at compile time some checkings, and translations are performed. Proper locking allways should be performed.
Methods:
1. Atomic operators (uninterruptible operations)
- atomic_set(), atomic_inc(), atomic_dec(), and others.
2. Spinlocks
- They are defined in include/linux/spinlock.h
- when code tries to acquire an unavailable spinlock, the process will continue trying (spinning) until the lock becames available. Obviously it’s eating CPU and the process is not allowed to sleep.
- API:
- spinlock_irqsave(&spinlock_t, unsigned long), spinlock_irqrestore(.same.), SMP safe, and interrupt handlers safe.
- spin_lock(spinlock_t), spin_unlock(spinlock_t) used basically for user-context kernel code unique data (in syscalls), no interaction with interrupts.
- spin_lock_bh(), spin_unlock_bh(), standard spinlock that disables software interrupts
- some other, check the code
- when using spinlocks don’t call from inside a critical section code that may sleep
. You’r kernel will definetly explode
. (ie copy_from{to}_user(), kmalloc(GPF_KERNEL) etc)
- don’t protect code that takes much time to execute. In fact, protect only shared data if it’s possible.
3. Semaphores:
- It’s defined in include/asm/semaphore.h
- When execution encounters a code section locked by a semaphore, the task will sleep, not spin
, therefore semaphores are used when spinlocks are not possible.
- As semaphores are sleeping locks (not spinning), task context switch happens and much time is wasted, so semaphores are not used with code that takes short time to execute.
- struct semaphore that describes a semaphore contains an usage count and a list with waiting tasks. Usage count is initialized at semaphore creation and represents the number of tasks that may concurently acquire the semaphore.
- A semaphore with usage count equal with 1 is called a mutex
- API:
- sem_init(struct semaphore &, int usage_count),
- down(semaphore&), puts task in uninterruptible sleep if usage count is less or equal with 0. After aquiring the semaphore,it decreases the usage count. Marks the begining of critical section.
- up(semaphore&), increases the usage count, marks the end of critical section.
- down_interruptibile(sempahore&), returns > 0 if semaphore was not acquired, else is like down(), but is interruptibile
4. Reader/Writer locks:
- Are are variants of spinlocks, or semaphores.
- They allows multiple concurently readers, or one single writer with no readers
- Reader/Writer spinlock:
rwlock_t mr_rwlock = RW_LOCK_UNLOCKED; read_lock(&mr_rwlock); /* critical section (read only) ... */ read_unlock(&mr_rwlock); write_lock(&mr_rwlock); /* critical section (read and write) ... */ write_unlock(&mr_rwlock);
- Reader/Writer semaphore
struct rw_semaphore mr_rwsem; init_rwsem(&mr_rwsem); down_read(&mr_rwsem); /* critical region (read only) ... */ up_read(&mr_rwsem); down_write(&mr_rwsem); /* critical region (read and write) ... */ up_write(&mr_rwsem);