Appearance
代码: 管道
一个更复杂的例子是xv6的管道实现,它使用sleep
和wakeup
来同步生产者和消费者。我们在第1章中看到了管道的接口:写入管道一端的字节被复制到内核缓冲区,然后可以从管道的另一端读取。未来的章节将探讨围绕管道的文件描述符支持,但现在让我们看看pipewrite
和piperead
的实现。
每个管道由一个struct pipe
表示,其中包含一个lock
和一个data
缓冲区。字段nread
和nwrite
计算从缓冲区读取和写入的总字节数。缓冲区是环形的:在buf[PIPESIZE-1]
之后写入的下一个字节是buf[0]
。计数器不会回绕。这个约定让实现能够区分一个满的缓冲区(nwrite == nread+PIPESIZE
)和一个空的缓冲区(nwrite == nread
),但这意味着索引缓冲区必须使用buf[nread % PIPESIZE]
而不是仅仅buf[nread]
(对于nwrite
也是如此)。
假设piperead
和pipewrite
的调用同时在两个不同的CPU上发生。pipewrite
开始时获取管道的锁,该锁保护计数、数据及其相关的不变性。piperead
然后也尝试获取锁,但无法获取。它在acquire
中自旋等待锁。当piperead
等待时,pipewrite
循环遍历正在写入的字节(addr[0..n-1]
),依次将每个字节添加到管道中。在这个循环中,可能会发生缓冲区已满的情况。在这种情况下,pipewrite
调用wakeup
来提醒任何休眠的读取者缓冲区中有数据在等待,然后在&pi->nwrite
上休眠,等待读取者从缓冲区中取出一些字节。sleep
在使pipewrite
的进程进入休眠状态时释放管道的锁。
现在piperead
获取了管道的锁并进入其临界区:它发现pi->nread != pi->nwrite
(pipewrite
进入休眠是因为pi->nwrite == pi->nread + PIPESIZE
),所以它进入for
循环,从管道中复制数据,并将在复制的字节数上增加nread
。现在有这么多字节可供写入,所以piperead
在返回之前调用wakeup
来唤醒任何休眠的写入者。wakeup
找到一个在&pi->nwrite
上休眠的进程,即之前运行pipewrite
但在缓冲区满时停止的进程。它将该进程标记为RUNNABLE
。
管道代码为读者和写者使用不同的休眠通道(pi->nread
和pi->nwrite
);在有大量读者和写者等待同一个管道的不太可能的情况下,这可能会使系统更有效率。管道代码在一个检查休眠条件的循环内休眠;如果有多个读者或写者,除了第一个被唤醒的进程外,所有进程都会看到条件仍然为假并再次休眠。