本系列文章为MIT6.S081的学习笔记,包含了参考手册、课程、实验三部分的内容,前面的系列文章链接如下
操作系统MIT6.S081:[xv6参考手册第1章]->操作系统接口
操作系统MIT6.S081:[xv6参考手册第2章]->操作系统组织结构
操作系统MIT6.S081:[xv6参考手册第3章]->页表
操作系统MIT6.S081:[xv6参考手册第4章]->Trap与系统调用
操作系统MIT6.S081:P1->Introduction and examples
操作系统MIT6.S081:P2->OS organization and system calls
操作系统MIT6.S081:P3->Page tables
操作系统MIT6.S081:P4->RISC-V calling conventions and stack frames
操作系统MIT6.S081:P5->Isolation & system call entry/exit
操作系统MIT6.S081:P6->Page faults
操作系统MIT6.S081:Lab1->Unix utilities
操作系统MIT6.S081:Lab2->System calls
操作系统MIT6.S081:Lab3->Page tables
操作系统MIT6.S081:Lab4->Trap
操作系统MIT6.S081:Lab5->Lazy allocation

文章目录

  • 一、操作系统内存使用情况
  • 二、中断的处理流程与硬件设备
  • 三、驱动
  • 四、在xv6中设置中断
  • 五、UART驱动的top部分
  • 六、UART驱动的bottom部分
  • 七、Interrupt相关的并发
  • 八、UART读取键盘输入
  • 九、中断的演进
  • 十、问答

一、操作系统内存使用情况

在真正的操作系统中,内存是如何使用的

登录到Athena计算机(MIT内部共享使用的计算机),执行top指令输出内存的使用情况。

查看Mem这一行,可以看到:
①计算机中总共有多少内存(33048332)
②大部分内存都被使用了(4214604+26988148),应用程序用得较少,buff/cache用得较多。这在操作系统中很常见,因为我们不想让物理内存就在那闲置着,所以这里大块的内存被用作buff/cache。
③还有一小块内存是空闲的(1845580),但是并不多。
注:
①以上是一个非常常见的场景,大部分操作系统运行时几乎没有任何空闲的内存。这意味着如果应用程序或者内核需要使用新的内存,那么我们需要丢弃一些已有的内容。现在的空闲内存(free)或许足够几个page用,但是在某个时间点如果需要大量内存的话,要么是从应用程序,要么是从buffer/cache中,需要撤回已经使用的一部分内存。所以,当内核在分配内存的时候,通常都不是一个低成本的操作,因为并不总是有足够的可用内存,为了分配内存需要先撤回一些内存。
②如果你查看输出的每一行,VIRT表示的是虚拟内存地址空间的大小,RES是实际使用的内存数量。从这里可以看出,实际使用的内存数量远小于地址空间的大小。所以,我们上节课讨论的基于虚拟内存和page fault提供的非常酷的功能在这都有使用,比如说demand paging。

二、中断的处理流程与硬件设备

中断的使用场景

中断对应的场景很简单,就是硬件想要得到操作系统的关注。例如:
----网卡收到了一个packet,网卡会生成一个中断。
----用户通过键盘按下了一个按键,键盘会产生一个中断。
操作系统需要做的是:保存当前的工作---->处理中断---->恢复之前的工作。这里的保存和恢复工作与之前系统调用过程非常相似。所以系统调用、page fault、中断都使用相同的机制。

中断与系统调用的区别:

①asynchronous(异步)。当硬件生成中断时,中断处理程序与CPU上当前运行的进程没有任何关联。但如果是系统调用的话,需要进入内核,然后在调用进程的上下文中运行。
②concurrency(并发)。对于中断来说,CPU和生成中断的设备是并行运行。网卡自己独立处理来自网络的网络包,然后在某个时间点产生中断。同时,CPU也在正常运行。所以CPU和设备之间是并行运行的,我们必须管理这里的并行。
③program devices(对硬件编程)。我们这节课主要关注外部设备,例如网卡、UART。这些设备需要被编程,每个设备都有一个编程手册,就像RISC-V有一个包含了指令和寄存器的手册一样。设备的编程手册包含了它有什么样的寄存器、它能执行什么样的操作、在读写控制寄存器时设备会如何响应。不幸的是,设备的手册不如RISC-V的手册清晰,这会使得对设备的编程更加复杂。

中断的产生

我们这节课会讨论的内容:
----控制台中的提示符$ 是如何显示出来的。
----如果你在键盘输入ls,这个ls字符是怎么读入并显示在控制台。
----实现上述目标所需要的所有机制。
我们首先要关心的是:中断是从哪里产生的?
因为我们主要关心的是外部设备的中断,而不是定时器中断或软件中断。外设中断来自于主板上的设备,下图是一个SiFive主板(QEMU模拟的那个),可以发现有大量的设备连接在或者可以连接到这个主板上。

