2015年7月18日星期六

一个Linux内核的自旋锁设计-接力嵌套堆栈式自旋锁

本邮件内容由第三方提供,如果您不想继续收到该邮件,可 点此退订
一个Linux内核的自旋锁设计-接力嵌套堆栈式自旋锁  阅读原文»

锁的开销是巨大的,特别是对于多核多处理来讲。
引入多处理,本身就是为了将并行化处理以提高性能,然而由于存在共享临界区,而这个临界区同时只能有一个线程访问(特别是对于写操作),那么本来并行的执 行流在这里被串行化了,形象地看,这里好像是宽阔马路上的一个瓶颈,由于串行化是本质上存在的,因此该瓶颈就是不可消除的。问题是线程执行流如何度过这个 瓶颈,很显然,它们谁都绕不开,现在问题是是它们到达这个瓶颈时该怎么办。
很显然,斗殴抢先是一种不合理但实用的简单方案,朴素的自旋锁就是这么设计的。然而,更加绅士的做法就是,既然暂时得不到通行权,那么自己先让出道 路,sleep一会儿,这种方式就是sleep-wait方式,与之对应的就是持续spin-wait方式。问题是线程本身如何对这二者做出明智的选择。 事实上,这种选择权交给了程序,而不是执行中的线程。
不要试图比较sleep-wait和spin-wait的性能,它们都是损耗了性能-因为串行化。其中,sleep-wait的开销在切换(涉及到进程, 线程的切换比较,比如寄存器上下文,堆栈,某些处理器上cache刷新,MMU tlb刷新等),而spin-wait的开销很显然,就是持续浪费CPU周期,虽然没有切换的开销,但事实上,它这段时间什么也做不了。
为什么要引入spin-wait这种方式呢?因为如果始终是sleep-wait,那么sleep线程A切换到别的线程B后,锁被释放了,系统不一定能保 证sleep的那个线程可以获得CPU从而切回来,即便如此,付出巨大的切换代价,有时间隔很短,线程B并没有由于线程A的绅士行为获得收益,这种姿态显 然是不值得的,还不如保留本质,持续斗殴。
和中断相比,锁的开销更加巨大,如果可以通过关中断的方式换取无锁操作,那么这是值得的,但是(最讨厌且永恒的"但是"),你要考虑关中断的副作用,比如增加了处理延迟,然后你必须拿这种新的代价和锁的开销进行对比,权衡这种交易是否值得。
要开始本文了,为了简便起见,我不会给出太多的和处理器相关的细节,而是集中针对自旋锁本身。这些细节比如CPU cache机制,缓存一致性协议,内存屏障,Intel的pause指令,流水线,总线锁等,还好,这些概念都是可以很方便baidu出来的,不用去墙 外。

Linux内核一开始就引入了自旋锁,所谓的自旋锁在等待锁过程中的行为就是原地打转,有人会觉得这浪费了CPU周期,但是你要明白,系统设计就是一场博弈,虽然不是零和,但是也要尽力寻找折中点,然而却没有两全其美的方案。
如果不原地打转,又该如何呢?很显然就是切换到别的线程,等待持锁者释放锁的时候,再将它唤醒,然而这里又有两次斗殴,首先,如果有多方都竞争一个锁,那 么全部将它们唤醒,提供一个斗殴场地,等待一个胜出吗?退而求其次,即便仅仅唤醒首先排队的那个线程,task切换的开销算进去了吗?所以说,如果原地打 转浪费的CPU周期小于两次切换开销浪费的CPU周期,那么原地打转就是合理的,这么说来,原地打转的时间越短越好。
就是如此。自旋锁的应用场合就是短时间持有临界区的场合,它是一种短期锁,如果占据临界区过久,随着原地打转浪费的CPU周期的增加,其开销将逐渐大于两 次切换(切换开销是固定的-不考虑cache等),因此,理论上,能算出自旋锁在持锁期间可以执行多少代码。爆炸!

Linux自旋锁历史概览

