Linux自旋锁(1)

Linux自旋锁

前言

  • 当两个进程访问了一个共享的数据结构,并且其中一个进程会更新共享的数据结构时,那么就需要对这个共享的数据结构加锁;
  • 要注意的是,加锁的是共享数据,而不是某一个过程,所以在确定加锁的位置时要注意尽可能减少加锁区域;

自旋锁的实现

自旋锁有两个基础操作:acquire和release,acquire用来获得锁,其中含有一个死循环,不断判断锁对象的locked字段是否为0,为0表明没有持锁者,当前的acquire的调用可以获得锁,然后将锁对象的locked字段置为1来获取锁,最后返回;release用来释放锁,将locked字段置为0;

acquire(struct lock *l)
{
	while (1) {
		if (l->locked == 0) { // A
			l->locked = 1;// B 
			return;
		}
	}
}

因为同一时间,等锁的cpu都有可能判断locked为0,而进入B阶段,竞争条件导致锁失效;因此需要A/B阶段在一个步骤内原子完成,即保证test and set的原子性;该原子性由硬件支持,在RISC-V上使用的是amoswap(address, r1, r2)指令,atomic memory swap。这个这令接受三个参数,分别为address, 寄存器r1和寄存器r2;这条指令首先会锁住address,将address的数据保存在一个临时变量tmp中,然后将r1中的数据写入到地址中,然后将保存的tmp中的数据写入到寄存器r2中,最后对地址进行解锁;最终的效果是,address中的数据存放于寄存器r2中,寄存器r1中的数据存放于address中;

锁地址依赖于内存系统是如何工作的:

  • 多个处理器共用一个内存控制器,内存控制器可以给一个特定的地址加锁,处理器处理2-3个指令之后,然后再解锁;因为所有的处理器都需要通过内从控制器完成读写,所以内存控制器可以对操作进行排序和加锁;
  • 如果内存位于一个共享的总线上,那么需要总线控制器来支持,总线控制器需要以原子的方式执行多个内存操作;
  • 如果处理器有缓存,那么缓存一致性会确保持有了我们想要更新的数据的cache line只有一个写入者,相应的处理器会对cache line加锁,完成两个操作;

简单看下risc-v中的test-and-se的c以及对应的核心汇编代码实现:

while (__sync_lock_test_and_set(&lk->locked, 1) != 0);
amoswap.w.aq a5,a5,(s1)

上面的s1即locked的内存地址,a5为传递的参数1,即寄存器r1,同时对应a5也是寄存器r2;执行完毕该指令之后,原来的a5将1写入地址s1,即locked会更新为1,同时将locked的原值通过临时变量再写回a5,则通过判断a5的值可以确定是否获得锁:如果a5,即locked旧值为1,则此时仍为1,返回的a5也是1,则未获得锁;如果a5此时的值时0,即locked旧值为0,则此时locked会变为1,即有人持锁状态,而当前线程就是获得锁的人(最终的locked写1的人持有锁);释放锁,即为写0,也不必循环判断,因为永远是持锁者写0来释放锁;

MCS锁

自旋锁的基本实现原理即为第一节描述,但是自旋锁有几个弊端,例如:

  • 因为锁对象本身在内存中,,每个cpu都自旋在同一个锁对象上,而CPU与内存之间有缓存存在,所以当锁对象每次被更新,无论cpu是否获得锁,都需要更新缓存中的锁对象对应的cache line;
  • 同样是因为缓存,当前的多核cpu有很多不同的cluster,同cluster之间与不同cluster之间享有不同层级的缓存共享关系,则在cpu释放锁之后,不同cluster之间获得更新的时间稍有差异(需要缓存将数据一层一层传递),从而造成不公平现象;
  • 获得锁的随机性不能保证顺序性 早来的自旋持锁cpu抢不过晚来的cpu;

MCS锁可以对上面的弊端做一定的改进:

  • 每个cpu自旋在自己的变量中,避免频繁更新缓存信息;
  • 根据时间来对cpu进行排队,获得锁顺序有先后;

MCS锁的结构体变量为struct mcs_spinlock

struct mcs_spinlock {
	struct mcs_spinlock *next; // 排队在自己之后的锁
	int locked; // 是否持锁状态
}

MCS锁的加锁过程如下:

1.初始定义一个全局mcs锁,对应的next 为null, locked 为0,表明当前无人持锁;在这里插入图片描述
2. cpu1来获得mcs锁,CPU1创建一个本地副本mcs_1,将全局变量中的next交换为mcs_1的地址,查看原全局变量中的next为null,即为无人持锁,自己获得锁,将全局变量以及本地变量的locked置为1,此时CPU1持锁;在这里插入图片描述

3.cpu2来获得mcs锁,cpu2创建一个本地副本mcs_2,将全局变量中的next交换为mcs_2的地址,查看原全局变量中的next为mcs_1的地址,即有人持锁,于是根据地址找到mcs_1,并将mcs_1的next变量值为mcs_2,表示当前排在mcs_1的后面,因为自己没有持锁,所以将mcs_2的locked值为0,并且自旋在自己局部变量的locked上;在这里插入图片描述

4.cpu3再来持锁时,与cpu2类似,将全局变量中的next置为cpu3,并根据置换回来的值找到cpu2,将其next值指向自己,然后设置自己的局部变量locked为0,自旋在局部变量的locked上等待其变为1;在这里插入图片描述
5.cpu1结束时会释放锁,通过检查自己的next不为null,则排队在自己有人在等待,根据next的地址找到cpu2,将其locked置为1,则cpu2结束自旋,获得锁;在这里插入图片描述
6. 同理cpu2结束时,也会将锁传递给cpu3,而中途有其他cpu要获得锁,也是按照之前的方式修改全局变量中的next和之前队列末尾的next值,来排队到队列上;
7. 当最后一个持锁cpu释放时,其检查当前自己的next为null,并且全局中的next为当前cpu(任意时刻,全局变量中的next指针指向队列中的最后一个等锁cpu处的副本),即当前队列中无其他cpu,此时可以设置全局变量中的locked 为0 以及next为null,全局锁恢复到没有cpu持锁的状态;在这里插入图片描述
总结

  • 各个cpu在自己本地创建副本,则不需要频繁的因为全局变量而更新缓存;
  • 队列保证了获得锁的顺序;

队列自旋锁

mcs锁解决了缓存抖动以及顺序问题,队列自旋锁在此基础上对其内存占用以及其他小方面进行了进一步优化;[Linux自旋锁(2)]

链接

https://github.com/huihongxiao/MIT6.S081/blob/master/lec10-multiprocessors-and-locking/10.8-spin-lock-2.md

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值