这里连接了许多设备,如以太网卡、MicroUSB、MicroSD、重置按钮等,主板上的各种线路将外设和CPU连接在一起。下面两个引脚一个是用来传输的UART0 Tx,另外一个是用来接收的UART0 Re。

SiFive文档
下图是SiFive有关处理器的文档,图中的右侧是各种各样的设备,如UART0。我们在之前的课程已经知道UART0这些硬件设备会映射到内核内存地址中低于0x80000000的地方,而左边所有的DRAM都映射在地址空间的0x80000000之上。通过向相应的设备地址执行load/store指令,我们就可以对设备进行编程。

所有的设备都连接到处理器上,处理器通过Platform Level Interrupt Control(PLIC)来处理设备中断。我们进一步查看PLIC的结构图:

从左上角可以看出,我们有53个不同的来自于设备的中断。这些中断到达PLIC之后,PLIC会路由这些中断。图的右下角是CPU的核,PLIC会将中断路由到某一个CPU核。如果所有的CPU核都正在处理中断,PLIC会保留中断直到有一个CPU核可以用来处理中断。所以PLIC需要保存一些内部数据来跟踪中断的状态。
这里的流程总结如下:
①PLIC通知当前有一个待处理的中断
②其中一个CPU核会声称接收中断,这样PLIC就不会把中断发给其他的CPU处理
③CPU核处理完中断之后,CPU会通知PLIC处理完毕
④PLIC将不再保存中断的信息

问答

学生提问:PLIC有没有什么执行机制来确保公平?
Frans教授:这里取决于内核以什么样的方式来对PLIC进行编程。PLIC只是分发中断,而内核需要对PLIC进行编程来告诉它中断应该分发到哪。实际上,内核可以对中断优先级进行编程,这里非常的灵活。

三、驱动

驱动的概念

通常来说,管理设备的代码称为驱动,xv6中所有的驱动都在内核中。我们今天要看的是UART设备的驱动,代码在uart.c文件中。

驱动的结构

如果我们查看驱动代码的结构,我们可以发现大部分驱动都分为两个部分:bottomtop
----bottom部分通常是中断处理程序(Interrupt handler)。当一个中断送到了CPU并且CPU接收这个中断,CPU会调用相应的中断处理程序。中断处理程序并不运行在任何特定进程的上下文中,它只是处理中断。
----top部分是用户进程或者内核的其他部分调用的接口。对于UART来说,这里有read/write接口,这些接口可以被更高层级的代码调用。

通常情况下,驱动中会有一些队列(或者说buffer),top部分的代码会从队列中读写数据,而中断处理程序(bottom部分)同时也会向队列中读写数据。这里的队列可以将顶部和底部解耦,并允许设备和CPU上的其他代码并行运行。

----通常对于中断处理程序来说存在一些限制,因为它并没有运行在任何进程的上下文中,所以进程的页表并不知道该从哪个地址读写数据,也就无法直接从中断处理程序读写数据。所以驱动的top部分通常与用户的进程交互,进行数据的读写。
----在很多操作系统中,驱动代码加起来可能会比内核还要大,主要是因为对于每个设备都需要一个驱动,而设备又很多。

设备编程

通常来说,对设备编程是通过内存映射I/O(memory mapped I/O)完成的。
----在SiFive的手册中,设备地址出现在物理地址的特定区间内,这个区间由主板制造商决定。
----操作系统需要知道这些设备位于物理地址空间的具体位置,然后再通过普通的load/store指令对这些地址进行编程。
----load/store指令实际上的工作就是读写设备的控制寄存器。例如,对网卡执行store指令时,CPU会修改网卡的某个控制寄存器,进而导致网卡发送一个网络包。所以这里的load/store指令不会读写内存,而是会操作设备。
----你还需要阅读设备的文档来弄清楚设备的寄存器和相应的行为,有的时候文档很清晰,有的时候文档不是那么清晰。
下图中是SiFive主板中的对应设备的物理地址。

----0x200_0000对应CLINT
----0xC000000对应的是PLIC
----在这个图中UART0对应的是0x1001_0000。但是在QEMU中,我们的UART0的地址略有不同,因为在QEMU中我们并不是完全的模拟SiFive主板,而是模拟与SiFive主板非常类似的东西。
UART文档
下图是UART的文档,16550是QEMU模拟的UART设备,QEMU用这个模拟的设备来与键盘和控制台进行交互。这是一个很简单的芯片,图中表明了芯片拥有的寄存器。