Linux 自旋锁发展了两代,第一代自旋锁是一种完全斗殴模式的无序自旋锁,也就是说,如果多个CPU同时争抢一个自旋锁,那么待持锁者解锁的时候,理论上它们获得 锁的机会是不固定的,和cache等一系列因素相关,这就造成了不公平,第一个开始争抢的CPU不一定第一个获得...这就需要引入一个秩序,于是第二代 Ticket自旋锁就设计出来了。
Ticket自旋锁的设计非常巧妙,它将一个CPU变量,比如32位的值分为高16位和低16位,每次lock的时候,CPU将高16位原子加上 0x01(通过锁总线),然后将该值和低16位比较,如果相等则成功,如果不等则自旋的同时持续比较这两个值,而unlock操作则是简单递增锁的低16 位加0x01(理论上不需要锁总线,因为不会有两个及以上的CPU同时拥有锁进行unlock操作,但是还是要考虑CPU的特性...)。这就是所谓的 Ticket自旋锁的全部。
最近遇到了锁优化的问题,众所周知,锁优化是一个很精细的活儿,它既不能太复杂也不能太简单,特别是自旋锁的设计,更是如此。然而自旋锁设计的优势也很明显,那就是它让你少考虑很多问题:
1.一个CPU同时只能在一个自旋锁上自旋;
2.一旦开始自旋,直到获得锁,中间不能放弃退出自旋。
自 旋锁的应用场合必须要明了,它不适合保护很大的临界区,因为这将导致自旋过久,它也不适合大量CPU的情况,因为它会导致自旋延时的N倍,虽然一段临界区 很小,但是一个CPU自旋的时间可能是N倍,N为CPU的数量。这就引出了一个争论,Ticket排队自旋锁真的比斗殴型哄抢自旋锁好吗?如果不考虑 cache亲和,斗殴型的自旋锁可以将每个CPU自旋的时间收敛到平均时间,而Ticket自旋锁将出现两极分化,也就是说,最长自旋时间和最短自旋时间 是一个固定的倍数关系,随着CPU数量的增加,排队公平导致的不合理性将加大,你要知道,任何情况下队列都不可能超过一个临界值,超过了不满情绪将会增 加,虽然这种长延时只是因为你来得晚导致的,看似很公平,实际上并不公平,因为并不清楚你来得晚的原因。
目前没有一种好的方案解决这个问题,一般情况,如果队列过长,工作人员会建议你一会儿再来看看,或者贴上大致的预估时间,你自己来绝对是排队还是放弃。

try lock返回的预估信息

我 建议在自旋锁加锁前,先try lock一次,正如现如今的实现一样,然而这个try并没有给出除了成功或者失败之外的信息,事实上更好的方式是,try,并且try返回一些有用的信 息,比如等待预估时间,队长等统计信息,以供调用者决定是就地自旋还是干点别的。谁说内核中不能做大数据分析,很多统计信息和建议都可以通过数据分析得 到。
此外,对于该统计信息,可以对spin操作本身进行优化,就是说,内部的pause指令可以进行优化,这对流水线是有益的。

为 什么我要设计一个新的自旋锁,一方面是我觉得Ticket自旋锁多个CPU虽然靠递增高位实现了排队,但是它们同时不断地检测自旋锁本身的值,cache 有点浪费,所有的CPU cache上均会出现完全一样的自旋锁,并且一旦锁被unlock,还会触发cache一致性协议行为,另一方面,我觉得用一个32位(或者随便其它什么 CPU内部类型)分为高低两部分来实现自旋锁虽然巧妙但是又过于简单,第三方面,我前些日子特别推崇小巧结构体实现的IP转发表,如今又重温更加小巧的 Ticket自旋锁,心里总是有些嫉妒,所以怎么也得按照我的思路来一个"符合我的观念的"设计吧,事情就是这么开始的。

在per CPU结构体上自旋

