Skip to content

代码: 管道

一个更复杂的例子是xv6的管道实现,它使用sleepwakeup来同步生产者和消费者。我们在第1章中看到了管道的接口:写入管道一端的字节被复制到内核缓冲区,然后可以从管道的另一端读取。未来的章节将探讨围绕管道的文件描述符支持,但现在让我们看看pipewritepiperead的实现。

每个管道由一个struct pipe表示,其中包含一个lock和一个data缓冲区。字段nreadnwrite计算从缓冲区读取和写入的总字节数。缓冲区是环形的:在buf[PIPESIZE-1]之后写入的下一个字节是buf[0]。计数器不会回绕。这个约定让实现能够区分一个满的缓冲区(nwrite == nread+PIPESIZE)和一个空的缓冲区(nwrite == nread),但这意味着索引缓冲区必须使用buf[nread % PIPESIZE]而不是仅仅buf[nread](对于nwrite也是如此)。

假设pipereadpipewrite的调用同时在两个不同的CPU上发生。pipewrite开始时获取管道的锁,该锁保护计数、数据及其相关的不变性。piperead然后也尝试获取锁,但无法获取。它在acquire中自旋等待锁。当piperead等待时,pipewrite循环遍历正在写入的字节(addr[0..n-1]),依次将每个字节添加到管道中。在这个循环中,可能会发生缓冲区已满的情况。在这种情况下,pipewrite调用wakeup来提醒任何休眠的读取者缓冲区中有数据在等待,然后在&pi->nwrite上休眠,等待读取者从缓冲区中取出一些字节。sleep在使pipewrite的进程进入休眠状态时释放管道的锁。

现在piperead获取了管道的锁并进入其临界区:它发现pi->nread != pi->nwritepipewrite进入休眠是因为pi->nwrite == pi->nread + PIPESIZE),所以它进入for循环,从管道中复制数据,并将在复制的字节数上增加nread。现在有这么多字节可供写入,所以piperead在返回之前调用wakeup来唤醒任何休眠的写入者。wakeup找到一个在&pi->nwrite上休眠的进程,即之前运行pipewrite但在缓冲区满时停止的进程。它将该进程标记为RUNNABLE

管道代码为读者和写者使用不同的休眠通道(pi->nreadpi->nwrite);在有大量读者和写者等待同一个管道的不太可能的情况下,这可能会使系统更有效率。管道代码在一个检查休眠条件的循环内休眠;如果有多个读者或写者,除了第一个被唤醒的进程外,所有进程都会看到条件仍然为假并再次休眠。