首先說說系統(tǒng)調(diào)用是什么,當(dāng)你的代碼需要做IO操作(open、read、write)、或者是進(jìn)行內(nèi)存操作(mmpa、sbrk)、甚至是說要獲取一個(gè)系統(tǒng)時(shí)間(),就需要通過系統(tǒng)調(diào)用來和內(nèi)核進(jìn)行交互。無論你的用戶程序是用什么語言實(shí)現(xiàn)的,是php、c、java還是go,只要你是建立在Linux內(nèi)核之上的,你就繞不開系統(tǒng)調(diào)用。
圖1 系統(tǒng)調(diào)用在計(jì)算機(jī)系統(tǒng)中的位置
大家可以通過命令來查看到你的程序正在執(zhí)行哪些系統(tǒng)調(diào)用。比如我查看了一個(gè)正在生產(chǎn)環(huán)境上運(yùn)行的nginx當(dāng)前所執(zhí)行的系統(tǒng)調(diào)用,如下:
# 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
簡單介紹了下系統(tǒng)調(diào)用,那么相信各位同學(xué)都聽說過一個(gè)建議,就是系統(tǒng)調(diào)用的開銷很大,要盡量減少系統(tǒng)調(diào)用的次數(shù),以提高你的代碼的性能。那么問題來了,我們是否可以給出量化的指標(biāo)。一次系統(tǒng)調(diào)用到底要多大的開銷,需要消耗掉多少CPU時(shí)間?好了,廢話不多說,我們直接進(jìn)行一些測(cè)試,用數(shù)據(jù)來說話。
使用命令進(jìn)行實(shí)驗(yàn)
首先我對(duì)線上正在服務(wù)的nginx進(jìn)行統(tǒng)計(jì),可以看出系統(tǒng)調(diào)用的耗時(shí)大約分布在1-15us左右。因此我們可以大致得出結(jié)論,系統(tǒng)調(diào)用的耗時(shí)大約是1us級(jí)別的,當(dāng)然由于不同系統(tǒng)調(diào)用執(zhí)行的操作不一樣,執(zhí)行當(dāng)時(shí)的環(huán)境不一樣,因此不同的時(shí)刻,不同的調(diào)用之間會(huì)存在耗時(shí)上的上下波動(dòng)。
# 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
使用time命令進(jìn)行實(shí)驗(yàn)
我們?cè)偈止懚未a,對(duì)read系統(tǒng)調(diào)用進(jìn)行測(cè)試,代碼如下
#include
#include
#include
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庫函數(shù)來進(jìn)行測(cè)試,不要使用fread。因此fread是庫函數(shù)在用戶態(tài)保留了緩存的,而read是你每調(diào)用一次,內(nèi)核就老老實(shí)實(shí)幫你執(zhí)行一次read系統(tǒng)調(diào)用。
首先創(chuàng)建一個(gè)固定大小為1M的文件
dd if=/dev/zero of=in.txt bs=1M count=1
然后再編譯代碼進(jìn)行測(cè)試
#gcc main.c -o main
#time ./main
real 0m0.258s
user 0m0.030s
sys 0m0.227s
由于上述實(shí)驗(yàn)是循環(huán)了100萬次,所以平均每次系統(tǒng)調(diào)用耗時(shí)大約是200ns多一些。
Perf命令查看系統(tǒng)調(diào)用消耗的CPU指令數(shù)
x86-64 CPU有一個(gè)特權(quán)級(jí)別的概念。內(nèi)核運(yùn)行在最高級(jí)別,稱為Ring0,用戶程序運(yùn)行在Ring3。正常情況下,用戶進(jìn)程都是運(yùn)行在Ring3級(jí)別的,但是磁盤、網(wǎng)卡等外設(shè)只能在內(nèi)核Ring0級(jí)別下來來訪問。因此當(dāng)我們用戶態(tài)程序需要訪問磁盤等外設(shè)的時(shí)候,要通過系統(tǒng)調(diào)用進(jìn)行這種特權(quán)級(jí)別的切換
對(duì)于普通的函數(shù)調(diào)用來說,一般只需要進(jìn)行幾次寄存器操作,如果有參數(shù)或返回函數(shù)的話,再進(jìn)行幾次用戶棧操作而已。而且用戶棧早已經(jīng)被CPU cache接住,也并不需要真正進(jìn)行內(nèi)存IO。
但是對(duì)于系統(tǒng)調(diào)用來說,這個(gè)過程就要麻煩一些了。系統(tǒng)調(diào)用時(shí)需要從用戶態(tài)切換到內(nèi)核態(tài)。由于內(nèi)核態(tài)的棧用的是內(nèi)核棧,因此還需要進(jìn)行棧的切換。SS、ESP、、CS和EIP寄存器全部都需要進(jìn)行切換。
而且棧切換后還可能有一個(gè)隱性的問題,那就是CPU調(diào)度的指令和數(shù)據(jù)一定程度上破壞了局部性原來,導(dǎo)致一二三級(jí)數(shù)據(jù)緩存、TLB頁表緩存的命中率一定程度上有所下降。
除了上述堆棧和寄存器等環(huán)境的切換外,系統(tǒng)調(diào)用由于特權(quán)級(jí)別比較高,也還需要進(jìn)行一系列的權(quán)限校驗(yàn)、有效性等檢查相關(guān)操作。所以系統(tǒng)調(diào)用的開銷相對(duì)函數(shù)調(diào)用來說要大的多。我們?cè)诘幕A(chǔ)上計(jì)算一下每個(gè)系統(tǒng)調(diào)用需要執(zhí)行的CPU指令數(shù)。
# 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%]
對(duì)實(shí)驗(yàn)代碼進(jìn)行稍許改動(dòng),把for循環(huán)中的read調(diào)用注釋掉,再重新編譯運(yùn)行
# 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

