因为我硬件知识有点欠缺,所以碰到硬件相关的都尽量专门写一篇笔记。
物理设备和设备控制器和中断控制器:网卡,声卡,显卡,键盘鼠标这些所有都是物理设备,而管理这些设备的叫设备控制器。CPU只和设备控制器进行交互,而不关心背后的设备具体如何。这节课中我们主要学习的设备控制器是UART,它并不管理具体的某个设备,而是在QEMU模拟器的帮助下管理从宿主机传进来的输入数据流和发往宿主机中断的输出数据流。设备控制器能够直接和CPU交互,即内核可以从设备控制器输入输出信息。但是设备控制器需要引起CPU关注时,它作为中断源,必须向中断控制器发送信号,中断控制器负责仲裁优先级,最后向CPU发起真正的中断。
而RISC-V中,负责记录和分发中断的组件是PLIC。
上面是我们之前已经在中断那里学到过的。现在我们结合一下E1000这里的例子深入一下。
E1000接收数据包时产生的中断信号传递过程:
1.E1000网卡内部:当数据包到达时,DMA完成,描述符写回后,中断信号会记录在E1000内部的ICR(Interrupt Cause Register)寄存器中。只要ICR里有未被清除的位,网卡就会持续拉高它连接到PLIC的那根物理导线为高电平。
2.PLIC:只要E1000拉高了导线,PLIC的网关就会立刻感知到。在PLIC的内部有一个Pending Bit Array(等待位数组),对应E1000的那个比特位会被置为1。哪怕CPU正在关中断,PLIC依然会将这个Pending Bit置为1.
3.CPU最终感知:当CPU开启中断时,CPU就会看到一个外部中断请求,然后跳转到trap处理程序。在trap处理程序中,会调用plic_claim()函数,然后PLIC将Pending Bit置零。
为什么我们在e1000_recv函数中一次只处理一个数据包可能会导致丢失中断信号:
void e1000_intr(void) { // 1. 读取 ICR 寄存器,看看发生了什么事 // 【关键动作】:读取 ICR 会自动清除 E1000 内部的中断标志! // 这相当于告诉网卡:“我知道了,别吵了,把手放下。” int icr = regs[E1000_ICR]; // 2. 调用你的接收函数 e1000_recv(); }1.数据到达:假设在调用e1000_intr之前,qemu瞬间往接收环中塞了5个数据包。E1000网卡检测到数据,拉高中断线,PLIC看到高电平,将对应的Pending Bit置位。
2.CPU响应:CPU发现Pending位,跳转到e1000_intr。Pending Bit被置零,e1000_intr函数读取ICR寄存器,将中断线拉低,此时,物理层面上没有任何中断信号了。
3.e1000_recv:只处理第一个包,显然,后面包的中断信号全部丢失。
为什么批量处理能够避免丢失中断信号:
1.数据到达:和之前相同,到达5个数据包。
2.CPU响应:在PLIC将对应的Pending Bit置零,E1000信号线拉低。
3.e1000_recv:在e1000_recv中,由于我们采用的是批量处理的逻辑,所以所有的这5个数据包都会被读入。而假如在处理过程中,数据包再次到达,那么会重新将E1000信号线拉高,Pending Bit置位。当CPU处理完这个中断后,又能看到下一次中断的信号,然后继续处理第二次中断的数据包。
总结:之所以采用批量处理,是因为不论PLIC的Pending Bit还有信号线为高电平还是低电平都只能记录0或1。假如它们能够记录更大的值,那么我们就可以在每次一个数据包到达时使intrs++,然后CPU根据intrs的值来知道有多少次中断信号,这样就不会丢失中断信号了。
但是,由于只能记录0和1,所以0和1只能抽象为无数据包和有数据包的简单逻辑,而我们的recv函数也只知道有数据包,而不知道有多少数据包,因此采取批量处理是必然的。
推而广之,硬件中断对于 CPU 来说只是一个唤醒通知,而不是一个精确的任务计数器。作为驱动程序,收到通知后必须主动检查并处理所有堆积的任务,直到缓冲区为空。