Appearance
代码:使用锁
Xv6 在很多地方使用锁来避免竞争。如上所述,kalloc
和 kfree
是一个很好的例子。尝试练习 1 和 2,看看如果这些函数省略了锁会发生什么。你很可能会发现很难触发不正确的行为,这表明很难可靠地测试代码是否没有锁错误和竞争。Xv6 很可能还有尚未发现的竞争。
使用锁的一个难点是决定使用多少个锁,以及每个锁应该保护哪些数据和不变量。有几个基本原则。首先,任何时候一个变量可以被一个 CPU 写入,而同时另一个 CPU 可以读取或写入它,就应该使用一个锁来防止这两个操作重叠。其次,记住锁保护的是不变量:如果一个不变量涉及多个内存位置,通常所有这些位置都需要由一个锁来保护,以确保不变量得以维持。
上面的规则说明了什么时候需要锁,但没有说明什么时候不需要锁,为了效率,不过度加锁是很重要的,因为锁会降低并行性。如果并行性不重要,那么可以安排只有一个线程,而不用担心锁。一个简单的内核可以在多处理器上通过一个单一的锁来实现这一点,这个锁必须在进入内核时获取,在退出内核时释放(尽管阻塞系统调用如管道读取或 wait
会带来问题)。许多单处理器操作系统已经使用这种方法被转换成在多处理器上运行,有时被称为“大内核锁”,但这种方法牺牲了并行性:一次只有一个 CPU 可以在内核中执行。如果内核进行任何繁重的计算,使用更大的一组更细粒度的锁会更有效率,这样内核就可以在多个 CPU 上同时执行。
作为粗粒度锁定的一个例子,xv6 的 kalloc.c
分配器有一个由单个锁保护的单个空闲列表。如果不同 CPU 上的多个进程试图同时分配页面,每个进程都必须通过在 acquire
中自旋来等待轮到自己。自旋会浪费 CPU 时间,因为它不是有用的工作。如果对锁的争用浪费了相当一部分 CPU 时间,那么也许可以通过改变分配器设计来提高性能,使其具有多个空闲列表,每个列表都有自己的锁,以允许真正的并行分配。
作为细粒度锁定的一个例子,xv6 为每个文件都有一个单独的锁,这样操作不同文件的进程通常可以继续进行而无需等待对方的锁。如果想要允许进程同时写入同一文件的不同区域,文件锁定方案可以变得更加细粒度。最终,锁的粒度决策需要由性能测量和复杂性考虑来驱动。
随着后续章节解释 xv6 的每个部分,它们将提到 xv6 使用锁来处理并发的例子。作为预览,图~\ref{fig:locktable} 列出了 xv6 中的所有锁。
锁 | 描述 |
---|---|
bcache.lock | 保护块缓冲缓存条目的分配 |
cons.lock | 序列化对控制台硬件的访问,避免输出混杂 |
ftable.lock | 序列化文件表中 struct file 的分配 |
itable.lock | 保护内存中 inode 条目的分配 |
vdisk_lock | 序列化对磁盘硬件和 DMA 描述符队列的访问 |
kmem.lock | 序列化内存分配 |
log.lock | 序列化对事务日志的操作 |
pipe's pi->lock | 序列化对每个管道的操作 |
pid_lock | 序列化 next_pid 的增量 |
proc's p->lock | 序列化对进程状态的更改 |
wait_lock | 帮助 wait 避免丢失唤醒 |
tickslock | 序列化对滴答计数器的操作 |
inode's ip->lock | 序列化对每个 inode 及其内容的操作 |
buf's b->lock | 序列化对每个块缓冲区的操作 |