----对于控制寄存器000,当你执行load指令时,它将持有数据。如果执行store指令,则将数据传输到线路之外。
----UART可以让你能够通过串口发送数据bit。发送是一条线路,接收是一条线路。基本上,你取一个字节,它们在这条线路上是多路复用或串行化的。在线路的另一侧会有另一个UART芯片,能够将数据bit组合成一个个Byte。
----这里还有一些其他可以控制的地方,例如控制寄存器001,可以通过它来控制UART是否产生中断。
----实际上对于一个寄存器,其中的每个bit都有不同的作用。例如对于寄存器001,也就是IER寄存器,bit0-bit3分别控制了不同的中断。
----这个文档还有很多内容,但是对于我们这节课来说,上图就足够了。不过即使是这么简单的一个设备,它的文档也有很多页。

问答

学生提问:如果你写入数据到Transmit Holding Register,然后再次写入,那么前一个数据不会被覆盖掉吗?
Frans教授:我们通过load将数据写入到这个寄存器中,之后UART芯片会通过串口线将这个Byte送出。当完成了发送,UART会生成一个中断给内核,这个时候才能再次写入下一个数据。所以内核和设备之间需要遵守一些协议才能确保一切工作正常。上图中的UART芯片会有一个容量是16的FIFO,但是你还是要小心,因为如果阻塞了16个Byte之后再次写入还是会造成数据覆盖。

四、在xv6中设置中断

控制台显示$的原理

对于$ 来说,实际上就是设备会将字符传输给UART的寄存器,UART之后会在发送完字符之后产生一个中断。在QEMU中,模拟的线路的另一端会有另一个UART芯片,这个UART芯片连接到了虚拟的控制台,它会进一步将$ 显示在控制台上。

键盘输入ls并显示在控制台的原理

对于ls,这是用户输入的字符。键盘连接到了UART的输入线路,当你在键盘上按下一个按键,UART芯片会将按键字符通过串口线发送到另一端的UART芯片。另一端的UART芯片先将数据bit合并成一个Byte,之后再产生一个中断,并告诉处理器这里有一个来自于键盘的字符。之后中断处理程序会处理来自于UART的字符。

RISC-V与中断相关的寄存器

①SIE(Supervisor Interrupt Enable)寄存器。这个寄存器中有一个bit(E)专门针对类似UART的外部设备的中断。有一个bit(S)专门针对软件中断,软件中断可能由一个CPU核触发给另一个CPU核。还有一个bit(T)专门针对定时器中断。
②SSTATUS(Supervisor Status)寄存器。这个寄存器中有一个bit来打开或者关闭中断。每一个CPU核都有独立的SIE和SSTATUS寄存器,除了通过SIE寄存器来单独控制特定的中断,还可以通过SSTATUS寄存器中的一个bit来控制所有的中断。
③SIP(Supervisor Interrupt Pending)寄存器。当发生中断时,处理器可以通过查看这个寄存器知道当前是什么类型的中断。
④SCAUSE寄存器。它会表明当前状态的原因是中断。
⑤STVEC寄存器。它会保存当trap、page fault、中断发生时,CPU运行的用户程序的程序计数器,这样才能在稍后恢复程序的运行。

xv6中设置中断

机器启动时,start.c中的start函数被调用,它以M模式运行,禁用了页表。很快过后,内核又进入Supervisor Mode,可以设置页表。

这里将所有的中断都设置在Supervisor mode,然后设置SIE寄存器来接收外部、软件、定时器中断,之后初始化定时器。
main函数中是如何处理External中断:

我们第一个外设是console,这是我们print的输出位置。查看console.c中的consoleinit函数。

这里首先初始化了锁,然后调用了uart.c中的uartinit函数。这个函数实际上就是配置好UART芯片使其可以被使用。

这里的流程是先关闭中断,之后设置波特率,设置字符长度为8bit,重置FIFO,最后再重新打开中断。
学生提问:什么是波特率?
Frans教授:这是串口线的传输速率。
以上就是uartinit函数,运行完这个函数之后,原则上UART就可以生成中断了。但是因为我们还没有对PLIC编程,所以中断不能被CPU感知。最终,在main函数中需要调用plicinit函数。

-----PLIC与外设一样,也占用了一个I/O地址(在Kernel/memlayout.h中可以看到,是0xC000_0000)。
----代码的第一行使能了UART的中断,这里实际上就是设置PLIC会接收哪些中断,进而将中断路由到CPU。
----类似的,代码的第二行设置PLIC接收来自IO磁盘的中断。
main函数中,plicinit之后就是plicinithart函数。plicinit是由0号CPU运行,之后每个CPU的核都需要调用plicinithart函数表明对于哪些外设中断感兴趣。

