6.3 代码:使用锁

Xv6在许多地方使用锁来避免竞争条件(race conditions)。如上所述,kalloc(kernel/kalloc.c:69)和kfree(kernel/kalloc.c:47)就是一个很好的例子。尝试练习1和练习2,看看如果这些函数省略了锁会发生什么。你可能会发现很难触发不正确的行为,这表明很难可靠地测试代码是否经历了锁错误和竞争后被释放。xv6有一些竞争是有可能发生的。

使用锁的一个困难部分是决定要使用多少锁,以及每个锁应该保护哪些数据和不变量。有几个基本原则。首先,任何时候可以被一个CPU写入,同时又可以被另一个CPU读写的变量,都应该使用锁来防止两个操作重叠。其次,请记住锁保护不变量(invariants):如果一个不变量涉及多个内存位置,通常所有这些位置都需要由一个锁来保护,以确保不变量不被改变。

上面的规则说什么时候需要锁,但没有说什么时候不需要锁。为了提高效率,不要向太多地方上锁是很重要的,因为锁会降低并行性。如果并行性不重要,那么可以安排只拥有一个线程,而不用担心锁。一个简单的内核可以在多处理器上做到这一点,方法是拥有一个锁,这个锁必须在进入内核时获得,并在退出内核时释放(尽管如管道读取或wait的系统调用会带来问题)。许多单处理器操作系统已经被转换为使用这种方法在多处理器上运行,有时被称为“大内核锁(big kernel lock)”,但是这种方法牺牲了并行性:一次只能有一个CPU运行在内核中。如果内核做一些繁重的计算,使用一组更细粒度的锁的集合会更有效率,这样内核就可以同时在多个处理器上执行。

作为粗粒度锁的一个例子,xv6的kalloc.c分配器有一个由单个锁保护的空闲列表。如果不同CPU上的多个进程试图同时分配页面,每个进程在获得锁之前将必须在acquire中自旋等待。自旋会降低性能,因为它只是无用的等待。如果对锁的争夺浪费了很大一部分CPU时间,也许可以通过改变分配器的设计来提高性能,使其拥有多个空闲列表,每个列表都有自己的锁,以允许真正的并行分配。

作为细粒度锁定的一个例子,xv6对每个文件都有一个单独的锁,这样操作不同文件的进程通常可以不需等待彼此的锁而继续进行。文件锁的粒度可以进一步细化,以允许进程同时写入同一个文件的不同区域。最终的锁粒度决策需要由性能测试和复杂性考量来驱动。

在后面的章节解释xv6的每个部分时,他们将提到xv6使用锁来处理并发的例子。作为预览,表6.3列出了xv6中的所有锁。

描述
bcache.lock 保护块缓冲区缓存项(block buffer cache entries)的分配
cons.lock 串行化对控制台硬件的访问,避免混合输出
ftable.lock 串行化文件表中文件结构体的分配
icache.lock 保护索引结点缓存项(inode cache entries)的分配
vdisk_lock 串行化对磁盘硬件和DMA描述符队列的访问
kmem.lock 串行化内存分配
log.lock 串行化事务日志操作
管道的pi->lock 串行化每个管道的操作
pid_lock 串行化next_pid的增量
进程的p->lock 串行化进程状态的改变
tickslock 串行化时钟计数操作
索引结点的 ip->lock 串行化索引结点及其内容的操作
缓冲区的b->lock 串行化每个块缓冲区的操作

​ Figure 6.3: Locks in xv6

copyright by duguosheng all right reserved,powered by Gitbook该文件修订时间: 2021-08-19 13:53:42

results matching ""

    No results matching ""