如何解决线程间同步问题,这个问题确实不好解决,但是自己管自己,也就是操作本地数据,总是一个合理的思路。
为什么要在自旋锁本身上集体自旋呢?如果有500多个CPU,大家一起探测同一个内存地址,然后持锁者释放锁,修改了该内存地址的CPU cache值,这将导致大量的cache一致性动作...为何不在自己的本地变量上自旋呢?如果持锁者释放锁,那么就将下一个等待者的本地变量置0,这意 味着CPU只需要拿本地变量和0比较即可。
因此就需要有一个本地的地方保存这个变量,我为每一个CPU申请了一个per CPU变量,内部保存一个栈,用于实现严格的"后加锁先解锁"顺序的自旋锁,当然这样对调用者有要求,或者说把栈改成一个空闲队列,用于实现任意顺序加锁 /解锁得自旋锁,这个对调用者没有要求,但是可能会引起死锁。
为了实现多个CPU同时争抢自旋锁的优雅排队,势必要维护一个队列,单向推进的即可,没有必要用list_head结构体。排入队中的元素就是栈帧,而栈帧是per CPU的。操作栈帧只有三个机会:
自己加锁时:加锁时需要用自己的栈帧排队,不可避免要操作链表,而可能同时有多个CPU的栈帧要排队,因此需要保证整个排队动作的单向链表操作是原子的,锁总线是一个办法。
自己自旋时:这个时段不涉及别的CPU,甚至自己的栈帧都不会到达别的CPU的cache中,栈帧是CPU本地变量。
排在前面的栈帧解锁时:这 个时候理论上也和其它CPU无关,因为队列是严格顺序的,取下一个即可,无需争抢,但是有一种竞争情况,即开始取下一个栈帧时,队列已经没有下一个,然而 按照空队列处理时,却有了下一个栈帧,这将使得刚刚排入的新栈帧永远无法获得锁,因此这个动作必须是原子的。至于说取到下一个排队栈帧,设置它时,就不用 保证原子了,因为它就是它后面的它,一个栈帧不会排到两个队列,且排入了就不能放弃,别人也不会动它的。

下面用一个图示展示一下我的这个排队自旋锁的设计吧
wKiom1WoNiOQuhbzAAUslMkjCNA607.jpg

看起来有点复杂了,那么性能一定不高,其实确实比Ticket自旋锁复杂,但是你能找到比Ticket自旋锁更简单优雅的设计吗?
我不图更简单,但图够优雅。虽然我的一个原子操作序列比Ticket自旋锁的单纯加1操作复杂了很多,涉及到很多链表操作,但是我的局部性利用会更好,特 别是采用了per CPU机制,在集体自旋的时间段,CPU cache数据同步效率会更高,你要知道,自旋的时间和锁总线的时间相比,那可久多了。采用数组实现的堆栈(即便是空闲链表也一样),数据的局部性利用效 果会更好,也就说,一个CPU的所有自旋锁的自旋体均在这个数组中,这个数组有多大,取决于系统同时持有的自旋锁有多少。

以下是根据上述的图示写出的测试代码,代码未经优化,只是能跑。在用户空间做过测试。

  #define NULL    0  /* 总线锁定的开始与结束 */  #define LOCK_BUS_START  #define LOCK_BUS_END  /* pause指令对性能非常重要,具体可参阅Intel指令手册,     然而它并不是有优化作用,而是减轻了恶化效果  */  #define cpu_relax()  /* 内存屏障对于多核心特别重要 */  #define barrier()  #define MAX_NEST    8  /*   *  定义per CPU自旋栈的栈帧   * */  struct per_cpu_spin_entry {      struct per_cpu_spin_entry *next_addr;      char status;  };  /*   *  定义自旋锁的per CPU自旋栈   * */  struct per_cpu_stack {      /* per CPU自旋栈 */      struct per_cpu_spin_entry stack[MAX_NEST];      /* 为了清晰,将CPU ID和栈顶ID独立为char型(仅支持256个CPU) */      char top;      char cpuid;  };  /*   *  定义自旋锁   * */  typedef struct {      /* 为了代码清晰,减少bit操作,独立使用一个8位类型 */      char    lock;      /* 指向的下一个要转交给的per CPU栈帧 */      struct per_cpu_spin_entry *next;      /* 指向排队栈帧的最后一个per CPU栈帧 */      struct per_cpu_spin_entr
Windows Azure 配置实现虚拟机外网IP绑定(云服务)  阅读原文»

Windows Azure 配置实现虚拟机外网IP绑定(云服务)

Windows Azure 配置实现虚拟机外网IP绑定(云服务)

我们上一篇介绍了如何对已存在的虚拟网络的外网IP做绑定,今天主要介绍配置实现虚拟机外网绑定,两者区别在于一个已存在,一个不存在,如果不存在的话,我们需要新建一个云服务,同时对新建的云服务做标记,标记后,可创建对应得虚拟机来完成外网IP绑定,具体见下:首先注意配置保留虚拟机外网IP需要注意以下事项

