Linux中國

系統調用,讓世界轉起來!

我其實不想將它分解開給你看,用戶應用程序其實就是一個可憐的 瓮中大腦 brain in a vat

它與外部世界的每個交流都要在內核的幫助下通過系統調用才能完成。一個應用程序要想保存一個文件、寫到終端、或者打開一個 TCP 連接,內核都要參與。應用程序是被內核高度懷疑的:認為它到處充斥著 bug,甚至是個充滿邪惡想法的腦子。

這些系統調用是從一個應用程序到內核的函數調用。出於安全考慮,它們使用了特定的機制,實際上你只是調用了內核的 API。「 系統調用 system call 」這個術語指的是調用由內核提供的特定功能(比如,系統調用 open())或者是調用途徑。你也可以簡稱為:syscall

這篇文章講解系統調用,系統調用與調用一個庫有何區別,以及在操作系統/應用程序介面上的刺探工具。如果徹底了解了應用程序藉助操作系統發生的哪些事情,那麼就可以將一個不可能解決的問題轉變成一個快速而有趣的難題。

那麼,下圖是一個運行著的應用程序,一個用戶進程:

它有一個私有的 虛擬地址空間—— 它自己的內存沙箱。整個系統都在它的地址空間中(即上面比喻的那個「瓮」),程序的二進位文件加上它所使用的庫全部都 被映射到內存中。內核自身也映射為地址空間的一部分。

下面是我們程序 pid 的代碼,它通過 getpid(2) 直接獲取了其進程 id:

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main()
{
    pid_t p = getpid();
    printf("%dn", p);
}

pid.c download

在 Linux 中,一個進程並不是一出生就知道它的 PID。要想知道它的 PID,它必須去詢問內核,因此,這個詢問請求也是一個系統調用:

它的第一步是開始於調用 C 庫的 getpid(),它是系統調用的一個封裝。當你調用一些函數時,比如,open(2)read(2) 之類,你是在調用這些封裝。其實,對於大多數編程語言在這一塊的原生方法,最終都是在 libc 中完成的。

封裝為這些基本的操作系統 API 提供了方便,這樣可以保持內核的簡潔。所有的內核代碼運行在特權模式下,有 bug 的內核代碼行將會產生致命的後果。能在用戶模式下做的任何事情都應該在用戶模式中完成。由庫來提供友好的方法和想要的參數處理,像 printf(3) 這樣。

我們拿一個 web API 進行比較,內核的封裝方式可以類比為構建一個儘可能簡單的 HTTP 介面去提供服務,然後提供特定語言的庫及輔助方法。或者也可能有一些緩存,這就是 libc 的 getpid() 所做的:首次調用時,它真實地去執行了一個系統調用,然後,它緩存了 PID,這樣就可以避免後續調用時的系統調用開銷。

一旦封裝完成,它做的第一件事就是進入了內核 超空間 hyperspace 。這種轉換機制因處理器架構設計不同而不同。在 Intel 處理器中,參數和 系統調用號載入到寄存器中的,然後,運行一個 指令 將 CPU 置於 特權模式 中,並立即將控制權轉移到內核中的全局系統調用 入口。如果你對這些細節感興趣,David Drysdale 在 LWN 上有兩篇非常好的文章(其一其二)。

內核然後使用這個系統調用號作為進入 sys_call_table 的一個 索引,它是一個函數指針到每個系統調用實現的數組。在這裡,調用了 sys_getpid

在 Linux 中,系統調用大多數都實現為架構無關的 C 函數,有時候這樣做 很瑣碎,但是通過內核優秀的設計,系統調用機制被嚴格隔離。它們是工作在一般數據結構中的普通代碼。嗯,除了完全偏執的參數校驗以外。

一旦它們的工作完成,它們就會正常返回,然後,架構特定的代碼會接手轉回到用戶模式,封裝將在那裡繼續做一些後續處理工作。在我們的例子中,getpid(2) 現在緩存了由內核返回的 PID。如果內核返回了一個錯誤,另外的封裝可以去設置全局 errno 變數。這些細節可以讓你知道 GNU 是怎麼處理的。

如果你想要原生的調用,glibc 提供了 syscall(2) 函數,它可以不通過封裝來產生一個系統調用。你也可以通過它來做一個你自己的封裝。這對一個 C 庫來說,既不神奇,也不特殊。

這種系統調用的設計影響是很深遠的。我們從一個非常有用的 strace(1) 開始,這個工具可以用來監視 Linux 進程的系統調用(在 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, "14678n", 614678
)                  = 6
exit_group(6)                           = ?

