一. 并发与竞争
-
Linux系统并发(访问同一个共享资源)产生的原因
- 多线程并发访问
- 抢占式并发访问,调度程序可以在任意时刻抢占正在运行的线程
- 中断服务程序的并发访问
- SMP(多核)核间并发访问
-
Linux并发和竞争的几种解决方法:原子操作,自旋锁,信号量,互斥体
二. 原子操作:不能再进一步分割的操作,一般用于变量或位操作
-
Linux内核使用atomic_t 的结构体来完成整型数据的原子操作,该结构体定义在源码目录include/linux/types.h
typedef struct { int counter; } atomic_t;
-
相关API函数
-
整型数据原子操作的示例代码
atomic_t val = ATOMIC_INIT(10); /* 定义并初始化原子变量 val = 10 */ atomic_set(&val,100); /* 设置 val = 100 */ atomic_read(&val); /* 读取 val 的值 */ atomic_inc(&val); /* val的值加 1 */
-
原子位操作,是直接对内存进行操作的
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 位的值 /* 将 p 地址的第 nr 位置 1,并且返回 nr 位原来的值 */ int test_and_set_bit(int nr, void *p); /* 将 p 地址的第 nr 位清零,并且返回 nr 位原来的值 */ int test_and_clear_bit(int nr, void *p); /* 将 p 地址的第 nr 位翻转,并且返回 nr 位原来的值 */ int test_and_change_bit(int nr, void *p);
三. 自旋锁
-
一个线程要访问某个共享资源时,需要先获取相应的锁;若没有则会循环尝试获取该锁(自旋),直到有另一线程释放锁。
-
存在的问题
- 当线程处于自旋时,会消耗处理器时间,降低系统性能,所以一般自旋锁持有时间不能太长。
- 自旋锁会自动禁止内核抢占。当线程A得到自旋锁时,会禁止内核抢占;若此时线程A进入休眠状态,线程B需要得到自旋锁;此时线程B得不到自旋锁,也无法调度则线程A无法运行,形成死锁。
- 当线程A得到自旋锁时,发生中断B,中断B服务函数需要自旋锁;此时线程A无法得到运行不能释放锁,中断B得不到锁一直自旋,形成死锁。
-
Linux内核使用结构体spinlock_t 表示自旋锁,该结构体定义在源码目录include/linux/spinlock_types.h
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函数 — 不考虑中断
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
-
自旋锁相关API函数 — 考虑中断
/* 保存中断状态,禁止本地中断,并获取自旋锁 */ void spin_lock_irqsave(spinlock_t *lock, unsigned long flags); /* 将中断状态恢复到以前的状态,并且激活本地中断,释放自旋锁 */ void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags);
-
自旋锁示例
spinlock_t lock; //定义自旋锁 spin_lock_init(&lock); //初始化自旋锁 /* 线程A */ void ThreadA() { unsigned long flag; //保存中断状态 spin_lock_irqsave(&lock,flag); //获取自旋锁,禁止中断 /* 临界操作 */ spin_unlock_irqrestore(&lock,flag); //释放自旋锁,开启中断 } /* 中断服务函数B */ void IrqB() { spin_lock(&lock); //获取自旋锁 /* 临界操作 */ spin_unlock(&lock); //释放自旋锁 }
四. 信号量
-
信号量特点
- 信号量会使等待资源的线程进入休眠状态,因此适用于占用共享资源时间较久的场合。
- 中断不能进入睡眠状态,故信号量不能在中断中使用。
- 占用共享资源时间较短的场合,不适合使用信号量,因为频繁的线程切换会带来较大的系统开销。
-
使用semaphore结构体表示信号量,该结构体定义在源码目录include/linux/semaphore.h
struct semaphore { raw_spinlock_t lock; unsigned int count; struct list_head wait_list; };
-
信号量API函数
/* 初始化信号量 sem,设置信号量值为 val */ void sema_init(struct semaphore *sem, int val); /* 获取信号量,因为会导致休眠,因此不能在中断中使用 */ void down(struct semaphore *sem); /*获取信号量,和down类似,只是使用down进入休眠状态的线程不能被信号打断。而使用此函数进入休眠以后是可以被信号打断的(此信号是指外部中断信号,如ctrl+c)*/ int down_interruptible(struct semaphore *sem); /* 尝试获取信号量,获取返回0。不能获取就返回非0,不会进入休眠 */ int down_trylock(struct semaphore *sem); /* 释放信号量 */ void up(struct semaphore *sem);
-
示例
struct semaphore sem; //定义信号量 sema_init(&sem,1); //初始化二值信号量 down(&sem); //获取信号量 /* 临界操作 */ up(&sem); //释放信号量
五. 互斥体
-
Linux内核使用mutex结构体表示互斥体,该结构体定义在源码目录include/linux/mutex.h
struct mutex { /* 1: unlocked, 0: locked, negative: locked, possible waiters */ atomic_t count; spinlock_t wait_lock; };
-
互斥体特点
- 使用互斥体,一次只允许一个线程访问共享资源,不能递归申请互斥体
- 互斥体会导致休眠,因此不能在中断中使用,中断中只能使用自旋锁
-
互斥体API函数
void mutex_init(mutex *lock); //初始化mutex /* 获取mutex,也就是给mutex上锁。如果获取不到就进休眠 */ void mutex_lock(struct mutex *lock); /* 释放mutex,也就给mutex 解锁 */ void mutex_unlock(struct mutex *lock); /* 尝试获取mutex,如果成功就返回1,如果失败就返回0 */ int mutex_trylock(struct mutex *lock);
-
示例
struct mutex mux_lock; //互斥变量 mutex_init(&mux_lock); //初始化互斥体 mutex_lock(&mux_lock); //给mutex上锁 /* 临界区操作 */ mutex_unlock(&mux_lock); //给mutex解锁
六. 扩展 — 死锁
- 产生死锁的原因:资源不足;资源分配不当;进程/线程运行顺序不当。
- 产生死锁的必要条件:
- 互斥条件:一个资源每次只能被一个进程/线程使用
- 请求与保持条件:当一个进程/线程因某个事件阻塞时(如请求某个资源),对已有的资源不释放
- 不可剥夺条件:当进程/线程未使用完某个资源时,不可剥夺
- 循环等待条件:若干进程/线程首尾相接,形成循环等待资源的状态
- 死锁预防:破坏产生死锁的必要条件
- 破坏请求与保持条件。静态分配资源法:当进程/线程执行前,申请所有需要的资源;动态分配资源法:进程/线程在申请资源时,本身不占有系统资源。
- 破坏不可剥夺条件。进程/线程进入阻塞(如申请资源)时,释放已获得的资源到系统资源列表;下次执行时,需要获得先前的所有资源以及跳出阻塞(申请到资源)状态。
- 破坏循环等待条件。根据资源使用的频繁程度,顺序进行编号;申请资源时必须按编号进行,只有获得较小编号资源才能申请较大编号的资源
以上是我在学习过程中的总结,不当之处请在评论区指出。