----所以在plicinithart函数中,每个CPU的核都表明自己对来自于UART和VIRTIO的中断感兴趣。
----因为我们忽略中断的优先级,所以我们将优先级设置为0。
到目前为止,我们有了生成中断的外部设备,有了PLIC可以传递中断到单个的CPU。但是CPU自己还没有设置好接收中断,因为我们还没有设置好SSTATUS寄存器。在main函数的最后,程序调用了scheduler函数。

scheduler函数主要是运行进程。但是在实际运行进程之前,会执行intr_on函数来使得CPU能接收中断。

intr_on函数只完成一件事情,就是设置SSTATUS寄存器,打开中断标志位。在这个时间点,中断被完全打开了。如果PLIC正好有pending的中断,那么这个CPU核会收到中断。以上就是中断的基本设置。
学生提问:哪些核在intr_on之后打开了中断?
Frans教授:任何一个调用了intr_on的CPU核都会接收中断,实际上所有的CPU核都会运行intr_on函数。

五、UART驱动的top部分

输出$ 的流程

我们来看下从Shell程序输出提示符$到Console的流程:
①首先我们看init.c中的main函数,这是系统启动后运行的第一个进程。

----首先这个进程的main函数通过mknod操作创建了一个代表Console的设备。
----因为这是第一个打开的文件,所以这里返回文件描述符0。
----之后通过dup创建stdout(标准输出)和stderr(标准错误)。这里实际上通过复制文件描述符0,得到了另外两个文件描述符1,2。最终文件描述符0、1、2都用来代表Console。
②Shell程序首先打开文件描述符0、1、2,之后Shell向文件描述符2打印提示符$

----尽管Console背后是UART设备,但是从应用程序来看,它就像是一个普通的文件。
----Shell程序只通过fprintf向文件描述符2写了数据,它并不知道文件描述符2对应的是什么。
fprintf工作原理
在Unix系统中,设备是由文件表示。我们来看一下这里的fprintf是如何工作的。
①在printf.c文件中,代码只是调用了write系统调用。在我们的例子中,fd对应的就是文件描述符2,c是字符$

②所以由Shell输出的每一个字符都会触发一个write系统调用。之前我们已经看过了write系统调用最终会走到sysfile.c文件的sys_write函数。

③这个函数中首先对参数做了检查,然后又调用了file.c中的filewrite函数。

④在filewrite函数中首先会判断文件描述符的类型。
----mknod生成的文件描述符属于设备(FD_DEVICE),而对于设备类型的文件描述符,我们会为这个特定的设备执行设备相应的write函数。
----因为我们现在的设备是Console,所以我们知道这里会调用console.c中的consolewrite函数。

⑤这里先通过either_copyin将字符拷入,之后调用uart.c中的uartputc函数。uartputc函数将字符写入给UART设备,并完成实际的打印字符。所以你可以认为consolewrite是一个UART驱动的top部分。

uartputc
uartputc函数比较有趣。在UART的内部会有一个buffer用来发送数据,buffer的大小是32个字符。同时还有一个为consumer提供的读指针和为producer提供的写指针,来构建一个环形的buffer(可以认为是环形队列)。

在我们的例子中,Shell是producer,所以需要调用uartputc函数。
----在函数中第一件事情是判断环形buffer是否已经满了。
----如果读写指针相同,那么buffer是空的。如果写指针加1等于读指针,那么buffer满了。
----当buffer是满的时候,向其写入数据是没有意义的,所以这里会sleep一段时间,将CPU出让给其他进程。
----当然,对于我们来说,buffer必然不是满的,因为提示符$是我们送出的第一个字符。所以代码会走到else,字符会被送到buffer中,更新写指针,之后再调用uartstart函数。

uartstart函数
uartstart就是通知设备执行操作。
----首先是检查当前设备是否空闲,如果空闲的话,我们会从buffer中读出数据,然后将数据写入到THR(Transmission Holding Register)发送寄存器。这里相当于告诉设备,我这里有一个字节需要你来发送。
----一旦数据送到了设备,系统调用会返回,用户应用程序Shell就可以继续执行。这里从内核返回到用户空间的机制与lec06的trap机制是一样的。
----与此同时,UART设备会将数据送出。在某个时间点,我们会收到中断,因为
我们之前设置了要处理UART设备中断。

六、UART驱动的bottom部分

发生中断时,实际会发生的事情