輸出的每一行都顯示了一個系統調用、它的參數,以及返回值。如果你在一個循環中將 getpid(2) 運行 1000 次,你就會發現始終只有一個 getpid() 系統調用,因為,它的 PID 已經被緩存了。我們也可以看到在格式化輸出字元串之後,printf(3) 調用了 write(2)

strace 可以開始一個新進程,也可以附加到一個已經運行的進程上。你可以通過不同程序的系統調用學到很多的東西。例如,sshd 守護進程一天都在幹什麼?

~/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&apos;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&apos;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) protocolsn#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 的調用是塊難啃的骨頭,但是,如果搞懂它你就學會了跟蹤。能夠看到應用程序打開的是哪個文件是有用的(「這個配置是從哪裡來的?」)。如果你有一個出現錯誤的進程,你可以 strace 它,然後去看它通過系統調用做了什麼?當一些應用程序意外退出而沒有提供適當的錯誤信息時,你可以去檢查它是否有系統調用失敗。你也可以使用過濾器,查看每個調用的次數,等等:

~/code/x86-os$ strace -T -e trace=recv curl -silent www.google.com. > /dev/null

recv(3, "HTTP/1.1 200 OKrnDate: 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>

我鼓勵你在你的操作系統中的試驗這些工具。把它們用好會讓你覺得自己有超能力。

但是,足夠有用的東西,往往要讓我們深入到它的設計中。我們可以看到那些用戶空間中的應用程序是被嚴格限制在它自己的虛擬地址空間里,運行在 Ring 3(非特權模式)中。一般來說,只涉及到計算和內存訪問的任務是不需要請求系統調用的。例如,像 strlen(3)memcpy(3) 這樣的 C 庫函數並不需要內核去做什麼。這些都是在應用程序內部發生的事。

C 庫函數的 man 頁面所在的節(即圓括弧里的 23)也提供了線索。節 2 是用於系統調用封裝,而節 3 包含了其它 C 庫函數。但是,正如我們在 printf(3) 中所看到的,庫函數最終可以產生一個或者多個系統調用。

如果你對此感到好奇,這裡是 Linux (也有 Filippo 的列表)和 Windows 的全部系統調用列表。它們各自有大約 310 和 460 個系統調用。看這些系統調用是非常有趣的,因為,它們代表了軟體在現代的計算機上能夠做什麼。另外,你還可能在這裡找到與進程間通訊和性能相關的「寶藏」。這是一個「不懂 Unix 的人註定最終還要重新發明一個蹩腳的 Unix 」 的地方。(LCTT 譯註:原文 「Those who do not understand Unix are condemned to reinvent it,poorly。」 這句話是 Henry Spencer 的名言,反映了 Unix 的設計哲學,它的一些理念和文化是一種技術發展的必須結果,看似糟糕卻無法超越。)

與 CPU 周期相比,許多系統調用花很長的時間去執行任務,例如,從一個硬碟驅動器中讀取內容。在這種情況下,調用進程在底層的工作完成之前一直處於休眠狀態。因為,CPU 運行的非常快,一般的程序都因為 I/O 的限制在它的生命周期的大部分時間處於休眠狀態,等待系統調用返回。相反,如果你跟蹤一個計算密集型任務,你經常會看到沒有任何的系統調用參與其中。在這種情況下,top(1) 將顯示大量的 CPU 使用。

在一個系統調用中的開銷可能會是一個問題。例如,固態硬碟比普通硬碟要快很多,但是,操作系統的開銷可能比 I/O 操作本身的開銷 更加昂貴。執行大量讀寫操作的程序可能就是操作系統開銷的瓶頸所在。向量化 I/O 對此有一些幫助。因此要做 文件的內存映射,它允許一個程序僅訪問內存就可以讀或寫磁碟文件。類似的映射也存在於像視頻卡這樣的地方。最終,雲計算的經濟性可能導致內核消除或最小化用戶模式/內核模式的切換。

最終,系統調用還有益於系統安全。一是,無論如何來歷不明的一個二進位程序,你都可以通過觀察它的系統調用來檢查它的行為。這種方式可能用於去檢測惡意程序。例如,我們可以記錄一個未知程序的系統調用的策略,並對它的異常行為進行報警,或者對程序調用指定一個白名單,這樣就可以讓漏洞利用變得更加困難。在這個領域,我們有大量的研究,和許多工具,但是沒有「殺手級」的解決方案。

這就是系統調用。很抱歉這篇文章有點長,我希望它對你有用。接下來的時間,我將寫更多(短的)文章,也可以在 RSSTwitter 關注我。這篇文章獻給 glorious Clube Atlético Mineiro。

via:https://manybutfinite.com/post/system-calls/

作者:Gustavo Duarte 譯者:qhwdw 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的電子郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國