同步技术简介
- 在中断处理程序中能避免并发访问的安全代码被称为中断安全代码;在多核处理程序中能避免并发访问的安全代码被称为SMP安全代码;在内核抢占时能避免并发访问的安全代码被称为抢占安全代码。
- 避免死锁的重点一般如下:
- 按顺序加锁,使用嵌套的锁时必须以相同的顺序获取锁
- 防止发生饥饿,试问,如果A不发生,B要一直等下去么?
- 不要重复请求同一个锁
- 设计应力求简单,却复杂的加锁方案越容易造成死锁
- 设计初期,加锁方案应该力求简单,仅当需要时再进一步优化加锁方案
原子操作
- 原子类型的数据定义为atomic_t,初始化的宏为atomic_t u = ATOMIC_INIT(0)。
- 原子类型的操作为atomic_set(),atomic_inc(),atomic_dec(),atomic_add(),atomic_sub(),atomic_read()等。
- 64位的原子类型位atomic64_t,初始化的宏为ATOMIC64_INIT()。
- 原子类型的操作为atomic64_set(),atomic64_inc(),atomic64_dec(),atomic64_add(),atomic64_sub(),atomic64_read()等。
- 除了整数的原子操作之外,还可以对数据进行位域的原子操作,常用的包括set_bit(),clear_bit(),change_bit()。
自旋锁
- 由于获取不到自旋锁的线程会重复检查自旋锁,因此自旋锁适合于短期内的轻量级加锁处理。持有自旋锁的时间最好少于完成两次上下文切换使用的时间。
- 自旋锁的定义和使用一般如下:DEFINE_SPINLOCK(mr_lock),spin_lock(&mr_lock),spin_unlock(&mr_lock)
- 自旋锁可以使用在中断处理程序中,但是必须先对其进行关中断。否则持有自旋锁的中断处理程序被其他中断打断后,容易造成死锁。
- 内核提供的禁止中断同时请求自选锁的接口如下:spin_lock_irqsave(&mr_lock, flags)和spin_lock_irqrestore(&mr_lock, flags)。
读写自旋锁
- 一个或多个任务可以并发访问读锁,但是只能互斥访问写锁。
- 读写自旋锁的定义和使用一般如下:DEFINE_RWLOCK(mr_lock),read_lock(&mr_lock),read_unlock(&mr_lock),write_lock(&mr_lock),write_unlock(&mr_lock)。
- 读写自旋锁一般应用于读和写完全分开的场景,如果读和写不能清晰的分开的话,那么建议还是使用一般的自选锁即可。
- 读写自选锁照顾读比照顾写更多一点,假设有一个线程持有读锁,则写锁需要等待读锁释放,但是其他线程的读锁可以继续持有,这样有可能会出现大量读锁饿死写锁的场景。
信号量
- Linux中的信号量是一种睡眠锁,如果一个线程试图持有一个已经占用的信号量时,则该线程将会被挂起,并推入到一个等待队列中进行睡眠。当其等待的信号量被释放后,该线程会被重新唤醒,并获得该信号量。
- 持有信号量时可以去睡眠,因为其他试图持有该信号量的线程也会去睡眠,而持有信号量的线程终归会被唤醒后执行。
- 同时最多允许一个线程持有的信号量被称为二值信号量 或者互斥信号量,而同时最多允许多个线程持有的信号量被称为计数信号量。
- 信号量的结构体为struct semaphore,可以静态声明一个信号量DECLARE_MUTEX(),也可以动态声明一个信号量init_MUTEX()。
- down_interruptible()和down()用来获取信号量,区别在于前者进入睡眠后仍可以接收信号,而后者进入睡眠后不再响应信号。
- up()用来释放信号量,如果睡眠队列不为空,则唤醒其中的一个任务。
读写信号量
- 与自旋锁一样,信号量也有区分读写业务的场景。读写信号量都是引用计数为1的互斥信号量,读和读不互斥,读和写或者写和写互斥。
- 读写信号量在内核中的结构体为 struct rw_semaphore,可以静态声明一个读写信号量DECLARE_RWSEM(),也可以动态的创建一个读写信号量init_rwsem(struct * rw_semaphore sem)。
- 所有读写锁引起的睡眠都不会被信号打断,因此读写锁的主要操作为down_read()和down_write(),up_read()和up_write()。
- 读写信号量相较于读写自旋锁多一种操作,downgrade_write(),这个函数可以动态的将写锁降级为读锁。
互斥体
- 互斥体是一种用来实现互斥的,简化的睡眠锁。
- 互斥体在内核中的结构体为 struct mutex,可以静态声明一个互斥体DEFINE_MUTEX(),也可以动态的创建一个互斥体mutex_init(struct * mutex)。
- mutex_lock()和mutex_unlock()用来加锁和解锁。
- 给互斥体上锁者必须负责给互斥体解锁,不允许在一个上下文中加锁,在另一个上下文中解锁
- 对mutex进行递归的上锁和解锁是不允许的。
- 当持有一个mutex时,进程不可以退出。除此以外,mutex不能在中断或下半部中使用,使用mutex_trylock()也不行。
完成变量
- 在内核中一个任务需要发出信号通知另一个任务发生了某个特定的事件时,使用完成变量是使两个任务得以同步的简单方法。
- 完成变量在内核中的结构体为completion,可以静态声明一个完成变量DECLARE_COMPLETION(),也可以动态的创建一个完成变量init_completion()。
- 在一个指定的完成变量上,需要等待的任务需要调用wait_for_completion(),产生事件的任务调用complete()。
BKL 大内核锁
- BKL是一个全局自旋锁,持有BKL的任务仍然可以睡眠,但是睡眠的任务,锁会自动被放弃,当任务重新被调度时,锁又重新被获得。
- BKL是一种递归锁,一个进程可以多次请求一个锁。但是请求和释放的次数必须相同,最后一次解锁后才算是真正的被释放。
- BKL只可以用在进程上下文中,和自旋锁不同,不能用于中断上下文中。
- 函数lock_kernel()和unlock_kernel()用来请求锁和释放锁。
顺序锁
- 顺序锁用于读写共享数据,其依靠一个计数器。当数据被写入时,得到顺序锁,并且该计数器的值被修改成为一个奇数。在读数据之前和之后,会检查该序列值,若未发生变化,则说明该值在读期间未进行修改。
- 顺序锁的定义方法为seqlock_t mylock = DEFINE_SEQLOCK(mylock)。
- 写锁的方法为write_seqlock()和write_sequnlock()。
- 读锁的方法为read_seqbegin()和read_seqretyr()。
- 与读写自旋锁或者读写信号量不同,顺序锁是一种对写更有利的同步方式,在没有其他的写任务时,写锁总是能被成功的获得。
禁止抢占
- 内核可以通过preempt_disable()和preempt_enable()来禁止和打开内核抢占功能,并且该函数允许嵌套调用,当最后一次preempt_enable()被调用时,内核抢占才会被重新开启。
- 通常为了更好的访问每处理上的变量,可以使用get_cpu()来获取处理器编号,该函数也会禁止内核抢占,使用put_cpu()来释放内核抢占。
顺序和屏障
- rmb()保证该指令之前的读操作不会重排到该指令之后的读操作的后面。
- wmb()保证该指令之前的写操作不会重排到该指令之后的写操作的后面。
- mb()保证该指令之前的读写操作不会重排到该指令之后的读写操作的后面。
- read_barrier_depends()仅仅针对后续的读操作以来前面的读操作进行内存屏障处理。