内核同步方法
- 1 原子操作
- 原子整数操作
- 原子性与顺序性的比较
- 原子位操作
- 2 自旋锁
- 自旋锁是不可递归的
- 其他针对自旋锁的操作
- 自旋锁和下半部
- 3 读-写自旋锁
- 4 信号量
- 创建和初始化信号量
- 使用信号量
- 5 读-写信号量
- 6 自旋锁和信号量
- 7 完成变量
- 8 互斥锁
- 互斥锁API
- 9 禁止抢占
- 10 顺序和屏障
1 原子操作
原子操作是保证指令以原子的方式执行,执行过程不被打断。内核提供了两组原子接口,一组针对整数进行操作,另一组针对单独的位进行操作。在Linux支持的所有体系结构上都实现了这两组接口。
原子整数操作
针对整数的原子操作只能对atomic_t类型的数据进行处理。Linux支持的所有机器上的整数数据都是32位的,但是使用atomic_t的代码只能将该类型的数据当做24位来用。这个限制完全是因为在SPARC体系结构上,原子操作的实现不同于其他体系结构:32位int类型的低8位嵌入了一个锁,
因为SPARC体系结构对原子操作缺乏指令级的支持,所有只能利用该锁来避免对原子类型数据的并发访问。
原子操作的声明在<asm/atomic.h>文件中。所有的体系结构内核会提供一些相同的方法,有些体系结构会提供一些在该体系结构上使用的额外原子操作方法。
定义一个atomic_t类型的数据,还可以在定义时给它设定初值:
atomic_t v; /* 定义v */
atomic_t u = ATOMIC_INIT(0); /* 定义u并把u初始化为0 */
原子整数操作列表如下:
原子性与顺序性的比较
原子性确保指令执行期间不被打断,要么全部执行完,要么根本不执行。而顺序性确保即使两条或多条指令出现在独立的执行线程中,它们要执行顺序要按规定的执行。例如,给一个整数初始化为10,要么初始化成功,要么初始化失败,这就是原子性。接着又有一个操作给整数初始化为20,原子性不管是先初始化为10还是先初始化为20,这是顺序性的责任。
原子位操作
原子性操作是与体系结构相关的操作,定义在文件<asm/bitops.h>中。位操作函数是对普通的内存地址进行操作的,它的参数是一个指针和一个尾号。原子位操作的列表如下:
内核还提供了两个例程用来从指定的地址开始搜素第一个被设置(未被设置)的位
int find_first_bit(unsigned long *addr,unsigned int size);
int find_first_zero_bit(unsigned long *addr,unsigned int size);
2 自旋锁
Linux内核最常见的锁是自旋锁(spin lock)。自旋锁最多只能被一个可执行线程持有。如果一个执行线程试图获得一个被争用(已经被使用)的自旋锁,那么该线程就会一直进行忙循环,等待锁重新可用。在任何时刻,自旋锁都可以防止多余一个的执行线程同时进入临界区。
如果自旋锁已经被争用了,那么请求它的线程在等待锁重新可用时将一直自旋,所以特别浪费处理器时间,因此自旋锁不应该被长时间持有。
自旋锁的实现和体系结构密切相关,代码往往通过汇编实现。这些与体系结构相关的代码定义在文件<asm/spinlock.h>中,实际需要用到的接口定义在文件<linux/spinlock.h>中。自旋锁的基本使用形式如下:
spinlock_t mr_lock = SPIN_LOCK_UNLOCKED;spin_lock(&mr_lock);
/* 临界区 */
spin_unlock(&mr_lock);
因为自旋锁在同一时刻至多被一个执行线程持有,所以一个时刻只能有一个线程位于临界区内,这就为多处理器机器提供了防止并发访问所需的保护机制。注意在单处理器机器上,编译的时候并不会加入自旋锁,它仅仅被当做一个设置内核抢占机制是否被启用的开关。如果禁止内核抢占,那么在编译时自旋锁会被完全剔除内核。
自旋锁是不可递归的
Linux内核实现的自旋锁是不可递归的,如果你请求一个你已经持有的自旋锁,那么你将会自旋,等待释放这个锁,由于自旋,释放这个锁的操作不会执行,所有会一直处于自旋忙等待中,于是你被自己锁死了。
自旋锁可以使用在中断处理程序中。在中断处理程序中使用自旋锁时,一定要在获取锁之前,首先禁止本地中断(当前处理器上的中断请求),否则,中断处理程序就会打断正持有锁的内核代码,有可能试图去争用这个已经被持有的自旋锁。这样一来,中断处理程序就会自旋,但是锁的持有者在这个中断处理程序执行完毕前不可能运行,会造成死锁。注意,需要关闭的只是当前处理器上的中断,如果中断发生在不同的处理器上,即使中断处理程序在同一锁上自旋,也不会妨碍锁的持有者最终释放锁。
内核提供的禁止中断同时请求锁的接口:
spinlock_t mr_lock = SPIN_LOCK_UNLOCKED;
unsigned long flags;spin_lock_irqsave(&mr_lock,flags);
/* 临界区 */
spin_lock_irqrestore(&mr_lock,flags);
函数spin_lock_irqsave保存中断的当前状态,并禁止中断,然后再去获取指定的锁。反过来spin_lock_irqrestore对指定的锁解锁,然后让中断恢复到加锁前的状态。
配置选项CONFIG_DEBUG_SPINLOCK为使用自旋锁的代码加入了许多调试检测手段。
其他针对自旋锁的操作
spin_lock_init()用来初始化动态创建的自旋锁。spin_try_lock试图获得某个特定的自旋锁,其他自旋锁操作如下:
自旋锁和下半部
由于下半部可以抢占进程上下文中的代码,所以当下半部和进程上下文共享数据时,必须对进程上下文中的共享数据进行保护,所以需要加锁的同时还要禁止下半部执行。同样,由于中断程序程序可以抢占下半部,所以如果中断处理程序和下半部共享数据,那么就必须在获取恰当的锁的同时还要禁止中断。
3 读-写自旋锁
Linux提供了专门的读写自旋锁,这种自旋锁为读和写分别提供了不同的锁,一个或多个读任务可以并发的持有读者锁;相反,用于写的锁最多只能被一个写任务持有,而且此时不能有并发的读操作。有时把读写锁叫做共享排斥锁,或者并发排斥锁,因为这种锁以共享(对读者而言)和排斥(对写着而言)的形式获得使用。
加锁逻辑:
- 假设临界区内没有任何的thread,这时候任何read thread或者write thread可以进入
- 假设临界区内有一个read thread,这时候新来的read thread可以任意进入,但是write thread不可以进入
- 假设临界区有一个write thread,这时候任何的read thread或者write thread 都不可以进入
- 假设临界区内有一个或者多个read thread,write thread当然不可以进入临界区,但是该write thread也无法阻止后续read thread的进入,他要一直等到临界区一个read thread也没有的时候,才可以进入。可见,rw spinlock给reader赋予了更高的权限。
读写自旋锁的使用方式类似于普通自旋锁:
rwlock_t my_rwlock = RW_LOCK_UNLOCKED; /* 初始化 */
read_lock(&my_rwlock);
/* 临界区 只读*/
read_unlock(&my_lock);
在可以写的临界区加上如下代码:
write_lock(&my_rwlock);
/* 临界区(写) */
write_unlock(&my_rwlock);
不能同时请求读锁和写锁:
read_lock(&my_rwlock);
write_lock(&my_rwlock);
这样将会带来死锁,因为写锁会不断自旋,而读锁得不到释放。
针对读写自旋锁的操作如下:
在使用Linux读-写自旋锁时,最后要考虑的一点是这种锁照顾读比照顾写要多一点,当读锁被持有时,写操作为了互斥访问只能等待,但是,读者却可以继续成功地占用锁。而自旋等待的写者在所有读者释放锁之前是无法获得锁的。
自旋锁提供了一种快速简单的锁实现方式,如果加锁时间不长并且代码不会休眠,利用自旋锁时最佳选择。
4 信号量
Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个已经被占用的信号量时,信号量会将其放到一个等待队列,然后让其睡眠。这时处理器能去执行其他代码。当持有信号量的进程将信号量释放后,处于等待队列中的那个任务将被唤醒,并获得该信号量。
信号量可以同时允许任意数量的锁持有者,而自旋锁在一个时刻最多允许一个任务持有它。信号量同时允许的持有者数量可以在声明信号量时指定,这个值称为使用者数量。通常情况下,信号量和自旋锁一样,在一个时刻仅允许有一个锁持有者。当数量等于1,这样的信号量被称为二值信号量或者被称为互斥信号量;初始化时也可以把数量设置为大于1的非0值,这种情况,信号量被称为计数信号量,它允许在一个时刻至多有count个锁持有者。
信号量支持两个原子操作P()和V()。前者叫做测试操作,后者叫做增加操作,后来系统把这两种操作分别叫做down()和up(),Linux也遵从这种叫法。down()通过对信号量减1来请求一个信号量,如果减1结果是0或者大于0,那么就获得信号量锁,任务就可以进入临界区,如果结果是负的,那么任务会被放入等待队列。相反,当临界区的操作完成后,up()操作用来释放信号量,如果在该信号量上的等待队列不为空,那么处于队列中等待的任务被唤醒。
创建和初始化信号量
信号量的实现是与体系结构有关的,具体实现定义在文件<asm/semaphore.h>中。struct semaphore类型表示信号量。可以通过以下方式静态声明信号量:
static DECLARE_SEMAPHORE_GENERIC(name,count);
其中name是信号量变量名,count是信号量的使用者数量。创建更为普通的互斥信号量可以使用以下方式:
static DECLARE_MUTEX(name);
我们可以使用sema_init对信号量进行动态初始化:
sema_init(sem,count);
sem是指针,count是信号量的使用者数量。初始化一个动态创建的互斥信号量时使用以下函数:
sema_MUTEX(sem)
使用信号量
函数down_interruptible()试图获取指定的信号量,如果获取失败,它将以TASK_INTERRUPTIBLE状态进入睡眠。如果进程在等待获取信号量的时候接受到了信号,那么该进程就会被唤醒,而函数down_interruptible()会返回EINTR。另外一个函数down()获取信号量失败会让进程在TASK_UNINTERRUPTIBLE状态下睡眠,我们应该避免这种情况,因为进程等待信号量的时候就不再响应信号了。
使用down_trylock()函数,可以尝试获取指定的信号量,在信号量被占用时,它立刻返回非0值,否则,返回0,并且成功获取信号量锁。
要释放指定的信号,需要调用up()函数。
针对信号量的操作如下表:
5 读-写信号量
与自旋锁一样,信号量也有区分读写访问的可能,。读写信号量在内核中是由rw_semaphore结构表示的,定义在文件<linux/rwsem.h>中。通过以下语句可以创建静态声明的读写信号量:
static DECLARE_RWSEM(name);
动态创建读写信号量可以通过下面的函数:
init_rwsem(struct rw_semaphore *sem)
所有的读写信号量都是互斥信号量(它们的引用计数等于1)。只要没有写着,并发持有读锁的读者数不限。相反,只有唯一的写者(没有读者时)可以获得写锁。所有的读写锁的睡眠都不会被信号打断,它只有一个down()操作:
6 自旋锁和信号量
在中断上下文中只能使用自旋锁,在任务睡眠时只能使用信号量。
7 完成变量
如果在内核中一个任务需要发出信号通知另一个任务发生了某个特定事件,利用完成变量是使两个任务以同步的简单方法。如果一个任务要执行一些工作时,另一任务就会在完成变量上等待,当这个任务完成后,会使用完成变量去唤醒在等待的任务。
完成变量由结构体completion表示,定义在<linux/cmpletion.h>中。可以通过以下方式创建:
DECLARE_COMPLETION(mr_comp) /* 静态创建 */
init_completion() /* 动态创建 */
在一个指定的完成变量上,需要等待的任务调用wait_for_completion()来等待特定事件。当特定事件发生后,产生事件的任务调用complete()来发送信号唤醒正在等待的任务。
8 互斥锁
Mutex(互斥锁)是较常用的锁机制,为了理解它是怎么工作的,来看一看它在include/linux/mutex.h中的结构定义:
/** Simple, straightforward mutexes with strict semantics:** - only one task can hold the mutex at a time* - only the owner can unlock the mutex* - multiple unlocks are not permitted* - recursive locking is not permitted* - a mutex object must be initialized via the API* - a mutex object must not be initialized via memset or copying* - task may not exit with mutex held* - memory areas where held locks reside must not be freed* - held mutexes must not be reinitialized* - mutexes may not be used in hardware or software interrupt* contexts such as tasklets and timers** These semantics are fully enforced when DEBUG_MUTEXES is* enabled. Furthermore, besides enforcing the above rules, the mutex* debugging code also implements a number of additional features* that make lock debugging easier and faster:** - uses symbolic names of mutexes, whenever they are printed in debug output* - point-of-acquire tracking, symbolic lookup of function names* - list of all locks held in the system, printout of them* - owner tracking* - detects self-recursing locks and prints out all relevant info* - detects multi-task circular deadlocks and prints out all affected* locks and tasks (and only those tasks)*/
struct mutex {atomic_long_t owner;raw_spinlock_t wait_lock;
#ifdef CONFIG_MUTEX_SPIN_ON_OWNERstruct optimistic_spin_queue osq; /* Spinner MCS lock */
#endifstruct list_head wait_list;
#ifdef CONFIG_DEBUG_MUTEXESvoid *magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOCstruct lockdep_map dep_map;
#endif
};
其结构中有一个链表类型字段:wait_list,当竞争者无法获取资源时,就会加入到该链表中,竞争者从调度器的运行列表中删除,放入处于睡眠状态的等待链表(wait_list)中。然后内核调度并执行其他任务,当锁被释放时,等待队列中的等待者被唤醒,从wait_list移除,然后重新被调度。
互斥锁API
- 声明
静态声明:
#define DEFINE_MUTEX(mutexname) \struct mutex mutexname = __MUTEX_INITIALIZER(mutexname)
动态声明:
struct mutex my_mutex;
mutex_init(&my_mutex);
- 获取互斥锁
void mutex_lock(struct mutex *lock);
int mutex_lock_interruptible(struct mutex *lock);
int mutex_lock_killable(struct mutex *lock);
int mutex_trylock(struct mutex *lock);
mutex_lock在不能获得互斥锁时,会把任务加入到睡眠队列中,必须要保证互斥锁能够释放。mutex_lock_interruptible在不能获得互斥锁时,会把任务加入到睡眠队列中,但是任务在睡眠队列时有信号到达,mutex_lock_interruptible会返回 -EINTR,程序会自动往下运行。
mutex_lock_killable在不能获得互斥锁时,会把任务加入到睡眠队列中,只有杀死任务的信号,才能中断驱动程序,程序会继续往下运行。
mutex_trylock如果不能获得互斥锁,不会把任务加入到睡眠列表中,直接返回。
3. 释放互斥锁
void mutex_unlock(struct mutex *lock);
有时需要检查互斥锁是否锁定。为此使用mutex_is_locked函数:
/*** mutex_is_locked - is the mutex locked* @lock: the mutex to be queried** Returns true if the mutex is locked, false if unlocked.*/
extern bool mutex_is_locked(struct mutex *lock);
已经被其他任务获取了,返回true,没有被使用返回false
互斥锁没有轮询机制,每次在互斥锁上调用mutex_unlock时,内核都会检查wait_list中的等待任务,如果有等待者,则其中的一个将被唤醒,它们唤醒的顺序域它们入睡的顺序相同。
9 禁止抢占
由于内核时抢占性的,内核中的进程在任何时候都可能停下来以便另一个更高优先级的进程运行。这意味着一个任务与被抢占的任务可能会在同一个临界区内运行,为了避免这种情况,内核抢占代码使用自旋锁作为非抢占区域的标记。如果一个自旋锁被持有,内核便不能进行抢占。因为内核抢占和SMP面对相同的并发问题,并且内核已经是SMP安全的,因此,这种简单的变化使得内核也是抢占安全的。
实际中,某些情况下并不需要自旋锁,但是仍然需要关闭内核抢占,出现最频繁的情况就是每个处理器上的数据。如果数据对每个处理器是唯一的,那么这样的数据可能就不需要使用锁来保护,因为数据只能被一个处理器访问,如果自旋锁没有被持有,内核又是抢占的,那么一个新调度的任务就可能访问同一个变量,如下所示
这样,即使这是一个单处理器,变量foo也会被多个进程以伪并发的方式访问。通常,这个变量会请求得到一个自旋锁(防止多处理器上的真并发)。但是如果这是每个处理器上独立的变量,可能就不需要锁。
为了解决这个问题,可以通过preempt_disable()禁止内核抢占。这是一个可以嵌套调用的函数,可以调用任意次。每次调用都必须有一个相应的preempt_enable()调用。当最后一次preempt_enable()被调用后,内核抢占才重新启用。
抢占计数存放着持有锁的数量和preempt_disable()的调用次数,如果计数是0,那么内核可以进行抢占,如果为1或更大的值,那么内核就不会进行抢占。函数preempt_count()返回这个值。
为了更简洁的方法解决每个处理器上的数据访问问题,可以通过get_cpu获得处理器编号。这个函数在返回当前处理器号前会首先关闭内核抢占。put_cpu()会恢复内核抢占:
10 顺序和屏障
屏障是告诉编译器不要对给定点周围的指令序列进行重新排序。
rmb()方法提供了一个读内存屏障,它确保在rmb()之前的载入操作不会被重新排在该调用之后,在rmb()之后的载入操作不会被重新排在该调用之前。
wmb()提供了一个写内存屏障,这个函数的功能和rmb()类似,区别仅仅是它是针对存储而非载入。
mb()方法即提供了读屏障也提供了写屏障。
内核和编译器屏障方法如下:
注意,对于不同体系结构,屏障的实际效果差别很大。例如,如果一个体系结构不执行打乱存储(比如intel x86芯片),那么wmb()就什么也不做。