8_linux并发与竞争

一、并发与竞争简介

并发与竞争:对于多核 CPU,多线程同时运行时,有可能访问同一个共享资源,可能会相互覆盖这些共享资源,导致内存数据混乱,因此要保证共享资源的有序访问。

临界区:指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源又无法同时被多个线程访问的特性。当有线程进入临界区段时,其他线程或是进程必须等待。

​ linux 系统并发与竞争产生原因

​ 1、多线程并发访问

​ 2、抢占式并发访问

​ 3、中断程序并发访问

​ 4、SMP(多核)间并发访问

二、并发与竞争处理方法

1、原子操作

1)原子操作简介

​ 原子操作就是指不能在进一步分割的指令,一般原子操作用于变量或者位操作。假设对一个整形变量赋值,c 语言下的表现为:

a = 3;

​ 而在汇编的表现为:

ldr r0, =0X30000000 /* 变量a地址 */ 
ldr r1, = 5 /* 要写入的值 */
str r1, [r0] /* 将5写入到a变量中 */

​ 在汇编环境下,如果代码执行完第二行但还没执行第三行时,有另一个线程覆盖了 r1 寄存器的值,这就会导致最后的赋值不正确。如果把三行汇编语句作为一个整体去运行,也就是作为原子存在,就不会出现以上情况。

2)原子操作结构体

​ linux 内核定义了叫做 atomic_t 的结构体来完成整形数据的原子操作,在使用中用原子变量来代替整形变量,此结构体定义在 include/linux/types.h 文件中,定义如下:

typedef struct {
	int counter;
 } atomic_t;

​ 使用原子操作 API 函数前,先定义一个原子变量:

atomic_t a;

​ 可通过宏 ATOMIC_INIT 赋初值:

a = ATOMIC_INIT(0);

3)原子整形操作常用API函数(32位)

函数描述
ATOMIC_INIT(int i)定义原子变量的时候对其初始化。
int atomic_read(atomic_t *v)读取 v 的值,并且返回。
void atomic_set(atomic_t *v, int i)向 v 写入 i 值。
void atomic_add(int i, atomic_t *v)给 v 加上 i 值。
void atomic_sub(int i, atomic_t *v)从 v 减去 i 值。
void atomic_inc(atomic_t *v)给 v 自增 1。
void atomic_dec(atomic_t *v)给 v 自减 1。
int atomic_dec_return(atomic_t *v)给 v 自增 1,并返回 v 的值。
int atomic_inc_return(atomic_t *v)给 v 自减 1,并返回 v 的值。
int atomic_sub_and_test(int i, atomic_t *v)从 v 减去 i ,如果结果为 0 返回真,否则返回假。
int atomic_dec_and_test(atomic_t *v)从 v 减去 1,如果结果为 0 返回真,否则返回假。
int atomic_inc_and_test(atomic_t *v)给 v 加上 1,如果结果为 0 返回真,否则返回假。
int atomic_add_negative(int i, atomic_t *v)给 v 加上 i,如果结果为负返回真,否则返回假。

4)原子整形位操作常用API函数

函数描述
void set_bit(int nr, void *p)将 p 地址的第 nr 位置 1。
void clear_bit(int nr,void *p)将 p 地址的第 nr 位清零。
void change_bit(int nr, void *p)将 p 地址的第 nr 位进行翻转。
int test_bit(int nr, void *p)获取 p 地址的第 nr 位的值。
int test_and_set_bit(int nr, void *p)将 p 地址的第 nr 位置 1,并且返回 nr 位原来的值。
int test_and_clear_bit(int nr, void *p)将 p 地址的第 nr 位清零,并且返回 nr 位原来的值。
int test_and_change_bit(int nr, void *p)将 p 地址的第 nr 位翻转,并且返回 nr 位原来的值。

2、自旋锁

1)自旋锁简介

​ 当一个线程要访问某个共享资源的时候首先要先获取相应的锁,锁只能被一个线程持有,只要此线程不释放持有的锁,那么其他的线程就不能获取此锁。

​ 对于自旋锁而言,如果自旋锁正在被线程 A 持有,线程 B 想要获取自旋锁,那么线程 B 就会处于忙循环 -旋转 -等待状态,线程 B 不会进入休眠状态或者说去做其他的处理。因此如果线程锁持有时间较长,就不应该使用自旋锁来处理。

2)自旋锁结构体

​ Linux内核使用结构体 spinlock_t 表示自旋锁,结构体定义如下所示:

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;

​ 使用自旋锁之前,先定义一个自旋锁变量:

spinlock_t lock;

3)自旋锁API函数

函数描述
DEFINE_SPINLOCK(spinlock_t lock)定义并初始化一个自选变量。
int spin_lock_init(spinlock_t *lock)初始化自旋锁。
void spin_lock(spinlock_t *lock)获取指定的自旋锁,也叫做加锁。
void spin_unlock(spinlock_t *lock)释放指定的自旋锁。
int spin_trylock(spinlock_t *lock)尝试获取指定的自旋锁,如果没有获取到就返回0
int spin_is_locked(spinlock_t *lock)检查指定的自旋锁是否被获取,如果没有被获取就返回非 0,否则返回 0。
void spin_lock_irq(spinlock_t *lock)禁止本地中断,并获取自旋锁。
void spin_unlock_irq(spinlock_t *lock)激活本地中断,并释放自旋锁。
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags)保存中断状态,禁止本地中断,并获取自旋锁。
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags)将中断状态恢复到以前的状态,并且激活本地中断,释放自旋锁。
void spin_lock_bh(spinlock_t *lock)关闭下半部,并获取自旋锁。
void spin_unlock_bh(spinlock_t *lock)打开下半部,并释放自旋锁。

