一次系统调用开销到底有多大?

首先说说系统调用是什么,当你的代码须要作IO操做(open、read、write)、或者是进行内存操做(mmpa、sbrk)、甚至是说要获取一个系统时间(gettimeofday),就须要经过系统调用来和内核进行交互。不管你的用户程序是用什么语言实现的,是php、c、java仍是go,只要你是创建在Linux内核之上的,你就绕不开系统调用。php

file

你们能够经过strace命令来查看到你的程序正在执行哪些系统调用。好比我查看了一个正在生产环境上运行的nginx当前所执行的系统调用,以下:java

# strace -p 28927
Process 28927 attached  
epoll_wait(6, {{EPOLLIN, {u32=96829456, u64=140312383422480}}}, 512, -1) = 1  
accept4(8, {sa_family=AF_INET, sin_port=htons(55465), sin_addr=inet_addr("10.143.52.149")}, [16], SOCK_NONBLOCK) = 13  
epoll_ctl(6, EPOLL_CTL_ADD, 13, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=96841984, u64=140312383435008}}) = 0  
epoll_wait(6, {{EPOLLIN, {u32=96841984, u64=140312383435008}}}, 512, 60000) = 1

简单介绍了下系统调用,那么相信各位同窗都据说过一个建议,就是系统调用的开销很大,要尽可能减小系统调用的次数,以提升你的代码的性能。那么问题来了,咱们是否能够给出量化的指标。一次系统调用到底要多大的开销,须要消耗掉多少CPU时间?好了,废话很少说,咱们直接进行一些测试,用数据来讲话。nginx

实验1

首先我对线上正在服务的nginx进行strace统计,能够看出系统调用的耗时大约分布在1-15us左右。所以咱们能够大体得出结论,系统调用的耗时大约是1us级别的,固然因为不一样系统调用执行的操做不同,执行当时的环境不同,所以不一样的时刻,不一样的调用之间会存在耗时上的上下波动。redis

# strace -cp 8527  
strace: Process 8527 attached  
% time     seconds  usecs/call     calls    errors syscall  
------ ----------- ----------- --------- --------- ----------------  
 44.44    0.000727          12        63           epoll_wait  
 27.63    0.000452          13        34           sendto 
 10.39    0.000170           7        25        21 accept4  
  5.68    0.000093           8        12           write  
  5.20    0.000085           2        38           recvfrom  
  4.10    0.000067          17         4           writev  
  2.26    0.000037           9         4           close  
  0.31    0.000005           1         4           epoll_ctl

实验2

咱们再手工写段代码,对read系统调用进行测试缓存

注意,只能用read库函数来进行测试,不要使用fread。所以fread是库函数在用户态保留了缓存的,而read是你每调用一次,内核就老老实实帮你执行一次read系统调用。

首先建立一个固定大小为1M的文件安全

dd if=/dev/zero of=in.txt bs=1M count=1

而后再编译代码进行测试frontend

#cd tests/test02/  
#gcc main.c -o main  
#time ./main  
real    0m0.258s   
user    0m0.030s  
sys     0m0.227s

因为上述实验是循环了100万次,因此平均每次系统调用耗时大约是200ns多一些。函数

系统调用到底在干什么?

先看看系统调用花费的CPU指令数

x86-64 CPU有一个特权级别的概念。内核运行在最高级别,称为Ring0,用户程序运行在Ring3。正常状况下,用户进程都是运行在Ring3级别的,可是磁盘、网卡等外设只能在内核Ring0级别下来来访问。所以当咱们用户态程序须要访问磁盘等外设的时候,要经过系统调用进行这种特权级别的切换性能

对于普通的函数调用来讲,通常只须要进行几回寄存器操做,若是有参数或返回函数的话,再进行几回用户栈操做而已。并且用户栈早已经被CPU cache接住,也并不须要真正进行内存IO。测试

可是对于系统调用来讲,这个过程就要麻烦一些了。系统调用时须要从用户态切换到内核态。因为内核态的栈用的是内核栈,所以还须要进行栈的切换。SS、ESP、EFLAGS、CS和EIP寄存器所有都须要进行切换。

并且栈切换后还可能有一个隐性的问题,那就是CPU调度的指令和数据必定程度上破坏了局部性原来,致使一二三级数据缓存、TLB页表缓存的命中率必定程度上有所降低。