平均每次系統(tǒng)調(diào)用CPU需要執(zhí)行的指令數(shù)(1,008,492,870 - 3,359,090)/ = 1005。
深挖系統(tǒng)調(diào)用的實(shí)現(xiàn)
如果非要扒到內(nèi)核的實(shí)現(xiàn)上,我建議大家參考一下《深入理解LINUX內(nèi)核-第十章系統(tǒng)調(diào)用》。最初系統(tǒng)調(diào)用是通過匯編指令int(中斷)來實(shí)現(xiàn)的,當(dāng)用戶態(tài)進(jìn)程發(fā)出int $0x80指令時(shí)有哪些是系統(tǒng)進(jìn)程調(diào)用,CPU切換到內(nèi)核態(tài)并開始執(zhí)行函數(shù)。 只不過后來大家覺得系統(tǒng)調(diào)用實(shí)在是太慢了,因?yàn)閕nt指令要執(zhí)行一致性和安全性檢查。后來Intel又提供了“快速系統(tǒng)調(diào)用”的指令,我們驗(yàn)證一下。
# 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
上述實(shí)驗(yàn)證明,系統(tǒng)調(diào)用確實(shí)是通過指令來進(jìn)行的。
結(jié)論系統(tǒng)調(diào)用確實(shí)開銷蠻大的,函數(shù)調(diào)用時(shí)ns級(jí)別的,系統(tǒng)調(diào)用直接上升到了百ns,甚至是十幾u(yù)s,所以確實(shí)應(yīng)該盡量減少系統(tǒng)調(diào)用。但是即使是10us有哪些是系統(tǒng)進(jìn)程調(diào)用,仍然是1ms的百分之一,所以還沒到了談系統(tǒng)調(diào)用色變的程度,能理性認(rèn)識(shí)到它的開銷既可。
為什么系統(tǒng)調(diào)用之間的耗時(shí)相差這么多?因?yàn)橄到y(tǒng)調(diào)用花在內(nèi)核態(tài)用戶態(tài)的切換上的時(shí)間是差不多的,但區(qū)別在于不同的系統(tǒng)調(diào)用當(dāng)進(jìn)入到內(nèi)核態(tài)之后要處理的工作不同,呆在內(nèi)核態(tài)里的時(shí)候相差較大。
關(guān)注知乎專欄《開發(fā)內(nèi)功修煉》或搜索微信公眾號(hào)“”,收獲更多知識(shí)!
參考資料
整理了寫「開發(fā)內(nèi)功修煉」時(shí)參考到的電子書,想深入學(xué)習(xí)的同學(xué)可以下載。
下載地址: /s/N7A
提取碼: 4uqa