Linux中关于API函数与系统调用
相信大家對 API并不陌生,對系統調用也不陌生,但是,對兩者之間的區別于聯系可能并不是十分清楚。
API
API(Application Programming Interface,應用程序編程接口),指的是我們用戶程序編程調用的如read(),write(),malloc(),free()之類的調用的是glibc庫提供的庫函數。API直接提供給用戶編程使用,運行在用戶態。這里要另外提一下,POSIX針對API提出標準,即針對API的函數名,返回值,參數類型進行規范約束,但是并不管API具體如何實現。
系統調用
操作系統為用戶態進程與硬件設備進行交互提供了一組接口——系統調用。
通過軟中斷或系統調用指令向內核發出一個明確的請求,內核將調用內核相關函數來實現(如sys_read(),sys_write())。用戶程序不能直接調用這些Sys_read,sys_write等函數。這些函數運行在內核態。
系統調用的好處
- 把用戶從底層的硬件編程中解放出來
- 極大的提高了系統的安全性
- 使用戶程序具有可移植性
兩者間的區別和聯系
API 和系統調用是不同的
- API只是一個函數定義
- 系統調用通過軟中斷向內核發出一個明確的請求
Libc庫定義的一些API引用了封裝例程(wrapper routine,目的就是發布系統調用)
- 一般每個系統調用對應一個封裝例程
- 庫再用這些封裝例程定義出給用戶的API
通常API函數庫中的函數(如read())會調用封裝例程,封裝例程負責發起系統調用(通過發軟中斷或系統調用指令),這些都運行在用戶態。
內核開始接收系統調用后,cpu從用戶態切換到內核態(cpu處于什么狀態,程序就處于什么狀態,所以很多地方也說程序從用戶態切換到內核態,實際是cpu運行級別的切換,在Linux中cpu 運行在3級表示用戶態,運行在0級表示內核態)。
內核調用相關的內核函數來處理再逐步返回給封裝例程,cpu進行一次內核態到用戶態的切換,API函數從封裝例程拿到結果,再處理完后返回給用戶。
但是API函數不一定都需要進行系統調用。
- API可能直接提供用戶態的服務(如,一些數學函數)
- 一個單獨的API可能調用幾個系統調用
- 不同的API可能調用了同一個系統調用
返回值
- 大部分封裝例程返回一個整數,其值的含義依賴于相應的系統調用
- -1在多數情況下表示內核不能滿足進程的請求
- Libc中定義的errno變量包含特定的出錯碼
我們編寫linux用戶程序的時候,是不能直接調用內核里面的函數的,內核里面的函數位于進程虛擬地址空間里面的內核空間,用戶空間函數及函數庫都處于進程虛擬地址空間里面的用戶空間。
用戶空間調用內核空間的函數只有一個通道——系統調用指令,所以通常要調用glibc等庫的接口函數。
glibc也是用戶空間的,但glibc自己實現了調用特殊的宏匯編系統調用指令進行cpu運行狀態的切換,把進程從用戶空間切換到內核空間。
使用庫函數API和使用系統調用號調用同一個系統調用
代碼如下圖所示
運行結果如圖所示
現在來分析代碼,第4行定義的兩個變量,pid 用來保存使用API 函數getpid()返回的進程號,pid-asm保存嵌入式匯編代碼使用系統調用號調用系統調用得到的進程號。
第5、6行通過系統調用getpid()輸出pid。
第8-14行是一段嵌入式匯編代碼。
“mov $0x14,%%eax\n\t”
將20(getpid系統調用號為20)傳進eax寄存器
“int $0x80\n\t”
發出系統調用指令。
“mov %%eax,%0\n\t”
將eax寄存器的值(其中保存的是系統調用返回值)存入變量pid-asm。
系統調用號
一個系統調用號,對應一個函數入口地址,glibc和內核里面的這個系統調用號是一致的,所以glibc調用匯編之類把系統調用號傳給內核的時候,內核找到這個具體的系統調用服務例程對應的函數入口地址,如sys_read。
參數傳遞
系統調用也需要輸入輸出參數,例如
- 實際的值
- 用戶態進程地址空間的變量的地址
- 甚至是包含指向用戶態函數的指針的數據結構的地址
system_call是linux中所有系統調用的入口點,每個系統調用至少有一個參數,即由eax傳遞的系統調用號
- 一個應用程序調用fork()封裝例程,那么在執行int $0x80之前就把eax寄存器的值置為2(即__NR_fork)。
- 這個寄存器的設置是libc庫中的封裝例程進行的,因此用戶一般不關心系統調用號
- 進入sys_call之后,立即將eax的值壓入內核堆棧
寄存器傳遞參數具有如下限制:
對于第一點,POSIX標準規定,如果寄存器里面裝不下那個長度的參數,那么必須改用參數的地址來傳遞。
對于第二點,有的系統調用參數大于6個,這種情況下,必須用一個單獨的寄存器執行進程地址空間的這些參數所在的一個內存區。
普通c函數的參數傳遞是通過把參數值寫入程序棧(用戶態棧或者內核態棧)實現的。
因為系統調用是一種跨用戶態和內核態的特殊函數,所以這兩個棧都不能用。
在發出系統調用之前,系統調用的參數寫入了cpu的寄存器(如glibc去寫好這些寄存器),然后發出系統調用之后,而在內核調用服務例程(如fork()服務例程)之前,內核再把存放在cpu中的參數拷貝的內核態的堆棧中(因為fork()只是普通的c函數,前面說過普通c函數的參數傳遞是通過把參數值寫入活動的程序棧(用戶態棧或者內核態棧)實現的)。
內核為什么不直接把用戶態的棧拷貝到內核態的棧而要去通過寄存器來傳呢?首先,同事操作兩個棧是比較復雜的,其次,寄存器的使用使得系統調用處理程序的結構與其它異常處理程序的結構類似。
進入退出系統調用
系統調用進入:
當用戶態進程發出int $0x80指令時,cpu切換到內核態并開始從地址system_call處開始執行指令。System_call()函數首先把系統調用號和這個異常處理程序可以用到的所有cpu寄存器保存到相應的內核棧中,不包括由控制單元已自動保存的eflags,cs,eip,ss,exp寄存器。
系統調用退出:
用戶態的寄存器剛進來到系統調用的時候被保存到了內核棧中,錯誤的返回值會被寫到剛開始傳遞系統調用號的那個eax寄存器所在棧的位置。(那么將來當用戶態恢復執行的時候,eax寄存器里面的內容就是系統調用的返回碼了。)
總結
現在,不但對API和系統調用之間的關系十分了解,而且深刻理解了調用一個API函數底層工作過程,有了更深層次的認識!
注:本文部分類容參考自 Linux編程中關于API與系統調用之間的關系
總結
以上是生活随笔為你收集整理的Linux中关于API函数与系统调用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 父子进程与fork函数
- 下一篇: 机器人研究方向的自我学习[2] Ma