Linux 2.6 新增的 vsyscall 系统服务调用机制(ZT)

Linux 2.6 新增的 vsyscall 系统服务调用机制


    与 Windows 的系统服务调用实现机制类似,Linux 内部为所有核心态系统调用,维护了一张按调用号排序的跳转表 (sys_call_table @ arch/i386/kernel/entry.S)。只不过对 Window 来说,类似的跳转表 (KeServiceDescriptorTable @ ntos/ke/kernldat.c) 按功能进一步细分为四部分,分别用于内核与 Win32 子系统等。而 Linux 的系统服务表,因为开放性、兼容性和移植性等问题,则相对稳定和保守得多。
以下内容为程序代码:

// sys_call_table @ arch/i386/kernel/entry.S

.data
ENTRY(sys_call_table)
.long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */
.long sys_exit
...
.long sys_request_key
.long sys_keyctl

syscall_table_size=(.-sys_call_table)


以下内容为程序代码:

// KSERVICE_TABLE_DESCRIPTOR @ ntos/ke/ke.h

#define NUMBER_SERVICE_TABLES 4

typedef struct _KSERVICE_TABLE_DESCRIPTOR {
    PULONG Base;
    PULONG Count;
    ULONG Limit;
    PUCHAR Number;
} KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR;

// KeServiceDescriptorTable @ ntos/ke/kernldat.c

KSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable[NUMBER_SERVICE_TABLES];

    而对用户态 API 来说,Linux 和 Windows NT/2K 下面都是通过传统的中断方式,Linux 使用 int 0x80;Windows 使用 int 0x2E。对 glibc 来说,其实就是一系列的宏定义,如 _syscall0 - _syscall6 等不同形式,如
以下内容为程序代码:

