几个案例
case 1 : Single Variable
i++;
汇编会进行如下操作
1. 获取变量 i 的值,并写入寄存器
2. 寄存器的值加 1
3. 寄存器的值写回变量 i 所在的内存空间
若两个线程同时进行对变量 i 进行 i++ 操作,则可能出现下面两种结果,因此,在共享内存的应用中,需要保证并发访问时共享资源是受保护的。
![]() | ![]() |
解决方式:原子操作,指令不被分解和打断。
case 2 : Queue
function A( ) 请求对 head 操作, function B( ) 请求对 tail 操作,kernel 的不同部分调用 A( ) 和 B( )都对该链表进程操作时就很有可能导致并发的问题。
解决方式: 锁。任何代码要访问队列首先获取锁,锁在某一时刻只能由一个线程持有。
死锁问题,典型代表 ABBA 锁,2 个线程 2 把锁的情况。
解决方式:注意锁的顺序,嵌套锁一定是要以相同的顺序去获取。
同步方法
原子操作
1. 整形原子操作
#include <linux/types.h>
#include <asm/atomic.h>
atomic_t u = ATMOIC_INIT(0); /* define u and init it to 0 */
atomic_set(&u, 4); /* u = 4 */
atomic_add(2, &u); /* u = u + 2 = 6 */
atomic_inc(&u); /* u = u + 1 = 7 */
printk("%d\n", atomic_read(&u)); /* will print 7 */
2. 位原子操作
#include <asm/bitops.h>
void set_bit(nr, void *addr); /* 设置addr地址的第 nr 位 */
void clear_bit(nr, void *addr);
void change_bit(nr, void *addr); /* 对 addr 的 nr 位反置 */
int test_bit(nr, void *addr); /* 返回 addr 地址的第 nr 位 */
自旋锁
自旋锁主要针对 SMP 或单 CPU 但内核可抢占的情况,对于单 CPU 和内核不支持抢占的系统,自旋锁退化为空操作。在单 CPU 和内核可抢占的系统中,自旋锁持有期间内核的抢占将被禁止。
i. SMP 体系结构,多个CPU使用共同的系统总线,可访问共同的外设和储存器
ii.Linux 2.6 内核支持抢占调度,一个进程在内核执行的时候可能被另一高优先级进程打断,进程与抢占它的进程访问共享资源的情况类似于 SMP 的多 个 CPU。
#include <asm/spinlock.h>
#include <linux/spinlock.h>
spinlock_t lock;
spin_lock_init(&lock);
spin_lock (&lock) ; //获取自旋锁,保护临界区
/* 临界区 */
spin_unlock (&lock) ; //解锁
自旋锁不是递归的,即持有锁时再去申请这一把锁将导致死锁。自旋锁可用于中断处理,考虑中断事件打断内核代码情况,需要失能当前CPU中断。
spinlock_t mr_lock;
unsigned long flags;
spin_lock_irqsave(&mr_lock, flags);
/* critical region ... */
spin_unlock_irqrestore(&mr_lock, flags);
spin_lock_irqsave() 保存当前中断状态并失能当前CPU中断,获取指定锁。
如果清楚的知道中断初始化时是使能的,就没必要存当前中断状态,使用 spin_lock_irq() 即可。(不推荐使用)
自旋锁是忙等锁,当临界区很大或有共享设备的时候,需要较长时间占用锁,使用自旋锁会降低系统的性能。
信号量
信号量在 linux 中属于休眠锁,当一个任务尝试获取的一个信号量不可用时,该信号量将任务放置到一个等待队列并休眠(常用于用户空间)。
二进制信号量和计数信号量
#include <asm/semaphore.h>
struct semaphore name;
int count;
sema_init(&name, count); //count > 1 计数信号量 同一时刻允许 count 个持有者,不常用
init_MUTEX(&name); //== sema_init (&name, 1) 二进制信号量
init_MUTEX_LOCKED (&name); //== sema_init (&name, 0) initially locked
down(&name); //获取信号量,进入睡眠状态的进程不能被信号打断,因此不能在中断上下文使用
/* critical region ... */
up(&name); //释放信号量
if (down_interruptible(&name)) /* 进入睡眠状态的进程能被信号打断,信号也会导致该函数返回,这时候函数的返回值非 0 */
{
return - ERESTARTSYS;
}
/* critical region ... */
up(&name); //释放信号量
down_trylock(&name); /* 若获得该信号量返回 0,否则,返回非 0 值。它不会导致调用者睡眠,可以在中断上下文使用*/
/* critical region ... */
up(&name); //释放信号量
互斥体
类似二进制信号量,但是接口更简单,性能更出色。mutex 的使用方法和信号量用于互斥的场合完全一样。
struct mutex my_mutex;
mutex_init(&my_mutex);
mutex_lock(&my_mutex);
/* critical region ... */
mutex_unlock(&my_mutex);
- 信号量所保护的临界区可包含可能引起阻塞的代码,而自旋锁则绝对要避免用来保护包含这样代码的临界区。因为阻塞意味着要进行进程的切换,如果进程被切换出去后,另一个进程企图获取本自旋锁,死锁就会发生。
- 信号量存在于进程上下文,因此,如果被保护的共享资源需要在中断或软中断情况下使用,则在信号量和自旋锁之间只能选择自旋锁。当然,如果一定要使用信号量,则只能通过 down_trylock()方式进行,不能获取就立即返回以避免阻塞。