Loko 本质上将设备驱动程序视为通信顺序进程。来自设备的 IRQ 被视为从设备到驱动程序的原始消息。配置设备或在设备与系统其余部分之间传输数据的请求也通过消息完成。设备驱动程序与其他 Scheme 程序没有本质区别。
本节使用术语 interrupt 表示处理器正常执行顺序的中断;IRQ 表示来自硬件设备的 interrupt;trap 表示来自处理器本身的 interrupt。还有其他类型的 interrupt,但这里不讨论。
IRQ 的处理方式与普通内核中的处理方式不同。来自 Richard P. Gabriel 的 The Rise of Worse is Better 中的一则轶事与此相关:
两位著名人物,一位来自 MIT,另一位来自 Berkeley(但从事 Unix 工作),曾会面讨论操作系统问题。来自 MIT 的人熟悉 ITS(MIT AI Lab 操作系统),并且一直在阅读 Unix 源代码。他对 Unix 如何解决 PC loser-ing 问题感兴趣。PC loser-ing 问题发生在用户程序调用系统例程执行一个可能具有重要状态的长时间操作(如 IO 缓冲区)时。如果操作期间发生中断,用户程序的状态必须被保存。由于系统例程的调用通常是单条指令,用户程序的 PC 不能充分捕获进程的状态。系统例程必须要么回退,要么继续前进。正确的做法是回退并将用户程序 PC 恢复到调用系统例程的指令,以便中断后恢复用户程序时,例如,重新进入系统例程。之所以称为 PC loser-ing,是因为 PC 被强制进入 loser 模式,其中 loser 是 MIT 对用户的亲切称呼。
MIT 的人没有看到任何处理这种情况的代码,因此问 New Jersey 的人这个问题是如何处理的。New Jersey 的人说 Unix 的人意识到了这个问题,但解决方案是让系统例程总是完成,但有时会返回一个错误代码,表示系统例程未能完成其操作。然后,正确的用户程序必须检查错误代码,以确定是否简单地重试系统例程。MIT 的人不喜欢这个解决方案,因为这不是正确的做法。
我们从 New Jersey 的人那里得到了 errno 值 -EINTR,所以他显然是更成功的一位。MIT 人的方法会是仔细设计系统调用,使其将完整状态保存在结构体或系统调用的参数中,这有点像在中断到来时做一个非常手动且繁琐的 call/cc。而没有人有时间做这个。
但当 New Jersey 的人觉得即使是 -EINTR 也太难管理时,我们得到了 uninterruptible sleep。这是一种程序没有运行,但也无法被杀死的状态。这发生在程序阻塞在系统调用中,同时持有内核中未知数量的锁和其他状态时。也许它在从文件读取,但某些东西卡住了并停止响应。进程不是收到错误代码,而是进入 uninterruptible sleep。这慢慢地从一个程序蔓延到另一个程序,因为其他方试图与冻结的进程通信。
所以 Loko 对这一切采取了不同的方法。
IRQ 由硬件设备在其需要处理器关注时产生。一个例子是刚收到一个字节的 UART(串行端口控制器)。它将产生一个 IRQ 让驱动程序从其缓冲区读取该字节。
处理 IRQ 的正常思路基本如下:安装作为驱动程序一部分的中断服务例程(ISR),将设备重置为正常状态,然后在中断控制器中启用 IRQ。ISR 是一段特殊的代码,必须准备好在任何时间运行(除了中断被禁用时)。通常 IRQ 有优先级顺序,因此它们可以被更高优先级的 IRQ 中断。无论哪种方式,当 IRQ 到来时,驱动程序快速服务硬件。在 UART 的例子中,它将从 UART 读取一个字节并将其放入软件控制的缓冲区中。
在这种做法中,不幸的是对 ISR 中可以做什么有严格的限制。ISR 在 interrupt context 中运行,其中许多通常的内核服务根本无法使用。任何会使程序阻塞的操作通常不可用,对内存的访问也有限。安排任意 Scheme 代码在 interrupt context 中运行是困难的。
出于这个原因,Loko 不在 ISR 中运行驱动程序代码。相反,ISR 是最小的代码片段,与进程调度器协作使驱动程序的进程变为可运行。IRQ 作为消息发送给进程,进程在空闲时处理它。
这种方法有些实验性,在传统硬件(如 UART)中可能存在一些延迟问题,但现代设备使用 总线主控 DMA,通常对中断服务延迟不敏感。总线主控 DMA 意味着设备可以访问系统内存。通常这样的设备与驱动程序协作,在系统内存中维护描述数据传输的队列。
Loko 的方法有优点也有缺点。缺点 IRQ 处理有一定延迟,但这对于现代设备通常不是问题。优点是它使驱动程序代码 非常 容易编写和维护。由于它消除了编写驱动程序的许多常见困难,甚至可能意味着大多数有能力接触硬件编程手册的 Scheme 程序员都可以编写设备驱动程序。(手动内存管理仍然存在一些困难,但它们并不难处理)。
即使延迟是潜在问题,Loko 的方法在吞吐量方面应该表现良好。驱动程序可以轻松地决定数据到来的速率如此之高以至于不需要使用中断,而是切换到周期性轮询。Linux 在其网络栈中使用了这种技术,名为神秘的 "New API"(NAPI)。
Loko 方法的另一个好处是解决了 "worse is better" 故事中的困境。用户程序永远不需要获得类似 -EINTR 的东西,程序员也不需要手动跟踪他们在系统调用处理中的位置。
Loko 尽可能将错误检查卸载到硬件内置机制上。它不使用显式类型检查,而是让硬件执行类型检查(在可能的情况下)。
诸如 (/ 1 0) 之类的编程错误在大多数编程环境中通常由硬件发出信号。Loko 进一步扩展了它,将其扩展到诸如 (car #f)、(vector-ref "foo" 0) 和 (1 + 2) 之类的错误。Loko 使用处理器的对齐检查功能来捕获对 pair、过程、字符串、向量和 bytevector 的错误使用。你可以在
Faster Dynamic Type Checks 中阅读更多信息。
中断处理程序位于 (loko arch amd64 pc-interrupts) 中,用汇编编写。
有两种根本不同的 interrupt 类型,它们都称为 interrupt,使用类似的机制,但来源非常不同。
Trap 是由运行程序中某些错误类触发的 interrupt。对于这些 interrupt,中断处理程序使程序调用用 Scheme 编写的错误处理程序。没有注意保留程序的当前栈帧或当前寄存器值,这意味着一些有用的调试信息会丢失。虽然这是不幸的,应该修复,但它在语义上仍然是正确的,因为这些错误都被归类为 &serious。
Trap 由中断描述符表(IDT)中的条目处理。IDT 控制处理器是否应更改特权级别、应跳转到哪个地址、应使用哪个栈以及是否应自动禁用 interrupt。
处理器将一些程序状态推入栈中,并将控制权交给处理程序。对于 trap,处理程序识别 trap 的原因并决定哪个库函数应接管程序的控制权。然后它对该函数执行尾调用。这些函数位于 (loko arch amd64 lib-traps) 中,负责调用用 Scheme 编写的错误处理程序。
Loko 中的 IRQ 处理依赖于处理器的 interrupt stack table(IST)和中断控制器的 specific end of interrupt(SEOI)和 special mask mode。在 AMD64 系统上,SEOI 功能在传统 PIC 和 APIC(早期版本除外)中可用。中断优先级(嵌套)不打算使用。
中断屏蔽是 Loko IRQ 处理中非常重要的部分。有三个地方可以屏蔽 interrupt:设备本身、中断控制器和处理器。驱动程序配置设备以适合驱动程序的方式产生 interrupt。中断控制器负责将 interrupt 传递给处理器,并可由处理器要求屏蔽某个 interrupt。中断控制器还跟踪哪些 interrupt 正在被服务,并临时屏蔽它们直到被确认。最后,处理器也可以通过标志寄存器中的 ‘RFLAGS.IF’ 位屏蔽 interrupt。当 ‘RFLAGS.IF’ 清零时,不会传递 interrupt。
Loko 的 IRQ 处理系统依赖于使用 ‘RFLAGS.IF’ 位屏蔽 interrupt 和延迟确认。‘RFLAGS.IF’ 用于在调度器运行时屏蔽 interrupt。当在调度器内部时,interrupt 只能在一个非常受控的情况下发生:在 sys_hlt 系统调用中。这用于没有进程可运行时,让处理器节省能量。sys_hlt 系统调用在 IRQ 被传递到处理器时返回。然后调度器找到注册了该 IRQ 的驱动程序进程,将其作为消息入队并使进程变为可运行。
正常进程始终在 interrupt 未屏蔽的情况下运行,除了可能在任务切换期间的一些短暂且棘手的时刻。当 interrupt 到来时,处理器使用 IST 切换到不同的栈。这对于避免破坏进程的 Scheme 栈是必要的。所有寄存器都保存在进程控制块中,以便以后可以恢复进程。IRQ 处理程序恢复调度器并让它知道发生了什么。
当进程恢复时,它的消息队列中将有一个 IRQ 号。进程何时想要出队此消息取决于它,但通常驱动程序进程会等待 IRQ 的到来。每当进程调用调度器时,它也可能收到待处理的消息。因此两种情况是:进程由于 IRQ 而变为可运行,以及进程已经可运行并将在以后看到 IRQ。
被通知 IRQ 的进程将处理它们,执行服务硬件中 IRQ 所需的任何任务,之后告诉调度器确认 IRQ。然后调度器向中断控制器发送指令以确认该特定 interrupt。此时设备可能再次选择在条件合适时产生 interrupt。
涉及驱动程序 IRQ 代码的编程错误有几类。驱动程序可能认为它已处理了一个 IRQ 并发送确认。然而,如果它遗漏了一个 interrupt 原因,设备可能会在确认后立即再次产生相同的 IRQ。这会因驱动程序中的不必要工作而减慢系统速度。
如果驱动程序没有正确处理所有 interrupt 原因,也可能发生相反的情况。症状可能是 IRQ 到达,似乎被驱动程序处理了,但之后再也没有 IRQ 到达,设备似乎冻结了。哪种错误更可能取决于设备使用边沿触发还是电平触发 interrupt。检查是否发生这种情况的一种方法是在等待 interrupt 时添加超时。
如前所述,Loko 对 IRQ 的处理是实验性的。迄今为止已观察到以下问题:
UART 和 i8042 可能需要特殊的中断处理程序。但它们是传统设备,许多 amd64 硬件甚至没有它们。
信号处理程序位于 (loko arch amd64 linux-start) 中。
Linux 下的信号类似于 interrupt。就像 interrupt 一样,一些信号是 trap(‘BUS’,‘SEGV’,‘FPE’,‘ILL’ 等)。Linux 将处理器的 trap 号放在信号处理程序的 sigcontext 中,这使得可以像在裸机硬件上处理 trap 那样处理它们。
其他信号来自外部来源,程序是无辜的,因此必须保留当前栈帧。在裸机硬件上,这通过 IST 实现,Linux 上的等效机制称为 sigaltstack(2)。
正常的用户空间 ABI(文档见 System V Application Binary Interface AMD64 Architecture Processor Supplement)没有被遵循。通常 interrupt 会在程序当前使用的同一个栈上传递,但这会因为 Loko 管理栈帧的方式而破坏 Scheme 代码。"red zone" 概念在 Loko 中不存在。