除了上述堆栈和寄存器等环境的切换外,系统调用因为特权级别比较高,也还须要进行一系列的权限校验、有效性等检查相关操做。因此系统调用的开销相对函数调用来讲要大的多。咱们在计算一下每一个系统调用须要执行的CPU指令数。

# perf stat ./main

 Performance counter stats for './main':

        251.508810 task-clock                #    0.997 CPUs utilized
                 1 context-switches          #    0.000 M/sec
                 1 CPU-migrations            #    0.000 M/sec
                97 page-faults               #    0.000 M/sec
       600,644,444 cycles                    #    2.388 GHz                     [83.38%]
       122,000,095 stalled-cycles-frontend   #   20.31% frontend cycles idle    [83.33%]
        45,707,976 stalled-cycles-backend    #    7.61% backend  cycles idle    [66.66%]
     1,008,492,870 instructions              #    1.68  insns per cycle
                                             #    0.12  stalled cycles per insn [83.33%]
       177,244,889 branches                  #  704.726 M/sec                   [83.32%]
             7,583 branch-misses             #    0.00% of all branches         [83.33%]

对实验代码进行稍许改动,把for循环中的read调用注释掉,再从新编译运行

# gcc main.c -o main  
# perf stat ./main  

 Performance counter stats for './main':  

          3.196978 task-clock                #    0.893 CPUs utilized
                 0 context-switches          #    0.000 M/sec
                 0 CPU-migrations            #    0.000 M/sec
                98 page-faults               #    0.031 M/sec
         7,616,703 cycles                    #    2.382 GHz                       [68.92%]
         5,397,528 stalled-cycles-frontend   #   70.86% frontend cycles idle      [68.85%]  
         1,574,438 stalled-cycles-backend    #   20.67% backend  cycles idle  
         3,359,090 instructions              #    0.44  insns per cycle  
                                             #    1.61  stalled cycles per insn  
         1,066,900 branches                  #  333.721 M/sec
               799 branch-misses             #    0.07% of all branches           [80.14%]  

       0.003578966 seconds time elapsed

平均每次系统调用CPU须要执行的指令数(1,008,492,870 - 3,359,090)/1000000 = 1005。

再深挖系统调用的实现

若是非要扒到内核的实现上,我建议你们参考一下《深刻理解LINUX内核-第十章系统调用》。最初系统调用是经过汇编指令int(中断)来实现的,当用户态进程发出int $0x80指令时,CPU切换到内核态并开始执行system_call函数。 只不事后来你们以为系统调用实在是太慢了,由于int指令要执行一致性和安全性检查。后来Intel又提供了“快速系统调用”的sysenter指令,咱们验证一下。

# perf stat -e syscalls:sys_enter_read ./main  

 Performance counter stats for './main':  

            1,000,001 syscalls:sys_enter_read  

       0.006269041 seconds time elapsed

上述实验证实,系统调用确实是经过sys_enter指令来进行的。

相关命令

  • strace

    • strace -p $PID: 实时统计进程陷入的系统调用
    • strace -cp $PID: 对进程执行一段时间内的汇总,而后以排行榜的形式给出来,很是实用
  • perf

    • perf list: 列出全部可以perf采样点
    • perf stat: 统计CPU指令数、上下文切换等缺省时间
    • perf stat -e 事件: 指定采样时间进行统计
    • perf top: 统计整个系统内消耗最多的函数或指令
    • perf top -e: 同上,可是能够指定采样点

结论

  • 系统调用虽然使用了“快速系统调用”指令,但耗时仍大约在200ns+,多的可能到十几us
  • 每一个系统调用内核要进行许多工做,大约须要执行1000条左右的CPU指令
系统调用确实开销蛮大的,函数调用时ns级别的,系统调用直接上升到了百ns,甚至是十几us,因此确实应该尽可能减小系统调用。可是即便是10us,仍然是1ms的百分之一,因此还没到了谈系统调用色变的程度,能理性认识到它的开销既可。

为何系统调用之间的耗时相差这么多?由于系统调用花在内核态用户态的切换上的时间是差很少的,但区别在于不一样的系统调用当进入到内核态以后要处理的工做不一样,呆在内核态里的时候相差较大。


file


开发内功修炼之CPU篇专辑:


个人公众号是「开发内功修炼」,在这里我不是单纯介绍技术理论,也不仅介绍实践经验。而是把理论与实践结合起来,用实践加深对理论的理解、用理论提升你的技术实践能力。欢迎你来关注个人公众号,也请分享给你的好友~~~

相关文章
相关标签/搜索