内核角落 - 适用于 AMD64 的内核模式 Linux

作者:Toshiyuki Maeda

内核模式 Linux (KML) 是一项使用户进程能够在内核模式下执行的技术。在我之前的文章“内核模式 Linux”中,我描述了 IA-32 架构上 KML 的基本概念和实现技术,该文章发表在 2003 年 5 月的《Linux Journal》杂志上(请参阅在线资源)。从那时起,我扩展了 KML 以支持 AMD64 或 x86-64 架构,它是 IA-32 架构的可行 64 位扩展。在本文中,我将简要介绍 KML 的背景,然后展示 AMD64 架构的 KML 实现技术。此外,还将介绍使用 MySQL 进行的性能实验结果。

硬件保护问题

传统的操作系统内核通过使用 CPU 的硬件设施来保护自身。例如,Linux 内核使用 CPU 内置的特权级别机制和内存保护机制来保护自身。因此,为了使用内核的服务,例如文件系统或网络,用户程序必须执行代价高昂且复杂的硬件操作。

例如,在适用于 AMD64 的 Linux 中,用户程序必须使用特殊的 CPU 指令 (SYSCALL/SYSRET) 才能使用内核服务。SYSCALL 可以被视为一种特殊的跳转指令,其目标地址受到内核的限制。为了利用系统服务,或者换句话说,调用系统调用,用户程序执行 SYSCALL 指令。然后,CPU 将其特权级别从用户模式提升到内核模式,并跳转到 SYSCALL 的目标地址,该地址由内核预先指定。然后,位于目标地址的代码通过使用 SWAPGS 指令将 CPU 的上下文从用户上下文切换到内核上下文。最后,它执行请求的系统服务。为了返回到用户程序,SYSRET 指令会反转这些步骤。

然而,这种硬件保护方法存在一些问题。一个问题是系统调用变得缓慢。例如,在我的 Opteron 系统上,SYSCALL/SYSRET 比仅仅的函数调用/返回慢大约 50 倍。

加速系统调用的一种显而易见的解决方案是在内核模式下执行用户进程。这样,系统调用就只能是通常的函数调用,因为用户进程可以直接访问内核。当然,让用户进程在内核模式下运行是危险的,因为它们可以访问内核的任意部分。

确保安全的一种简单解决方案是使用虚拟机 (VM) 技术,例如 VMware 和 Xen。如果用户程序和内核在虚拟内核模式下执行,用户程序可以直接访问内核。然而,这种虚拟机保护方法效果不佳,因为虚拟化的开销相当大。此外,尽管虚拟机可以防止用户程序破坏虚拟机外部的主机系统,但它无法防止它们破坏虚拟机内部的内核。即使诸如英特尔的 Vanderpool 和 AMD 的 Pacifica 等 CPU 为虚拟化提供了更好的支持,这些困难也不太可能得到解决。

安全地在内核模式下执行用户进程的推荐方法是使用安全语言,也称为强类型语言。静态程序分析或类型理论的最新进展可用于保护内核免受用户进程的侵害。例如,许多技术已经实现了这种软件保护方法,例如 Java 字节码、.NET CLI、Objective Caml、类型化汇编语言 (TAL) 和携带证明代码 (PCC)。我目前正在实现一种 TAL 变体,它足够强大,可以编写操作系统内核。

基于这个想法,我为 IA-32 实现了内核模式 Linux (KML),这是一个修改后的 Linux 内核,可以在内核模式下执行用户进程,称为内核模式用户进程。我之前的文章描述了 IA-32 的 KML。从那时起,我实现了 AMD64 的 KML,因为 AMD64 已经作为 IA-32 的可能继任者而被广泛使用。有趣的是,尽管 IA-32 和 AMD64 之间存在相似之处,但这两种架构的 KML 实现技术却大相径庭。因此,在本文的其余部分,我将描述 AMD64 的 KML 的基本概念、用法和实现技术。

如何使用 AMD64 的 KML

KML 以补丁的形式提供给原始 Linux 内核的源代码。要使用 KML,您所要做的就是使用 KML 补丁修补原始 Linux 内核的源代码,并在配置阶段启用内核模式 Linux 选项,就像您可能对其他内核补丁所做的那样。KML 补丁可从 KML 站点获得(请参阅资源)。

在当前的 KML 中,/trusted 目录下的程序作为内核模式用户进程执行。因此,如果您想在内核模式下执行 bash,您所要做的就是执行以下命令

% cp /bin/bash /trusted/bin
% /trusted/bin/bash
如何加速系统调用调用

在 IA-32 的 KML 中,系统调用调用会自动转换为快速的直接函数调用,而无需修改用户程序。这是可能的,因为最新的 IA-32 GNU C 库具有一种机制,可以选择内核提供的几种系统调用调用方法之一,而 KML 提供了直接函数调用作为调用系统调用的一种方式。

