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

相信各位同窗都據說過一個建議,就是系統調用比函數調用開銷大不少,要儘可能減小系統調用的次數,以提升你的代碼的性能。那麼問題來了,咱們是否能夠給出量化的指標。一次系統調用到底要多大的開銷,須要消耗掉多少CPU時間?php

圖片


1系統調用拾遺java

首先說說系統調用是什麼,當你的代碼須要作IO操做(open、read、write)、或者是進行內存操做(mmap、sbrk)、甚至是說要獲取一個網絡數據,就須要經過系統調用來和內核進行交互。不管你的用戶程序是用什麼語言實現的,是php、c、java仍是go,只要你是創建在Linux內核之上的,你就繞不開系統調用。nginx

圖片圖1 系統調用在計算機系統中的位置緩存

你們能夠經過strace命令來查看到你的程序正在執行哪些系統調用。好比我查看了一個正在生產環境上運行的nginx當前所執行的系統調用,以下(代碼可能須要左右滑動):安全

# strace -p 28927Process 28927 attached  epoll_wait(6, {{EPOLLIN, {u32=96829456, u64=140312383422480}}}, 512, -1) = 1accept4(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

介紹完了系統調用,廢話很少說,咱們直接進行一些測試,用數據來講話。網絡

2使用strace命令進行實驗app

首先我對線上正在服務的nginx進行strace統計,能夠看出系統調用的耗時大約分佈在1-15us左右。所以咱們能夠大體得出結論,系統調用的耗時大約是1us級別的,固然因爲不一樣系統調用執行的操做不同,執行當時的環境不同,所以不一樣的時刻,不一樣的調用之間會存在耗時上的上下波動(下圖可能須要左右滑動)。frontend

# 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
3使用time命令進行實驗

咱們再手工寫段代碼,對read系統調用進行測試,代碼以下ide

#include <fcntl.h>  #include <stdio.h> #include <stdlib.h>int main()  {      char    c;    int     in;  int   i;  in = open("in.txt", O_RDONLY);    for(i=0; i<1000000; i++){    read(in,&c,1);  }  return 0;  }

注意,只能用read庫函數來進行測試,不要使用fread。所以fread是庫函數在用戶態保留了緩存的,而read是你每調用一次,內核就老老實實幫你執行一次read系統調用。
函數

建立一個固定大小爲1M的文件

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

後再編譯代碼進行測試

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

因爲上述實驗是循環了100萬次,因此平均每次系統調用耗時大約是200ns多一些。

4Perf命令查看系統調用消耗的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。

5深挖系統調用實現

若是非要扒到內核的實現上,我建議你們參考一下《深刻理解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指令來進行的。

6結論

相比較函數調用時的不到1ns的耗時,系統調用確實開銷蠻大的。雖然使用了「快速系統調用」指令,但耗時仍大約在200ns+,多的可能到十幾us。每一個系統調用內核要進行許多工做,大約須要執行1000條左右的CPU指令,因此確實應該儘可能減小系統調用。可是即便是10us,仍然是1ms的百分之一,因此還沒到了談系統調用色變的程度,能理性認識到它的開銷既可。

另外爲何系統調用之間的耗時相差這麼多?由於系統調用花在內核態用戶態的切換上的時間是差很少的,但區別在於不一樣的系統調用當進入到內核態以後要處理的工做不一樣,呆在內核態裏的時候相差較大。

相關文章
相關標籤/搜索