问题: 在我们向Console输出字符时,如果发生了中断,RISC-V会做什么操作?
答: 我们之前已经在SSTATUS寄存器中打开了中断,所以处理器会被中断。假设键盘生成了一个中断并且发向了PLIC,PLIC会将中断路由给一个特定的CPU核。如果这个CPU核设置了SIE寄存器的E bit(针对外部中断的bit位),那么会发生以下事情:
①清除SIE寄存器相应的bit,这样可以阻止CPU核被其他中断打扰,该CPU核可以专心处理当前中断。处理完成之后,可以再次恢复SIE寄存器相应的bit。
②设置SEPC寄存器为当前的程序计数器。我们假设Shell正在用户空间运行,突然来了一个中断,那么当前Shell的程序计数器会被保存。
③保存当前的mode。在我们的例子里面,因为当前运行的是Shell程序,所以会记录user mode。
④将mode设置为Supervisor mode。
⑤最后将程序计数器的值设置成STVEC的值(STVEC用来保存trap处理程序的地址)。在XV6中,STVEC保存的是uservec或者kernelvec函数的地址,具体取决于发生中断时程序运行是在用户空间还是内核空间。

xv6的中断处理流程

在我们的例子中,Shell运行在用户空间,所以STVEC保存的是uservec函数的地址。而从之前的课程我们可以知道uservec函数会调用usertrap函数。所以接下来看一下trap.c文件中的usertrap函数,我们在lec06和lec08分别在这个函数中处理了系统调用和page fault,今天我们将要看一下如何处理中断。

①在trap.c的devintr函数中,首先会通过SCAUSE寄存器判断当前中断是否是来自于外设的中断。如果是的话,再调用plic_claim函数来获取中断。

②plic_claim函数位于plic.c文件中。在这个函数中,当前CPU核会告知PLIC自己要处理中断,PLIC_SCLAIM会将中断号返回。对于UART来说,返回的中断号是10。

③从devintr函数可以看出,如果是UART中断,那么会调用uart.c中的uartintr函数。uartintr会从UART的接收寄存器中读取数据,然后将获取到的数据传递给consoleintr函数。这里讨论的是向UART发送数据,但是我们现在还没有通过键盘输入任何数据,所以UART的接受寄存器现在为空。所以uartgetc()会返回-1,然后break出来。

④所以uartintr代码会直接运行到uartstart函数,这个函数会将Shell存储在buffer中的任意字符送出。实际上在提示符$之后,Shell还会输出一个空格字符。write系统调用可以在UART发送提示符$的同时,并发的将空格字符写入到buffer中。所以UART的发送中断触发时,可以发现在buffer中还有一个空格字符,之后会将这个空格字符送出。这样,驱动的top部分和bottom部分就解耦开了。
学生提问:UART对于键盘来说很重要,来自于键盘的字符通过UART走到CPU再到我们写的内核代码。但是我不太理解UART对于Shell输出字符究竟有什么作用?因为在这个场景中,并没有键盘的参与。
Frans教授:显示设备与UART也是相连的。所以UART连接了两个设备,一个是键盘,另一个是显示设备,也就是Console。QEMU也是通过模拟的UART与Console进行交互,而Console的作用就是将字符在显示器上画出来。

七、Interrupt相关的并发

接下来我们讨论一下与中断相关的并发,并发加大了中断编程的难度。这里的并发包括以下几个方面:

①设备与CPU是并行运行的。例如当UART向Console发送字符的时候,CPU会返回执行Shell,而Shell会调用其它的系统调用向buffer中写入另一个字符。这里的并行称为producer-consumer并行。
②中断会停止当前运行的程序。例如,Shell正在运行第212个指令,突然来了个中断,Shell的执行会立即停止。
----对于用户空间代码,这并不是一个大的问题。因为当我们从中断中返回时,我们会恢复用户空间代码,并继续执行执行停止的指令。我们已经在trap和page fault中看过了这部分内容。
----当内核被中断打断时,事情就不一样了。所以,代码运行在kernel mode也会被中断,这意味着即使是内核代码,也不是直接串行运行的。在两个内核指令之间,取决于中断是否打开,可能会被中断打断执行。对于一些代码来说,如果不能在执行期间被中断,这时内核需要临时关闭中断,来确保这段代码的原子性。
③驱动的top和bottom部分是并行运行的。例如,Shell会在传输完提示符$之后再调用write系统调用传输空格字符。代码会走到UART驱动的top部分(uartputc函数),将空格写入到buffer中。但是同时在另一个CPU核,可能会收到来自于UART的中断,进而执行UART驱动的bottom部分,查看相同的buffer。所以一个驱动的top和bottom部分可以并行的在不同的CPU上运行,这里我们通过锁来管理并行。因为这里有共享的数据,我们想要buffer在一个时间只被一个CPU核所操作。
producer/consumser并发
这里我们主要关注producer/consumser并发,这是驱动中的非常常见的典型现象。正如你们所见,在驱动中会有一个buffer,在我们之前的例子中,buffer是32字节大小。除此之外还有两个指针,分别是读指针和写指针。

