gettimeofday and vDSO

Jul 3, 2016 • JamesW


转载须注明出处:www.wufei.org

clocksource

Netflix关于性能调试的这篇文章里面提到, 在AWS上把Linux的clocksource从xen切换到tsc, 最好情况下CPU使用率降低了30%, 应用程序的延时则降低了43%. 且不论unstable的tsc clocksource会有哪些负面影响, 从性能角度看这个改变是非常有吸引力的. 那么clocksource到底在哪些时候会用到呢?

Clocksource提供的最主要的一个接口就是read(), 时间的维护更新也是通过这个接口来实现的, 也就是说只有trace这个接口就可以了解clocksource的使用情况, 对于tsc来说, 这个函数就是read_tsc().

# perf probe -a read_tsc
# perf record -e probe:read_tsc -agR sleep 5

在我们的系统中, 大部分的read_tsc操作都是应用程序通过gettimeofday/clock_gettime系统调用触发的. 看起来一切都合情合理, 但是再深入研究一下gettimeofday的实现, 就会发现其实有很多变数.

vsyscall/vDSO - 1

因为用户程序经常调用gettimeofday来获取时间, 每一次系统调用都需要从用户态切换到内核态, 从而带来很大的开销, 不管是通过老的int 80还是新的syscall/sysenter. gettimeofday本身又非常简单, 不过是读取内核中的变量, 最多再使用rdtsc进行校正, 只要用户态能够读取该变量, gettimeofday的功能完全可以在用户态实现, 从而避免陷入内核. vsyscall正是基于这种想法:

  • 把内核的相关变量映射到用户态
  • 内核更新变量, 比如对于gettimeofday, timekeeping_update -> update_vsyscall
  • 同时还实现了访问这些变量的函数, 供用户态直接调用.

如果仅仅是这些的话, 这些函数应该是可以由用户态比如libc来实现的, 不过到底是使用tsc还是hpet, 或是其他做clocksource, 只有内核知道.

不过vsyscall将这些代码是映射到一个固定的地址, 对于所有的程序都是一样:

$ cat /proc/self/maps |grep vsyscall
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

这会产生安全隐患, 所以后来又有了vDSO, vDSO和vsyscall的目的相同, 不过在每次运行的时候会把实现这些虚拟系统调用的代码映射到不同的地方.

# ldd /bin/ls | grep vdso
    linux-vdso.so.1 =>  (0x00007fffbc295000)
# ldd /bin/ls | grep vdso
    linux-vdso.so.1 =>  (0x00007ffffb3ff000)

vsyscall/vDSO - 2

如果像上面所说的, 使用vsyscall/vDSO之后, 在测试中应该是不会回到gettimeofday系统调用的, 不过它的确出现了. 在内核实现vDSO之后, vsyscall按理说就应该被放弃了, 可是vsyscall作为一个已经存在的ABI, 用户态代码不可能随内核同时更新, 所以还是需要支持. 不过vsyscall的实现也发生了改变, vDSO是推荐的方法, vsyscall只要支持原来的语义就好, 虽然还是在固定的位置, 但是通过直接使用syscall(NATIVE)指令产生系统调用(因为现在只有几条指令, 就可以消除安全漏洞, 或是降低风险? 但还是包括syscall, ret, 所以比较难, 需要更多security的知识)

(gdb) x/8i 0xffffffffff600000
   0xffffffffff600000:  mov    $0x60,%rax
   0xffffffffff600007:  syscall
   0xffffffffff600009:  retq
   0xffffffffff60000a:  int3
   0xffffffffff60000b:  int3
   0xffffffffff60000c:  int3
   0xffffffffff60000d:  int3
   0xffffffffff60000e:  int3

内核还提供另外一种vsyscall的EMULATE模式, 在page fault的时候通过emulate_vsyscall来模拟这个系统调用操作, 这个页面里面的内容可以是空的(没看具体代码), 只要访问特殊的地址(只实现了少数几个vsyscall)的导致page fault就可了(比如设置为不可执行), 这样对security应该是更有帮助的, 不过这个emulate的过程性能是不够好的.