一、 操作前的注意事项:

1. 如果虚拟机要使用虚拟网络,只能在Regional Vnet中使用ReservedIP,已经有部署的基于地缘组的虚拟网络无法直接转换为Regional Vnet

2. 这个操作目前只能由PowerShell完成

3. 必须先建立一个ReservedIP,再创建虚拟机

4. 目前ReservedIP可以应用到挂载在区域虚拟网络下的虚拟机,为其保留公网IP

5. ReservedIP是计费的

关闭虚拟机不会释放掉保留的RIP,但是当云服务中所有虚拟机都删除后,RIP会被释放,但并不会被从订阅中删除,您可以用来绑定到其他云服务中的虚拟机。

二、 创建Regional Vnet:

在门户的虚拟网络中创建新的虚拟网络,由于目前默认创建为"区域虚拟网络",所以直接在虚拟网络中创建即为区域虚拟网络。

三、下载并导入Publishsettings File

四、 创建ReservedIP

五、 在ReservedIP中创建新的虚拟机:

5.1指定当前的SubID和StorageAccount

   Set-AzureSubscription -SubscriptionName "XXXX" -CurrentStorageAccountName "  

标黄处分别为SubID的名称,即是之前在Publishsettings里中修改的Name字段

后面的是存储账号的名称,如果对应的区域无存储账号,则需要新建一个

5.2列出当前的Image列表之后,列出当前的Image列表:

输入:

  (Get-AzureVMImage)|Format-Table -Property imagename,PublishedDate AutoSize  

该操作会列出当前所有映像的名称和发布日期。默认情况下请选择时间最新的版本。请先复制imagename全文备用

六、 验证ReservedIP是否生效:

七、 删除ReservedIP:

由于ReservedIP是收费的,如果不想使用了,可以删除。 使用命令:Remove-AzureReservedIP -ReservedIPName "frankVIP"

具体操作见习:

首先是下载及安装powershell

我们都知道windows azure的相关操作都是依赖于windows azure for powershell来做管理的,所以我们需要安装azure powershell

通过以下方法就可以下载windowsazure订阅

https://manage.windowsauzre.cn/publishsettings

image

然后下载并且安装windowsazure powershell,然后在windowsazure powershell下导入该订阅文件;

访问www.windowsazure.cn ---> 文档和资源---> azure 命令行接口---->windows安装就会提示下载windowsazure powershell

我们同样可以查看上面的windows powershell的相关文档

http://www.windowsazure.cn/documentation/articles/install-configure-powershell/

image

Azure powershell 下载链接,下载的时候选择0.9.3版本

https://github.com/Azure/azure-powershell/releases

image

具体方法可以浏览网页内部

下载后就是安装,根据提示安装完powershell即可;我们可以通过

  (get-module azure).version  

查单当前使用的azure powershell版本

注:执行以下命令需要使用版本为0.9.3的azure powershell,不能用0.9.4的azure powershell,如果安装了最新的卸载 后安装就版本

image

image

  Import-AzurePublishSettingsFile 导入 .publishsettings 文件以供模块使用  

image

因为我已经导入过了,所以再次导入会提示以下信息

image

如果在windowsazure powershell中导入多个订阅文件的话,我们需要选择默认的

我们首先通过get-azuresubscrpit 查看当前powershell下已导入的订阅文件

image

我们发现有两个,所以我们需要通过以下命令来设置默认的即可

image

  Select-azuresubscript -subsciptionName "xxxxx" -default  

然后我们可以

Get-azurevm 查看当前订阅性的所有vm信息

"ServiceName" 为云服务名字,"name"为虚机名字

image

我们首先需要创建一个Reservedip( 无法具体指定某个 IP ,此操作 AzureAzure Azure平台会为客户随机指定一个 平台会为客户随机指定一个 VIP) VIP) :

  $ReservedIP = New-AzureReservedIP -ReservedIPName "RVIP" -Label "testvip" -Location "China North"  

-ReservedIPName的命名可以任意定义;在此我定义为:RVIP

-Label 为描述 在此

阅读更多内容

没有评论:

发表评论