XV6 RISCV源码阅读报告之 锁

二、xv6的锁

为了在多处理器上防止多个CPU操作同一片地址空间互相干扰引起的错误,以及即使是在单个处理器上防止中断处理程序与非中断代码之间互相干扰,xv6使用锁来实现互斥。

2.1代码阅读与分析

xv6定义锁的代码非常明确,即spinlock和sleeplock。spinlock会在无法获得锁时不断循环获取;而sleeplock为了防止长时间的等待循环,在获取不到锁时让进程陷入睡眠,锁释放时对睡眠进程进行唤醒。

2.1.1spinlock.h

spinlock.h定义了spinlock的数据结构,一个指向持有锁的CPU的指针,一个名字指针,核心是一个整数locked,值为0代表没有锁,值为1代表锁了。

2.1.2spinlock.c

包含了一系列spinlock的操作。

initlock()初始化锁的成员变量,将locked置为0。

acquire()使得当前进程获取指定名称的锁。首先使用push_off关中断以避免死锁(进程持有锁后发生中断,中断程序等待锁,导致中断无法返回,陷入死锁)。然后检查当前进程是否已经获取了这把锁,若获取则将panic。在这里,函数使用了riscv的原子操作来避免直接写while语句可能产生的并发错误。__sync_lock_test_and_set等效于以下代码,但是是一个原子操作,并在成功获取时返回0。随后函数将告知C编译器不能在优化时改变此处语句的前后顺序,以免出现错误。

 

while(1){  
    if(!lk->locked){  
        lk->locked=1;  
        break;  
    }  
} 

 

release()首先检查是否持有锁,如果无锁release就会引发panic。与获取锁的操作相同,需要__sync_synchronize,并使用原子化的操作__sync_lock_release来将locked置为0,最后使用pop_off开中断。

holding()检查当前进程是否持有某个锁,通过检查locked和cpuid。

push_off和pop_off是开关中断的函数。与intr_on/off不同的是多了计次功能,n个push_off需要n个pop_off来完全释放,就像push和pop的操作。

2.1.3 sleeplock.h

sleeplock.h定义了睡眠锁的结构包含记录是否已经被持有的locked和保护sleeplock的spinlock,还有pid和锁名。

2.1.4 sleeplock.c

initsleeplock()通过调用initlock()和赋值来对锁初始化。

acquiresleep()在获取sleeplock的过程中使用其中的spinlock保证函数的原子性。先获取对应的spinlock,然后判断要获取的锁是否已被持有,如果被持有则进入睡眠,当从睡眠醒来时,需要重新获取spinlock。如果可以获取锁,就改变locked和pid,然后释放spinlock。进入sleep函数时,会获取当前进程的lock,然后释放sleeplock的自旋锁,而sleep函数结束时会释放进程的锁,获取睡眠锁的锁。函数采用while语句,让每次进程被唤醒时都检查等待的条件是否满足,防止出现一个睡眠进程等待的锁没有空闲就被唤醒的情况。

releasesleep()同样使用自旋锁维护原子性,释放时把锁的pid和locked值设为0,然后使用wakeup函数,实际上是对设置进程状态p->state=RUNNABLE的包装。

holdingsleep()的操作与holding()相同。

2.2锁的使用

锁的使用非常简单,先声明锁,然后在初始化函数中把锁初始化,使用时在语句前后分别获取释放即可,但也有一些注意事项。

2.2.1 锁的顺序

如果一段代码要使用多个锁,那么必须要注意代码每次运行都要以相同的顺序获得锁,并以相反的顺序释放锁,否则就会发生死锁。假设某段代码中存在两条路径,路径1获得锁的顺序是 A、B,而路径2获得锁的顺序是 B、A。那么就可能1中持有A,2中持有B,二者互相等待无法继续运行,发生死锁。为了避免死锁,所有的代码路径获得锁的顺序必须相同。同样的,释放锁必须以获得的相反顺序运行。加锁的操作就像给代码加上花括号,内层对应内层,外层对应外层。

2.2.2 锁的粒度

在代码中使用一把大锁对于编码来说非常简单,但也牺牲了并发性能,极大地影响着程序的效率。因此,为了发挥多处理器的性能,需要设计细粒度的锁并恰当地编码以保证锁的正确性。正如我们在lab中为每个哈希桶或者每个CPU设计一把锁。

2.2.3 锁的选择

使用自旋锁基本保证了同步互斥问题能得到解决,但效率似乎并不理想,因为他是忙等待的,将一直占有CPU执行循环,所以引入了睡眠锁,让得不到锁的进程进入睡眠状态让出CPU重新调度。因此如果锁持有的时间较长,可能需要使用睡眠锁。

使用锁的一个难点在于要决定使用多少个锁,以及每个锁保护哪些数据、不变量。不过有几个基本原则。首先,当一个 CPU 正在写一个变量,而同时另一个 CPU 可能读/写该变量时,需要用锁防止两个操作重叠。第二,当用锁保护不变量时,如果不变量涉及到多个数据结构,通常每个数据结构都需要用一个单独的锁保护起来,这样才能维持不变量。

2.3小结

锁的机制比较简单,但在其他各个模块中都有广泛的应用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值