然而,AMD64 的 GNU C 库没有这种在系统调用调用方法之间进行选择的机制。因此,我为 GNU C 库创建了一个补丁。使用该补丁,内核模式用户进程可以快速调用系统调用,因为调用会自动转换为函数调用。该补丁可从 KML 站点获得(请参阅资源)。

内核模式用户进程可以做什么

KML 的优势之一是内核模式用户进程几乎与普通用户进程相同,除了它们的特权级别。也就是说,内核模式用户进程几乎可以做普通用户进程可以做的任何事情。例如,内核模式用户进程可以调用所有系统调用。这意味着它们可以使用文件系统。它们还可以调用 open、read、write 和其他函数,包括网络系统,以及 socket、connect 和 bind。它们甚至可以使用 fork、clone 和 execve 创建进程和线程。此外,它们拥有自己的内存地址空间,可以自由访问。即使内核模式用户进程使用大量内存,内核也会分页输出内存。

此外,原始 Linux 内核的调度机制和信号机制适用于内核模式用户进程。您可以通过执行以下命令来检查这一点

% cp /usr/bin/yes /trusted/bin
% /trusted/bin/yes

您应该注意到您的系统没有挂起。这是真的,因为内核的调度程序抢占了内核模式的 yes,并将 CPU 时间分配给其他进程。您可以通过发送 Ctrl-C 来停止内核模式的 yes。这意味着内核可以中断内核模式的 yes 并发送信号来终止它。

内核模式用户进程不能做什么

如上一节所述,内核模式用户进程是普通用户进程,几乎可以执行用户进程可以执行的每项任务。但是,也有一些例外

  1. 内核模式用户进程不能修改它们的 GS 段寄存器,因为 KML 在内部使用 GS 段寄存器来消除 SWAPGS 指令的开销。

  2. 32 位二进制文件不能在 AMD64 上以内核模式执行。适用于 AMD64 的 KML,像其他典型的适用于 AMD64 的操作系统内核一样,以 64 位模式运行,并且没有有效的方法让 32 位程序直接调用 64 位函数。

请注意,与 IA-32 的 KML 的情况一样,这些限制仅存在于内核模式用户进程中。普通用户进程可以更改它们的 GS 选择器,并且如果设置了 IA-32 仿真环境,则可以执行 IA-32 二进制文件。

KML 如何在内核模式下执行用户进程

在 AMD64 中,在内核模式下执行用户进程的方式与在 IA-32 中几乎相同。要在内核模式下执行用户进程,KML 唯一要做的就是使用 CS 段寄存器启动用户进程,该寄存器指向内核代码段而不是用户代码段。

在 AMD64 CPU 中,运行程序的特权级别由其代码段的特权级别决定。这与 IA-32 CPU 中几乎相同;唯一的区别是分段内存系统在 AMD64 中退化了。尽管段寄存器仍然在 AMD64 的 64 位模式中使用,但段寄存器可以使用的唯一段是 16 EB 平面段。因此,段描述符的作用只是指定特权级别。因此,在 64 位模式下,只存在四个段——内核代码段、内核数据段、用户代码段。

堆栈饥饿问题及其解决方案

尽管如上一节所示,在内核模式下执行用户进程相当容易,但存在一个大问题——堆栈饥饿问题。问题本身与 IA-32 的 KML 的问题几乎相同,因此我在此处简要描述一下。更多详细信息请参见我之前的文章。

适用于 AMD64 的原始 Linux 内核通过使用传统的门中断机制来处理中断和异常。对于每个中断/异常,内核预先(通常在启动时)使用中断门指定中断处理程序。如果发生中断,AMD64 CPU 会暂停正在运行的程序,保存程序的执行上下文,并执行中断门中指定的相应中断处理程序。

重要的一点是 AMD64 CPU 可能或可能不会在保存执行上下文之前切换堆栈,具体取决于暂停程序的特权级别。如果程序在用户模式下运行,CPU 会自动从运行程序的堆栈切换到内核堆栈,而如果程序在内核模式下运行,CPU 则不会切换堆栈。然后,CPU 将执行上下文——RIP、CS、RFLAGS、RSP 和 SS 寄存器——保存到堆栈中。

现在,让我们假设内核模式用户进程访问其内存堆栈,该堆栈未由 CPU 的页表映射。首先,CPU 引发页错误异常,暂停进程并尝试保存执行上下文。然而,这是无法完成的,因为 CPU 不会切换堆栈,并且 CPU 准备保存上下文的堆栈不存在。为了发出这个严重情况的信号,CPU 尝试引发一个特殊的异常,即双重错误异常。同样,CPU 尝试访问不存在的堆栈以保存上下文。最后,CPU 放弃并重置自身。这个过程被称为堆栈饥饿问题。

为了解决堆栈饥饿问题,IA-32 的 KML 使用 IA-32 CPU 的任务管理机制。该机制可用于在引发中断或异常时切换 CPU 上下文,包括所有寄存器和所有段寄存器。当引发双重错误时,IA-32 的 KML 使用该机制切换堆栈。然而,在 AMD64 上的 64 位模式下,任务管理机制无法使用,因为它根本不存在。

