一、什么是同步?
- 临界区
访问和操作共享数据的代码段。
必须被原子的访问。
在执行期间不可被打断。
- 竞争状态
两个或者多个内核任务处于同一个临界区。
具有随机性,难以控制。
- 同步
避免对临界区的并发或者并行的访问称为同步。
实现多个内核任务对临界区的安全访问。
二、为什么要同步?
- 并发与并行
并发和并行的区别就是一个处理器同时处理多个任务和多个处理器或者是多核的处理器同时处理多个不同的任务。
前者是逻辑上的同时发生(simultaneous),而后者是物理上的同时发生.
比喻:并发和并行的区别就是一个人同时吃三个馒头和三个人同时吃三个馒头。
- 引起并发的原因
中断
中断几乎可以在任何时刻发生,可能随时打断当前正在执行的代码。
内核抢占
如果内核具有抢占性,则内核中的任务可能会被另一个任务抢占。
睡眠及其与用户空间的同步
在内核执行的进程可能会睡眠,这就会唤醒调度程序,调度一个新的用户进程执行。
对称多处理器
两个或者多个处理器可以同时执行代码
- 并发和并行带来的问题
static int temp;
void change(int *a, int *b)
{
temp = *a;
*a = *b;
*b = temp;
}
思考:
在并发状态下,A和B进程"同时"调用change()函数,会出现什么情况?
在并行状态下,多个CPU同时调用change()函数,会出现什么情况?
三、如何实现同步?
- 实现对共享资源的互斥访问
一个执行单元在访问共享资源的时候,其他的执行单元被禁止访问。
- 对临界区加以保护
使用某种互斥机制对临界区加以保护,实现安全访问。
四、内核中同步的方法
- 中断屏蔽
描述
在进入临界区之前屏蔽系统的中断,从而保证正在执行的内核执行路径不被中断处理程序所抢占,防止某些静态条件的发生。
在退出临界区后,重新打开中断。
使用
local_irq_disable() //屏蔽中断
... //临界区
local_irq_enable() //开中断
缺点
ocal_irq_disable()和local_irq_enable()都只能禁止和使能本地CPU内的中断,并不能解决多处理器引发的竞态(并行)。
在屏蔽中断期间所有的中断都无法得到处理,因此长时间屏蔽中断是很危险的,有可能造成数据丢失甚至系统崩溃。
其他几个中断屏蔽函数
local_irq_save()/local_irq_restore()
local_bh_disable()/local_bh_enable()
2. 原子操作
描述
在执行过程中不会被别的代码路径所中断的操作。
用途
原子整数操作最常见的用途就是实现计数器。使用复杂的锁机制来保护一个单纯的计数器是很低效的,所以开发者最好使用原子
操作来操作计数器。
使用
1.整形原子操作
(1)设置原子变量的值
void atomic_set(atomic_t *v, int i) //设置原子变量的值为i
atomic_t v = ATOMIC_INIT(0);//定义并初始化原子变量为
(2)获取原子变量的值
atomic_read(atomic_t *v); //读取原子变量的值
(3)原子变量的加/减
void atomic_add(int i, atomic_t *v);//原子变量加i
void atomic_sub(int i, atomic_t *v);//原子变量减i
(4)原子变量自加/自减
void atomic_inc(atomic_t *v);//原子变量加1
void atomic_dec(atomic_t *v);//原子变量减1
(5)操作并测试
int atomic_inc_and_test(atomic_t *v);
int atomic_dec_and_test(atomic_t *v);
int atomic_sub_and_test(int i,atomic_t *v);
(6)操作并返回(返回原子变量的值)
int atomic_add_return(int i, atomic_t *v);
int atomic_sub_return(int i, atomic_t *v);
int atomic_inc_return(atomic_t *v);
atomic_dec_return(atomic_t *v);
2.位原子操作
unsigned int nr;
volatile unsigned long *addr; 位原子操作(续)
void set_bit(nr, addr);
void clear_bit(nr, addr);
void change_bit(nr, addr);
void test_bit(nr, addr);
int test_and_set_bit(nr, addr);
int test_and_clear_bit(nr, addr);
int test_and_change_bit(nr, addr);
3.自旋锁
描述
自旋锁是专为防止多处理器并行而引入的一种锁,如果一个内核任务试图请求一个已被持有的自旋锁,那这个任务就会一直执行
忙循环也就是自旋。
作用和特点
在短时间内进行轻量级的锁定。
忙等待,可用于中断上下文。
持有锁后不可睡眠。
使用
spinlock_t lock; //定义一个自旋锁
spin_lock_init(lock); //初始化自旋锁
spin_lock(lock); //获取自旋锁
spin_unlock(lock); //释放自旋锁
spin_trylock(lock); //试图获取自旋锁
使用自旋锁注意的问题
自旋锁实际上是忙等待,当锁不可用时,CPU一直循环执行"测试并设置"该锁直到可用而取得该锁。因此只有在占用锁的时间极短
的情况下,使用自旋锁才是合理的,当临界区很大或共享设备的时候,需要较长时间占用锁,使用自旋锁会降低系统的性能。
如果一个已经拥有某个自旋锁的CPU想第二次获得这个自旋锁的时候则造成死锁;如果一个进程获得自旋锁后再阻塞,则可能导致死锁。
其他类型的自旋锁
读写自旋锁
保留了"自旋"的模式,但在写操作方面,只能最多一个写进程,在读方面,同时可以有多个读执行单元。
rwlock_t lock = RW_LOCK_UNLOCKED;
read_lock(lock); //获取读锁
read_unlock(lock); //释放读锁
write_lock(lock); //获取写锁
write_unlock(lock); //释放写锁
顺序锁
是读写锁的一种优化,读执行单元可以在写执行单元对被顺序锁保护的共享资源进行写操作时仍然可以继续读,而不必等待写执行
单元完成写操作。
seqlock_t lock;
seqlock_init(&lock);
void write_seqlock(seqlock_t *sl);
void write_sequnlock(seqlock_t *sl);
4. 信号量
描述
信号量是一种睡眠锁,如果一个任务试图获取一个已被持有的信号量,信号量会将其推入等待队列,然后让其睡眠。这时处理器
获得自由,可以转去执行其他代码。当持有信号量的进程释放信号量后,在等待队列中的一个任务被唤醒,从而可以获得该信号量。
使用
static DECLARE_MUTEX(mr_sem); //声明并初始化互斥信号量
down(&mr_sem); //获取信号量
. .. //临界区
up(&mr_sem); //释放信号量
信号量和自旋锁