Linux 编程中的API函数和系统调用的关系【转】
轉自:http://blog.chinaunix.net/uid-25968088-id-3426027.html
原文地址:Linux 編程中的API函數和系統調用的關系?作者:up哥小號
?
API:(Application Programming Interface,應用程序編程接口)??指的是我們用戶程序編程調用的如read(),write(),malloc(),free()之類的調用的是glibc庫提供的庫函數。API直接提供給用戶編程使用,運行在用戶態。
? 我們經常說到的POSIX(Portable Operating System Interface of Unix)是針對API的標準,即針對API的函數名,返回值,參數類型等。POSIX兼容也就指定這些接口函數兼容,但是并不管API具體如何實現。 系統調用
??通過軟中斷或系統調用指令向內核發出一個明確的請求,內核將調用內核相關函數來實現(如sys_read(),sys_write(),sys_fork())。用戶程序不能直接調用這些Sys_read,sys_write等函數。這些函數運行在內核態。 兩者關系: 通常API函數庫(如glibc)中的函數會調用封裝例程,封裝例程負責發起系統調用(通過發軟中斷或系統調用指令),這些都運行在用戶態。內核開始接收系統調用后,cpu從用戶態切換到內核態(cpu處于什么狀態,程序就叫處于什么狀態,所以很多地方也說程序從用戶態切換到內核態,實際是cpu運行級別的切換,通常cpu 運行在3級表示用戶態,cpu 運行在0級表示內核態),內核調用相關的內核函數來處理再逐步返回給封裝例程,cpu進行一次內核態到用戶態的切換,API函數從封裝例程拿到結果,再處理完后返回給用戶。
?但是API函數不一定需要進行系統調用,如某些數學函數,沒有必要進行系統調用,直接glibc里面就給處理了,整個過程運行在用戶態。
? 所以作為我們編寫linux用戶程序的時候,是不能直接調用內核里面的函數的,內核里面的函數位于進程虛擬地址空間里面的內核空間,用戶空間函數及函數庫都處于進程虛擬地址空間里面的用戶空間,用戶空間調用內核空間的函數只有一個通道,這個通道就是系統調用指令,所以通常要調用glibc等庫的接口函數,glibc也是用戶空間的,但glibc自己實現了調用特殊的宏匯編系統調用指令進行cpu運行狀態的切換,把進程從用戶空間切換到內核空間。 用戶態函數執行全過程(這里只講需要進行系統調用的函數) ?用戶態xyz()函數,內核最終一般會調用形如sys_xyz()的服務例程來處理(不過也有一些例外,這里暫時不考慮) ? 函數xyz()是直接提供給用戶編程使用的。圖中“SYSCALL”,“SY***IT”表示真正的匯編指令(匯編指令具體調用的是哪個暫時不關心,我們只需在此關注發起和退出了一個系統調用)。
?發起系統調用:xyz()函數執行的過程中會執行SYSCALL匯編指令,此指令將會把cpu從用戶態切換到內核態。SYACALL匯編指令中會包含將要調用的內核函數的系統調用號和參數,內核在上圖系統調用處理程序中去查一個sys_call_talbe數組來找到這個系統調用號對應的服務例程(如sys_xyz())函數的地址,然后調用這個地址的函數執行。(這里glibc里面的系統調用號和內核里面的系統調用號必須完全相等,當然,這是約定好的)
? 系統調用返回:服務例程(如sys_xyz())函數返回值一般返回正數和0表示系統調用成功結束,而負數表示一個出錯條件。緊接著SY***IT退出系統調用,此指令將cpu從內核態切換到用戶態,glibc針對系統調用返回值如果出錯則需要設置好errno(通常在c庫頭文件/usr/include/errno.h中),然后返回一個值做為glibc封裝例程的返回值(如xyz()的返回值)。這里errno是libc自己用來定義的出錯碼,不一定是最后gblic封裝例程的返回值
這里涉及到幾個概念需要好好講講:
1.系統調用號
為了把系統調用號和相應的服務例程關聯起來,如64位系統中
cat /usr/include/asm/unistd_64.h
#ifndef __SYSCALL
#define __SYSCALL(a, b)
#define __NR_read?????????????????????????????? 0
__SYSCALL(__NR_read, sys_read)
#define __NR_write????????????????????????????? 1
等等
__SYSCALL(__NR_write, sys_write)
#define __NR_dup??????????????????????????????? 32
__SYSCALL(__NR_dup, sys_dup)
#define __NR_dup2?????????????????????????????? 33
__SYSCALL(__NR_dup2, sys_dup2)
等等
Glibc和內核里面的這個系統調用號是一致的,所以glibc調用匯編之類把系統調用號傳給內核的時候,內核通過自己的系統調用分派表sys_call_table(可以理解為一個系統調用號,對應一個函數入口地址)找到這個具體的系統調用服務例程對應的函數入口地址,如上面sys_read,sys_write等
2.參數傳遞
??? 在發起系統調用前,eax寄存器里面存儲了系統調用號。如用戶程序fork()函數,glibc 發出int 0x80或sysenter指令前,eax寄存器就會設置好內核的sys_fork函數對應的系統調用號,這是glibc里面的封裝例程會自動設置好的,程序員無需關心。
?? ?有些系統調用可能調用很多參數(除了系統調用號之外),普通c函數的參數傳遞是通過把參數值寫入活動的程序棧(用戶態?;蛘邇群藨B棧)實現的。因為系統調用是一種跨用戶態和內核態的特殊函數,所以這兩個棧都不能用。在發出系統調用之前,系統調用的參數寫入了cpu的寄存器(如glibc去寫好這些寄存器),然后發出系統調用之后,而在內核調用服務例程(如sys_fork()服務例程)之前,內核再把存放在cpu中的參數拷貝的內核態的堆棧中(因為sys_fork只是普通的c函數,前面說過普通c函數的參數傳遞是通過把參數值寫入活動的程序棧(用戶態?;蛘邇群藨B棧)實現的)。內核為什么不直接把用戶態的棧拷貝到內核態的棧而要去通過寄存器來傳呢?首先,同事操作兩個棧是比較復雜的,其次,寄存器的使用使得系統調用處理程序的結構與其它異常處理程序的結構類似。
??? 使用寄存器傳遞參數,必須滿足兩個條件:
????每個參數的長度不能超過寄存器的長度(比如寄存器長度32位,那參數長度就不能超過32位);
??? 參數的個數不能超過6個(除了eax中傳遞的系統調用號),因為80x86處理器的寄存器的數量是有限的。
??? 第一個條件總能成立,因為POSIX標準規定,如果寄存器里面裝不下那個長度的參數,那么必須改用參數的地址來傳遞。
??? 第二個條件有的系統調用參數大于6個,這種情況下,必須用一個單獨的寄存器執行進程地址空間的這些參數所在的一個內存區。
??? 存放系統調用號和系統調用參數的寄存器是eax(存放系統調用號),ebx,ecx,edx,esi,edi,ebp
3.SYSCALL,SY***IT進入退出系統調用
????這里SYSCALL,SY***IT只是個代號,具體匯編指令如下
??? 進入系統調用:內核2.6以前通過int $0x80匯編指令;內核2.6以后sysenter匯編語言指令。
??? 退出系統調用:舊的iret匯編指令,新的sy***it指令
??? 現在內核同時支持這兩類新舊指令
??? 向量128(0x80)對應于內核入口點,在內核初始化期間調用的函數trap_init(0,用以下方式建立對應于向量128的中斷描述符表項set_system_gate(0x80,&system_call).
??? 當用戶態進程發出int $0x80指令時,cpu切換到內核態并開始從地址system_call處開始執行指令。System_call()函數首先把系統調用號和這個異常處理程序可以用到的所有cpu寄存器保存到相應的內核棧中,不包括由控制單元已自動保存的eflags,cs,eip,ss,esp寄存器。隨后,在ebx中存放當前進程的thread_info數據結構的地址,這是通過獲得內核棧指針的值并把它取整到4kb或8kb的倍數而完成的。然后檢查thread_info結構flag字段的TIF_SYSCALL_TRACE和TIF_SYSCALL_AUDIT標識之一是否被設置為1,也就是檢查是否有某一調試程序正在跟蹤執行程序對系統調用的調用。如果被置1,那么system_call()函數兩次調用do_syscall_trace()函數:一次正好在這個系統調用服務例程執行之前,一次在其之后。Do_syscall_trace函數停止current,并因此允許調試進程收集關于current的信息。
????系統調用退出:
??? (1)用戶態的寄存器剛進來到系統調用的時候被保存到了內核棧中,錯誤的返回值會被寫的剛開始傳遞系統調用號的那個eax寄存器所在棧的位置。(那么將來當用戶態恢復執行的時候,eax寄存器里面的內容就是系統調用的返回碼了。)
??? (2)禁止本地中斷,并檢查current的thread_info結構中的標志。如果有任何標志被設置,那么在返回到用戶態之前還需要完成一些工作。
#define __NR_open?????????????????????????????? 2
__SYSCALL(__NR_open, sys_open)
剛開始我搜索“sys_open”,苦逼的找了幾遍沒找到具體實現的地方,都是調用這個函數的地方。。后經伯松提醒,可能被宏給替換了。。后想起代碼中出現過的SYSCALL_DEFINE宏,就進行了給name加上”sys_”前綴,所以找到SYSCALL_DEFINE中含有open的這句
點擊(此處)折疊或打開
#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)? //注意,這里name已經變成了”_name”,加上了下劃線了,所以”open”變成了“_open”了
點擊(此處)折疊或打開
#define SYSCALL_DEFINEx(x, sname, ...)??? __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)//前面name已經加上“_open”了,記住
進一步
#define __SYSCALL_DEFINEx(x, name, ...)?? asmlinkage long sys##name(__SC_DECL##x(__VA_ARGS__))//這里,變成了“sys_open”了
所以,這里變成了 asmlinkage long sys_open(__SC_DECL3(__VA_ARGS__))
而再進一步
#define __SC_DECL1(t1, a1)?t1 a1
#define __SC_DECL2(t2, a2, ...) t2 a2, __SC_DECL1(__VA_ARGS__)
#define __SC_DECL3(t3, a3, ...) t3 a3, __SC_DECL2(__VA_ARGS__),這里變成了asmlinkage long sys_open(const char __user*? filename,__SC_DECL2(__VA_ARGS__))
進一步變成了
asmlinkage long sys_open(const char __user*? fliename,int flags,__SC_DECL1(t1,a1))
進一步變成了
asmlinkage long sys_open(const char __user*? filename,int flags,umode_t mode)了!
所以,最終變成了
點擊(此處)折疊或打開
# ?
這個是一個字符串替換,如#define S(x)?? “a”#x
S(1),則變成了字符串“a1”了 ##,這個是個簡單的替換
如#define T(x)? a##x
T(1)則變成了 a1 了,比如你前面定義了int a1=2; 就可以printf”%d”,T(1)),即等價于printf(“%d”,a1);如果你用來上面的S(1)替換T(1)那就變成了printf(“%d”,“a1”)肯定就不對了,或者你#define S(x) a#x,用S(1)替換T(1)那就變成了printf(“%d”,a”1”)了,肯定也不對了,所以,用“##”有的時候也是必須 的 …
宏定義的參數,如#define X(...) printf(“%s %s”,__VA_ARGS__);
X(“a”,”b”)就變成了printf(“%s %s”,a,b);了
__VA_ARGS__就表示吧...參數給完整替換掉,”__VA_ARGS__”這個字符串缺一個字符都不可以。。
另外“#,##,...和__VA_ARGS_”也可參看http://www.cnblogs.com/zhujudah/archive/2012/03/22/2411240.html
的一些講解。 測試例程:
點擊(此處)折疊或打開
2
a1
1
1
可以看到宏中帶括號和不帶括號是一樣的效果 參考
1.《深入理解linux內核(第三版)》,
2.? kernel-3.6.7源碼
3.http://www.cnblogs.com/zhujudah/archive/2012/03/22/2411240.html
本文轉自張昺華-sky博客園博客,原文鏈接:http://www.cnblogs.com/sky-heaven/p/5708423.html,如需轉載請自行聯系原作者
總結
以上是生活随笔為你收集整理的Linux 编程中的API函数和系统调用的关系【转】的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Map的两种遍历
- 下一篇: Oracle表记录字节长长度的两种计算方