原子操作
相关函数
原子操作API
typedef struct {
int counter;
} atomic_t;函数 描述
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,如果结果为负就返回真,否则返回假
原子位操作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 位原来的值。
应用场景
用于int变量进行原子操作,或者对位进行原子操作的场合。
思考
原子操作能保证操作不可切割,但是能保证互斥吗?在SMP的情况下,原子操作能保证不会有多个线程同时对一个变量进行原子操作吗?这样不也会造成得到的结果与预期结果不一样吗?
答:原子操作是能保证互斥的,这是通过硬件实现的,当进行原子操作时,会先锁住总线,这样除了本CPU其他的CPU是无法通过总线进行存取数据的。如此一来,原子操作不仅不可切分,在SMP环境下还能保证互斥。
注意事项
原子操作只能对整形变量或者位进行保护,但是,在实际的使用环境中怎么可能只有整形
变量或位这么简单的临界区。举个最简单的例子,设备结构体变量就不是整型变量,我们对于结构体中成员变量的操作也要保证原子性,在线程 A 对结构体变量使用期间,应该禁止其他的线程来访问此结构体变量,这些工作原子操作都不能胜任
自旋锁
自旋之意:进程没有获取到临界资源就会原地循环等待资源,不会立刻下CPU
相关函数
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;函数 描述
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) 将中断状态恢复到以前的状态,并且激活本地中断,释放自旋锁。
应用场景
自旋锁适用于SMP或支持抢占的单CPU下线程之间的并发访问,也就是用于线程与线程之间。并且保护的临界资源区应该很小,访问临界区的时间应该很短才适合用自旋锁。
注意事项
被自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞的API 函数,否则的话会可能会导致死锁现象的发生。
不能递归申请自旋锁,因为一旦通过递归的方式申请一个你正在持有的锁,那么你就
必须“自旋”,等待锁被释放,然而你正处于“自旋”状态,根本没法释放锁。
在你占用信号量的同时不能占用自旋锁,因为在你等待信号量时可能会睡眠,而在持有自旋锁时是不允许睡眠的。
思考
自旋锁如何引起死锁?
!!自旋锁会自动禁止抢占!!,假设在单CPU的情况下,当线程 A得到锁以后会暂时禁止内核抢占。如果线程 A 在持有锁期间进入了休眠状态,那么线程 A 会自动放弃 CPU 使用权。线程 B 开始运行,线程 B 也想要获取锁,但是此时锁被 A 线程持有,而且内核抢占还被禁止了!线程 B 无法被调度出去,那么线程 A 就无法运行,锁也就无法释放,此时死锁发生。
中断中能使用自旋锁吗,如果能使用自旋锁需要注意什么?
中断中是可以使用自旋锁的,因为自旋锁不会引起阻塞和睡眠。中断中使用自旋锁的时候要预防死锁的发生:当线程A获取自旋锁后,在访问临界资源的过程中,如果此时中断到来,并且中断程序也要获取自旋锁并且访问临界资源,那么这个时候就会发生死锁。那么至于死锁的原因是,当中断服务开始执行会禁止本CPU调度,所以只有中断服务执行完,CPU才会空闲出来。由于中断服务中在自旋等待自旋锁,但是自旋锁被A线程持有,中断服务没有执行完A线程就无法上机,就无法释放自旋锁,死锁自此发生。
因此如果需要在中断中使用自旋锁访问临界资源。那么在线程中获取锁之前一定要禁止本地中断。
如何禁止和打开本地中断?
除了下面的函数,如果需要在中断中使用自旋锁,那么可以使用上面的函数。
void local_irq_save(unsigned long flags);
void local_irq_disable(void);//只有我们知道中断并未在其他地方被禁用的情况下,才能使用这个void local_irq_save(unsigned long flags);
void local_irq_disable(void);
信号量
相关函数
struct semaphore {raw_spinlock_t lock;unsigned int count;struct list_head wait_list;
};函数 描述
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 进入休眠状态的线程不能被信号打断。而使用此函数进入休眠以后是可以被信号打断的。
void up(struct semaphore *sem) 释放信号量
应用场景
信号量用于生产者和消费者的问题,但是也可以用作互斥访问临界资源,此时信号量初始化为1。
因为信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场
合。
注意事项
down函数没有获取到信号量会导致睡眠,因此不能在中断中使用
信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。
如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换
线程引起的开销要远大于信号量带来的那点优势。
思考
为什么在ISR(中断服务程序)中禁止睡眠(linux睡眠和挂起和阻塞都不会消耗cpu资源)?
因为睡眠会导致call scheduler以选择下一个可运行的程序,但是进程调度需要process contex,由于ISR中没有process contex,所以无法进行调度。
互斥体(其实是一种互斥信号)
相关函数
struct mutex {/* 1: unlocked, 0: locked, negative: locked, possible waiters */atomic_t count;spinlock_t wait_lock;
};函数 描述
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)使用此函数获取信号量失败进入休眠以后可以被信号打断。
应用场景
一次只有一个线程可以访问共享资源
注意事项
mutex 可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁。
和信号量一样,mutex 保护的临界区可以调用引起调度 API 函数。因为mutex和信号量都没有禁止内核抢占。
因为一次只有一个线程可以持有 mutex,因此,必须由 mutex 的持有者释放 mutex。并且 mutex 不能递归上锁和解锁。
顺序自旋锁
使用顺序锁的话可以允许在写的时候进行读操作,也就是实现同时读写,但是不允许同时进行并发的写操作。虽然顺序锁的读和写操作可以同时进行,但是如果在读的过程中发生了写操作,最好重新进行读取,保证数据完整性。顺序锁保护的资源不能是指针,因为如果在写操作的时候可能会导致指针无效,而这个时候恰巧有读操作访问指针的话就可能导致意外发生,比如读取野指针导致系统崩溃
读写自旋锁
读写自旋锁为读和写操作提供了不同的锁,一次只能允许一个写操作,也就是只能一个线
程持有写锁,而且不能进行读操作。但是当没有写操作的时候允许一个或多个线程持有读锁,可以进行并发的读操作
读写信号量
只有一个写着可以持有读信号量,可以同时多个读者持有信号量。所有读写信号量都只对写者互斥。