当前位置:操作系统 > Unix/Linux >>

Linux2.6对新型CPU快速系统调用的支持

文章分析了在 Linux 2.6 中引入的对 Intel CPU 快速系统调用指令 SYSENTER/SYSEXIT 支持的实现。Linux 驱动及内核开发者通过了解快速系统调用指令的机制,可以在自己的代码中通过利用这一机制,提高系统性能,并避开由快速系统调用方式带来的一些局限(如系统调用中嵌套系统调用)。

  前言

  在 Linux 2.4 内核中,用户态 Ring3 代码请求内核态 Ring0 代码完成某些功能是通过系统调用完成的,而系统调用的是通过软中断指令(int 0x80)实现的。在 x86 保护模式中,处理 INT 中断指令时,CPU 首先从中断描述表 IDT 取出对应的门描述符,判断门描述符的种类,然后检查门描述符的级别 DPL 和 INT 指令调用者的级别 CPL,当 CPL<=DPL 也就是说 INT 调用者级别高于描述符指定级别时,才能成功调用,最后再根据描述符的内容,进行压栈、跳转、权限级别提升。内核代码执行完毕之后,调用 IRET 指令返回,IRET 指令恢复用户栈,并跳转会低级别的代码。

  其实,在发生系统调用,由 Ring3 进入 Ring0 的这个过程浪费了不少的 CPU 周期,例如,系统调用必然需要由 Ring3 进入 Ring0(由内核调用 INT 指令的方式除外,这多半属于 Hacker 的内核模块所为),权限提升之前和之后的级别是固定的,CPL 肯定是 3,而 INT 80 的 DPL 肯定也是 3,这样 CPU 检查门描述符的 DPL 和调用者的 CPL 就是完全没必要。正是由于如此,Intel x86 CPU 从 PII 300(Family 6,Model 3,Stepping 3)之后,开始支持新的系统调用指令 sysenter/sy易做图it。sysenter 指令用于由 Ring3 进入 Ring0,SYSEXIT 指令用于由 Ring0 返回 Ring3。由于没有特权级别检查的处理,也没有压栈的操作,所以执行速度比 INT n/IRET 快了不少。

  不同系统调用方式的性能比较:

  下面是一些来自互联网的有关 sysenter/sy易做图it 指令和 INT n/IRET 指令在 Intel Pentium CPU 上的性能对比:

  表1:系统调用性能测试 测试硬件:Intel? Pentium? III CPU, 450 MHz Processor Family: 6 Model: 7 Stepping: 2

  

  数据来源:[1]

  

  数据来源:[2]

  表2:各种 CPU 上 INT 0x80 和 SYSENTER 执行速度的比较

  

  上述数据为对 100000 次 getppid() 系统调用所花费的 CPU 时钟周期取的平均值

  数据来源[3]

  自这种技术推出之后,人们一直在考虑在 Linux 中加入对这种指令的支持,在 Kernel.org 的邮件列表中,主题为 "Intel P6 vs P7 system call performance" 的大量邮件讨论了采用这种指令的必要性,邮件中列举的理由主要是 Intel 在 Pentium 4 的设计上存在问题,造成 Pentium 4 使用中断方式执行的系统调用比 Pentium 3 以及 AMD Athlon 所耗费的 CPU 时钟周期多上 5~10 倍。因此,在 Pentium 4 平台上,通过 sysenter/sy易做图it 指令来执行系统调用已经是刻不容缓的需求。

  sysenter/sy易做图it 系统调用的机制:

  在 Intel 的软件开发者手册第二、三卷(Vol.2B,Vol.3)中,4.8.7 节是关于 sysenter/sy易做图it 指令的详细描述。手册中说明,sysenter 指令可用于特权级 3 的用户代码调用特权级 0 的系统内核代码,而 SYSEXIT 指令则用于特权级 0 的系统代码返回用户空间中。sysenter 指令可以在 3,2,1 这三个特权级别调用(Linux 中只用到了特权级 3),而 SYSEXIT 指令只能从特权级 0 调用。

  执行 sysenter 指令的系统必须满足两个条件:1.目标 Ring 0 代码段必须是平坦模式(Flat Mode)的 4GB 的可读可执行的非一致代码段。2.目标 RING0 堆栈段必须是平坦模式(Flat Mode)的 4GB 的可读可写向上扩展的栈段。

  在 Intel 的手册中,还提到了 sysenter/sy易做图it 和 int n/iret 指令的一个区别,那就是 sysenter/sy易做图it 指令并不成对,sysenter 指令并不会把 SYSEXIT 所需的返回地址压栈,sy易做图it 返回的地址并不一定是 sysenter 指令的下一个指令地址。调用 sysenter/sy易做图it 指令地址的跳转是通过设置一组特殊寄存器实现的。这些寄存器包括:

  SYSENTER_CS_MSR - 用于指定要执行的 Ring 0 代码的代码段选择符,由它还能得出目标 Ring 0 所用堆栈段的段选择符;

  SYSENTER_EIP_MSR - 用于指定要执行的 Ring 0 代码的起始地址;

  SYSENTER_ESP_MSR-用于指定要执行的Ring 0代码所使用的栈指针

  这些寄存器可以通过 wrmsr 指令来设置,执行 wrmsr 指令时,通过寄存器 edx、eax 指定设置的值,edx 指定值的高 32 位,eax 指定值的低 32 位,在设置上述寄存器时,edx 都是 0,通过寄存器 ecx 指定填充的 MSR 寄存器,sysenter_CS_MSR、sysenter_ESP_MSR、sysenter_EIP_MSR 寄存器分别对应 0x174、0x175、0x176,需要注意的是,wrmsr 指令只能在 Ring 0 执行。

  这里还要介绍一个特性,就是 Ring0、Ring3 的代码段描述符和堆栈段描述符在全局描述符表 GDT 中是顺序排列的,这样只需知道 SYSENTER_CS_MSR 中指定的 Ring0 的代码段描述符,就可以推算出 Ring0 的堆栈段描述符以及 Ring3 的代码段描述符和堆栈段描述符。

  在 Ring3 的代码调用了 sysenter 指令之后,CPU 会做出如下的操作:

  1. 将 SYSENTER_CS_MSR 的值装载到 cs 寄存器

  2. 将 SYSENTER_EIP_MSR 的值装载到 eip 寄存器

  3. 将 SYSENTER_CS_MSR 的值加 8(Ring0 的堆栈段描述符)装载到 ss 寄存器。

  4. 将 SYSENTER_ESP_MSR 的值装载到 esp 寄存器

  5. 将特权级切换到 Ring0

  6. 如果 EFLAGS 寄存器的 VM 标志被置位,则清除该标志

  7. 开始执行指定的 Ring0 代码

  在 Ring0 代码执行完毕,调用 SYSEXIT 指令退回 Ring3 时,CPU 会做出如下操作:

  1. 将 SYSENTER_CS_MSR 的值加 16(Ring3 的代码段描述符)装载到 cs 寄存器

  2. 将寄存器 edx 的值装载到 eip 寄存器

  3. 将 SYSENTER_CS_MSR 的值加 24(Ring3 的堆栈段描述符)装载到 ss 寄存器

  4. 将寄存器 ecx 的值装载到 esp 寄存器

  5. 将特权级切换到 Ring3

  6. 继续执行 Ring3 的代码

  由此可知,在调用 SYSENTER 进入 Ring0 之前,一定需要通过 wrmsr 指令设置好 Ring0 代码的相关信息,在调用 SYSEXIT 之前,还要保证寄存器edx、ecx 的正确性。

  如何得知 CPU 是否支持 sysenter/sy易做图it 指令

  根据 Intel 的 CPU 手册,我们可以通过 CPUID 指令来查看 CPU 是否支持 sysenter/sy易做图it 指令,做法是将 EAX 寄存器赋值 1,调用 CPUID 指令,寄存器 edx 中第 11 位(这一位名称为 SEP)就表示是否支持。在调用 CPUID 指令之后,还需要查看 CPU 的 Family、Model、Stepping 属性来确认,因为据称 Pentium Pro 处理器会报告 SEP 但是却不支持 sysenter/sy易做图it 指令。只有 Family 大于等于 6,Model 大于等于 3,Stepping 大于等于 3 的时候,才能确认 CPU 支持 sysenter/sy易做图it 指令。

  Linux 对 sysenter/sy易做图it 系统调用方式的支持

  在 2.4 内核中,直到最近的发布的 2.4.26-rc2 版本,没有加入对 sysenter/sy易做图it 指令的支持。而对 sysenter/sy易做图it 指令的支持最早是2002 年,由 Linus Torvalds 编写并首次加入 2.5 版内核中的,经过多方测试和多次 patch,最终正式加入到了 2.6 版本的内核中。

  http://kerneltrap.org/node/view/531/1996

  http://lwn.net/Articles/18414/

  具体谈到系统调用的完成,不能孤立的看内核的代码,我们知道,系统调用多被封装成库函数提供给应用程序调用,应用程序调用库函数后,由 glibc 库负责进入内核调用系统调用函数。在 2.4 内核加上老版的 glibc 的情况下,库函数所做的就是通过 int 指令来完成系统调用,而内核提供的系统调用接口很简单,只要在 IDT 中提供 INT 0x80 的入口,库就可以完成中断调用。

  在 2.6 内核中,内核代码同时包含了对 int 0x80 中断方式和 sysenter 指令方式调用的支持,因此内核会给用户空间提供一段入口代码,内核启动时根据 CPU 类型,决定这段代码采取哪种系统调用方式。对于 glibc 来说,无需考虑系统调用方式,直接调用这段入口代码,即可完成系统调用。这样做还可以尽量减少对 glibc 的改动,在 glibc 的源码中,只需将 "int $0x80" 指令替换成 "call 入口地址" 即可。

  下面,以 2.6.0 的内核代码配合支持 SYSENTER 调用方式的 glibc2.3.3 为例,分析一下系统调用的具体实现。

  内核在启动时做的准备

  前面说到的这段入口代码,根据调用方式分为两个文件,支持 sysenter 指令的代码包含在文件 arch/i386/kernel/vsyscall-sysenter.S 中,支持int中断的代码包含在arch/i386/kernel/vsyscall-int80.S中,入口名都是__kernel_vsyscall,这两个文件编译出的二进制代码由arch/i386/kernel/vsyscall.S所包含,并导出起始地址和结束地址。

  2.6 内核在启动的时候,调用了新增的函数 sysenter_setup(参见 arch/i386/kernel/sysenter.c),在这个函数中,内核将虚拟内存空间的顶端一个固定地址页面(从 0xffffe000 开始到 0xffffeffff 的 4k 大小)映射到一个空闲的物理内存页面。然后
CopyRight © 2022 站长资源库 编程知识问答 zzzyk.com All Rights Reserved
部分文章来自网络,