[竞态]
[1] 概念
竞争使用独占共享资源的状态
[2] 产生条件
1. 并发
2. "同时"访问"独占共享资源"
[避免竞态 -- 使代码可重入]
[1] 如何使file_operations可重入?
1. 什么是可重入?
多个任务执行同一段代码,彼此不影响
2. 如何使代码可重入?
单输入, 单输出--函数的单输入、单输出是指所有的输入都来自于函数的形参,所有的输出从形参或返回值输出
--不直接使用全局变量或静态局部变量
[解决竞态]
[2] 操作系统中的并发
1. 多CPU之间的并发
2. 单CPU上进程之间的并发
3. 单CPU上进程和中断之间的并发
4. 单CPU上中断之间的并发
[3] 抢占(一个进程强制剥夺另一个进程的CPU叫抢占)
抢占发生在什么时候?
int main(void)
{
什么程序(OS中的)可以插到中间运行?(只有中断程序可以做到)
while (1);
return 0;
}
[4] 中断屏蔽
1. 原理
消除单CPU上的并发
2. 用法
#include <linux/irqflags.h>
/*
* @brief 关闭中断
*/
local_irq_disable()
...... // 访问独占共享资源
/*
* @brief 使能中断
*/
local_irq_enable()
/*
* @brief 保存中断状态并关闭中断
* @param[out] flags 保存中断状态
*/
local_irq_save(unsigned long flags)
....... // 访问独占共享资源
/*
* @brief 恢复中断状态
* @param[in] flags 中断状态
*/
local_irq_restore(unsigned long flags)
3. 适用
(1) 解决单cpu上的竞态
(2) 关闭中断时间不宜过长(操作系统利用时钟中断计时)
[5] 原子操作
例:防止文件被多次打开
int i = 1;
int xxx_open(struct inode *inode, struct file *filp)
{
if (i - 1 < 0) {
return -EBUSY;
}
i -= 1;
分析如下:
A进程 B进程
i - 1 < 0
切换到B进程执行
i - 1 < 0
切换回A进程
i -= 1;
切换到B进程执行
...
return 0;
}
int xxx_release(struct inode *inode, struct file *filp)
{
i++;
}
1. 原理
解决整数(保证整数的访问是一个原子操作)的竞态访问
2. 用法
#include <asm/atomic.h>
// v->counter = i
void atomic_set(atomic_t *v, int i);
// v->counter = 0
atomic_t v = ATOMIC_INIT(0);
// retun v->counter
atomic_read(atomic_t *v);
// v->counter += i;
void atomic_add(int i, atomic_t *v);
// v->counter -=i
void atomic_sub(int i, atomic_t *v);
// v->counter++
void atomic_inc(atomic_t *v);
// v->counter--
void atomic_dec(atomic_t *v);
// (++v->counter) == 0
int atomic_inc_and_test(atomic_t *v);
// (--v->counter) == 0
int atomic_dec_and_test(atomic_t *v);
// (v->counter - i) == 0
int atomic_sub_and_test(int i, atomic_t *v);
// return (v->counter + i)
int atomic_add_return(int i, atomic_t *v);
// return (v->counter - i)
int atomic_sub_return(int i, atomic_t *v);
// return ++v->counter;
int atomic_inc_return(atomic_t *v);
// return --v->counter;
int atomic_dec_return(atomic_t *v);
// addr[nr] = 1
void set_bit(nr, void *addr);
// addr[nr] = 0
void clear_bit(nr, void *addr);
// ~addr[nr]
void change_bit(nr, void *addr);
// 检测第nr位是否为1 (拿到内核代码最好确认一下)
test_bit(nr, void *addr);
// 检测第nr位是否为1,然后设置第nr位为1,如果来的nr位1,则返回真,否则假
int test_and_set_bit(nr, void *addr);
// 检测第nr位是否为1,然后清除第nr位为0,如果来的nr位1,则返回真,否则假
int test_and_clear_bit(nr, void *addr);
// 检测第nr位是否为1,然后改变第nr位为0,如果来的nr位1,则返回真,否则假
int test_and_change_bit(nr, void *addr);
例:防止文件被多次打开
atomic_t i = ATOMIC_INIT(1);
int xxx_open(struct inode *inode, struct file *filp)
{
if (!atomic_dec_and_test(i)) {
atomic_inc(i);
return -EBUSY;
}
...
return 0;
}
int xxx_release(struct inode *inode, struct file *filp)
{
atomic_inc(i);
}
3. 适用
可以解决多CPU之间的整数的竞态访问
[6] 自旋锁
1. 原理
见《4.自旋锁.bmp》
2. 用法
#include <linux/spinlock.h>
spinlock_t lock; // 定义
/*
* @brief 初始化
* @param[out] lock 锁
*/
spin_lock_init(spinlock_t * lock)
/*
* @brief 获得锁,未获得不返回
* @param[out] lock 锁
*/
void spin_lock(spinlock_t * lock)
或:
/*
* @brief 尝试锁定,获得返回真,未获得返回假
* @param[out] lock 锁
*/
int spin_trylock(spinlock_t * lock)
...... // 访问独占的共享资源
/*
* @brief 释放锁
* @param[out] lock 锁
*/
void spin_unlock(spinlock_t * lock)
其它用法见PPT
例:防止文件被多次打开
int i = 1;
int xxx_open(struct inode *inode, struct file *filp)
{
spin_lock(&lock);
if (i - 1 < 0) {
spin_unlock(&lock);
return -EBUSY;
}
i -= 1;
spin_unlock(&lock);
...
return 0;
}
int xxx_release(struct inode *inode, struct file *filp)
{
spin_lock(&lock);
i++;
spin_unlock(&lock);
}
3. 分析
(1) 多CPU上的并发
CPU0 CPU1
... ...
lock(加锁)
lock(等待自旋锁)
...
操作
...
ulock(解锁)
lock(加锁)
...
操作
...
ulock(解锁)
(2) 单CPU上的进程之间的并发
消除了(因为关闭了抢占)
(3) 单CPU上的进程和中断之间的并发
进程 中断
lock(加锁)
...
被中断
lock(等待, 永远加不上锁,也不会退出,死机)
总结: 进程加锁时,一定同时关闭中断
进程 中断
lock(加锁)
...
访问资源
ulock(解锁)
...
返回
lock(加锁)
访问资源
unlock(解锁)
总结: 进程中加锁并关闭中断,中断中,只需要加锁
(4) 单CPU上的中断和中断之间的并发
中断 中断(高)
lock(加锁)
...
被中断
...
lock(等待, 永远加不上锁,也不会退出,死机)
总结:低优先级的中断,加锁同时一定要关闭中断
中断 中断(高)
...
lock(加锁)
...
访问资源
...
ulock(解锁)
返回
lock(加锁)
...
unlock(解锁)
总结:加锁时,关闭中断
4. 适用
(1) 可以解决多CPU之间的并发引起的竞态(实现多CPU之间的互斥访问)
(2) 消除单CPU上的并发
方法:
1. 单CPU上,进程之间的并发,直接加锁
2. 单CPU上,进程与中断之间的并发和中断与中断之间的并发,加锁且关闭中断
(3) 加锁时间不能太长
(4) 获得自旋锁期间,不能有引起调度的函数,自己放弃cpu(休眠是典型的代表)
例:
假设下列AB进程运行于同一CPU:
A进程 B进程
加锁
......
sleep后正好切换到B进程加同一锁
加锁(一直自旋,且占有CPU,导致本CPU死锁)
解锁
[7] 读写自旋锁
1. 原理
AB两个线程同时做如下操作:
A B buf(完整单词才能理解)
hello
读 读 可以理解读到的内容
读 写world hello w不能理解
写world 写! hello w!orld 内容不能理解
总结: 两个任务都是读,不需要加锁,只要有一个任务是写的时候需要加锁
读不锁定读,读会锁定写, 写锁定读和写
2. 用法
#include <linux/spinlock.h>
rwlock_t lock; // 定义
rwlock_init(&lock); // 初始化
// 读 获取锁
read_lock(&lock); // 可以换成read_lock_...
... // 读临界资源
read_unlock(&lock); // 可以换成read_unlock_...
// 写 获取锁
write_lock_irqsave(&lock, flags); // 可以换成write_lock_...
... // 写临界资源
write_unlock_irqrestore(&lock, flags); // 可以换成write_unlock_...
其它用法见PPT
例: 见PPT
3. 分析
(1) 读 读
CPU0 CPU1
... ....
加读锁
....
读资源 加读锁(可以加到读锁)
... ...
... 读资源
解读锁 解读锁
(2) 读 读写
CPU0 CPU1
加读锁
读资源
加写锁(自旋, 等待)
...
解读锁
加写锁
读写资源
解写锁
(3) 读写 读
CPU0 CPU1
加写锁
读写资源
加读锁(自旋, 等待)
...
解写锁
加读锁
读
解读锁
(3) 读写 读写
(4) 写 读写
(5) 读写 写
跟普通锁完全相同
4. 适用
读的概率(时间)大于写的概率(时间)
读写自旋锁适用频率低于自旋锁
[8] 顺序自旋锁
1. 原理
读写锁的改进,读不锁定写,写锁定读
2. 用法
#include <linux/seqlock.h>
seqlock_t lock; // 定义
seqlock_init(&lock); // 初始化
// 写 获取锁
write_seqlock(&lock); // 可以换成write_seqlock_...
... // 写临界资源
write_sequnlock(&lock); // 可以换成write_sequnlock_...
// 读 不会锁定写,但会被写锁定
unsigned seqnum;
do {
seqnum = read_seqbegin(&lock); // 可以换成read_seqbegin_irqsave(&lock, flags)
... // 读临界资源
} while (read_seqretry(&lock, seqnum)); // 可以换成read_seqretry_restore(&lock, flags)
例:
A线程 B线程
do {
seqnum = read_seqbegin(&lock);
......
write_seqlock(&lock);
p = NULL;
write_sequnlock(&lock);
*p = 5;
} while ((read_seqretry(&lock, seqnum));
3. 分析
(1) 读 锁定 写
CPU0 CPU1
加读锁
加写锁(假设成功)
读 写(碰到了读,写不会出错)
解写锁
解读锁
(检测出来,
是否加过写锁
写过,重读)
总结: 读 可以不锁定 写
(2) 写 锁定 读
CPU0 CPU1
加写锁
写
加读锁(假设成功)
写 读(碰到了写,读出错)
解读锁
解写锁
(检测出来,
是否读过)
总结:写 必须锁定 读
4. 适用
(1) 读独占共享资源的概率(时间),远大于写独占共享资源的概率(时间)
(2) 共享资源不能指针
[9] 信号量
1. 原理
见《5-信号量》图
2. 用法
#include <linux/semaphore.h>
struct semaphore sem; // 定义
sema_init(&sem, 1); // 初始化
down(&sem); // 获取信号量
或:
/*
* 调用本驱动的进程收到消息,本函数会返回
* 获取到信号量返回0,否则1
*/
down_interruptible(&sem);
/*
* 尝试获取信号量,未获取信号量立即返回,不休眠
* 获取到信号量返回0,否则1(注意跟自旋锁不同)
*/
down_trylock(&sem)
... // 临界资源访问
up(&sem); // 释放信号量
3. 分析
(1) 多CPU之间的并发
可以
(2) 单CPU进程之间的并发
可以
(3) 单CPU进程和中断之间的并发
不可以使用,中断中绝对不能调用可能会引起休眠的函数
(4) 中断之间的并发
中断中绝对不能调用可能会引起休眠的函数
4. 适用
(1) 多cpu之间的并发所引起的竞态
(2) 单CPU上,多进程之间的并发引起的竞态
(3) 不能用于中断上下文中(中断里面不能休眠,操作系统不支持)
(4) 获得信号量的时间可以很长(等待信号量时,线程休眠不浪费系统资源(相对自旋锁而言),信号量的持有时间还是应该尽可能的短)
[10] 读写信号量
1. 原理
读不锁定读,读锁定写,写锁定写
2. 用法
PPT
3. 适用
用于需要用到信号量的情况(读的概率(时间)大于写的概率(时间))
[11] 互斥体
1. 原理
采用信号量实现1个资源的互斥访问
2. 用法
见PPT
3. 适用
可以适用信号量互斥的地方,互斥体都可以用
[12] 实践
CPU0 CPU1
读 写
加读锁
...
....
加写锁
...
...
写资源
...
解写锁
解读锁,返回
重读
写 读
加写锁
....
....
写资源
... 加读锁(锁定,一直等待,等待写解锁)
...
写解锁
加读锁
读资源
...
...
解读锁