注意事项:

​ 被自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞的 API 函数,否则的话会可能会导致死锁现象的发生。自旋锁会自动禁止抢占,也就说当线程 A 得到锁以后会暂时禁止内核抢占。如果线程 A 在持有锁期间进入了休眠状态,那么线程 A 会自动放弃 CPU 使用权。线程 B 开始运行,线程 B 也想要获取锁,但是此时锁被 A 线程持有,而且内核抢占还被禁止了!线程 B 无法被调度出去,那么线程 A 就无法运行,锁也就无法释放。

​ 中断里面也可以使用自旋锁,但是使用自旋锁时要先禁止本地中断,否则可能会发生死锁现象。因为中断内获取了自旋锁,此时假设被另一中断打断,而该中断也要获取自旋锁,但是自旋锁在前一个中断还没被释放,因此就会发生死锁。
推荐一般在线程中使用 spin_lock_irqsave / spin_unlock_irqrestore,以保存一些中断的状态。而中断中使用 spin_lock / spin_unlock

​ 不能递归申请自旋锁。

4)自旋锁使用示例:

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) /* 释放锁 */ 
}

3、信号量

1)信号量简介

​ 相比于自旋锁,信号量可以使等待的线程进入休眠状态。等临界区被释放后,休眠等待的线程会被唤醒。信号量有以下特点:

  • 因为信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场合。

  • 信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。

  • 如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势。

    信号量分为两种,一种是计数型信号量,一种是二值型信号量

    计数型信号量有一个信号量值,假设设置该量值为 5,则同时访问共享资源的线程可以是 5 个。

    二值型信号量,信号量值只能是 0 或 1,使得共享资源只能互斥访问

2)信号量结构体

​ Linux内核使用 semaphore 结构体表示信号量,结构体内容如下所示:

struct semaphore { 
    raw_spinlock_t lock; 
    unsigned int count; 
    struct list_head wait_list; 
};

3)信号量API函数

函数描述
DEFINE_SEAMPHORE(name)定义一个信号量,并且设置信号量的值为 1。
void sema_init(struct semaphore *sem, int val)初始化信号量 sem,设置信号量值为 val。
void down(struct semaphore *sem)获取信号量,因为会导致休眠,因此不能在中断中使用。
int down_trylock(struct semaphore *sem);尝试获取信号量,如果能获取到信号量就获取,并且返回 0。如果不能就返回非 0,并且不会进入休眠。
int down_interruptible(struct semaphore *sem)获取信号量,和 down 类似,只是使用 down 进入休眠状态的线程不能被信号打断。而使用此函数进入休眠以后是可以被信号打断的。如果有中断打断的话会返回–EINTR。
void up(struct semaphore *sem)释放信号量

4)信号量使用示例:

struct semaphore sem; /* 定义信号量 */ 
sema_init(&sem, 1)/* 初始化信号量 */ 
    down(&sem); /* 申请信号量 */ 
	/* 临界区 */ 
	up(&sem); /* 释放信号量 */

4、互斥体

1)互斥体简介

​ 信号量设置为 1 可以实现共享资源的互斥访问,但是 linux 内核中提供了一个更专业的处理机制,互斥体(mutex)。互斥访问表示一次只有一个线程可以访问共享资源。使用 mutex 时需要注意以下几点:

  • mutex 可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁。
  • 和信号量一样,mutex 保护的临界区可以调用引起阻塞的 API 函数。
  • 因为一次只有一个线程可以持有 mutex,因此,必须由 mutex 的持有者释放 mutex。并且 mutex 不能递归上锁和解锁。

2)互斥体结构体

​ linux 内核使用 mutex 来表示互斥体,定义如下:

struct mutex { /* 1: unlocked, 0: locked, negative: locked, possible waiters */ 
    atomic_t count; 
    spinlock_t wait_lock;
};

3)互斥体API函数

函数描述
DEFINE_MUTEX(name)定义并初始化一个 mutex 变量。
void mutex_init(mutex *lock)初始化 mutex。
void mutex_lock(struct mutex *lock)获取 mutex,也就是给 mutex 上锁。如果获取不到就进休眠。
void mutex_unlock(struct mutex *lock)释放 mutex,也就给 mutex 解锁。
int mutex_trylock(struct mutex *lock)尝试获取 mutex,如果成功就返回 1,如果失败就返回 0。
int mutex_is_locked(struct mutex *lock)判断 mutex 是否被获取,如果是的话就返回 1,否则返回 0。
int mutex_lock_interruptible(struct mutex *lock)使用此函数获取信号量失败进入休眠以后可以被信号打断。

4)互斥体使用示例:

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、付费专栏及课程。

余额充值