spinlock分析
1. initlock
spinlock分析spinlock:
主要是初始化锁的状态为0,并记录锁的名字用于调试分析。
2. pushcli和popcli
在xv6中,用spinlock来保证多核之间数据的同步访问,但对于单核上内核程序和中断程序之间的数据同步访问采用的是一种很粗暴的方法,在获取锁时,关闭中断。。。。这样就避免了内核程序和中断程序之间的冲突。
pushcli和popcli有这样的特性:使用了n次pushcli后必须使用n次popcli;popcli之后中断的开关状态和pushcli之前是一样的。
2.1 pushcli
调用pushcli的次数被记录在了当前cpu结构体中的ncli中。可以看出,只要调用pushcli是一定会关闭中断的。
2.2 popcli
可以看出,只有最后一次调用popocli并且调用popcli之前中断是打开的,popcli才会调用sti()打开中断。
3. holding函数
检查某个锁是否被当前cpu锁持有:
可以看出,检查时是关闭中断状态的,检查的条件是:
- 锁是锁着的。
- 拥有锁的cpu是当前cpu。
4. 记录栈帧
getcallerpcs记录栈帧,用于调试时使用。比如acquire调用getcallerpcs,记录的是调用acquire的函数的返回地址,然后依次回溯。要想用getcallerpcs记录函数的调用关系。
栈帧的概念如下:
再来看看getcallerpcs的代码:
- 其中v参数必须是调用getcallerpcs的函数的第一个参数的地址,然后通过第一个参数的地址来计算ebp指针的值。这是因为函数参数压栈是从右往左依次压栈的。
- pcs[]数组定义在每个lock结构体中,大小是10个。循环记录每个调用者(栈帧)的返回地址,也就是将eip的值保存在pcs数组中,最多可以回溯10个。
- 如果栈帧回溯不满10个,则将剩下的数组空间清零。
当调用getcallerpcs时栈帧结构如下所示:
5. 获取spinlock
在xv6中,当调用获取锁的接口时,会通过pushcli关闭当前中断,然后检查当前cpu是否持有锁,按理不应该持有的,如果持有的话,就报错,因为这样会造成死锁。
xv6使用了x86的原子指令xchg来交换寄存器和内存中的数值,xchg()函数返回值是内存中的原始值。如果返回值是1,表示锁之前已经被占有,则继续通过while循环检测,也就是除非有别的核释 放锁,否则会一直在这里自旋;如果返回是0,则表示锁之前没有被占有,同时也表示锁被此cpu成功获取。
xv6通过使用__sync_synchronize();来进行实现内存屏障。用于内存同步?让不同核看到的同一内存地址上的值都是一样的?
锁获取成功后,记录获取锁的cpu和调用栈信息。
6. 释放spinlock
- 首先检查锁是否被当前cpu持有,按理说应该是,如果不是,则报错。
- 然后清空锁中的栈帧调试信息和cpu信息。
- 同样用__sync_synchronize实现内存屏障。
- xv6在release的时候使用汇编指令来将lk->locked清零,因为在x86中,movl指令保证以原子的方式更新内存4个字节内容,也就是要么4个字节不变,要么全部更新完成。这防止了在使用xchg的时候看到的lk->locked值是没有完全更新的值。这里不能用c语言赋值,因为c语言不能保证4个字节是原子性的同步更新的。
- 在release的最后使用popcli恢复之前的中断状态。
通过上面的分析可以看出,acquire中xchg保证了内存和寄存器值交换这个过程是原子性的;release中通过使用汇编指令movl保证对4个字节内存的赋值是原子性的(那为啥不用xchg来赋值为0???)。
评论