一次系統調用開銷到底有多大?

首先說說系統調用是什麼,當你的代碼須要作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篇專輯:


個人公衆號是「開發內功修煉」,在這裏我不是單純介紹技術理論,也不僅介紹實踐經驗。而是把理論與實踐結合起來,用實踐加深對理論的理解、用理論提升你的技術實踐能力。歡迎你來關注個人公衆號,也請分享給你的好友~~~