一次系统调用开销到底有多大?
相信各位同學都聽說過一個建議,就是系統調用比函數調用開銷大很多,要盡量減少系統調用的次數,以提高你的代碼的性能。那么問題來了,我們是否可以給出量化的指標。一次系統調用到底要多大的開銷,需要消耗掉多少CPU時間?
1
系統調用拾遺
首先說說系統調用是什么,當你的代碼需要做IO操作(open、read、write)、或者是進行內存操作(mmap、sbrk)、甚至是說要獲取一個網絡數據,就需要通過系統調用來和內核進行交互。無論你的用戶程序是用什么語言實現的,是php、c、java還是go,只要你是建立在Linux內核之上的,你就繞不開系統調用。
圖1 系統調用在計算機系統中的位置大家可以通過strace命令來查看到你的程序正在執行哪些系統調用。比如我查看了一個正在生產環境上運行的nginx當前所執行的系統調用,如下(代碼可能需要左右滑動):
# 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 ?簡單介紹完了系統調用,廢話不多說,我們直接進行一些測試,用數據來說話。
2
使用strace命令進行實驗
首先我對線上正在服務的nginx進行strace統計,可以看出系統調用的耗時大約分布在1-15us左右。因此我們可以大致得出結論,系統調用的耗時大約是1us級別的,當然由于不同系統調用執行的操作不一樣,執行當時的環境不一樣,因此不同的時刻,不同的調用之間會存在耗時上的上下波動(下圖可能需要左右滑動)。
# 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_ctl3
使用time命令進行實驗
我們再手工寫段代碼,對read系統調用進行測試,代碼如下
#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多一些。
4
Perf命令查看系統調用消耗的指令數
x86-64 CPU有一個特權級別的概念。內核運行在最高級別,稱為Ring0,用戶程序運行在Ring3。正常情況下,用戶進程都是運行在Ring3級別的,但是磁盤、網卡等外設只能在內核Ring0級別下來來訪問。因此當我們用戶態程序需要訪問磁盤等外設的時候,要通過系統調用進行這種特權級別的切換
對于普通的函數調用來說,一般只需要進行幾次寄存器操作,如果有參數或返回函數的話,再進行幾次用戶棧操作而已。而且用戶棧早已經被CPU cache接住,也并不需要真正進行內存IO。
但是對于系統調用來說,這個過程就要麻煩一些了。系統調用時需要從用戶態切換到內核態。由于內核態的棧用的是內核棧,因此還需要進行棧的切換。SS、ESP、EFLAGS、CS和EIP寄存器全部都需要進行切換。
而且棧切換后還可能有一個隱性的問題,那就是CPU調度的指令和數據一定程度上破壞了局部性原理,導致一二三級數據緩存、TLB頁表緩存的命中率一定程度上有所下降。
除了上述堆棧和寄存器等環境的切換外,系統調用由于特權級別比較高,也還需要進行一系列的權限校驗、有效性等檢查相關操作。所以系統調用的開銷相對函數調用來說要大的多。我們在前面實驗的基礎上計算一下每個系統調用需要執行的CPU指令數(下圖可能需要左右滑動)。
# perf stat ./mainPerformance counter stats for './main':251.508810 task-clock ? ? ? ? ? ? ? ?# ? ?0.997 CPUs utilized1 context-switches ? ? ? ? ?# ? ?0.000 M/sec1 CPU-migrations ? ? ? ? ? ?# ? ?0.000 M/sec97 page-faults ? ? ? ? ? ? ? # ? ?0.000 M/sec600,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調用注釋掉,再重新編譯運行(下圖可能需要左右滑動)。
平均每次系統調用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 ./mainPerformance?counter?stats?for?'./main':1,000,001?syscalls:sys_enter_read0.006269041 seconds time elapsed上述實驗證明,系統調用確實是通過sys_enter指令來進行的。
6
結論
相比較函數調用時的不到1ns的耗時,系統調用確實開銷蠻大的。雖然使用了“快速系統調用”指令,但耗時仍大約在200ns+,多的可能到十幾us。每個系統調用內核要進行許多工作,大約需要執行1000條左右的CPU指令,所以確實應該盡量減少系統調用。但是即使是10us,仍然是1ms的百分之一,所以還沒到了談系統調用色變的程度,能理性認識到它的開銷既可。
另外為什么系統調用之間的耗時相差這么多?因為系統調用花在內核態用戶態的切換上的時間是差不多的,但區別在于不同的系統調用當進入到內核態之后要處理的工作不同,呆在內核態里的時候相差較大。
總結
以上是生活随笔為你收集整理的一次系统调用开销到底有多大?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 通过实例理解 Go 逃逸分析
- 下一篇: Go gomaxprocs 调高引起调度