Linux内核解析-内核同步方法

临界区和竞争条件

临界区:访问和操作共享数据的代码段。
竞争条件:两个或多个执行线程处于同一个临界区中同时执行,那么会存在线程竞争,即竞争条件。

所谓同步,其实防止在临界区中形成竞争条件。

如果临界区里是原子操作(即整个操作完成前不会被打断),那么自然就不会出竞争条件。

但在实际应用中,临界区中的代码往往不会那么简单,所以为了保持同步,引入了锁机制。

保护机制
原子操作

对单个变量保护一般是使用原子操作。

原子操作是指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何上下文切换,原子操作通常是内联函数,往往通过内嵌汇编实现。多数处理器都提供了指令来原子地读变量、增加变量,然后再写回变量。两个原子操作交错执行根本就不可能发生,因为处理器会从物理上保证这种不可能。

内核提供了两组原子操作接口:①对整数操作 ②对位进行操作

  1. 原子整数操作,有32位和64位。头文件分别为<asm/atomic.h><asm/atomic64.h>

  2. 原子位操作。头文件 <asm/bitops.h>

原子操作头文件与具体的体系结构有关,比如x86架构的相关头文件在 arch/x86/include/asm/*.h

加锁

原子操作只适合单个变量或比较简单的操作,若需要保护的操作是一段比较复杂的程序,那就需要用到加锁机制了。
可以使用锁对临界区标记,当一个线程已经获取锁后,其它线程将被禁止访问。

Linux中的锁有多种多样的形式,加锁的粒度范围也各不相同,各种锁机制区别主要在于:当锁已经被其他线程持有,因为不可用时的行为表现(一般都是等待)。

锁是采用原子操作实现的,所以加锁解锁过程中不存在竞争。

自旋锁

自旋锁是Linux最常见的锁,自旋锁有如下特点:

  1. 自旋锁最多只能被一个可执行线程持有。
  2. 线程试图获取一个已被持有的自旋锁,那么会一直处于等待(自旋)。
  3. 由于2的特点,所以自旋锁不应该被长时间持有,持有自旋锁的时间最好小于完成两次上下文切换的耗时。
  4. 自旋锁是不可递归的,递归的请求同一个自旋锁会自己锁死自己。
  5. 线程获取自旋锁之前,要禁止当前处理器上的中断。(防止获取锁的线程和中断形成竞争条件)。

自旋锁和下半部

由于下半部可以抢占进程上下文的代码,所以当下半部和进程上下文共享数据时,必须对进程上下文中的共享数据进行保护,即需要在加锁的同时禁止下半部的执行。
中断处理程序也会抢占下半部,所以下半部和中断处理程序有共享数据时,就必须在获取恰当的锁的同时还要禁止中断。
同一种tasklet不能同时运行,所以同类tasklet中的共享数据不需要保护。
不同类tasklet中共享数据时,其中一个tasklet获得锁后,不用禁止其他tasklet的执行,因为同一个处理器上不会有tasklet相互抢占的情况
同类型或者非同类型的软中断在共享数据时,也不用禁止下半部,因为同一个处理器上不会有软中断互相抢占的情况

自旋锁方法列表如下:

spin_lock() 获取指定的自旋锁
spin_lock_irq() 禁止本地中断并获取指定的锁
spin_lock_irqsave() 保存本地中断的当前状态,禁止本地中断,并获取指定的锁
spin_unlock() 释放指定的锁
spin_unlock_irq() 释放指定的锁,并激活本地中断
spin_unlock_irqstore() 释放指定的锁,并让本地中断恢复到以前状态
spin_lock_init() 动态初始化指定的spinlock_t
spin_trylock() 试图获取指定的锁,如果未获取,则返回0
spin_is_locked() 如果指定的锁当前正在被获取,则返回非0,否则返回0

读写自旋锁

读写自旋锁除了和普通自旋锁一样有自旋特性以外,还有以下特点:

读锁之间是共享的
即一个线程持有了读锁之后,其他线程也可以以读的方式持有这个锁
写锁之间是互斥的
即一个线程持有了写锁之后,其他线程不能以读或者写的方式持有这个锁
读写锁之间是互斥的
即一个线程持有了读锁之后,其他线程不能以写的方式持有这个锁

信号量

Linux中的信号量是一种睡眠锁,当一个线程试图获取一个不可用的信号量时,信号量会将线程推进一个等待队列,然后让其睡眠,直到信号量释放时,线程才会被唤醒。

信号量有如下特点:

由于线程获取已被获取的信号量会导致睡眠,不会占用CPU时间,所以信号量可以用于长时间锁的情况。
相反,短时间锁信号量就不太合适了,因为睡眠、维护等待队列以及唤醒需要花费一些开销。
由于信号量可能会导致线程睡眠,所以信号量只能在进程上下文中使用,中断上下文无法使用,因为中断上下文是不能进行调度的。
占用信号量的同时不能占用自旋锁。因为等待信号量时可能会导致睡眠,而持有自旋锁时不允许睡眠的。

二值信号量表示信号量只有2个值,即0和1。信号量为1时,表示临界区可用,信号量为0时,表示临界区不可访问。

二值信号量表面看和自旋锁很相似,区别在于争用自旋锁的线程会一直循环尝试获取自旋锁,

而争用信号量的线程在信号量为0时,会进入睡眠,信号量可用时再被唤醒。

计数信号量有个计数值,比如计数值为5,表示同时可以有5个线程访问临界区。

读写信号量

读写信号量和信号量之间的关系 与 读写自旋锁和普通自旋锁之间的关系 差不多。
读写信号量都是二值信号量,即计数值最大为1,增加读者时,计数器不变,增加写者,计数器才减一。
也就是说读写信号量保护的临界区,最多只有一个写者,但可以有多个读者。

读写信号量的相关内容参见:<asm/rwsem.h> 具体实现与硬件体系结构有关。

互斥体

互斥体(mutex)是一种可以睡眠的锁,相当于二值信号量,但是比信号量简单些。

互斥体特点:

任何时刻中只有一个任务可以持有mutex,即mutex的使用计数永远是1.
给mutex上锁者必须负责给其再解锁
递归地上锁和解锁是不允许的。
当持有一个mutex时,进程不可以退出。
mutex不能在中断或者下半部中使用,只能在进程上下文中使用。
mutex只能通过官网API管理,不能自己实现代码操作。

以上各种锁的关系如下:

需求建议的加锁方法
低开销加锁优先使用自旋锁
短期锁定优先使用自旋锁
长期加锁优先使用互斥体
中断上下文中加锁使用自旋锁
持有锁需要睡眠使用互斥体
并发执行的原因

内核中造成并发的原因:

中断 中断随时会发生,也就会随时打断当前执行的代码。如果中断和被打断的代码在相同的临界区,就产生了竞争条件
软中断和tasklet 软中断和tasklet也会随时被内核唤醒执行,也会像中断一样打断正在执行的代码
内核抢占 内核具有抢占性,发生抢占时,如果抢占的线程和被抢占的线程在相同的临界区,就产生了竞争条件
睡眠及用户空间的同步 用户进程睡眠后,调度程序会唤醒一个新的用户进程,新的用户进程和睡眠的进程可能在同一个临界区中
对称多处理 2个或多个处理器可以同时执行相同的代码

我们加锁的对象并不是程序,而是数据,辨认出真正需要共享的数据和相应的临界区,才是真正有挑战的地方。
所以在开始写代码之前就需要考虑加入锁,而不是事后才想到,切勿亡羊补牢。

什么数据需要保护?

如果有其他执行线程可以访问这些数据,那么就给这些数据加上某些形式的锁:如果任何其他什么东西都能看到它,那么就要锁住它。

中断安全代码:在中断处理程序中能避免并发访问的安全代码。
抢占安全代码:在内核抢占是能避免并发访问的安全代码。
SMP安全代码:在对称多处理器的机器中能避免并发访问的安全代码。

死锁

死锁:一个或多个执行线程和一个或多个资源,每个线程都在等待其中一个资源,但所有资源都已经被占用了。所有线程都在相互等待,但它们永远不会释放已经占有的资源。于是任何线程无法继续,这便意味着死锁的发生。

条件:①互斥 ②不可抢占 ③持有并等待 ④循环等待

例子:
最简单的例子就是自死锁:一个执行线程试图去获得一个自己已经持有的锁,则它将会进入死锁状态。

ABBA死锁:A加锁后调用B,B里面再次加锁,则导致死锁
如:

  mutex;  //代表一个全局互斥对象
  void A()
   {
     mutex.lock();
     //这里操作共享数据
     B(); //这里调用B方法
     mutex.unlock();
     return;
   }
 
  void B()
   {
     mutex.lock();
     //这里操作共享数据
     mutex.unlock();
     return;
   }

只调用了lock而没有调用unlock就return也会导致死锁。

static int A(void)
{
	pthread_mutex_lock(&m);
	printf("A:\n");
	usleep(100);
	return 1;
	pthread_mutex_unlock(&m);
}

循环申请锁导致死锁

死锁预防:

按顺序加锁,如线程1以cat、dog,然后是fox的顺序获得了锁,那么其他任何线程都必须以同样的顺序来获得这些锁,也是cat、dog和fox的顺序,若先获取fox然后再dog,就有发生死锁的可能。
防止发生饥饿
不要重复请求同一个锁
设计应力求简单

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值