我们也可以把vDSO dump出来, 比如vDSO就跟普通的so文件一样.

(gdb) info proc map 
	  0x7ffff7ffb000     0x7ffff7ffc000     0x1000        0x0 [vdso]
(gdb) dump binary memory /tmp/vdso.so 0x7ffff7ffb000 0x7ffff7ffc000

$ file /tmp/vdso.so 
/tmp/vdso.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=5c994b65d2e02ef4bde564d0c8a0b295b179a553, stripped

$ objdump -T /tmp/vdso.so

/tmp/vdso.so:     file format elf64-x86-64

DYNAMIC SYMBOL TABLE:
ffffffffff700354 l    d  .eh_frame_hdr  0000000000000000              .eh_frame_hdr
ffffffffff700600  w   DF .text  00000000000002f5  LINUX_2.6   clock_gettime
0000000000000000 g    DO *ABS*  0000000000000000  LINUX_2.6   LINUX_2.6
ffffffffff700900 g    DF .text  0000000000000174  LINUX_2.6   __vdso_gettimeofday
ffffffffff700aa0 g    DF .text  000000000000003d  LINUX_2.6   __vdso_getcpu
ffffffffff700900  w   DF .text  0000000000000174  LINUX_2.6   gettimeofday
ffffffffff700a80  w   DF .text  0000000000000016  LINUX_2.6   time
ffffffffff700aa0  w   DF .text  000000000000003d  LINUX_2.6   getcpu
ffffffffff700600 g    DF .text  00000000000002f5  LINUX_2.6   __vdso_clock_gettime
ffffffffff700a80 g    DF .text  0000000000000016  LINUX_2.6   __vdso_time

vsyscall/vDSO - 3

vsyscall的使用方法可以参考以前glibc的实现sysdeps/unix/sysv/linux/x86_64/gettimeofday.S:

#define VSYSCALL_ADDR_vgettimeofday     0xffffffffff600000
ENTRY (__gettimeofday)
		/* Align stack.  */
		sub     $0x8, %rsp
		cfi_adjust_cfa_offset(8)
		movq    $VSYSCALL_ADDR_vgettimeofday, %rax
		callq   *%rax
		/* Check error return.  */
		cmpl    $-4095, %eax
		jae     SYSCALL_ERROR_LABEL

L(pseudo_end):
		add     $0x8, %rsp
		cfi_adjust_cfa_offset(-8)
		ret
PSEUDO_END(__gettimeofday)

vDSO的使用可以参考sysdeps/unix/sysv/linux/x86/gettimeofday.c:

void *
gettimeofday_ifunc (void)
{
  PREPARE_VERSION_KNOWN (linux26, LINUX_2_6);

  /* If the vDSO is not available we fall back to syscall.  */
  return (_dl_vdso_vsym ("__vdso_gettimeofday", &linux26)
	  ?: (void*) (&__gettimeofday_syscall));
}

gettimeofday

在我们的系统中由于使用的是vsyscall, 实际还是syscall, 所以并没有使用到这种方法带来的好处. 如果通过升级glibc切换成vDSO会怎么样呢, 内核是否就绪?

notrace static inline long vgetns(void)
{
	long v;
	cycles_t cycles;
	if (gtod->clock.vclock_mode == VCLOCK_TSC)
		cycles = vread_tsc();
	else
		cycles = vread_hpet();
	v = (cycles - gtod->clock.cycle_last) & gtod->clock.mask;
	return (v * gtod->clock.mult) >> gtod->clock.shift;
}

可见我们现在的内核只提供了2种clocksource供使用, 在AWS上如果使用xen clocksource的话, 使用的却是hpet, 这肯定还是有问题, 可以参考KVM是怎么支持VCLOCK_PVCLOCK.

还要注意的是, 这里使用的并不是clocksource.read()函数.