如果两个指针相等,那么buffer是空的。当Shell调用uartputc函数时,会将字符(例如提示符$)写入到写指针的位置,并将写指针加1。这就是producer对于buffer的操作。

----producer可以一直写入数据,直到写指针+1等于读指针,因为这时buffer已经满了。当buffer满了的时候,producer必须停止运行。我们之前在uartputc函数中看过,如果buffer满了,代码会sleep,暂时搁置Shell并运行其他的进程。
----中断处理程序(也就是uartintr函数)在这个场景下是consumer,每当有一个中断,并且读指针落后于写指针,uartintr函数就会从读指针中读取一个字符再通过UART设备发送,并且将读指针加1。当读指针追上写指针,也就是两个指针相等的时候,buffer为空,这时就不用做任何操作。

问答

学生提问:这里的buffer对于所有的CPU核都是共享的吗?
Frans教授:这里的buffer存在于内存中并且只有一份,所以所有的CPU核都并行的与这一份数据交互,因此我们才需要锁。
学生提问:对于uartputc中的sleep,它怎么知道应该让Shell去sleep,那里写入的只有地址。

Frans教授: sleep会让当前运行的进程进入睡眠状态,sleep函数传入的是正在等待的东西。在这种情况下,这个地址有一个channel id或者一种与sleep程序交互的方式。在这个例子中传入的是uart_tx_r的地址。在uartstart函数中,一旦buffer中有了空间,会调用与sleep对应的函数wakeup,传入的也是uart_tx_r的地址。

任何等待在这个地址的进程都会被唤醒。所以sleep和wakeup这两个调用是相互的,有时候这种机制被称为conditional synchronization(条件同步)。以上就是Shell输出提示符$ 的全部内容。如你们所见,过程还挺复杂的,许多代码一起工作才将这两个字符传输到了Console。

八、UART读取键盘输入

在UART的另一侧,会有类似的事情发生:

在某个时候,Shell打印$和 ,然后会调用read读取来自键盘的输入。在read系统调用的底层,会调用fileread函数。在这个函数中,如果读取的文件类型是设备,会调用相应设备的read函数。

在我们的例子中,read函数就是console.c文件中的consoleread函数。

这里与UART类似,也有一个buffer,包含了128个字符。其他的基本一样,也有producer和consumser。但是在这个场景下Shell变成了consumser,因为Shell是从buffer中读取数据。而键盘是producer,它将数据写入到buffer中。

从consoleread函数中可以看出,当读指针和写指针一样时,说明buffer为空,进程会sleep。
----所以Shell在打印完$ 之后,如果键盘没有输入,Shell进程会sleep,直到键盘有一个字符输入。
----所以在某个时间点,假设用户通过键盘输入了l,这会导致l被发送到主板上的UART芯片,产生中断之后再被PLIC路由到某个CPU核。
----之后会触发devintr函数,devintr可以发现这是一个UART中断,然后通过uartgetc函数获取到相应的字符,之后再将字符传递给consoleintr函数。

----默认情况下,字符会通过consputc输出到console上给用户查看。之后,字符被存放在buffer中。在遇到换行符的时候,唤醒之前sleep的进程(也就是Shell),再从buffer中将数据读出。
----所以这里也是通过buffer将consumer和producer之间解耦,这样它们才能按照自己的速度独立地并行运行。如果某一个运行的过快了,那么buffer要么是满的要么是空的,consumer和producer其中一个会sleep并等待另一个追上来。

九、中断的演进

最后我们介绍一下Interrupt在最近几十年的演进。

