系統(tǒng)調(diào)用,讓世界轉(zhuǎn)起來!
我其實(shí)不想將它分解開給你看,用戶應(yīng)用程序其實(shí)就是一個(gè)可憐的甕中大腦:
它與外部世界的每個(gè)交流都要在內(nèi)核的幫助下通過系統(tǒng)調(diào)用才能完成。一個(gè)應(yīng)用程序要想保存一個(gè)文件、寫到終端、或者打開一個(gè) TCP 連接,內(nèi)核都要參與。應(yīng)用程序是被內(nèi)核高度懷疑的:認(rèn)為它到處充斥著 bug,甚至是個(gè)充滿邪惡想法的腦子。
這些系統(tǒng)調(diào)用是從一個(gè)應(yīng)用程序到內(nèi)核的函數(shù)調(diào)用。出于安全考慮,它們使用了特定的機(jī)制,實(shí)際上你只是調(diào)用了內(nèi)核的 API。“系統(tǒng)調(diào)用”這個(gè)術(shù)語指的是調(diào)用由內(nèi)核提供的特定功能(比如,系統(tǒng)調(diào)用 open()
)或者是調(diào)用途徑。你也可以簡稱為:syscall。
這篇文章講解系統(tǒng)調(diào)用,系統(tǒng)調(diào)用與調(diào)用一個(gè)庫有何區(qū)別,以及在操作系統(tǒng)/應(yīng)用程序接口上的刺探工具。如果徹底了解了應(yīng)用程序借助操作系統(tǒng)發(fā)生的哪些事情,那么就可以將一個(gè)不可能解決的問題轉(zhuǎn)變成一個(gè)快速而有趣的難題。
那么,下圖是一個(gè)運(yùn)行著的應(yīng)用程序,一個(gè)用戶進(jìn)程:
它有一個(gè)私有的 虛擬地址空間—— 它自己的內(nèi)存沙箱。整個(gè)系統(tǒng)都在它的地址空間中(即上面比喻的那個(gè)“甕”),程序的二進(jìn)制文件加上它所使用的庫全部都 被映射到內(nèi)存中。內(nèi)核自身也映射為地址空間的一部分。
下面是我們程序 pid
的代碼,它通過 getpid(2) 直接獲取了其進(jìn)程 id:
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
pid_t p = getpid();
printf("%d\n", p);
}
pid.c download
在 Linux 中,一個(gè)進(jìn)程并不是一出生就知道它的 PID。要想知道它的 PID,它必須去詢問內(nèi)核,因此,這個(gè)詢問請求也是一個(gè)系統(tǒng)調(diào)用:
它的***步是開始于調(diào)用 C 庫的 getpid(),它是系統(tǒng)調(diào)用的一個(gè)封裝。當(dāng)你調(diào)用一些函數(shù)時(shí),比如,open(2)
、read(2)
之類,你是在調(diào)用這些封裝。其實(shí),對于大多數(shù)編程語言在這一塊的原生方法,最終都是在 libc 中完成的。
封裝為這些基本的操作系統(tǒng) API 提供了方便,這樣可以保持內(nèi)核的簡潔。所有的內(nèi)核代碼運(yùn)行在特權(quán)模式下,有 bug 的內(nèi)核代碼行將會(huì)產(chǎn)生致命的后果。能在用戶模式下做的任何事情都應(yīng)該在用戶模式中完成。由庫來提供友好的方法和想要的參數(shù)處理,像 printf(3)
這樣。
我們拿一個(gè) web API 進(jìn)行比較,內(nèi)核的封裝方式可以類比為構(gòu)建一個(gè)盡可能簡單的 HTTP 接口去提供服務(wù),然后提供特定語言的庫及輔助方法?;蛘咭部赡苡幸恍┚彺?,這就是 libc 的 getpid()
所做的:***調(diào)用時(shí),它真實(shí)地去執(zhí)行了一個(gè)系統(tǒng)調(diào)用,然后,它緩存了 PID,這樣就可以避免后續(xù)調(diào)用時(shí)的系統(tǒng)調(diào)用開銷。
一旦封裝完成,它做的***件事就是進(jìn)入了超空間:內(nèi)核。這種轉(zhuǎn)換機(jī)制因處理器架構(gòu)設(shè)計(jì)不同而不同。在 Intel 處理器中,參數(shù)和 系統(tǒng)調(diào)用號 是 加載到寄存器中的,然后,運(yùn)行一個(gè) 指令 將 CPU 置于 特權(quán)模式 中,并立即將控制權(quán)轉(zhuǎn)移到內(nèi)核中的全局系統(tǒng)調(diào)用 入口。如果你對這些細(xì)節(jié)感興趣,David Drysdale 在 LWN 上有兩篇非常好的文章(其一,其二)。
內(nèi)核然后使用這個(gè)系統(tǒng)調(diào)用號作為進(jìn)入 sys_call_table
的一個(gè) 索引,它是一個(gè)函數(shù)指針到每個(gè)系統(tǒng)調(diào)用實(shí)現(xiàn)的數(shù)組。在這里,調(diào)用了 sys_getpid
:
在 Linux 中,系統(tǒng)調(diào)用大多數(shù)都實(shí)現(xiàn)為架構(gòu)無關(guān)的 C 函數(shù),有時(shí)候這樣做 很瑣碎,但是通過內(nèi)核優(yōu)秀的設(shè)計(jì),系統(tǒng)調(diào)用機(jī)制被嚴(yán)格隔離。它們是工作在一般數(shù)據(jù)結(jié)構(gòu)中的普通代碼。嗯,除了完全偏執(zhí)的參數(shù)校驗(yàn)以外。
一旦它們的工作完成,它們就會(huì)正常返回,然后,架構(gòu)特定的代碼會(huì)接手轉(zhuǎn)回到用戶模式,封裝將在那里繼續(xù)做一些后續(xù)處理工作。在我們的例子中,getpid(2) 現(xiàn)在緩存了由內(nèi)核返回的 PID。如果內(nèi)核返回了一個(gè)錯(cuò)誤,另外的封裝可以去設(shè)置全局 errno
變量。這些細(xì)節(jié)可以讓你知道 GNU 是怎么處理的。
如果你想要原生的調(diào)用,glibc 提供了 syscall(2) 函數(shù),它可以不通過封裝來產(chǎn)生一個(gè)系統(tǒng)調(diào)用。你也可以通過它來做一個(gè)你自己的封裝。這對一個(gè) C 庫來說,既不神奇,也不特殊。
這種系統(tǒng)調(diào)用的設(shè)計(jì)影響是很深遠(yuǎn)的。我們從一個(gè)非常有用的 strace(1) 開始,這個(gè)工具可以用來監(jiān)視 Linux 進(jìn)程的系統(tǒng)調(diào)用(在 Mac 上,參見 dtruss(1m) 和神奇的 dtrace;在 Windows 中,參見 sysinternals)。這是對 pid
程序的跟蹤:
~/code/x86-os$ strace ./pid
execve("./pid", ["./pid"], [/* 20 vars */]) = 0
brk(0) = 0x9aa0000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7767000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=18056, ...}) = 0
mmap2(NULL, 18056, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7762000
close(3) = 0
[...snip...]
getpid() = 14678
fstat64(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 1), ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7766000
write(1, "14678\n", 614678
) = 6
exit_group(6) = ?
輸出的每一行都顯示了一個(gè)系統(tǒng)調(diào)用、它的參數(shù),以及返回值。如果你在一個(gè)循環(huán)中將 getpid(2)
運(yùn)行 1000 次,你就會(huì)發(fā)現(xiàn)始終只有一個(gè) getpid()
系統(tǒng)調(diào)用,因?yàn)椋?PID 已經(jīng)被緩存了。我們也可以看到在格式化輸出字符串之后,printf(3)
調(diào)用了 write(2)
。
strace
可以開始一個(gè)新進(jìn)程,也可以附加到一個(gè)已經(jīng)運(yùn)行的進(jìn)程上。你可以通過不同程序的系統(tǒng)調(diào)用學(xué)到很多的東西。例如,sshd
守護(hù)進(jìn)程一天都在干什么?
~/code/x86-os$ ps ax | grep sshd
12218 ? Ss 0:00 /usr/sbin/sshd -D
~/code/x86-os$ sudo strace -p 12218
Process 12218 attached - interrupt to quit
select(7, [3 4], NULL, NULL, NULL
[
... nothing happens ...
No fun, it's just waiting for a connection using select(2)
If we wait long enough, we might see new keys being generated and so on, but
let's attach again, tell strace to follow forks (-f), and connect via SSH
]
~/code/x86-os$ sudo strace -p 12218 -f
[lots of calls happen during an SSH login, only a few shown]
[pid 14692] read(3, "-----BEGIN RSA PRIVATE KEY-----\n"..., 1024) = 1024
[pid 14692] open("/usr/share/ssh/blacklist.RSA-2048", O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
[pid 14692] open("/etc/ssh/blacklist.RSA-2048", O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
[pid 14692] open("/etc/ssh/ssh_host_dsa_key", O_RDONLY|O_LARGEFILE) = 3
[pid 14692] open("/etc/protocols", O_RDONLY|O_CLOEXEC) = 4
[pid 14692] read(4, "# Internet (IP) protocols\n#\n# Up"..., 4096) = 2933
[pid 14692] open("/etc/hosts.allow", O_RDONLY) = 4
[pid 14692] open("/lib/i386-linux-gnu/libnss_dns.so.2", O_RDONLY|O_CLOEXEC) = 4
[pid 14692] stat64("/etc/pam.d", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
[pid 14692] open("/etc/pam.d/common-password", O_RDONLY|O_LARGEFILE) = 8
[pid 14692] open("/etc/pam.d/other", O_RDONLY|O_LARGEFILE) = 4
看懂 SSH 的調(diào)用是塊難啃的骨頭,但是,如果搞懂它你就學(xué)會(huì)了跟蹤。能夠看到應(yīng)用程序打開的是哪個(gè)文件是有用的(“這個(gè)配置是從哪里來的?”)。如果你有一個(gè)出現(xiàn)錯(cuò)誤的進(jìn)程,你可以 strace
它,然后去看它通過系統(tǒng)調(diào)用做了什么?當(dāng)一些應(yīng)用程序意外退出而沒有提供適當(dāng)?shù)腻e(cuò)誤信息時(shí),你可以去檢查它是否有系統(tǒng)調(diào)用失敗。你也可以使用過濾器,查看每個(gè)調(diào)用的次數(shù),等等:
~/code/x86-os$ strace -T -e trace=recv curl -silent www.google.com. > /dev/null
recv(3, "HTTP/1.1 200 OK\r\nDate: Wed, 05 N"..., 16384, 0) = 4164 <0.000007>
recv(3, "fl a{color:#36c}a:visited{color:"..., 16384, 0) = 2776 <0.000005>
recv(3, "adient(top,#4d90fe,#4787ed);filt"..., 16384, 0) = 4164 <0.000007>
recv(3, "gbar.up.spd(b,d,1,!0);break;case"..., 16384, 0) = 2776 <0.000006>
recv(3, "$),a.i.G(!0)),window.gbar.up.sl("..., 16384, 0) = 1388 <0.000004>
recv(3, "margin:0;padding:5px 8px 0 6px;v"..., 16384, 0) = 1388 <0.000007>
recv(3, "){window.setTimeout(function(){v"..., 16384, 0) = 1484 <0.000006>
我鼓勵(lì)你在你的操作系統(tǒng)中的試驗(yàn)這些工具。把它們用好會(huì)讓你覺得自己有超能力。
但是,足夠有用的東西,往往要讓我們深入到它的設(shè)計(jì)中。我們可以看到那些用戶空間中的應(yīng)用程序是被嚴(yán)格限制在它自己的虛擬地址空間里,運(yùn)行在 Ring 3(非特權(quán)模式)中。一般來說,只涉及到計(jì)算和內(nèi)存訪問的任務(wù)是不需要請求系統(tǒng)調(diào)用的。例如,像 strlen(3) 和 memcpy(3) 這樣的 C 庫函數(shù)并不需要內(nèi)核去做什么。這些都是在應(yīng)用程序內(nèi)部發(fā)生的事。
C 庫函數(shù)的 man 頁面所在的節(jié)(即圓括號里的 2
和 3
)也提供了線索。節(jié) 2 是用于系統(tǒng)調(diào)用封裝,而節(jié) 3 包含了其它 C 庫函數(shù)。但是,正如我們在 printf(3)
中所看到的,庫函數(shù)最終可以產(chǎn)生一個(gè)或者多個(gè)系統(tǒng)調(diào)用。
如果你對此感到好奇,這里是 Linux (也有 Filippo 的列表)和 Windows 的全部系統(tǒng)調(diào)用列表。它們各自有大約 310 和 460 個(gè)系統(tǒng)調(diào)用。看這些系統(tǒng)調(diào)用是非常有趣的,因?yàn)椋鼈兇砹?em>軟件在現(xiàn)代的計(jì)算機(jī)上能夠做什么。另外,你還可能在這里找到與進(jìn)程間通訊和性能相關(guān)的“寶藏”。這是一個(gè)“不懂 Unix 的人注定最終還要重新發(fā)明一個(gè)蹩腳的 Unix ” 的地方。(LCTT 譯注:原文 “Those who do not understand Unix are condemned to reinvent it,poorly。” 這句話是 Henry Spencer 的名言,反映了 Unix 的設(shè)計(jì)哲學(xué),它的一些理念和文化是一種技術(shù)發(fā)展的必須結(jié)果,看似糟糕卻無法超越。)
與 CPU 周期相比,許多系統(tǒng)調(diào)用花很長的時(shí)間去執(zhí)行任務(wù),例如,從一個(gè)硬盤驅(qū)動(dòng)器中讀取內(nèi)容。在這種情況下,調(diào)用進(jìn)程在底層的工作完成之前一直處于休眠狀態(tài)。因?yàn)?,CPU 運(yùn)行的非常快,一般的程序都因?yàn)?I/O 的限制在它的生命周期的大部分時(shí)間處于休眠狀態(tài),等待系統(tǒng)調(diào)用返回。相反,如果你跟蹤一個(gè)計(jì)算密集型任務(wù),你經(jīng)常會(huì)看到?jīng)]有任何的系統(tǒng)調(diào)用參與其中。在這種情況下,top(1) 將顯示大量的 CPU 使用。
在一個(gè)系統(tǒng)調(diào)用中的開銷可能會(huì)是一個(gè)問題。例如,固態(tài)硬盤比普通硬盤要快很多,但是,操作系統(tǒng)的開銷可能比 I/O 操作本身的開銷 更加昂貴。執(zhí)行大量讀寫操作的程序可能就是操作系統(tǒng)開銷的瓶頸所在。向量化 I/O 對此有一些幫助。因此要做 文件的內(nèi)存映射,它允許一個(gè)程序僅訪問內(nèi)存就可以讀或?qū)懘疟P文件。類似的映射也存在于像視頻卡這樣的地方。最終,云計(jì)算的經(jīng)濟(jì)性可能導(dǎo)致內(nèi)核消除或最小化用戶模式/內(nèi)核模式的切換。
最終,系統(tǒng)調(diào)用還有益于系統(tǒng)安全。一是,無論如何來歷不明的一個(gè)二進(jìn)制程序,你都可以通過觀察它的系統(tǒng)調(diào)用來檢查它的行為。這種方式可能用于去檢測惡意程序。例如,我們可以記錄一個(gè)未知程序的系統(tǒng)調(diào)用的策略,并對它的異常行為進(jìn)行報(bào)警,或者對程序調(diào)用指定一個(gè)白名單,這樣就可以讓漏洞利用變得更加困難。在這個(gè)領(lǐng)域,我們有大量的研究,和許多工具,但是沒有“殺手級”的解決方案。
這就是系統(tǒng)調(diào)用。很抱歉這篇文章有點(diǎn)長,我希望它對你有用。接下來的時(shí)間,我將寫更多(短的)文章,也可以在 RSS 和 Twitter 關(guān)注我。這篇文章獻(xiàn)給 glorious Clube Atlético Mineiro。