#define _syscall1(type,name,type1,arg1) /
type name(type1 arg1) /
{ /
long __res; /
__asm__ volatile ("int 0x80" /
: "=a" (__res) /
: "0" (__NR_##name),"b" ((long)(arg1))); /
__syscall_return(type,__res); /
}

    而在系统加载的时候,接管 0x80 中断服务历程,完成基于 sys_call_table 跳转表的派发。如
以下内容为程序代码:

static void __init set_system_gate(unsigned int n, void *addr)
{
_set_gate(idt_table+n,15,3,addr,__KERNEL_CS); // 允许再 ring 3 调用此陷阱 (15) 的系统门
}

#define SYSCALL_VECTOR 0x80

asmlinkage int system_call(void);

void __init trap_init(void)
{
  ...
  set_system_gate(SYSCALL_VECTOR,&system_call);
  ...
}

    这里的 trap_init 函数 (arch/i386/kernel/traps.c) 负责初始化各种中断的处理例程,其将被系统初始化 start_kernel (init/main.c) 函数中调用。对 SYSCALL_VECTOR (0x80) 调用,由 system_call 函数 (arch/i386/kernel/entry.S) 进行实际处理工作。

    而对 Windows 用户态 DLL 如 NtDll 来说,也是通过类似的方式实现。因为这方面讨论的文章较多,这里就不再罗嗦。有兴趣的朋友可以参考 Inside Win2K 一书,以及  《剖析Windows系统服务调用机制》 等文章。

    但是这种基于中断的系统服务调用机制,对于 Intel P4 以上 CPU 来说存在着很大的性能隐患。根据实测结果 P3 850 在中断模式的系统调用上,比 P4 2G 有将近一倍的性能优势,而对 Xeon 等高端 CPU 来说中断的处理性能甚至更差。

     Intel P6 vs P7 system call performance

    这也是为什么从 Windows XP/2003 开始,MS 偷偷将 Intel 2E 的系统调用换成了 CPU 特殊的指令 sysenter (Intel) 和 syscall (AMD)。例如在 Win2003 系统中,NTDLL 中的系统调用已经不再使用 Int 0x2E,而改为调用固定地址上的系统调用代码:
以下内容为程序代码:

0:001> u ntdll!ZwSuspendProcess
ntdll!NtSuspendProcess:
77f335bb b806010000       mov     eax,0x106
77f335c0 ba0003fe7f       mov     edx,0x7ffe0300
77f335c5 ffd2             call    edx
77f335c7 c20400           ret     0x4

0:001> u 0x7ffe0300
SharedUserData!SystemCallStub:
7ffe0300 8bd4             mov     edx,esp
7ffe0302 0f34             sysenter
7ffe0304 c3               ret

0:001> u 7ffe0314
SharedUserData!SystemCallStub+0x14:
7ffe0314 8bd4             mov     edx,esp
7ffe0316 0f05             syscall
7ffe0318 c3               ret


    对 Intel x86 架构来说,sysenter/sysexit 指令是从 PII 开始加入到指令集中,专门用于从用户态 (ring 1-3) 切换到核心态 (ring 0)。与普通的中断使用 IDT 或 call/jmp 直接给定目的地址不同,此系列命令直接从 CPU 相关的 MSR 寄存器中读取目标代码和堆栈的段选择符与地址偏移。因此只需要在系统加载的时候,一次性将这些设置好,就可以如上代码所示那样直接使用 sysenter 指令进行切换。正因为如此,使用 sysenter/sysexit 执行进行 ring 0 和 ring 3 之间切换,是在两个预定义好的稳定状态之间进行切换,所以无需进行中断处理时一系列的状态转换的特权检查,大大提高了切换处理的效率。而对 AMD 芯片,syscall 的实现原理基本类似。

    为了适应这种变化,提高系统调用的效率,Linux 内核从 2.5.53 开始增加了对 sysenter/sysexit 模式的系统服务调用机制的支持。新增的 sysenter.c (arch/i386/kernel/) 中代码,会根据当前启动 CPU 是否支持 sysenter/sysexit 指令,动态判断是否启用支持。
以下内容为程序代码:

#define X86_FEATURE_SEP (0*32+11) /* SYSENTER/SYSEXIT */

static int __init sysenter_setup(void)
{
void *page = (void *)get_zeroed_page(GFP_ATOMIC);

__set_fixmap(FIX_VSYSCALL, __pa(page), PAGE_READONLY_EXEC);

if (!boot_cpu_has(X86_FEATURE_SEP)) {
memcpy(page,
       &vsyscall_int80_start,
       &vsyscall_int80_end - &vsyscall_int80_start);
return 0;
}

memcpy(page,
       &vsyscall_sysenter_start,
       &vsyscall_sysenter_end - &vsyscall_sysenter_start);

on_each_cpu(enable_sep_cpu, NULL, 1, 1);
return 0;
}

__initcall(sysenter_setup);

    sysenter_setup 函数 (arch/i386/kernel/sysenter.c) 将在内核被加载时,获取一个只读并可执行的内存页,将基于 int 0x80 调用,或者 sysenter 调用的系统服务调用代码,加载到此内存页中。并根据情况,调用 on_each_cpu 函数对每个 CPU 启用 sysenter/sysexit 指令支持。
以下内容为程序代码:

#define MSR_IA32_SYSENTER_CS 0x174
#define MSR_IA32_SYSENTER_ESP 0x175
#define MSR_IA32_SYSENTER_EIP 0x176

extern asmlinkage void sysenter_entry(void);

void enable_sep_cpu(void *info)
{
int cpu = get_cpu();
struct tss_struct *tss = &per_cpu(init_tss, cpu);

tss->ss1 = __KERNEL_CS;
tss->esp1 = sizeof(struct tss_struct) + (unsigned long) tss;
wrmsr(MSR_IA32_SYSENTER_CS, __KERNEL_CS, 0);
wrmsr(MSR_IA32_SYSENTER_ESP, tss->esp1, 0);
wrmsr(MSR_IA32_SYSENTER_EIP, (unsigned long) sysenter_entry, 0);
put_cpu();
}

    可以看到 enable_sep_cpu 函数 (arch/i386/kernel/sysenter.c) 实际上是为每个 CPU 设置其 MSR 寄存器的相关值,以便 sysenter 指令被调用时,能够直接切换到预定义(包括代码和堆栈的段选择符和偏移地址)的内核状态。关于 sysenter/sysexit 和 MSR 的相关资料,可以参考 Intel IA-32 开发手册的第三卷,系统编程手册,4.8.7 节和附录 B。
以下内容为程序代码:

.text
.globl __kernel_vsyscall
.type __kernel_vsyscall,@function
__kernel_vsyscall:
.LSTART_vsyscall:
push %ecx
.Lpush_ecx:
push %edx
.Lpush_edx:
push %ebp
.Lenter_kernel:
movl %esp,%ebp
sysenter

...

    上述代码是 vsyscall-sysenter.S (arch/i386/kernel/) 中负责实际调用的,可以看到与前面 Win2003 的实现代码非常类似。

    而与 Win2003 不同的是,Linux 的系统调用存在一个被中断的问题。也就是说在一个系统调用执行时,因为调用本身等待某种资源或被阻塞,调用没有完成时可能就被中断而强制返回 -EINTR。此时系统调用本身并没有发生错误,因此应该提供某种自动重试机制。这也就是为什么 vsyscall-sysenter.S 中,在 sysenter 下面还有如下处理代码的原因。
以下内容为程序代码:

.Lenter_kernel:
movl %esp,%ebp
sysenter

/* 7: align return point with nop's to make disassembly easier */
.space 7,0x90

/* 14: System call restart point is here! (SYSENTER_RETURN - 2) */
jmp .Lenter_kernel
/* 16: System call normal return point is here! */
.globl SYSENTER_RETURN /* Symbol used by entry.S.  */
SYSENTER_RETURN:
pop %ebp
.Lpop_ebp:
pop %edx
.Lpop_edx:
pop %ecx
.Lpop_ecx:
ret
.LEND_vsyscall:
.size __kernel_vsyscall,.-.LSTART_vsyscall

    这里 sysenter 代码之后,实际上存在两个返回点:jmp .Lenter_kernel 指令是在调用被中断时的返回点;SYSENTER_RETURN 则是调用正常结束时的返回点。Linux 内核通过在实际调用函数中进行判断,调整返回地址 EIP 的方法,解决了这个自动重试的问题。
    Linus 在一篇 邮件列表的讨论中解释了这个问题。具体关于系统调用中断与重试等机制的解释,可以参考  《The Linux Kernel》一书的 4.5 节。

    不过因为某些原因,Linux 2.6 内核仍然没有把 _syscall0 那套函数改用新的调用方法,而是在现有机制之外,增加了一套名为 vsyscall 的扩展机制,专供对系统服务调用效率要求较高的服务使用。这种机制实际上是将一部分固定地址的内核空间虚拟内存页面,直接暴露并允许用户态进行访问。也就是前面 sysenter_setup 函数中的 __set_fixmap 调用。
以下内容为程序代码:

#define __FIXADDR_TOP 0xfffff000

#define FIXADDR_TOP ((unsigned long)__FIXADDR_TOP)

#define PAGE_SHIFT 12

#define __fix_to_virt(x) (FIXADDR_TOP - ((x) << PAGE_SHIFT))

void __set_fixmap (enum fixed_addresses idx, unsigned long phys, pgprot_t flags)
{
unsigned long address = __fix_to_virt(idx);

if (idx >= __end_of_fixed_addresses) {
BUG();
return;
}
set_pte_pfn(address, phys >> PAGE_SHIFT, flags);
}

enum fixed_addresses {
FIX_HOLE,
FIX_VSYSCALL,
  ...
};

static int __init sysenter_setup(void)
{
  ...
  __set_fixmap(FIX_VSYSCALL, __pa(page), PAGE_READONLY_EXEC);
  ...
}

    可以看到 __set_fixmap 函数实际上是把 0xfffff000 开始的若干页虚拟内存地址,固定分配给 vsyscall 等不同用途的功能,由其自行分配物理内存并放置功能代码。对 FIX_VSYSCALL 这块内存,内存管理模块会特殊对待。
以下内容为程序代码:

/*
 * This is the range that is readable by user mode, and things
 * acting like user mode such as get_user_pages.
 */
#define FIXADDR_USER_START (__fix_to_virt(FIX_VSYSCALL))
#define FIXADDR_USER_END (FIXADDR_USER_START + PAGE_SIZE)

int in_gate_area(struct task_struct *task, unsigned long addr)
{
#ifdef AT_SYSINFO_EHDR
if ((addr >= FIXADDR_USER_START) && (addr < FIXADDR_USER_END))
return 1;
#endif
return 0;
}

int get_user_pages(...)
{
  ...
vma = find_extend_vma(mm, start);
  if (!vma && in_gate_area(tsk, start)) {
    ...
    if (write) /* user gate pages are read-only */
  return i ? : -EFAULT;
...
  }
  ...
}

    对 vsyscall 这一页内存,get_user_pages 函数将直接允许用户态的读操作。因此完全可以从用户态通过 call FIXADDR_USER_START 类似的代码,通过基于 sysenter/sysexit 的系统服务调用机制,快速执行指定服务号的系统服务。如  《The Linux Kernel》一书给出了一个例子:
以下内容为程序代码:

#include <stdio.h>

int pid;

int main() {
        __asm__(
                "movl 20, %eax    /n"
                "call 0xffffe400   /n"
                "movl %eax, pid    /n"
        [img]/images/wink.gif[/img];
        printf("pid is %d/n", pid);
        return 0;
}

    而这种机制的系统服务调用,据其测试能有将近一倍的性能提升。
以下为引用:

  An example of the kind of timing differences: John Stultz reports on an experiment where he measures gettimeofday() and finds 1.67 us for the int 0x80 way, 1.24 us for the sysenter way, and 0.88 us for the vsyscall.)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值