①当Unix刚被开发出来的时候,Interrupt处理还是很快的,这使得硬件可以很简单。当外设有数据需要处理时,硬件可以中断CPU的执行,并让CPU处理硬件的数据。
②而现在,中断相对处理器来说变慢了。从前面的介绍可以看出来这一点,需要很多步骤才能真正的处理中断数据。如果一个设备在高速的产生中断,处理器将会很难跟上。所以如果查看现在的设备,可以发现现在的设备相比之前做了更多的工作。所以在产生中断之前,设备上会执行大量的操作,这样可以减轻CPU的处理负担。所以现在硬件变得更加复杂。
③如果你有一个高性能的设备,例如你有一个千兆网卡,这个网卡收到了大量的小包,网卡每秒可以生成1.5Mpps。这意味着每一个微秒,CPU都需要处理一个中断,这就超过了CPU的处理能力。那么当网卡收到大量包,并且处理器不能处理这么多中断的时候该怎么办呢?
----这里的解决方法就是使用轮询(polling)。除了依赖Interrupt,CPU可以一直读取外设的控制寄存器来检查是否有数据。对于UART来说,我们可以一直读取RHR寄存器来检查是否有数据。
----这种方法浪费了CPU cycles,当我们在使用CPU不停地检查寄存器的内容时,我们并没有用CPU来运行任何程序。在我们之前的例子中,如果没有数据,内核会让Shell进程sleep,这样可以运行另一个进程。
----所以,对于一个慢设备,你肯定不想一直轮询它来得到数据,我们想要在没有数据的时候切换出来运行一些其他程序。但是如果是一个快设备,那么中断的开销也会很高,那么我们最好还是轮询设备,因为我们很快就能获得数据 。
----所以对于一个高性能的网卡,如果有大量的包要传入,那么应该用polling。对于一些精心设计的驱动,它们会在轮询和中断之间动态切换(也就是网卡的NAPI)。

十、问答

学生提问:uartinit只被调用了一次,所以才导致了所有的CPU核都共用一个buffer吗?
Frans教授:这里只有一个UART设备,一个buffer只针对一个UART设备,而这个buffer会被所有的CPU核共享,这样运行在多个CPU核上的多个程序可以同时向控制台打印输出。
学生提问:所以只有一个队列会使用UART执行任务?
Frans教授:是的,这有点复杂。假设有多个核心正在将一个字符放入缓冲区,所以在uartputc中第一件事就是需要一把锁,让这些核心中的一个获得锁。获得锁的那个CPU可以查看写指针,把字符放入其中,否则就去sleep。当它完成了所有事情之后就可以解锁,然后下一个核心可以进入,获取锁并做一些事情。所以这个锁会序列化对UART的并发访问。

学生提问:我们之所以需要锁是因为有多个CPU核,但是却只有一个Console,对吧?
Frans教授:是的,如我们之前说的驱动的top和bottom部分可以并行运行。所以一个CPU核可以执行uartputc函数,而另个一CPU核可以执行uartintr函数,我们需要确保它们是串行执行的,而锁确保了这一点。
学生提问:那是不是意味着,某个时间其他所有的CPU核都需要等待某一个CPU核的处理?
Frans教授:这里并不是死锁,其他的CPU核还是可以在等待的时候运行别的进程。这里的大概意思是如果有多个调用uartputc,此时缓冲区已满,之后在某个时刻这个中断会释放锁。这里会调用sleep,而sleep使用那个锁作为参数。在sleep将进程睡眠之前,它会释放那个锁。
学生提问:所以是调用sleep带上锁参数,然后意味着可以在睡眠的时候释放锁,让其他进程去做。然后当从sleep返回后,再次获取锁。
学生提问:当UART触发中断的时候,所有的CPU核都会被中断吗?
Frans教授:取决于你如何对PLIC进行编程。对于XV6来说,所有的CPU都能收到中断,但是只有一个CPU会Claim相应的中断。
学生回答:懂了,PLIC不是软件实现,而是硬件实现。

学生提问:在Prime实验中,我们看到实际上是交错输出,是不是因为锁只是在putc,但是来自多个核心的putc调用可以交错,这意味着单个printf不能保证是原子的。
Frans教授:是的。

学生提问:计时器中断是在机器模式下处理的,我想知道我们在做traps实验时,它是在哪里处理的。比如在traps实验中,在哪里切换到机器模式。
Frans教授:我们查看start代码,可以看到机器启动的开始时间,这个start运行在M模式,它会对计时器检查进行编程。

timeinit对CLINT编程,这里这个是本地中断,在时钟中断发生时生成中断。

这个w_mtvec是最重要的函数,它将机器模式的 trap 处理函数设置为这个 timervec函数。

这个timervec函数是由汇编编写,当计时器中断发生时,这个函数会被调用。所以,在user mode和supervisor mode时,CLINT 生成一个中断到达这一行,它将切换到机器模式并调用timervec。我们看下Kerbelvec.S,看下timervec,它做的就是在这里编写几行代码对CLINT重新编程以生成未来的中断,然后向管理程序发出软件中断。然后在mret的时候,从机器模式返回到管理员模式。如果中断启用了管理者模式,这个时候可能产生管理者软件中断。现在内核也会做同样的事情,转到kernelvec,保存恢复所以寄存器,然后转到内核trap。内核trap会看到那是计时器中断。

