上周總結了《C 標准庫的基礎 IO》,其實這些功能函數通過「系統調用」也能實現相應功能。這次文章並不是要詳細介紹各系統調用接口的使用方法,而是要深入理解「庫函數」與「系統」調用之間的關系和區別。
一、系統調用
系統調用,我們可以理解是操作系統為用戶提供的一系列操作的接口(API),這些接口提供了對系統硬件設備功能的操作。這么說可能會比較抽象,舉個例子,我們最熟悉的 hello world
程序會在屏幕上打印出信息。程序中調用了 printf()
函數,而庫函數 printf
本質上是調用了系統調用 write()
函數,實現了終端信息的打印功能。
二、庫函數
庫函數可以理解為是對系統調用的一層封裝。系統調用作為內核提供給用戶程序的接口,它的執行效率是比較高效而精簡的,但有時我們需要對獲取的信息進行更復雜的處理,或更人性化的需要,我們把這些處理過程封裝成一個函數再提供給程序員,更方便於程序猿編碼。
庫函數有可能包含有一個系統調用,有可能有好幾個系統調用,當然也有可能沒有系統調用,比如有些操作不需要涉及內核的功能。可以參考下圖來理解庫函數與系統調用的關系。
三、系統調用意義
- 避免了用戶直接對底層硬件進行編程。比如最簡單的
hello world
程序是將信息打印到終端,終端對系統來說是硬件資源,如果沒有系統調用,用戶程序需要自己編寫終端設備的驅動,以及控制終端如何顯示的代碼。 - 隱藏背后的技術細節。比如讀寫文件,如果使用了系統調用,用戶程序無須關心數據在磁盤的哪個磁道和扇區,以及數據要加載到內存什么位置。
- 保證系統的安全性和穩定性。要知道用戶程序是不能直接操作內核地址空間的,比如一個剛出道的程序猿,讓他直接去訪問內核底層的數據,那么內核系統的安全性就無法保證。而系統調用的功能是由內核來實現,用戶只需要調用接口,無需關心細節,也避免了系統的安全隱患。
- 方便程序的移植性。如果針對一個系統資源的功能操作比如 write(),大家都按照自己思路去實現這個功能,那么我們寫出來的程序的移植性就會非常差。
總而言之,我們只需要把系統調用當作一個接口,而這個接口能實現我們的一個功能,既方便又安全。
四、庫函數 vs 系統調用
參考了《C 專家編程》書籍中的附錄 A.4,書中關於兩者區別的回答是這樣的,函數庫調用是語言或應用程序的一部分,而系統調用是操作系統的一部分。
- 所有 C 函數庫是相同的,而各個操作系統的系統調用是不同的。
- 函數庫調用是調用函數庫中的一個程序,而系統調用是調用系統內核的服務。
- 函數庫調用是與用戶程序相聯系,而系統調用是操作系統的一個進入點
- 函數庫調用是在用戶地址空間執行,而系統調用是在內核地址空間執行
- 函數庫調用的運行時間屬於「用戶」時間,而系統調用的運行時間屬於「系統」時間
- 函數庫調用屬於過程調用,開銷較小,而系統調用需要切換到內核上下文環境然后切換回來,開銷較大
- 在C函數庫libc中大約 300 個程序,在 UNIX 中大約有 90 個系統調用
- 函數庫典型的 C 函數:system, fprintf, malloc,而典型的系統調用:chdir, fork, write, brk
據書中記載,庫函數調用大概花費時間為半微妙,而系統調用所需要的時間大約是庫函數調用的 70 倍(35微秒),因為系統調用會有內核上下文切換的開銷。純粹從性能上考慮,你應該盡可能地減少系統調用的數量,但是,你必須記住許多 C 函數庫中的程序通過系統調用來實現功能。
五、正確理解庫函數高效於系統調用
首先解釋,上述說明的庫函數性能遠高於系統調用的前提是,庫函數種沒有使用系統調用。再來解釋下某些包含系統調用的庫函數,然而其性能確實也要高於系統調用。比如上篇文章中關於文件 IO 函數 fread、fwrite、fputc、fgetc 等,這些函數通常情況下性能確實比系統調用高,原因在於這些庫函數使用了緩沖區,減少了系統調用的次數,因而顯得性能比較高。
六、系統調用是如何運行的
上述內容基本說清楚了庫函數與系統調用的概念以及它們之間的關系,下面我們來理解系統調用到底是如何運行的。
當一個進程正在運行,遇到讀寫文件操作,會發生一個中斷,中斷后系統會把當前用戶進程的一些寄存器信息保存在內核堆棧中,接着去處理中斷服務程序,這里是要去執行系統調用,Linux 中通過執行 int $0x80
來執行系統調用的中斷,但內核實現了很多系統調用,這時需要傳遞「系統調用號」來指明需要哪個系統調用。
為了更清楚的說明系統調用的過程,我們這里參考網上的一段代碼來實現系統調用:
int main()
{
time_t tt;
struct tm *t;
asm volatile (
"mov $0,%%ebx\n\t"
"mov $0xd,%%eax\n\t"
"int $0x80\n\t"
"mov %%eax,%0\n\t"
: "=m" (tt)
);
t = localtime(&tt);
printf("Time: %d-%02d-%02d %02d:%02d:%02d\n",
t->tm_year + 1900,
t->tm_mon + 1, t->tm_mday,
t->tm_hour, t->tm_min, t->tm_sec);
}
[linuxblogs@host ~]$ gcc a.c -oa && ./a
Time: 2018-05-06 03:23:46
首先通過 mov $0xd %%eax
來將系統調用放入 %eax
寄存器中,time() 的系統調用號是 13,然后執行 int $0x80
系統就會去執行 time() 這個系統調用了。其實代碼中的匯編部分就是實現 time() 系統調用的功能,匯編代碼不懂沒關系(我也不太懂),這里主要是為了說清楚系統調用的整個過程。