相反,AMD64 的 KML 使用中断堆栈表 (IST) 机制,这是 AMD64 架构新引入的机制。在 AMD64 中,任务状态段 (TSS) 具有七个指向中断堆栈的指针字段。此外,每个中断门描述符都有一个字段,用于指定 CPU 是否应该使用 IST 机制而不是传统的堆栈切换,如果应该使用,则指定应该使用哪个中断堆栈。如果发生指定使用 IST 机制的中断,CPU 将无条件地从用户堆栈切换到中断门描述符中指定的中断堆栈。

在 AMD64 的 KML 中,所有中断和异常都使用 IST 机制处理。因此,即使在内核模式用户进程运行时,当其 %rsp 指向无效内存时,发生中断或异常,内核也可以继续运行而不会出现任何问题,因为 CPU 会自动切换堆栈。

AMD64 的 KML 不仅使用 IST 机制处理双重错误,还处理其他中断和异常,原因有二。一个原因是 IST 机制产生的开销可以忽略不计。因此,我认为保持简单更好。像 IA-32 的 KML 那样,仅使用 IST 机制处理双重错误需要对原始内核进行复杂的修改。其次,堆栈的红色区域是 AMD64 架构的 System V 应用程序二进制接口所要求的。红色区域是位于堆栈正下方的 128 字节内存范围,即从 %rsp - 8 到 %rsp - 128。AMD64 的 System V ABI 规定用户程序可以使用红色区域进行临时数据存储和信号处理程序,而中断处理程序绝不应触及该区域。如果 KML 使用通常的中断处理机制处理中断,则此红色区域将被破坏,因为没有切换堆栈。在这种情况下,如果内核模式用户进程正在运行,则某些 CPU 上下文将被覆盖到红色区域。因此,AMD64 的 KML 使用 IST 机制处理所有中断/异常,以便正确地为用户程序提供 System V ABI。

IA-32 的 KML 也存在一个限制:内核模式用户进程无法更改其 CS 段寄存器。这是不可能的,因为 IA-32 的 KML 至少需要一个暂存寄存器,以便在引发异常或中断时手动从用户堆栈切换到内核堆栈。它通过使用保存 CS 寄存器的内存来准备寄存器。此限制不适用于 AMD64 的 KML,因为堆栈由 IST 机制切换。然而,在 AMD64 的 64 位模式下更改 CS 段寄存器并不那么重要,因为只能有两个代码段。

性能测量

为了了解可能的性能提升幅度,我在原始 Linux 内核和 KML 上都运行了 MySQL 的 Wisconsin 基准测试,使用了 MySQL 自带的 sql-bench。实验环境如表 1 所示。在 KML 上的测试中,MySQL 服务器和基准测试客户端都作为内核模式用户进程执行,并使用了打过补丁的 GNU C 库来消除系统调用调用的开销。此外,测试的循环计数增加到 10,000,因为默认的 10 次循环计数太小,无法产生有意义的结果。

表 1. 实验环境

CPUOpteron 850 (2.4GHz, L2 缓存 1MB) x 4
内存8GB (Registered DDR1-333 SDRAM)
硬盘146GB (Ultra320 SCSI 73GB x 2, RAID-0, XFS)
操作系统Linux 内核 2.6.11 (KML_2.6.11_002)
LibcGNU libc 2.3.5 + KML 补丁
MySQLMySQL 4.1.11

结果如表 2 所示。第二列显示了基准测试消耗的总 CPU 时间。第三列和第四列显示了总 CPU 时间的细分。第三列显示了用户进程消耗的 CPU 时间,第四列显示了内核消耗的 CPU 时间。

表 2. Wisconsin 基准测试结果(秒)

 CPU用户系统
原始 Linux753.86611.78142.08
KML728.61605.95122.66

结果表明,总 CPU 时间提高了约 3%。用户 CPU 时间提高了约 1%,系统 CPU 时间提高了约 14%。结果表明,KML 可以通过消除系统调用调用的开销,略微提高数据库应用程序的性能。

结论和未来工作

KML 是一个修改后的 Linux 内核,可以在内核模式下执行用户进程。通过在内核模式下执行,例如,通过消除系统调用调用的开销,可以提高用户程序的性能。除了性能提升之外,KML 还可以用于简化内核的检查和调试以及内核模块的开发,因为内核模式用户进程可以访问内核并使用大量的内存和 CPU 时间。我现在正在考虑实现一个辅助库,以便为内核模式用户进程提供一种简单的方法来访问内核函数和数据,方法是将它们导出为某种共享对象。

本文资源: /article/8327

Toshiyuki Maeda 是东京大学计算机科学博士候选人。他最喜欢的漫画家是手塚治虫、藤子·F·不二雄和冈田斗司夫。

加载 Disqus 评论