目录
自旋锁spinlock
自旋锁的特点
1.spin lock是一种死等的锁机制。当发生访问资源冲突的时候,可以有两个选择:一个是死等,一个是挂起当前进程,调度其它进程进行执行,spin lock就是一种死等的机制,当前的执行的thread会不断的重新尝试直到获取锁进入临界区
2.只允许一个thread进入,semaphore可以允许多个thread进入,spin lock不行,一次只能有一个thread获取锁并进入临界区,其他的thread都是在门口不断的尝试
3.执行时间短,由于spin lock死等这种特性,因此它在使用那些代码不是非常复杂的临界区(当然也不能太简单,否则使用原子操作或者其他适用简单场景的同步机制就可以了),如果临界区执行时间太长,那么不断在临界区门口“死等”的那些thread是多么的浪费CPU啊(当然,现代CPU的设计都会考虑同步原语的实现,例如ARM提供了WFE和SEV这样的类似指令,避免CPU进入busy loop的悲惨境地)
4.可以在中断上下文执行,由于不睡眠,英雌spin lcok可以在·中断上下文中使用
场景分析
对于spin lock,其保护的资源可能来自多个CPU CORE上的进程上下文或中断上下文的中的访问,其中,进程上下文包括:用户进程通过系统调用访问,内核线程直接访问,来自workqueue中 work function的访问(本质上也是内核线程),中断上下文包括:HW interrupt context(中断handler)、软中断上下文(soft irq,当然由于各种原因,该softirq被推迟到softirqd的内核中执行的时候就不属于这个场景了,属于进程上下文那个分类了),timer的callback函数(本质上也是softirq),tasklet(本质上也是softirq)
先看最简单的单CPU上的进程上下文的访问,如果一个全局的资源被多个进程上下文访问,这时候,内核如何交错执行呢?对于那些没有打开preempttive选项的内核,所有的系统调用都是串行化执行的,因此不存在资源争抢的问题,如果内核线程也访问这个全局资源呢?本质上内核线程也是进程,类似普通进程,只不过普通进程时而在用户态运行时而通过系统调用陷入内核执行,而内核线程永远都是在内核态运行,但是,结果都是一样的,对于non-preeemptive的linux kernel,只要在被合台,就不会发生进度调度,因此,这种场景下,共享数据根本不需要保护(没有并发,谈何保护呢)。如果时间停留在这里多好,单纯而美好,在继续前进时,让我们先享受这一刻!!!
当打开preemptive选项后,事情就变的复杂了,我们考虑下面的场景:
1.进程A在某个系统调用过程中访问了共享资源R2.进程B在某个系统调用过程中也访问了共享资源R
会不会造成冲突呢?假设在A访问共享资源R的过程中发生了中断,中断唤醒了沉睡中的,优先级更高的B,在中断返回现场的时候,发生进程切换,B启动执行,并通过系统调用访问了R,如果没有锁保护,则会出现两个thread进入临界区,导致程序执行不正确。OK,我们加上spin lock看看如何:A在进入临界区之前获取了spin lock ,同样的,在A访问共享资源的过程中发生了中断,中断唤醒了沉睡中的,优先级更高的B,B在访问临界区之前仍然会试图获取spin lock,这时候由于A进程持有spin lock而导致B进程进入了永久的spin(死锁).......怎么破?linux的内核很简单,在A进程获取spin lock的时候禁止本CPU上的抢占(上面的永久spin的场合仅仅在本CPU的进程抢占本CPU的当前进程这样的场景中发生)。如果A和B运行在不同的CPU上,那么情况会简单一些:A进程虽然持有spin lock而导致B进程进入spin状态,不过由于运行在不同的CPU上,A进程会持续执行并很快释放spin lock,解除B进程的spin状态
继续向前分析,现在要加入中断上下文这个因素,访问共享资源的thread包括
1.运行在CPU0上的进程A在某个系统调用过程中访问了共享资源R
2.运行在CPU1上的进程B在某个系统调用过程中也访问了共享资源R
3.外设P的中断handler中也会访问共享资源R
在这样的场景下,使用spin lock可以保护访问共享资源R的临界区吗?我们假设CPU0上的进程A持有spin lock进入临界区,这时候,外设P发生了中断事件,并且调度到了CPU1上执行,看起来没什么问题,执行在CPU1上的handler会稍等一会CPU0上的进程A,等它离开临界区就会释放spin lock的,但是,如果外设P的中断事件被调度到CPU0上执行会发生什么呢?CPU0上的进程A持有spin lock的状态下被中断上下文抢占,而抢占它的CPU0上的handler在进入临界区之前任然会试图获取spin lock 。悲剧发生了,CPU0上的P外设的中断handler永远的进入了spin状态,这时候,CPU1上的进程也不可避免在试图持有spin lock的时候失败进入 spin状态,为了解决这些问题,linux kernel采用了这样的方法:如果涉及到中断上下文的访问,spin lock需要和禁止本CPU上的中断联合使用
linux kernel中提供了丰富的bottom half的机制,虽然同属中断上下文,不过还是稍有不同。我们可以把上面的场景简单修改一下:外设P不是中断handler中访问共享资源R,而是在的bottom half中访问。使用spin lock+禁止本地中断当然是可以达到保护共享资源的效果,但是使用牛刀来杀鸡似乎有点小题大做,这时候disable bottom half就OK了
最后,我们讨论一下中断上下文之间的竞争。同一种中断handler之间在uni core和multi core上都不会并行执行,这是linux kernel的特性。如果不同中断handler需要使用spin lock保护共享资源,对于新的内核(不区分fast handler和slow handler),所有handler都是关闭中断的,因此使用spin lock不需要关闭中断的配合。bottom half又分成softirq和tasklet,同一种softirq会在不同的CPU上并发执行,因此如果某个驱动中的sofirq的handler中会访问某个全局变量,对该全局变量是需要使用spin lock保护的,不用配合disable CPU中断或者bottom half。tasklet更简单,因为同一种tasklet不会多个CPU上并发,具体我就不分析了,大家自行思考吧。
数据结构:
根据前面的分析,我们可以基本推断出spin lock的实现。首先定义一个spinlock_t的数据类型,其本质上是一个整数型(对该数值的操作需要保证原子性),该数值表示spin lock是否可用。初始化的时候被设定为1.当thread想要持有锁的时候调用spin_lock函数,该函数将spin lock按个证书值减去1,然后进行判断如果等于0,表示可以获取spinlock,如果时负数,则说明其他thread的持有该锁,本thread需要spin
内核中的spinlock的数据类型定义如下:
typedef struct spinlock{
struct raw_spinlock rlock;
}spinlock_t;
typedef struct raw_spinlock{
arch _spinlock_t raw_lock;
}raw_spinlock_t;
由于各种原因(各种锁的debug、锁的validate机制,多平台支持什么的),spinlock_t的定义没有那么直观,为了让事情简单一些,我们去掉那些繁琐的成员。struct spinlock中定义了一个struct raw_spinlock的成员,为何会如此呢?好吧,我们又需要回到kernel历史课本中去了。在旧的内核中(比如我熟悉的linux 2.6.23内核),spin lock的命令规则是这样:
通用(适用于各种arch)的spin lock使用spinlock_t这样的type name,各种arch定义自己的struct raw_spinlock。听起来不错的主意和命名方式,直到linux realtime tree(PREEMPT_RT)提出对spinlock的挑战。real time linux是一个试图将linux kernel增加硬实时性能的一个分支(你知道的,linux kernel mainline只是支持soft realtime),多年来,很多来自realtime branch的特性被merge到了mainline上,例如:高精度timer、中断线程化等等。realtime tree希望可以对现存的spinlock进行分类:一种是在realtime kernel中可以睡眠的spinlock,另外一种就是在任何情况下都不可以睡眠的spinlock。分类很清楚但是如何起名字?起名字绝对是个技术活,起得好了事半功倍,可维护性好,什么文档啊、注释啊都素那浮云,阅读代码就是享受,如沐春风。起得不好,注定被后人唾弃,或者拖出来吊打(这让我想起给我儿子起名字的那段不堪回首的岁月……)。最终mspin lock的命名规范定义如下:
(1)spinlock,在rt linux(配置了PREEMPT_RT)的时候可能会被抢占(实际底层可能是使用支持PI(优先级翻转)的mutext)。
(2)raw_spinlock,即便是配置了PREEMPT_RT也要顽强的spin
(3)arch_spinlock,spin lock是和architecture相关的,arch_spinlock是architecture相关的实现
对于UP平台,所有的arch_spinlock_t都是一样的,定义如下:
typedef struct { } arch_spinlock_t;
什么都没有,一切都是空啊。当然,这也符合前面的分析,对于UP,即便是打开的preempt选项,所谓的spin lock也不过就是disable preempt而已,不需定义什么spin lock的变量。
对于SMP平台,这和arch相关,我们在下一节描述。
Spinlock是内核中提供的一种比较常见的锁机制, 自旋锁是”原地等待“的方式解决资源冲突的,即,一个线程获取了一个自旋锁后,另外一个线程期望获取该自旋锁,获取不到,只能原地”打转“(忙等待)。由于自旋锁的这个忙等待的特性,注定了他使用场景上的限制-----自旋锁不应该被长时间的持有(消耗CPU资源)
自旋锁的使用
在Linu内核的实现中,经常会遇到这样的场景:共享数据被中断上下文访问,该如何保护呢?如果只有进程上下文的访问,那么可以考虑使用semaphore或者mutex的锁机制,但是现在中断上下文也掺和进来没那么可以导致睡眠的lock就不能使用了,这时候可以考虑使用spinlock
换而言之:中断上下文要用锁,首选spinlock
使用自旋锁 ,有两种方式定义一个锁:
动态的:
spinlock_t lock
spin_lock_init(&lock)
静态的:
DEFINE_SPINLOCK(lock)
自旋锁的死锁和解决
自旋锁不可递归,自己等待自己已经获取的锁,会导致死锁
自旋锁可以在中断上下文中使用,但是试想一个场景:一个线程获取了一个锁,但是被中断处理程序打断,中断处理程序也获取了这个锁(但是之前已经被锁住了,无法获取到们只能自旋),中断无法退出,导致线程中后面释放锁的代码无法被执行,导致死锁(如果确定中断中不会访问和线程中同一个锁,其实无所谓)
在单CPU中:在使用spin_lock时,内容主要就是禁止抢占(preempt_disable)和关中断
在多CPU中:使用原子变量就可以实现,但是还要保证公平,先到先得
函数名 | 作用 |
spin_lock_init(_lock) | 初始刷自旋锁为unlock状态 |
void spin_lock(spinlock_t *lock) | 获取自旋锁(加锁),返回后肯定获得了锁 |
int spin_trylock(spinlock_t *lock) | 尝试获得自旋锁,成功获得锁返回1,否则返回0 |
void spin_unlock(spinlock_t *lock) | 释放自旋锁,或称解锁 |
int spin_is_locked(spinlock_t *lock) | 返回自旋锁的状态,已加锁返回1,否则返回0 |
自旋锁,顾名思义:自己在原地打转,等待资源可用,一旦可用就上锁霸占它
问题来了,假设别人已经上锁了,你原地打转会占用CPU资源了,别的程序怎么运行?它没有CPU怎么解锁?
这个问题有两个答案:
1.原地打转的是CPUx,以后CPUy会解锁:这涉及多个CPU,适用于SMP系统
2.对于单CPU系统,自旋锁i的“自旋”功能就去掉了:只剩下禁止抢占,禁止中断
我先禁止别的线程来打断我(preempt_isable),我慢慢享用临界资源,用完再使能系统抢占(preemt_enable),这样别人就可以来抢资源了
要理解spinlock,要通过两个情景来分析:
1.一开始,怎么争抢资源?不能两个程序都抢到
这个挺好解决的,直接使用原子变量就可以实现
2.某个程序已经获得资源,怎么防止别人来同时使用这个资源
这是使用spinlock是要注意的地方,对应会有不同的衍生函数(_bh/_irq/_irqsave/_restore)
自旋锁的内核结构体
spinlock对应的结构体如下定义,不同的架构可能有不同的实现
上述_raw_tickets结构体中有owner,next两个成员,这是在SMP系统中实现spinlock的关键
spinlock在UP系统中的实现
对于自旋锁,它的本意是:如果还没获得锁,我就原地打转,等待谁释放锁?
1.其他CPU
2.其他线程/进程
对于但CPU系统,没有“其他CPU” ,如果内核不支持preempt,当前在内核态执行的线程也不可能别其他线程抢占,也就没有其他线程/进程。所以,遂于不支持preempt的单CPU系统,spin_lock是空函数,不需要做其他事情
如果单CPU系统的内核支持preempt,即当前线程正在执行内核态函数是,它是有可能被别的线程抢占的,这是spin_lock的实现就是调用“preempt_disable()”:你想抢占我,我干脆禁止你运行
在UP系统中,spin_lock函数定义如下:
spinlock在SMP系统中的实现
要让多CPU中只能由一个获得临界资源,使用原子变量就可以实现,但是要保证公平,先到先得。比如有CPU0,CPU1,CPU2都调用spin_lock想获得临界资源,谁先申请到谁先获得
信号量semaphore
注意这里是信号量,不是信号。在前面的学习异步通知时,驱动程序给应用程序发送信号,现在我们讲的信号量是一种同步,互斥机制
信号量的定义及操作函数都在Linux内核文件include\linux\semaphore.h中定义,如下:
struct semaphore{
raw_spinlock_t lock;//信号量的实现需要借助spinlock
unsigned int count;//允许多少人使用
struct list_head wait_list;//等待信号量的线程放在这里
}
初始化semaphore之后,就可以使用down函数或者其他衍生版本来获取信号量,使用up函数释放信号量,我们只分析down,up函数的实现down函数的实现
如果semaphore中的count大于0,那么down函数就可以获得信号量,否则就休眠,在读取、修改count时,要使用spinlock来实现互斥
等待是时,要把当前进程放在semaphore的wait_list链表中,别的进程释放信号量时去wait_list中把进程取出,唤醒
代码如下
up函数的实现
如果由其他进程在等待信号量,则cpunt只无需调整,直接取出第一个等待信号量的进程,把信号量给它,并把它唤醒
如果没有其他进程在等待信号量则调整count
整个过程需要使用spinlock来保护,代码如下:
互斥量mutex
mutex的定义及操作函数都在Linux内核文件include\linux\mutex.h中定义
初始化mutex之后,就可以使用mutex_lock函数或者其他衍生版本来获取信号量,使用mutex_unlock函数来释放信号量,我们只分析mutex_lock\mutex_unlock函数的实现
这里要勘误一下:前面所讲的mutex中的owner是用来记录获得mutex的进程,以后必须由它来释放mutex。这是错的
从上面的代码可以看出,owner并不一定存在。
owner由两个用途:debug(CONFIG_DEBUG_MUTEXES)或spin_on_owner(CONFIG_MUTEX_SPIN_ON_OWNER)。
什么叫spin on owner呢?
我们使用mutex的目的一般是用来保护一小段代码,这段代码运行的事件很快,这意味着一个获得mutex的进程可能很快就会释放掉mutex。
针对这点可以进行优化,特别时当前获得mutex的进程是在别的CPU上运行,并且“我”是唯一等待这个mutex的进程。在这种情况下。那'我'就原地spin等待吧:懒得去休眠了,休眠又唤醒就太慢了
所以,mutex是做了特殊的优化,比semaphore效率更高。但是在代码上,并没有要求“谁获得mutex,就必须由谁释放mutex”,只是在使用惯例上是“谁获得mutex,就必须由谁释放mutex”
mutex_lock函数的实现
1.fastpath
mutex的涉及非常精巧,比semaphore复杂,但是更加高效
首先要直到mutex的操作函数中由fastpath,slowpath两条路径(快速,慢速):如果fastpath成功就不必使用slowpath.
怎么解释?
这需要把mutex中的count值再拓展一下,之前它只有1、0两个取值,1表示unlocked,0表示locked,还有一类值“负数”表示"locked,并且可能有其他程序在等待"
代码如下:
先看看fastpath的函数:__mutex_fastpath_lock,这个函数在下面两个文件中都有定义:
include/asm-generic/mutex-xchg.h
include/asm-generic/mutex-dec.h
使用哪一个文件呢?看看arch/arm/include/asm/mutex.h,内容如下:
#if __LINUX_ARM_ARCH__ < 6
#include <asm-generic/mutex-xchg.h>
#else
#include <asm-generic/mutex-dec.h>
#endif
所以,对于ARMv6一下的架构,使用include/asm-generic/mutex-xchg.h中的__mutex_fastpath_lock函数;对于ARMv6及以上的架构,使用include /asm-generic/mutex-dec.h中的__mutex_fastpath_lock函数。这两个文件中的__,utex_fastpath_lock函数是类似的,mutex-dec.h中的代码如下:
大部分情况下,mutex当前值都是1,所以通过fastpath函数可以非常快速的获得mutex
2.slowpath
如果mutex当前值为0或者是负数,则需要调用__mutex_lock_slowpath慢慢处理:可能会休眠等待。
__mutex_lock_common函数也是在内核文件kernel/locking/mutex.h中实现的,下面分段讲解:
1.第一段代码:
2.分析第二段代码:
3.分析第三段代码:
这个wait_list是FIFO(First In Firs Out),谁先排队,谁就可以先得到mutex
4.分析第四段代码:for循环,这是重点
5.分析第五段代码:收尾工作
mutex_unlock函数的实现
mutex_unlock函数中也有fastpath,slowpath两条路径可以选(快速、慢速):如果fastpath成功,就不必使用slowpath:
代码如下:
1.fastpath
先看看fastpath的函数:__mutex_fastpath_lock,这个函数在下面两个文件中都有定义:
include/asm-generic/mutex-xchg.h
include/asm-generic/mutex-dec.h
使用哪一个文件呢?看看arch/arm/include/asm/mutex.h,内容取下:
#include __LINUX_ARM_ARCH__ < 6
#include <asm-generic/mutex-xchg.h>
#else
#include <asm-generic/mutex-dec.h>
#endif
所以,对于ARMv6一下的架构,使用include /asm-generic/mutex-xchg.h中的__mutex_fastpath_lock函数,对于ARMv6及以上的架构,使用include/asm-generic/mutex-dec.h中的__mutex_fastpath_lock函数。这两个文件中的__mutex_fastpath_lock函数是类似的,mutex-dec.h中的代码如下:
大部分情况下,加一后mutex的值都是1,表示无人等待mutex。所以通过fastpath函数直接增加mutex的count值为1就可以了
如果,utex的值加一后还是小于等于0美酒表示有人在等待mutex,需要去wait_list把他取出唤醒,这需要用到slowpath的函数:__mutex_unlock_slowpath.
2.slowpath
如果mutex的当前值为0或者是负数,则需要调用__mutex_unlock_slowpath慢慢处理:需要唤醒其他进程。
__mutex_unlock_cpmmon_slowpath函数代码如下,主要工作就是从wait_list中取出并唤醒第一个进程: