10.6 XV6中UART模块对于锁的使用
Last updated
Last updated
接下来我们看一下XV6的代码,通过代码来理解锁是如何在XV6中工作的。我们首先查看一下uart.c,在上节课介绍中断的时候我们提到了这里的锁,现在我们具体的来看一下。因为现在我们对锁更加的了解了,接下来将展示一些更有趣的细节。
从代码上看UART只有一个锁。
所以你可以认为对于UART模块来说,现在是一个coarse-grained lock的设计。这个锁保护了UART的的传输缓存;写指针;读指针。当我们传输数据时,写指针会指向传输缓存的下一个空闲槽位,而读指针指向的是下一个需要被传输的槽位。这是我们对于并行运算的一个标准设计,它叫做消费者-生产者模式。
所以现在有了一个缓存,一个写指针和一个读指针。读指针的内容需要被显示,写指针接收来自例如printf的数据。我们前面已经了解到了锁有多个角色。第一个是保护数据结构的特性不变,数据结构有一些不变的特性,例如读指针需要追赶写指针;从读指针到写指针之间的数据是需要被发送到显示端;从写指针到读指针之间的是空闲槽位,锁帮助我们维护了这些特性不变。
我们接下来看一下uart.c中的uartputc函数。
函数首先获得了锁,然后查看当前缓存是否还有空槽位,如果有的话将数据放置于空槽位中;写指针加1;调用uartstart;最后释放锁。
如果两个进程在同一个时间调用uartputc,那么这里的锁会确保来自于第一个进程的一个字符进入到缓存的第一个槽位,接下来第二个进程的一个字符进入到缓存的第二个槽位。这就是锁帮助我们避免race condition的一个简单例子。如果没有锁的话,第二个进程可能会覆盖第一个进程的字符。
接下来我们看一下uartstart函数,
如果uart_tx_w不等于uart_tx_r,那么缓存不为空,说明需要处理缓存中的一些字符。锁确保了我们可以在下一个字符写入到缓存之前,处理完缓存中的字符,这样缓存中的数据就不会被覆盖。
最后,锁确保了一个时间只有一个CPU上的进程可以写入UART的寄存器,THR。所以这里锁确保了硬件寄存器只有一个写入者。
当UART硬件完成传输,会产生一个中断。在前面的代码中我们知道了uartstart的调用者会获得锁以确保不会有多个进程同时向THR寄存器写数据。但是UART中断本身也可能与调用printf的进程并行执行。如果一个进程调用了printf,它运行在CPU0上;CPU1处理了UART中断,那么CPU1也会调用uartstart。因为我们想要确保对于THR寄存器只有一个写入者,同时也确保传输缓存的特性不变(注,这里指的是在uartstart中对于uart_tx_r指针的更新),我们需要在中断处理函数中也获取锁。
所以,在XV6中,驱动的bottom部分(注,也就是中断处理程序)和驱动的up部分(注,uartputc函数)可以完全的并行运行,所以中断处理程序也需要获取锁。我们接下来会介绍,在实现锁的时候,为了确保这里能正常工作还是有点复杂的。
(注,下面问答来自课程结束部分)
学生提问:UART的缓存中,读指针是不是总是会落后于写指针?
Frans教授:从读指针到写指针之间的字符是要显示的字符,UART会逐次的将读指针指向的字符在显示器上显示,同时printf可能又会将新的字符写入到缓存。读指针总是会落后于写指针直到读指针追上了写指针,这时两个指针相同,并且此时缓存中没有字符需要显示。