[临界区和竞争条件]
所谓临界区就是访问和操作共享数据的代码段。多个执行线程并发访问同一个资源通常是不安全的,为了避免在临界区中并发访问,coder必须保证这些代码原子执行。
如果两个执行线程有可能处于同一个临界区中同时执行,那么这就是程序包含的一个bug。如果这种情况确实发生了,我们就称它是竞争条件(race conditions)。避免并发和防止竞争条件称为同步(synchronization)。
[造成并发执行的原因]
用户空间之所以需要同步,是因为用户程序会被调度程序抢占和重新调度。在内核中有类似可能造成并发执行的原因:
中断:中断几乎可以在任何时刻异步发生,也就是随时打断当前正在执行的代码;
软中断和tasklet:内核能在任何时刻唤醒或调度软中断和tasklet,打断当前正在执行的代码;
内核抢占:因为内核具有抢占性,所以内核中的任务可能会被另一任务抢占;
睡眠及与用户空间的同步:在内核执行的进程可能会睡眠,这就会唤醒调度程序,从而导致调度一个新的用户进程执行;
对称处理器:两个或多个处理器可以同时执行代码。
[哪些代码需要同步]
我们在编写内核代码时,你要问自己下面这些问题:
这个数据是不是全局的?除了当前线程外,其他线程能不能访问它?
这个数据会不会在进程上下文和中断上下文种共享?它是不是要在两个不同的中断处理程序中共享?
进程在访问数据时可不可能被抢占?被调度的新程序会不会访问同一数据?
当前进程是不是会睡眠(阻塞)在某些资源上,如果是,它会让共享数据处于何种状态?
怎样防止数据失控?
如果这个函数又在另一个处理上被调度将会发生什么呢?
如何确保代码远离并发威胁呢?
简而言之,几乎访问所有的内核全局变量和共享数据都需要某种形式的同步方法。
[死锁]
死锁的产生需要一定的条件:要一个或多个执行线程和一个或多个资源,每个线程都在等待其中的一个资源,但所有的资源都已经被占用了。所有线程都在相互等待,但它们永远不会释放已经占有的资源,于是任何资源都无法继续,这就意味着死锁的发生。
Example:有两个线程和两把锁
线程1 线程2
获得锁A 获得锁B
试图获得锁B 试图获得锁A
等待锁B 等待锁A
[原子操作]
原子操作可以保证指令以原子的方式执行-执行过程不被打断。内核提供了两组原子操作接口:一组针对整数进行操作,另一组针对单独的位进行操作。
原子整数类型
- typedef struct {
- intcounter;
- } atomic_t;
[自旋锁]
自旋锁(spin lock)最多只能被一个可执行线程持有。如果一个执行线程试图获得一个被已经持有(即所谓的争用)的自旋锁,那么该线程就会一直进行忙循环-旋转-等待锁重新可用。
spinlock结构体:
- typedef struct spinlock {
- union {
- struct raw_spinlock rlock;
- #ifdef CONFIG_DEBUG_LOCK_ALLOC
- # define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))
- struct {
- u8 __padding[LOCK_PADSIZE];
- struct lockdep_map dep_map;
- };
- #endif
- };
- } spinlock_t;
一个被争用的自旋锁是的请求它的线程在等待锁重新可用时自旋,特别浪费处理器时间,这种行为是自旋锁的要点。所以自旋锁不应该被长时间持有。持有自旋锁的时间最好小于完成两次上下文切换的耗时。
自旋锁可以用于中断处理程序中,但是信号量不可以,信号量会导致睡眠。
使用锁的时候一定要对症下药,要有针对性。要知道需要保护的是数据而不是代码。
[信号量]
Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个不可用(已经被占用)的信号量时,信号量会将其推进一个等待队列中,然后让其睡眠。这时处理器能重获自由,从而去执行其他代码。当持有的信号量可用(被释放)后,处于等待列队中的那个任务将被唤醒,并获得该信号量。
semaphore结构体:
- struct semaphore {
- spinlock_t lock;
- unsigned int count;
- struct list_head wait_list;
- };
使用信号量应注意的地方:
由于争用信号量的进程在等待锁重新变为可用时会睡眠,所以信号量适合用于锁会被长时间持有的情况;
相反,锁被短时间持有时,使用信号量就不能太适合了。因为睡眠、维护等待队列以及唤醒所花费的开销可能比锁被占用的全部时间还要长;
由于执行线程在锁被争用时会睡眠,所以只能在进程上下文中才能获取信号量锁,因为在中断上下文是不能进行调度的;
你可以在持有信号量时去睡眠,因为当其他进程试图获得同一信号量时不会因此而死锁。
在你占用信号量的同时不能占用自旋锁。因为在你等待信号量时可能会睡眠,而在持有自旋锁时是不允许睡眠的。
[互斥体]
Linux最新的linux内核中,互斥体mutex是一种实现互斥的特定睡眠锁。Mutex在内核中对应数据结构mutex,其行为和使用计数为1的信号量类似,但操作接口更简单,实现也更高效,而且使用限制更强。
mutex结构体:
- struct mutex {
- /* 1: unlocked, 0: locked, negative: locked, possible waiters */
- atomic_t count;
- spinlock_t wait_lock;
- struct list_head wait_list;
- #if defined(CONFIG_DEBUG_MUTEXES) || defined(CONFIG_SMP)
- struct task_struct *owner;
- #endif
- #ifdef CONFIG_DEBUG_MUTEXES
- const char *name;
- void *magic;
- #endif
- #ifdef CONFIG_DEBUG_LOCK_ALLOC
- struct lockdep_map dep_map;
- #endif
- };
Mutex使用限制:
任何时候中只有一个任务可以持有mutex,也就是说,mutex的使用计数永远是1;
给mutex上锁者必须负责给其再解锁,你不能在一个上下文中锁定一个mutex,而在另一个上下文中给它解锁。这个限制使得mutex不合适内核同用户空间复杂的同步场景。最常使用的方式是:在同一上下文中上锁和解锁。
递归地上锁和解锁是不允许的。也就是说,你不能递归地持有同一个锁,同样你也不能再去解锁一个已经被解开的mutex;
Mutex不能在中断或者下半部中使用;
Mutex只能通过官方API管理
[信号量和互斥体]
互斥锁和信号量很相似,内核中两者共存会令人混淆。所幸,它们的标准使用方式都有很简单的规范:除非mutex的某个约束妨碍你使用,否则相比信号量要优先使用mutex。当你写新代码时,只有碰到特殊场合才会需要使用信号量。因此建议首选mutex。
[自旋锁和互斥体]
了解何时使用自旋锁,何时使用互斥体或者信号量对编写优良代码很重要,但是多数情况下,并不需要太多的考虑,因为在中断上下文中只能使用自旋锁,而在任务睡眠时只能使用互斥体。
需求 | 建议加锁方法 |
低开销加锁 | 优先使用自旋锁 |
短期锁定 | 优先使用自旋锁 |
长期加锁 | 优先使用互斥体 |
中断上下文中加锁 | 使用自旋锁 |
下半部加锁 | 使用自旋锁 |
持有锁需要睡眠 | 使用互斥体 |