1.linux并发产生的原因
(1)多线程并发访问,
(2)抢占式并发访问,进程调度可以在任意时刻抢占正在运行的线程,从而运行其他的线程
(3)终端程序并发访问
(4)SMP(多核)间并发访问
并发访问的后果就是竞争,一般像全局变量,设备结构体这些肯定是要保护的,至于其他的数据就要根据实际的驱动程序而定了。
2.原子操作
(1)原子操作的原因
对于c语言来说,a=3,编译为汇编语言时可能被翻译成如下汇编(仅作为理解)
ldr r0, =0X30000000 /* 变量 a 地址 */
ldr r1, = 3 /* 要写入的值 */
str r1, [r0] /* 将 3 写入到 a 变量中 */
当并发出现时可能出现这种情况:
(2)原子操作api
,为避免上述情况,Linux 内核定义了叫做 atomic_t 的结构体来完成整形数据的原子操作,在使用中用原子变量来代替整形变量,此结构体定义在 include/linux/types.h 文件中,
typedef struct {
int counter;
} atomic_t;
typedef struct {
long long counter;
} atomic64_t;//64操作系统
声明变量可以这样,atomic_t a;
linux提供了多个可以对原子变量进行操作的函数,
3 自旋锁
原子操作只能对整形变量或者位进行保护,但是,在实际的使用环境中怎么可能只有整形变量或位这么简单的临界区。举个最简单的例子,设备结构体变量就不是整型变量,我们对于结构体中成员变量的操作也要保证原子性,在线程 A 对结构体变量使用期间,应该禁止其他的线程来访问此结构体变量,这些工作原子操作都不能胜任,需要锁机制,在 Linux内核中就是自旋锁。
当一个线程要访问某个共享资源的时候首先要先获取相应的锁,锁只能被一个线程持有,只要此线程不释放持有的锁,那么其他的线程就不能获取此锁。对于自旋锁而言,如果自旋锁正在被线程 A 持有,线程 B 想要获取自旋锁,那么线程 B 就会处于忙循环-旋转-等待状态,线程 B 不会进入休眠状态或者说去做其他的处理,而是会一直傻傻的在那里“转圈圈”的等待锁可用。
从这里我们可以看到自旋锁的一个缺点:那就等待自旋锁的线程会一直处于自旋状态,这样会浪费处理器时间,降低系统性能,所以自旋锁的持有时间不能太长。所以自旋锁适用于短时期的轻量级加锁。
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;
自旋锁接口函数
自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞的API 函数,否则的话会可能会导致死锁现象的发生,经典死锁的发生例子:
线程a在持有锁期间进入了休眠,被调度出去,此时线程b想要获取锁,但是锁被线程a持有,并且内核抢占被禁止了,线程b无法调度出去一直等待锁被释放,而线程a此时也无法再次被调度运行,死锁就发生了。
还有就是中断里使用锁的时候也一定注意死锁的发生,例如
线程 A 先运行,并且获取到了 lock 这个锁,当线程 A 运行 functionA 函数的时候中断发生了,中断抢走了 CPU 使用权。右边的中断服务函数也要获取 lock 这个锁,但是这个锁被线程 A 占有着,中断就会一直自旋,等待锁有效。
最好的解决方法就是获取锁之前关闭本地中断
DEFINE_SPINLOCK(lock) /* 定义并初始化一个锁 */
/* 线程 A */
void functionA (){
unsigned long flags; /* 中断状态 */
spin_lock_irqsave(&lock, flags) /* 是在这里禁止本地中断,并获取锁 */
/* 临界区 */
spin_unlock_irqrestore(&lock, flags) /* 释放锁 */
}
/* 中断服务函数 */
void irq() {
spin_lock(&lock) /* 获取锁 */
/* 临界区 */
spin_unlock(&lock) /* 释放锁 */
}
自旋锁的使用注意事项;
(1)因为在等待自旋锁的时候处于“自旋”状态,因此锁的持有时间不能太长,一定要
短,否则的话会降低系统性能。如果临界区比较大,运行时间比较长的话要选择其他的并发处
理方式,比如信号量和互斥体。
(2)自旋锁保护的临界区内不能调用任何可能导致线程休眠的 API 函数,否则的话可能
导致死锁.
(3)不能递归申请自旋锁,因为一旦通过递归的方式申请一个你正在持有的锁,那么你就
必须“自旋”
4.信号量
相比较自旋锁,信号量可以使线程进入休眠状态,比如 A 与 B、C 合租了一套房子,这个房子只有一个厕所,一次只能一个人使用。某一天早上 A 去上厕所了,过了一会 B 也想用厕所,因为 A 在厕所里面,所以 B 只能等到 A 用来了才能进去。B 要么就一直在厕所门口等着,等 A 出来,这个时候就相当于自旋锁。B 也可以告诉 A,让 A 出来以后通知他一下,然后 B 继续回房间睡觉,这个时候相当于信号量。使用信号量会提高处理器的使用效率,但是,信号量的开销要比自旋锁大,因为信号量使线程进入休眠状态以后会切换线程,切换线程就会有开销。
信号量的特点:
(1)因为信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场
合。
(2)信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。
(3)如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势。
信号量一般有一个信号量值,可通过信号量值控制方位共享资源的访问数量。例如一个房间有10吧钥匙,相当于信号量值为10,如果想要进入房间,那就需要先获取一把钥匙,信号量值减1,直到10把钥匙都被拿走,信号量为0,此时就不允许人进入房间了,如果有人出来,那么就可以在允许一个人进入。
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
信号量的 API 函数:
struct semaphore sem; /* 定义信号量 */
sema_init(&sem, 1); /* 初始化信号量 */
down(&sem); /* 申请信号量 */
/* 临界区 */
up(&sem); /* 释放信号量 */
5.互斥体
将信号量的值设置为 1 就可以使用信号量进行互斥访问,但是互斥体有专门的结构体,
struct mutex {
/* 1: unlocked, 0: locked, negative: locked, possible waiters */
atomic_t count;
spinlock_t wait_lock;
};
特点:
(1)mutex 可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁。
(2)和信号量一样,mutex 保护的临界区可以调用引起阻塞的 API 函数。
(3)因为一次只有一个线程可以持有 mutex,因此,必须由 mutex 的持有者释放 mutex。并
且 mutex 不能递归上锁和解锁.
互斥体的接口函数:
struct mutex lock; /* 定义一个互斥体 */
mutex_init(&lock); /* 初始化互斥体 */
mutex_lock(&lock); /* 上锁 */
/* 临界区 */
mutex_unlock(&lock); /* 解锁 */