操作系统MIT6.S081:P7->Interrupts相关推荐

  1. 操作系统MIT6.S081:[xv6参考手册第4章]->Trap与系统调用

    本系列文章为MIT6.S081的学习笔记,包含了参考手册.课程.实验三部分的内容,前面的系列文章链接如下 操作系统MIT6.S081:[xv6参考手册第1章]->操作系统接口 操作系统MIT6. ...

  2. 操作系统MIT6.S081:Lab4->Trap

    本系列文章为MIT6.S081的学习笔记,包含了参考手册.课程.实验三部分的内容,前面的系列文章链接如下 操作系统MIT6.S081:[xv6参考手册第1章]->操作系统接口 操作系统MIT6. ...

  3. 操作系统-MIT6.S081学习

    操作系统 Lec01 1.2 操作系统结构 以距型表示: 硬件资源包括:CPU.内存.磁盘.网卡,在底层 架构上层,需要运行各种的应用程序,例如 文本编译器(vi).c编译器(cc),cli 存在的 ...

  4. MIT6.S081 2021

    MIT6.S081 2021 环境配置 Xv6 and Unix utilities vscode格式化头文件排序问题 以地址空间的视角看待变量 其他 代码参考 system calls trace ...

  5. Mit6.S081学习记录

    Mit6.S081学习记录 前言 一.课程简述 二.课程资源 1,课程主页 2,参考书 3,实验环境 三.学习过程 Mit6.S081-实验环境搭建 Mit6.S081-GDB使用 Mit6.S081 ...

  6. MIT6.S081 2021 Copy-on-Write Fork for xv6

    MIT6.S081 2021 Copy-on-Write Fork for xv6 简要介绍 debug 代码参考 简要介绍 There is a saying in computer systems ...

  7. MIT6.S081简单总结

    在复盘MIT6.828的时候,偶然看到了一个将MIT6.S081的视频翻译成文字的gitbook:https://mit-public-courses-cn-translatio.gitbook.io ...

  8. MIT6.S081 Multithreading

    MIT6.S081 Multithreading xv6 book记录 Uthread Using threads Barrier xv6 book记录 阅读xv6 book之前,简要看一下<深 ...

  9. MIT6.S081操作系统实验——操作系统是如何在qemu虚拟机中启动的?

    前言 为了更好的理解基于RISC-V体系的Xv6操作系统是如何在qemu中启动的,我将详细地梳理从执行make qemu命令开始到Xv6的shell启动为止的具体流程. 执行make qemu后发生了 ...

最新文章

  1. 简单介绍oracle重置序列的方法
  2. HDU 1429 胜利大逃亡(续)
  3. 阿里云打好云基础 敬候电商开发者上云
  4. java对象创建的流程到底是什么样子的?new一个对象是真的直接放在堆里吗?其实大有学问!
  5. 运行连接远程服务器失败,应用技巧:如何应付远程控制服务器失败
  6. python中的sklearn.svm.svr_支持向量机SVM--sklearn 参数说明
  7. F: 绝对值 山东科技大学OJ C语言
  8. *Algs4-2.4.23Multiway的堆(未解决)
  9. 2019南昌网络赛H The Nth Item(二阶线性数列递推 + 广义斐波那契循环节 + 分段打表)题解...
  10. android状态栏自定义,如何自定义Android状态栏颜色
  11. 服务器上ssl协议禁止,“SSL协议被禁用,无法安全的连接”怎么处理
  12. cesium获取经纬度
  13. 搜集的育儿电子书下载大全
  14. 一台计算机英语美式发音,请教美式英语的几个发音问题!
  15. 解决 Chrome 浏览器跨域加载本地文件的问题
  16. 人民日报喊你学数学!实力不允许?8本书带你入门
  17. 如何提升自己的网站打开速度
  18. 【专家级前端JavaScript面试题】快来尝试一下
  19. 注册Apple ID
  20. 【VMCloud云平台】SCAP(四)租户(二)

热门文章

  1. 光眼图和电眼图_直调光模块中ER和OMA
  2. Android 最完整的开发工具类(blankj:utilcode)
  3. 重庆python培训价格
  4. 员工考勤管理系统html,一种员工考勤管理系统的制作方法
  5. 当我写博客时,我在想什么
  6. 谷歌实现移动VR“白日梦”
  7. Python可变/不可变对象
  8. 编写Java程序,模拟教练员和运动员出国比赛场景,其中运动员包括乒乓球运动员和篮球运动员。教练员包括乒乓球教练和篮球教练。为了方便出国交流,根乒乓球相关的人员都需要学习英语。
  9. C#项目:未找到引用错误解决方案
  10. require https rid: 5f30fa30-76a72ecb-495cddc1