linux并发与竞争(原子/自旋锁/信号量/互斥体理解)

文章详细介绍了Linux系统中处理并发问题的几种机制,包括多线程并发、原子操作、自旋锁、信号量和互斥体。原子操作用于保证整型数据的更新不被中断,自旋锁适用于保护短时间的临界区,信号量允许线程在无法获取资源时休眠,互斥体则是一种线程互斥的同步工具,不能在中断中使用。各种机制各有优缺点,适用于不同的并发场景。
摘要由CSDN通过智能技术生成

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); /* 解锁 */

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值